From e5c35ab9d1a31e71d70cbf36e9ffe89bd2457e37 Mon Sep 17 00:00:00 2001 From: Riyaz Faizullabhoy Date: Tue, 12 Sep 2017 09:39:13 -0700 Subject: [PATCH] cli: introduce NotaryClient getter Signed-off-by: Riyaz Faizullabhoy --- cli/command/cli.go | 51 +++++++++++++++++++++++ cli/command/cli_test.go | 18 ++++++++ cli/command/image/trust.go | 7 ++-- cli/command/service/trust.go | 2 +- cli/command/trust/helpers.go | 68 ------------------------------- cli/command/trust/helpers_test.go | 28 ------------- cli/command/trust/inspect.go | 4 +- cli/command/trust/revoke.go | 7 +++- cli/command/trust/sign.go | 8 ++-- cli/trust/trust.go | 59 +++++++++++++++++++++++---- 10 files changed, 137 insertions(+), 115 deletions(-) delete mode 100644 cli/command/trust/helpers_test.go diff --git a/cli/command/cli.go b/cli/command/cli.go index f26b808ac2..d6a8f3c5c3 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -12,13 +12,18 @@ import ( cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/cli/cli/trust" dopts "github.com/docker/cli/opts" + "github.com/docker/distribution/reference" "github.com/docker/docker/api" "github.com/docker/docker/client" + "github.com/docker/docker/registry" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" "github.com/docker/notary" + notaryclient "github.com/docker/notary/client" "github.com/docker/notary/passphrase" + digest "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -40,6 +45,7 @@ type Cli interface { SetIn(in *InStream) ConfigFile() *configfile.ConfigFile ServerInfo() ServerInfo + NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) } // DockerCli is an instance the docker command line client. @@ -161,6 +167,51 @@ func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(pa } } +// NotaryClient provides a Notary Repository to interact with signed metadata for an image +func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { + return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) +} + +// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name +// as a ImageRefAndAuth struct +func GetImageReferencesAndAuth(ctx context.Context, cli Cli, imgName string) (*trust.ImageRefAndAuth, error) { + ref, err := reference.ParseNormalizedNamed(imgName) + if err != nil { + return nil, err + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + authConfig := ResolveAuthConfig(ctx, cli, repoInfo.Index) + return trust.NewImageRefAndAuth(&authConfig, ref, repoInfo, getTag(ref), getDigest(ref)), nil +} + +func getTag(ref reference.Named) string { + switch x := ref.(type) { + case reference.Canonical, reference.Digested: + return "" + case reference.NamedTagged: + return x.Tag() + default: + return "" + } +} + +func getDigest(ref reference.Named) digest.Digest { + switch x := ref.(type) { + case reference.Canonical: + return x.Digest() + case reference.Digested: + return x.Digest() + default: + return digest.Digest("") + } +} + // ServerInfo stores details about the supported features and platform of the // server type ServerInfo struct { diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index 16ba7c4ff9..9a0a6a0f87 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" "github.com/docker/cli/internal/test/testutil" + "github.com/docker/distribution/reference" "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/client" @@ -196,3 +197,20 @@ func TestGetClientWithPassword(t *testing.T) { }) } } + +func TestGetTag(t *testing.T) { + ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") + assert.NoError(t, err) + tag := getTag(ref) + assert.Equal(t, "", tag) + + ref, err = reference.ParseNormalizedNamed("alpine:latest") + assert.NoError(t, err) + tag = getTag(ref) + assert.Equal(t, tag, "latest") + + ref, err = reference.ParseNormalizedNamed("alpine") + assert.NoError(t, err) + tag = getTag(ref) + assert.Equal(t, tag, "") +} diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 8337f74fad..a6c2710dd3 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -102,14 +102,13 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository fmt.Fprintln(streams.Out(), "Signing and pushing trust metadata") - repo, err := trust.GetNotaryRepository(streams, repoInfo, authConfig, "push", "pull") + repo, err := trust.GetNotaryRepository(streams.In(), streams.Out(), command.UserAgent(), repoInfo, &authConfig, "push", "pull") if err != nil { fmt.Fprintf(streams.Out(), "Error establishing connection to notary repository: %s\n", err) return err } // get the latest repository metadata so we can figure out which roles to sign - // TODO(riyazdf): interface change to get back Update _, err = repo.ListTargets() switch err.(type) { @@ -185,7 +184,7 @@ func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types. func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return err @@ -300,7 +299,7 @@ func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedT // Resolve the Auth config relevant for this server authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return nil, err diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go index e1be1e2030..4bfb6b590e 100644 --- a/cli/command/service/trust.go +++ b/cli/command/service/trust.go @@ -59,7 +59,7 @@ func trustedResolveDigest(ctx context.Context, cli command.Cli, ref reference.Na authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") if err != nil { return nil, errors.Wrap(err, "error establishing connection to trust repository") } diff --git a/cli/command/trust/helpers.go b/cli/command/trust/helpers.go index 2278331807..7d9c184ba4 100644 --- a/cli/command/trust/helpers.go +++ b/cli/command/trust/helpers.go @@ -1,83 +1,15 @@ package trust import ( - "context" - "fmt" "strings" - "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/trust" - "github.com/docker/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/registry" "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" ) const releasedRoleName = "Repo Admin" -// ImageRefAndAuth contains all reference information and the auth config for an image request -type ImageRefAndAuth struct { - authConfig *types.AuthConfig - reference reference.Named - repoInfo *registry.RepositoryInfo - tag string -} - -// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) AuthConfig() *types.AuthConfig { - return imgRefAuth.authConfig -} - -// Reference returns the Image reference for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { - return imgRefAuth.reference -} - -// RepoInfo returns the repository information for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { - return imgRefAuth.repoInfo -} - -// Tag returns the Image tag for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Tag() string { - return imgRefAuth.tag -} - -func getImageReferencesAndAuth(ctx context.Context, cli command.Cli, imgName string) (*ImageRefAndAuth, error) { - ref, err := reference.ParseNormalizedNamed(imgName) - if err != nil { - return nil, err - } - - tag, err := getTag(ref) - if err != nil { - return nil, err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - - authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - return &ImageRefAndAuth{&authConfig, ref, repoInfo, tag}, err -} - -func getTag(ref reference.Named) (string, error) { - var tag string - switch x := ref.(type) { - case reference.Canonical: - return "", fmt.Errorf("cannot use a digest reference for IMAGE:TAG") - case reference.NamedTagged: - tag = x.Tag() - default: - tag = "" - } - return tag, nil -} - // check if a role name is "released": either targets/releases or targets TUF roles func isReleasedTarget(role data.RoleName) bool { return role == data.CanonicalTargetsRole || role == trust.ReleasesRole diff --git a/cli/command/trust/helpers_test.go b/cli/command/trust/helpers_test.go deleted file mode 100644 index 5ca9d319aa..0000000000 --- a/cli/command/trust/helpers_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package trust - -import ( - "testing" - - "github.com/docker/distribution/reference" - "github.com/stretchr/testify/assert" -) - -func TestGetTag(t *testing.T) { - ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") - assert.NoError(t, err) - tag, err := getTag(ref) - assert.Error(t, err) - assert.EqualError(t, err, "cannot use a digest reference for IMAGE:TAG") - - ref, err = reference.ParseNormalizedNamed("alpine:latest") - assert.NoError(t, err) - tag, err = getTag(ref) - assert.NoError(t, err) - assert.Equal(t, tag, "latest") - - ref, err = reference.ParseNormalizedNamed("alpine") - assert.NoError(t, err) - tag, err = getTag(ref) - assert.NoError(t, err) - assert.Equal(t, tag, "") -} diff --git a/cli/command/trust/inspect.go b/cli/command/trust/inspect.go index 872e955217..a4590a0dcf 100644 --- a/cli/command/trust/inspect.go +++ b/cli/command/trust/inspect.go @@ -59,12 +59,12 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { func lookupTrustInfo(cli command.Cli, remote string) error { ctx := context.Background() - imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, remote) + imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, remote) if err != nil { return err } tag := imgRefAndAuth.Tag() - notaryRepo, err := trust.GetNotaryRepository(cli, imgRefAndAuth.RepoInfo(), *imgRefAndAuth.AuthConfig(), "pull") + notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPullOnly) if err != nil { return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) } diff --git a/cli/command/trust/revoke.go b/cli/command/trust/revoke.go index 1cad98c1c0..45097119a6 100644 --- a/cli/command/trust/revoke.go +++ b/cli/command/trust/revoke.go @@ -36,11 +36,14 @@ func newRevokeCommand(dockerCli command.Cli) *cobra.Command { func revokeTrust(cli command.Cli, remote string, options revokeOptions) error { ctx := context.Background() - imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, remote) + imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, remote) if err != nil { return err } tag := imgRefAndAuth.Tag() + if imgRefAndAuth.Tag() == "" && imgRefAndAuth.Digest() != "" { + return fmt.Errorf("cannot use a digest reference for IMAGE:TAG") + } if imgRefAndAuth.Tag() == "" && !options.forceYes { deleteRemote := command.PromptForConfirmation(os.Stdin, cli.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote)) if !deleteRemote { @@ -49,7 +52,7 @@ func revokeTrust(cli command.Cli, remote string, options revokeOptions) error { } } - notaryRepo, err := trust.GetNotaryRepository(cli, imgRefAndAuth.RepoInfo(), *imgRefAndAuth.AuthConfig(), "push", "pull") + notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPushAndPull) if err != nil { return err } diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go index 4828d43d3f..d791b96517 100644 --- a/cli/command/trust/sign.go +++ b/cli/command/trust/sign.go @@ -31,16 +31,19 @@ func newSignCommand(dockerCli command.Cli) *cobra.Command { func signImage(cli command.Cli, imageName string) error { ctx := context.Background() - imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, imageName) + imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, imageName) if err != nil { return err } tag := imgRefAndAuth.Tag() if tag == "" { + if imgRefAndAuth.Digest() != "" { + return fmt.Errorf("cannot use a digest reference for IMAGE:TAG") + } return fmt.Errorf("No tag specified for %s", imageName) } - notaryRepo, err := trust.GetNotaryRepository(cli, imgRefAndAuth.RepoInfo(), *imgRefAndAuth.AuthConfig(), "push", "pull") + notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPushAndPull) if err != nil { return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) } @@ -50,7 +53,6 @@ func signImage(cli command.Cli, imageName string) error { defer clearChangeList(notaryRepo) // get the latest repository metadata so we can figure out which roles to sign - // TODO(riyazdf): interface change to get back Update if _, err = notaryRepo.ListTargets(); err != nil { switch err.(type) { case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: diff --git a/cli/trust/trust.go b/cli/trust/trust.go index ec1153bdd3..8d27594b9a 100644 --- a/cli/trust/trust.go +++ b/cli/trust/trust.go @@ -2,6 +2,7 @@ package trust import ( "encoding/json" + "io" "net" "net/http" "net/url" @@ -10,8 +11,8 @@ import ( "path/filepath" "time" - "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" @@ -27,6 +28,7 @@ import ( "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" + digest "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -34,6 +36,10 @@ import ( var ( // ReleasesRole is the role named "releases" ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases")) + // ActionsPullOnly defines the actions for read-only interactions with a Notary Repository + ActionsPullOnly = []string{"pull"} + // ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository + ActionsPushAndPull = []string{"pull", "push"} ) func trustDirectory() string { @@ -86,7 +92,7 @@ func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { // GetNotaryRepository returns a NotaryRepository which stores all the // information needed to operate on a notary repository. // It creates an HTTP transport providing authentication support. -func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (client.Repository, error) { +func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *types.AuthConfig, actions ...string) (client.Repository, error) { server, err := Server(repoInfo.Index) if err != nil { return nil, err @@ -119,7 +125,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI } // Skip configuration headers since request is not going to Docker daemon - modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) + modifiers := registry.DockerHeaders(userAgent, http.Header{}) authTransport := transport.NewTransport(base, modifiers...) pingClient := &http.Client{ Transport: authTransport, @@ -152,7 +158,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI Actions: actions, Class: repoInfo.Class, } - creds := simpleCredentialStore{auth: authConfig} + creds := simpleCredentialStore{auth: *authConfig} tokenHandlerOptions := auth.TokenHandlerOptions{ Transport: authTransport, Credentials: creds, @@ -169,18 +175,18 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI data.GUN(repoInfo.Name.Name()), server, tr, - getPassphraseRetriever(streams), + getPassphraseRetriever(in, out), trustpinning.TrustPinConfig{}) } -func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { +func getPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever { aliasMap := map[string]string{ "root": "root", "snapshot": "repository", "targets": "repository", "default": "repository", } - baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) + baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap) env := map[string]string{ "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), @@ -278,3 +284,42 @@ func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.Rol return signableRoles, nil } + +// ImageRefAndAuth contains all reference information and the auth config for an image request +type ImageRefAndAuth struct { + authConfig *types.AuthConfig + reference reference.Named + repoInfo *registry.RepositoryInfo + tag string + digest digest.Digest +} + +// NewImageRefAndAuth creates a new ImageRefAndAuth struct +func NewImageRefAndAuth(authConfig *types.AuthConfig, reference reference.Named, repoInfo *registry.RepositoryInfo, tag string, digest digest.Digest) *ImageRefAndAuth { + return &ImageRefAndAuth{authConfig, reference, repoInfo, tag, digest} +} + +// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) AuthConfig() *types.AuthConfig { + return imgRefAuth.authConfig +} + +// Reference returns the Image reference for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { + return imgRefAuth.reference +} + +// RepoInfo returns the repository information for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { + return imgRefAuth.repoInfo +} + +// Tag returns the Image tag for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Tag() string { + return imgRefAuth.tag +} + +// Digest returns the Image digest for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest { + return imgRefAuth.digest +}