From 342ba18561236722df900d1ce0b703b43bd8fe42 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 25 Nov 2016 15:03:41 +0100 Subject: [PATCH] use user/pass flags Signed-off-by: Antonio Murdaca --- README.md | 29 +++++----- cmd/skopeo/copy.go | 35 ++++++++++- cmd/skopeo/delete.go | 13 ++++- cmd/skopeo/inspect.go | 5 ++ cmd/skopeo/main.go | 12 ---- cmd/skopeo/utils.go | 58 ++++++++++++++++--- docs/skopeo.1.md | 12 ++-- integration/check_test.go | 10 +--- integration/copy_test.go | 17 ++++++ .../github.com/containers/image/copy/copy.go | 8 ++- 10 files changed, 150 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 0d89309c..949ef4ad 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,12 @@ Copying images - `skopeo` can copy container images between various storage mechanisms, e.g. Docker registries (including the Docker Hub), the Atomic Registry, -and local directories: +local directories, and local OCI-layout directories: ```sh $ skopeo copy docker://busybox:1-glibc atomic:myns/unsigned:streaming $ skopeo copy docker://busybox:latest dir:existingemptydirectory +$ skopeo copy docker://busybox:latest oci:busybox_ocilayout ``` Deleting images @@ -65,14 +66,10 @@ $ skopeo delete docker://localhost:5000/imagename:latest Private registries with authentication - -When interacting with private registries, `skopeo` first looks for the Docker's cli config file (usually located at `$HOME/.docker/config.json`) to get the credentials needed to authenticate. When the file isn't available it falls back looking for `--username` and `--password` flags. The ultimate fallback, as Docker does, is to provide an empty authentication when interacting with those registries. +When interacting with private registries, `skopeo` first looks for `--creds` (for `skopeo inspect|delete`) or `--src-creds|--dest-creds` (for `skopeo copy`) flags. If those aren't provided, it looks for the Docker's cli config file (usually located at `$HOME/.docker/config.json`) to get the credentials needed to authenticate. The ultimate fallback, as Docker does, is to provide an empty authentication when interacting with those registries. Examples: ```sh -# on my system -$ skopeo --help | grep docker-cfg - --docker-cfg "/home/runcom/.docker" Docker's cli config for auth - $ cat /home/runcom/.docker/config.json { "auths": { @@ -88,16 +85,22 @@ $ skopeo inspect docker://myregistrydomain.com:5000/busybox {"Tag":"latest","Digest":"sha256:473bb2189d7b913ed7187a33d11e743fdc2f88931122a44d91a301b64419f092","RepoTags":["latest"],"Comment":"","Created":"2016-01-15T18:06:41.282540103Z","ContainerConfig":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"DockerVersion":"1.8.3","Author":"","Config":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"Architecture":"amd64","Os":"linux"} # let's try now to fake a non existent Docker's config file -$ skopeo --docker-cfg="" inspect docker://myregistrydomain.com:5000/busybox -FATA[0000] Get https://myregistrydomain.com:5000/v2/busybox/manifests/latest: no basic auth credentials +$ cat /home/runcom/.docker/config.json +{} -# passing --username and --password - we can see that everything goes fine -$ skopeo --docker-cfg="" --username=testuser --password=testpassword inspect docker://myregistrydomain.com:5000/busybox +$ skopeo inspect docker://myregistrydomain.com:5000/busybox +FATA[0000] unauthorized: authentication required + +# passing --creds - we can see that everything goes fine +$ skopeo inspect --creds=testuser:testpassword docker://myregistrydomain.com:5000/busybox {"Tag":"latest","Digest":"sha256:473bb2189d7b913ed7187a33d11e743fdc2f88931122a44d91a301b64419f092","RepoTags":["latest"],"Comment":"","Created":"2016-01-15T18:06:41.282540103Z","ContainerConfig":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) CMD [\"sh\"]"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"DockerVersion":"1.8.3","Author":"","Config":{"Hostname":"aded96b43f48","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["sh"],"Image":"9e77fef7a1c9f989988c06620dabc4020c607885b959a2cbd7c2283c91da3e33","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"Architecture":"amd64","Os":"linux"} + +# skopeo copy example: +$ skopeo copy --src-creds=testuser:testpassword docker://myregistrydomain.com:5000/private oci:local_oci_image ``` If your cli config is found but it doesn't contain the necessary credentials for the queried registry -you'll get an error. You can fix this by either logging in (via `docker login`) or providing `--username` -and `--password`. +you'll get an error. You can fix this by either logging in (via `docker login`) or providing `--creds` or `--src-creds|--dest-creds`. + Building - To build the manual you will need go-md2man. @@ -116,7 +119,7 @@ $ brew install gpgme $ make binary-local ``` -You may need to install additional development packages: gpgme-devel and libassuan-devel +You may need to install additional development packages: `gpgme-devel` and `libassuan-devel` ```sh $ dnf install gpgme-devel libassuan-devel ``` diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index c5bb3e32..8939ca67 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -7,9 +7,25 @@ import ( "github.com/containers/image/copy" "github.com/containers/image/transports" + "github.com/containers/image/types" "github.com/urfave/cli" ) +// contextsFromGlobalOptions returns source and destionation types.SystemContext depending on c. +func contextsFromGlobalOptions(c *cli.Context) (*types.SystemContext, *types.SystemContext, error) { + sourceCtx, err := contextFromGlobalOptions(c, "src-creds") + if err != nil { + return nil, nil, err + } + + destinationCtx, err := contextFromGlobalOptions(c, "dest-creds") + if err != nil { + return nil, nil, err + } + + return sourceCtx, destinationCtx, nil +} + func copyHandler(context *cli.Context) error { if len(context.Args()) != 2 { return errors.New("Usage: copy source destination") @@ -32,10 +48,17 @@ func copyHandler(context *cli.Context) error { signBy := context.String("sign-by") removeSignatures := context.Bool("remove-signatures") - return copy.Image(contextFromGlobalOptions(context), policyContext, destRef, srcRef, ©.Options{ + sourceCtx, destinationCtx, err := contextsFromGlobalOptions(context) + if err != nil { + return err + } + + return copy.Image(policyContext, destRef, srcRef, ©.Options{ RemoveSignatures: removeSignatures, SignBy: signBy, ReportWriter: os.Stdout, + SourceCtx: sourceCtx, + DestinationCtx: destinationCtx, }) } @@ -54,5 +77,15 @@ var copyCmd = cli.Command{ Name: "sign-by", Usage: "Sign the image using a GPG key with the specified `FINGERPRINT`", }, + cli.StringFlag{ + Name: "src-creds, screds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the source registry", + }, + cli.StringFlag{ + Name: "dest-creds, dcreds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the destination registry", + }, }, } diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index 897503c7..cafe30f4 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -18,7 +18,11 @@ func deleteHandler(context *cli.Context) error { return fmt.Errorf("Invalid source name %s: %v", context.Args()[0], err) } - if err := ref.DeleteImage(contextFromGlobalOptions(context)); err != nil { + ctx, err := contextFromGlobalOptions(context, "creds") + if err != nil { + return err + } + if err := ref.DeleteImage(ctx); err != nil { return err } return nil @@ -29,4 +33,11 @@ var deleteCmd = cli.Command{ Usage: "Delete image IMAGE-NAME", ArgsUsage: "IMAGE-NAME", Action: deleteHandler, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "creds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + }, + }, } diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index de00c6a6..82f86c01 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -34,6 +34,11 @@ var inspectCmd = cli.Command{ Name: "raw", Usage: "output raw manifest", }, + cli.StringFlag{ + Name: "creds", + Value: "", + Usage: "Use `USERNAME[:PASSWORD]` for accessing the registry", + }, }, Action: func(c *cli.Context) error { img, err := parseImage(c) diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index 96b0bfdf..4953c818 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -25,23 +25,11 @@ func createApp() *cli.App { app.Version = version.Version } app.Usage = "Various operations with container images and container image registries" - // TODO(runcom) - //app.EnableBashCompletion = true app.Flags = []cli.Flag{ cli.BoolFlag{ Name: "debug", Usage: "enable debug output", }, - cli.StringFlag{ - Name: "username", - Value: "", - Usage: "use `USERNAME` for accessing the registry", - }, - cli.StringFlag{ - Name: "password", - Value: "", - Usage: "use `PASSWORD` for accessing the registry", - }, cli.StringFlag{ Name: "cert-path", Value: "", diff --git a/cmd/skopeo/utils.go b/cmd/skopeo/utils.go index d3742141..e349402b 100644 --- a/cmd/skopeo/utils.go +++ b/cmd/skopeo/utils.go @@ -1,22 +1,56 @@ package main import ( + "errors" + "strings" + "github.com/containers/image/transports" "github.com/containers/image/types" "github.com/urfave/cli" ) -// contextFromGlobalOptions returns a types.SystemContext depending on c. -func contextFromGlobalOptions(c *cli.Context) *types.SystemContext { - tlsVerify := c.GlobalBoolT("tls-verify") - return &types.SystemContext{ +func contextFromGlobalOptions(c *cli.Context, credsFlag string) (*types.SystemContext, error) { + ctx := &types.SystemContext{ RegistriesDirPath: c.GlobalString("registries.d"), DockerCertPath: c.GlobalString("cert-path"), - DockerInsecureSkipTLSVerify: !tlsVerify, + DockerInsecureSkipTLSVerify: !c.GlobalBoolT("tls-verify"), } + if c.IsSet(credsFlag) { + var err error + ctx.DockerAuthConfig, err = getDockerAuth(c.String(credsFlag)) + if err != nil { + return nil, err + } + } + return ctx, nil } -// ParseImage converts image URL-like string to an initialized handler for that image. +func parseCreds(creds string) (string, string, error) { + if creds == "" { + return "", "", errors.New("credentials can't be empty") + } + up := strings.SplitN(creds, ":", 2) + if len(up) == 1 { + return up[0], "", nil + } + if up[0] == "" { + return "", "", errors.New("username can't be empty") + } + return up[0], up[1], nil +} + +func getDockerAuth(creds string) (*types.DockerAuthConfig, error) { + username, password, err := parseCreds(creds) + if err != nil { + return nil, err + } + return &types.DockerAuthConfig{ + Username: username, + Password: password, + }, nil +} + +// parseImage converts image URL-like string to an initialized handler for that image. // The caller must call .Close() on the returned Image. func parseImage(c *cli.Context) (types.Image, error) { imgName := c.Args().First() @@ -24,7 +58,11 @@ func parseImage(c *cli.Context) (types.Image, error) { if err != nil { return nil, err } - return ref.NewImage(contextFromGlobalOptions(c)) + ctx, err := contextFromGlobalOptions(c, "creds") + if err != nil { + return nil, err + } + return ref.NewImage(ctx) } // parseImageSource converts image URL-like string to an ImageSource. @@ -35,5 +73,9 @@ func parseImageSource(c *cli.Context, name string, requestedManifestMIMETypes [] if err != nil { return nil, err } - return ref.NewImageSource(contextFromGlobalOptions(c), requestedManifestMIMETypes) + ctx, err := contextFromGlobalOptions(c, "creds") + if err != nil { + return nil, err + } + return ref.NewImageSource(ctx, requestedManifestMIMETypes) } diff --git a/docs/skopeo.1.md b/docs/skopeo.1.md index 9b8a6bc7..f049485a 100644 --- a/docs/skopeo.1.md +++ b/docs/skopeo.1.md @@ -37,10 +37,6 @@ Most commands refer to container images, using a _transport_`:`_details_ format. **--debug** enable debug output - **--username** _username_ for accessing the registry - - **--password** _password_ for accessing the registry - **--cert-path** _path_ Use certificates at _path_ (cert.pem, key.pem) to connect to the registry **--policy** _path-to-policy_ Path to a policy.json file to use for verifying signatures and deciding whether an image is trusted, overriding the default trust policy file. @@ -70,6 +66,10 @@ Uses the system's trust policy to validate images, rejects images not trusted by **--sign-by=**_key-id_ add a signature using that key ID for an image name corresponding to _destination-image_ + **--src-creds** _username[:password]_ for accessing the source registry + + **--dest-creds** _username[:password]_ for accessing the destination registry + Existing signatures, if any, are preserved as well. ## skopeo delete @@ -81,6 +81,8 @@ Mark _image-name_ for deletion. To release the allocated disk space, you need t $ docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml ``` + **--creds** _username[:password]_ for accessing the registry + Additionally, the registry must allow deletions by setting `REGISTRY_STORAGE_DELETE_ENABLED=true` for the registry daemon. ## skopeo inspect @@ -92,6 +94,8 @@ Return low-level information about _image-name_ in a registry _image-name_ name of image to retrieve information about + **--creds** _username[:password]_ for accessing the registry + ## skopeo layers **skopeo layers** _image-name_ diff --git a/integration/check_test.go b/integration/check_test.go index e8654669..e3869fba 100644 --- a/integration/check_test.go +++ b/integration/check_test.go @@ -76,17 +76,13 @@ func (s *SkopeoSuite) TestVersion(c *check.C) { } func (s *SkopeoSuite) TestCanAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) { - // TODO(runcom) - c.Skip("we need to restore --username --password flags!") - wanted := ".*unauthorized: authentication required.*" - assertSkopeoFails(c, wanted, "--docker-cfg=''", "--username="+s.regV2WithAuth.username, "--password="+s.regV2WithAuth.password, "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) + wanted := ".*manifest unknown: manifest unknown.*" + assertSkopeoFails(c, wanted, "--tls-verify=false", "inspect", "--creds="+s.regV2WithAuth.username+":"+s.regV2WithAuth.password, fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) } func (s *SkopeoSuite) TestNeedAuthToPrivateRegistryV2WithoutDockerCfg(c *check.C) { - // TODO(runcom): mock the empty docker-cfg by removing it in the test itself (?) - c.Skip("mock empty docker config") wanted := ".*unauthorized: authentication required.*" - assertSkopeoFails(c, wanted, "--docker-cfg=''", "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) + assertSkopeoFails(c, wanted, "--tls-verify=false", "inspect", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) } // TODO(runcom): as soon as we can push to registries ensure you can inspect here diff --git a/integration/copy_test.go b/integration/copy_test.go index bbb303d9..d54413f4 100644 --- a/integration/copy_test.go +++ b/integration/copy_test.go @@ -397,3 +397,20 @@ func (s *CopySuite) TestCopyDockerSigstore(c *check.C) { splitSigstoreReadServerHandler = http.FileServer(http.Dir(splitSigstoreStaging)) assertSkopeoSucceeds(c, "", "--tls-verify=false", "--policy", policy, "--registries.d", registriesDir, "copy", ourRegistry+"public/busybox", dirDest) } + +func (s *SkopeoSuite) TestCopySrcWithAuth(c *check.C) { + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--dest-creds=testuser:testpassword", "docker://busybox", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) + dir1, err := ioutil.TempDir("", "copy-1") + c.Assert(err, check.IsNil) + defer os.RemoveAll(dir1) + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--src-creds=testuser:testpassword", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url), "dir:"+dir1) +} + +func (s *SkopeoSuite) TestCopyDestWithAuth(c *check.C) { + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--dest-creds=testuser:testpassword", "docker://busybox", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) +} + +func (s *SkopeoSuite) TestCopySrcAndDestWithAuth(c *check.C) { + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--dest-creds=testuser:testpassword", "docker://busybox", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url)) + assertSkopeoSucceeds(c, "", "--tls-verify=false", "copy", "--src-creds=testuser:testpassword", "--dest-creds=testuser:testpassword", fmt.Sprintf("docker://%s/busybox:latest", s.regV2WithAuth.url), fmt.Sprintf("docker://%s/test:auth", s.regV2WithAuth.url)) +} diff --git a/vendor/github.com/containers/image/copy/copy.go b/vendor/github.com/containers/image/copy/copy.go index fd445b86..4656fca5 100644 --- a/vendor/github.com/containers/image/copy/copy.go +++ b/vendor/github.com/containers/image/copy/copy.go @@ -75,10 +75,12 @@ type Options struct { RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature. SignBy string // If non-empty, asks for a signature to be added during the copy, and specifies a key ID, as accepted by signature.NewGPGSigningMechanism().SignDockerManifest(), ReportWriter io.Writer + SourceCtx *types.SystemContext + DestinationCtx *types.SystemContext } // Image copies image from srcRef to destRef, using policyContext to validate source image admissibility. -func Image(ctx *types.SystemContext, policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) error { +func Image(policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) error { reportWriter := ioutil.Discard if options != nil && options.ReportWriter != nil { reportWriter = options.ReportWriter @@ -87,14 +89,14 @@ func Image(ctx *types.SystemContext, policyContext *signature.PolicyContext, des fmt.Fprintf(reportWriter, f, a...) } - dest, err := destRef.NewImageDestination(ctx) + dest, err := destRef.NewImageDestination(options.DestinationCtx) if err != nil { return fmt.Errorf("Error initializing destination %s: %v", transports.ImageName(destRef), err) } defer dest.Close() destSupportedManifestMIMETypes := dest.SupportedManifestMIMETypes() - rawSource, err := srcRef.NewImageSource(ctx, destSupportedManifestMIMETypes) + rawSource, err := srcRef.NewImageSource(options.SourceCtx, destSupportedManifestMIMETypes) if err != nil { return fmt.Errorf("Error initializing source %s: %v", transports.ImageName(srcRef), err) }