Skip to content
This repository has been archived by the owner on Jun 13, 2021. It is now read-only.

Introduce and use resto for registry operations. Remove save and ls. #314

Merged
merged 1 commit into from
Aug 29, 2018

Conversation

mnottale
Copy link
Contributor

@mnottale mnottale commented Jul 25, 2018

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 and save 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)

original image_ 1920x1417

@codecov
Copy link

codecov bot commented Jul 25, 2018

Codecov Report

Merging #314 into master will decrease coverage by 9.55%.
The diff coverage is 48.41%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
cmd/docker-app/root.go 76.47% <ø> (-0.32%) ⬇️
internal/image/image.go 0% <ø> (-25.65%) ⬇️
cmd/docker-app/pull.go 0% <0%> (ø) ⬆️
pkg/resto/registry.go 32.18% <32.18%> (ø)
pkg/resto/resto.go 51.15% <51.15%> (ø)
internal/packager/extract.go 55.3% <60%> (-11.37%) ⬇️
internal/packager/registry.go 64.44% <70.27%> (-16.01%) ⬇️
internal/packager/init.go 51.23% <0%> (-14.71%) ⬇️
internal/helm/helm.go 55.32% <0%> (-7.43%) ⬇️
... and 35 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b83fb00...b939b8b. Read the comment docs.

@@ -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], ".")
Copy link
Member

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?

Copy link
Contributor Author

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.

return parsedReference{"https://" + domain, reference.Path(ref), tag}, nil
}

func getCredentials(domain string) (string, string) {
Copy link
Member

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.

}

// ListRegistry lists all the repositories in a registry
func ListRegistry(ctx context.Context, endpoint string, username, password string, insecure bool) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

endpoint string -> endpoint

}

// ListRepository lists all the tags in a repository
func ListRepository(ctx context.Context, reponame string, username, password string, insecure bool) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reponame string -> repoName

}

// PullConfig pulls a configuration file from a registry
func PullConfig(ctx context.Context, repoTag string, username, password string, insecure bool) (string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repoTag string -> repoTag

}

// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repoTag string -> repoTag

}

// 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) {
Copy link
Member

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

}

// 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) {
Copy link
Member

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 {
Copy link
Member

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.

if !strings.Contains(err.Error(), "manifest invalid") && !strings.Contains(err.Error(), "manifest Unknown") {
return "", err
}
// try legacy mode
Copy link
Member

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

@mnottale mnottale force-pushed the resto branch 2 times, most recently from 9de6a74 to d610a77 Compare July 27, 2018 09:42
}

// 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) {
Copy link
Contributor

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) …

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], "/")) {
Copy link
Contributor

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)

if err == io.EOF {
break
for k, v := range payload {
// poor man's security
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

}
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)
Copy link
Contributor

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")

}
}
return fmt.Errorf("failed to find our layer in tarball")
return nil
Copy link
Contributor

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)

return "", "", err
}
cr.Close()
if domain == "https://registry-1.docker.io" {
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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

return nil, err
}
if username == "" {
username, password, _ = getCredentials(pr.domain)
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

}
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)
Copy link
Contributor

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 ? 🤔

Copy link
Contributor Author

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

}

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as Pull…

}
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)
Copy link
Member

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://?

Copy link
Contributor Author

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.

Copy link
Contributor

@vdemeester vdemeester left a 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 😭

if !strings.Contains(err.Error(), "HTTP response to HTTPS client") {
return repo, err
}
if os.Getenv("RESTO_INSECURE_AUTH") != "1" {
Copy link
Member

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)

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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},
Copy link
Member

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.

Copy link
Contributor Author

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.


return cmd.Run()
}
}
Copy link
Contributor

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

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") != ""})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to @thaJeztah comment

cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "error loading image %s", repotag)
repoComps := strings.Split(repotag, ":")
Copy link
Contributor

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]
}

Copy link
Contributor

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 👼

defer os.Remove(file)
f, err := os.Open(file)
appDir := filepath.Join(outputDir, internal.DirNameFromAppName(filepath.Base(repo)))
err = os.Mkdir(appDir, 0755)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Mkdirall instead?

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok!

break
for k, v := range payload {
// poor man's security
if strings.Contains(k, "/") || strings.Contains(k, "\\") {
Copy link
Contributor

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
Copy link
Contributor

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 ?

Copy link
Contributor Author

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.

Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content := &bytes.Buffer{}

Copy link
Contributor Author

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.

Copy link
Contributor

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.`

Copy link
Contributor Author

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.

return "", err
}
if opts.Username == "" {
opts.Username, opts.Password, _ = getCredentials(pr.domain)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why dropping the error?

Copy link
Contributor Author

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

Copy link
Contributor

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?

if err == nil {
return dgst.String(), nil
}
if !strings.Contains(err.Error(), "manifest invalid") && !strings.Contains(err.Error(), "manifest Unknown") {
Copy link
Contributor

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
}

},
RootFS: ociv1.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{payloadUncompressedDigest}, //nope { payloadDesc.Digest},
Copy link
Contributor

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

@silvin-lubecki
Copy link
Contributor

Please rebase 😇

@silvin-lubecki
Copy link
Contributor

silvin-lubecki commented Aug 6, 2018

Also, there is no unit tests at all for pkg/resto 😭

+1

cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "error loading image %s", repotag)
repoComps := strings.Split(repotag, ":")
Copy link
Contributor

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"
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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)

Copy link
Contributor Author

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!

@vdemeester
Copy link
Contributor

Also …

e2e test suite should cover us.

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…

Copy link
Contributor Author

@mnottale mnottale left a 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.

@mnottale mnottale force-pushed the resto branch 2 times, most recently from 5a490df to 960de41 Compare August 20, 2018 13:34
}

// ListRegistry lists all the repositories in a registry
func ListRegistry(ctx context.Context, endpoint string, opts RegistryOptions) ([]string, error) {
Copy link
Contributor

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).

@mnottale mnottale force-pushed the resto branch 4 times, most recently from 81d1a63 to d078381 Compare August 27, 2018 08:30
Copy link
Contributor

@silvin-lubecki silvin-lubecki left a 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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants