-
Notifications
You must be signed in to change notification settings - Fork 174
Introduce and use resto for registry operations. Remove save and ls. #314
Conversation
91b3f54
to
b4d8594
Compare
Codecov Report
@@ Coverage Diff @@
## master #314 +/- ##
==========================================
- Coverage 62.87% 53.31% -9.56%
==========================================
Files 61 49 -12
Lines 3189 2757 -432
==========================================
- Hits 2005 1470 -535
- Misses 944 1046 +102
- Partials 240 241 +1
Continue to review full report at Codecov.
|
cmd/docker-app/pull.go
Outdated
@@ -12,7 +12,7 @@ func pullCmd() *cobra.Command { | |||
Short: "Pull an application from a registry", | |||
Args: cli.ExactArgs(1), | |||
RunE: func(cmd *cobra.Command, args []string) error { | |||
return packager.Pull(args[0]) | |||
return packager.Pull(args[0], ".") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we outright remove this now that we don't have push
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have push. Pull is in experimental for now.
pkg/resto/resto.go
Outdated
return parsedReference{"https://" + domain, reference.Path(ref), tag}, nil | ||
} | ||
|
||
func getCredentials(domain string) (string, string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there's a chance we miss an error from this function, would be good to make a Credentials
struct and return that with an error or return string, string, error
.
pkg/resto/resto.go
Outdated
} | ||
|
||
// ListRegistry lists all the repositories in a registry | ||
func ListRegistry(ctx context.Context, endpoint string, username, password string, insecure bool) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
endpoint string
-> endpoint
pkg/resto/resto.go
Outdated
} | ||
|
||
// ListRepository lists all the tags in a repository | ||
func ListRepository(ctx context.Context, reponame string, username, password string, insecure bool) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reponame string
-> repoName
pkg/resto/resto.go
Outdated
} | ||
|
||
// PullConfig pulls a configuration file from a registry | ||
func PullConfig(ctx context.Context, repoTag string, username, password string, insecure bool) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repoTag string
-> repoTag
pkg/resto/resto.go
Outdated
} | ||
|
||
// PullConfigMulti pulls a set of configuration files from a registry | ||
func PullConfigMulti(ctx context.Context, repoTag string, username, password string, insecure bool) (map[string]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repoTag string
-> repoTag
pkg/resto/resto.go
Outdated
} | ||
|
||
// PushConfig pushes a configuration file to a registry and returns its digest | ||
func PushConfig(ctx context.Context, payload string, repoTag string, username, password string, insecure bool, labels map[string]string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
payload string, repoTag string
-> payload, repoTag
pkg/resto/resto.go
Outdated
} | ||
|
||
// PushConfigMulti pushes a set of configuration files to a registry and returns its digest | ||
func PushConfigMulti(ctx context.Context, payload map[string]string, repoTag string, username, password string, insecure bool, labels map[string]string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repoTag string
-> repoTag
res := make(map[string]string) | ||
for { | ||
header, err := tarReader.Next() | ||
if err == io.EOF { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must also handle other errors here otherwise we never leave this loop.
pkg/resto/resto.go
Outdated
if !strings.Contains(err.Error(), "manifest invalid") && !strings.Contains(err.Error(), "manifest Unknown") { | ||
return "", err | ||
} | ||
// try legacy mode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to separate out the legacy and new modes into functions so that this is easier to follow
9de6a74
to
d610a77
Compare
pkg/resto/resto.go
Outdated
} | ||
|
||
// PullConfigMulti pulls a set of configuration files from a registry | ||
func PullConfigMulti(ctx context.Context, repoTag, username, password string, insecure bool) (map[string]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use a simpler signature for PullConfigMulti
, with the required element as argument (like repotag
above) and a config object for the rest.
func PullConfigMulti(ctx context.Context, reference string, options PullOptions) …
internal/packager/registry.go
Outdated
return errors.Wrapf(err, "error loading image %s", repotag) | ||
repoComps := strings.Split(repotag, ":") | ||
repo := repoComps[0] | ||
if len(repoComps) == 3 || (len(repoComps) == 2 && strings.Contains(repoComps[1], "/")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really isn't clear what we're testing here (worst case scenario would be to extract this into a well named function)
internal/packager/registry.go
Outdated
if err == io.EOF { | ||
break | ||
for k, v := range payload { | ||
// poor man's security |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
internal/packager/registry.go
Outdated
} | ||
err = ioutil.WriteFile(filepath.Join(outputDir, internal.DirNameFromAppName(filepath.Base(repo))), data, 0644) | ||
return errors.Wrap(err, "error writing output file") | ||
return errors.Wrap(err, "failed to write output file "+target) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The target
will be in the *wrapped` error, so we don't need to add it to the message
errors.Wrap(err, "failed to write output file")
internal/packager/registry.go
Outdated
} | ||
} | ||
return fmt.Errorf("failed to find our layer in tarball") | ||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be simplified with
errors.Wrap(ioutil.WriteFile(target, []byte(v), 0644), "failed to write output file"))
(errors.Wrap
will return nil
if the err is nil
)
pkg/resto/resto.go
Outdated
return "", "", err | ||
} | ||
cr.Close() | ||
if domain == "https://registry-1.docker.io" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That part too should be handled somewhere in the docker cli, we should use that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
git-grepped for registry-1.docker.io, could not find the relevant bit
return auth.Username, auth.Password, nil | ||
} | ||
|
||
func makeTarGz(content map[string]string) ([]byte, digest.Digest, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we use what docker/docker
provide and uses (i.e. pkg/archive
) and what's done in docker/docker/distribution
for pushing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't make things any simpler from what I see of docker/docker/distribution's interface
pkg/resto/resto.go
Outdated
return nil, err | ||
} | ||
if username == "" { | ||
username, password, _ = getCredentials(pr.domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why ignoring the error here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because failure to obtain credentials is not an error. One might not need them if pushing to a plain registry.
pkg/resto/resto.go
Outdated
} | ||
repo, err := NewRepository(ctx, pr.domain, pr.path, username, password, insecure) | ||
if err != nil { | ||
repo, err = NewRepository(ctx, strings.Replace(pr.domain, "https://", "http://", 1), pr.path, username, password, insecure) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we do that ? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because repotag does not contain the protocol to use
pkg/resto/resto.go
Outdated
} | ||
|
||
// PushConfigMulti pushes a set of configuration files to a registry and returns its digest | ||
func PushConfigMulti(ctx context.Context, payload map[string]string, repoTag, username, password string, insecure bool, labels map[string]string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as Pull…
pkg/resto/resto.go
Outdated
} | ||
repo, err := NewRepository(ctx, pr.domain, pr.path, username, password, insecure) | ||
if err != nil { | ||
repo, err = NewRepository(ctx, strings.Replace(pr.domain, "https://", "http://", 1), pr.path, username, password, insecure) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks fishy; something failed (we don't know what), so leak credentials over plain http://
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it so that it drops credentials by default when fallbacking to http, and so that it fallbacks only if the error suggests we should.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, there is no unit tests at all for pkg/resto
😭
pkg/resto/registry.go
Outdated
if !strings.Contains(err.Error(), "HTTP response to HTTPS client") { | ||
return repo, err | ||
} | ||
if os.Getenv("RESTO_INSECURE_AUTH") != "1" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks to be global configuration, which means that Docker Hub can be MITM'd; For comparison, the docker daemon requires each registry that is allowed to be accessed insecurely to be configured (through the --insecure-registry
option).
For the cli
, connecting to an insecure registry, there should be an option to set it for each action (e.g., similar to how docker manifest inspect
has an --insecure
flag)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are not a daemon though. By default docker-app/resto won't transmit credentials over an insecure channel unless this override environment variable is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what will happen is that someone sets RESTO_INSECURE_AUTH=1
to test against a local registry, but now opens a major security hole by allowing insecure access all registries
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems an unlikely scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is people doing chmod 777 /var/run/docker.sock
, or sudo rm -rf /var/lib/docker
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't particularly intend to document this env variable, I see it more as an escape hatch in the unlikely scenario one user end up needing it. If that ever happens we will make sure to emphasis the advice to pass it directly on the command line and not to export it or god forbid add it to a .rc file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not an unlikely scenario moby/moby#8887, and it will happen; add this escape hatch, and there'll be no turning back
Mirror: false, | ||
URL: url, | ||
Version: 2, | ||
TLSConfig: &tls.Config{InsecureSkipVerify: opts.Insecure}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not use InsecureSkipVerify
. This should never be the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not the default. It's disabled by default, and enabled by a cli argument.
9459ea1
to
1cd97e5
Compare
internal/image/image.go
Outdated
|
||
return cmd.Run() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing end of line
internal/packager/registry.go
Outdated
app, err := Extract(appname) | ||
// Pull loads an app from a registry | ||
func Pull(repotag string, outputDir string) error { | ||
payload, err := resto.PullConfigMulti(context.Background(), repotag, resto.RegistryOptions{Insecure: os.Getenv("DOCKERAPP_INSECURE_REGISTRY") != ""}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to @thaJeztah comment
internal/packager/registry.go
Outdated
cmd.Stderr = os.Stderr | ||
if err := cmd.Run(); err != nil { | ||
return errors.Wrapf(err, "error loading image %s", repotag) | ||
repoComps := strings.Split(repotag, ":") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 5 following lines should be extracted to a sub function, and should be tested
func getRepo(repotag string) string{
repoComps := strings.Split(repotag, ":")
// handle the case where a port was specified in the domain part of repotag
if len(repoComps) == 3 || (len(repoComps) == 2 && strings.Contains(repoComps[1], "/")) {
return repoComps[1]
}
return repoComps[0]
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that distribution/reference
might probably handle that case already 👼
internal/packager/registry.go
Outdated
defer os.Remove(file) | ||
f, err := os.Open(file) | ||
appDir := filepath.Join(outputDir, internal.DirNameFromAppName(filepath.Base(repo))) | ||
err = os.Mkdir(appDir, 0755) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Mkdirall
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Certainly not. I want an error if the path already exists, And I want an error if outputDir was not already created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok!
internal/packager/registry.go
Outdated
break | ||
for k, v := range payload { | ||
// poor man's security | ||
if strings.Contains(k, "/") || strings.Contains(k, "\\") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least log something instead of dropping it silently?
err = json.Unmarshal([]byte(ma.Payload), &res) | ||
return res, err | ||
} | ||
// legacy image mode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extract the following code to a subfunction ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
20 lines called only once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more a matter of readability (or even reviewability 😄 )
return res, err | ||
} | ||
if header.Typeflag == tar.TypeReg { | ||
content := bytes.NewBuffer(nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
content := &bytes.Buffer{}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was voted the "least future-proof one-liner of the century" by an independent research team.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just reading the golang doc https://golang.org/pkg/bytes/#NewBuffer
In most cases, new(Buffer) (or just declaring a Buffer variable) is sufficient to initialize a Buffer.`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They say it's sufficient, not that it's recommended.
pkg/resto/resto.go
Outdated
return "", err | ||
} | ||
if opts.Username == "" { | ||
opts.Username, opts.Password, _ = getCredentials(pr.domain) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why dropping the error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above, creds failure, not fatal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least log it in debug?
pkg/resto/resto.go
Outdated
if err == nil { | ||
return dgst.String(), nil | ||
} | ||
if !strings.Contains(err.Error(), "manifest invalid") && !strings.Contains(err.Error(), "manifest Unknown") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invert the conditions? It will be more readable:
if strings.Contains(err.Error(), "manifest invalid") || strings.Contains(err.Error(), "manifest Unknown") {
return "", unsupportedMediaType{}
}
return "", err
Or even a switch could be nice:
switch{
case strings.Contains(err.Error(), "manifest invalid"):
return "", unsupportedMediaType{}
case strings.Contains(err.Error(), "manifest Unknown"):
return "", unsupportedMediaType{}
default:
return "", err
}
pkg/resto/resto.go
Outdated
}, | ||
RootFS: ociv1.RootFS{ | ||
Type: "layers", | ||
DiffIDs: []digest.Digest{payloadUncompressedDigest}, //nope { payloadDesc.Digest}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this comment
Please rebase 😇 |
+1 |
internal/packager/registry.go
Outdated
cmd.Stderr = os.Stderr | ||
if err := cmd.Run(); err != nil { | ||
return errors.Wrapf(err, "error loading image %s", repotag) | ||
repoComps := strings.Split(repotag, ":") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that distribution/reference
might probably handle that case already 👼
} | ||
|
||
// MediaTypeConfig is the media type used for configuration files. | ||
const MediaTypeConfig = "application/vndr.docker.config" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at some existing media type, this one should be something like application/vnd.docker.app.config.tar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not app-specific, the aim is to target arbitrary config files with resto, and it's not a tarball.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then application/vnd.docker.config.tar
(my point was mainly about the tar
part)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not a tar!
Also …
This is not really the case as the registry we use (either a local registry or the hub) do not seem to support the new media-type… so we're not really testing the new implementation, just the legacy behavior… |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, short of spawning patched registries there is no way to test the 'mediatype' implementation. And there is no unit tests because the only thing resto does is pushing/pulling to registries, and I'm not spawning a registry in an unit test.
5a490df
to
960de41
Compare
pkg/resto/resto.go
Outdated
} | ||
|
||
// ListRegistry lists all the repositories in a registry | ||
func ListRegistry(ctx context.Context, endpoint string, opts RegistryOptions) ([]string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to ListRegistries (as you called the other one ListTags).
81d1a63
to
d078381
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Signed-off-by: Matthieu Nottale <matthieu.nottale@docker.com>
Signed-off-by: Matthieu Nottale matthieu.nottale@docker.com
- What I did
Use our own image-backing mechanism to handle registry interactions.
We win windows support, but we loose
ls
andsave
as the local docker daemon is bypassed entirely.Compatibility is preserved with images pushed prior to this patch.
- How I did it
Introduce pkg/resto with registry operations, use it in place of the old system.
- How to verify it
e2e test suite should cover us.
- Description for the changelog
Windows users can now use the
push
operation, and use applications hosted on a registry.- A picture of a cute animal (not mandatory but encouraged)