cli: introduce NotaryClient getter

Signed-off-by: Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
This commit is contained in:
Riyaz Faizullabhoy 2017-09-12 09:39:13 -07:00
parent 7c5b836ca5
commit e5c35ab9d1
10 changed files with 137 additions and 115 deletions

View File

@ -12,13 +12,18 @@ import (
cliconfig "github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/trust"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/registry"
"github.com/docker/go-connections/sockets" "github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary" "github.com/docker/notary"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase" "github.com/docker/notary/passphrase"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -40,6 +45,7 @@ type Cli interface {
SetIn(in *InStream) SetIn(in *InStream)
ConfigFile() *configfile.ConfigFile ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
} }
// DockerCli is an instance the docker command line client. // 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 // ServerInfo stores details about the supported features and platform of the
// server // server
type ServerInfo struct { type ServerInfo struct {

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/test/testutil" "github.com/docker/cli/internal/test/testutil"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "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, "")
}

View File

@ -102,14 +102,13 @@ func PushTrustedReference(streams command.Streams, repoInfo *registry.Repository
fmt.Fprintln(streams.Out(), "Signing and pushing trust metadata") 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 { if err != nil {
fmt.Fprintf(streams.Out(), "Error establishing connection to notary repository: %s\n", err) fmt.Fprintf(streams.Out(), "Error establishing connection to notary repository: %s\n", err)
return err return err
} }
// get the latest repository metadata so we can figure out which roles to sign // 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() _, err = repo.ListTargets()
switch err.(type) { 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 { func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
var refs []target 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 { if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return 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 // Resolve the Auth config relevant for this server
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) 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 { if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return nil, err return nil, err

View File

@ -59,7 +59,7 @@ func trustedResolveDigest(ctx context.Context, cli command.Cli, ref reference.Na
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) 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 { if err != nil {
return nil, errors.Wrap(err, "error establishing connection to trust repository") return nil, errors.Wrap(err, "error establishing connection to trust repository")
} }

View File

@ -1,83 +1,15 @@
package trust package trust
import ( import (
"context"
"fmt"
"strings" "strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust" "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/client"
"github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/data"
) )
const releasedRoleName = "Repo Admin" 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 // check if a role name is "released": either targets/releases or targets TUF roles
func isReleasedTarget(role data.RoleName) bool { func isReleasedTarget(role data.RoleName) bool {
return role == data.CanonicalTargetsRole || role == trust.ReleasesRole return role == data.CanonicalTargetsRole || role == trust.ReleasesRole

View File

@ -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, "")
}

View File

@ -59,12 +59,12 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
func lookupTrustInfo(cli command.Cli, remote string) error { func lookupTrustInfo(cli command.Cli, remote string) error {
ctx := context.Background() ctx := context.Background()
imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, remote) imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, remote)
if err != nil { if err != nil {
return err return err
} }
tag := imgRefAndAuth.Tag() tag := imgRefAndAuth.Tag()
notaryRepo, err := trust.GetNotaryRepository(cli, imgRefAndAuth.RepoInfo(), *imgRefAndAuth.AuthConfig(), "pull") notaryRepo, err := cli.NotaryClient(*imgRefAndAuth, trust.ActionsPullOnly)
if err != nil { if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
} }

View File

@ -36,11 +36,14 @@ func newRevokeCommand(dockerCli command.Cli) *cobra.Command {
func revokeTrust(cli command.Cli, remote string, options revokeOptions) error { func revokeTrust(cli command.Cli, remote string, options revokeOptions) error {
ctx := context.Background() ctx := context.Background()
imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, remote) imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, remote)
if err != nil { if err != nil {
return err return err
} }
tag := imgRefAndAuth.Tag() tag := imgRefAndAuth.Tag()
if imgRefAndAuth.Tag() == "" && imgRefAndAuth.Digest() != "" {
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
}
if imgRefAndAuth.Tag() == "" && !options.forceYes { 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)) deleteRemote := command.PromptForConfirmation(os.Stdin, cli.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
if !deleteRemote { 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 { if err != nil {
return err return err
} }

View File

@ -31,16 +31,19 @@ func newSignCommand(dockerCli command.Cli) *cobra.Command {
func signImage(cli command.Cli, imageName string) error { func signImage(cli command.Cli, imageName string) error {
ctx := context.Background() ctx := context.Background()
imgRefAndAuth, err := getImageReferencesAndAuth(ctx, cli, imageName) imgRefAndAuth, err := command.GetImageReferencesAndAuth(ctx, cli, imageName)
if err != nil { if err != nil {
return err return err
} }
tag := imgRefAndAuth.Tag() tag := imgRefAndAuth.Tag()
if 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) 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 { if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
} }
@ -50,7 +53,6 @@ func signImage(cli command.Cli, imageName string) error {
defer clearChangeList(notaryRepo) defer clearChangeList(notaryRepo)
// get the latest repository metadata so we can figure out which roles to sign // 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 { if _, err = notaryRepo.ListTargets(); err != nil {
switch err.(type) { switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:

View File

@ -2,6 +2,7 @@ package trust
import ( import (
"encoding/json" "encoding/json"
"io"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -10,8 +11,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config" 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"
"github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/client/transport"
@ -27,6 +28,7 @@ import (
"github.com/docker/notary/trustpinning" "github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed" "github.com/docker/notary/tuf/signed"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -34,6 +36,10 @@ import (
var ( var (
// ReleasesRole is the role named "releases" // ReleasesRole is the role named "releases"
ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "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 { func trustDirectory() string {
@ -86,7 +92,7 @@ func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
// GetNotaryRepository returns a NotaryRepository which stores all the // GetNotaryRepository returns a NotaryRepository which stores all the
// information needed to operate on a notary repository. // information needed to operate on a notary repository.
// It creates an HTTP transport providing authentication support. // 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) server, err := Server(repoInfo.Index)
if err != nil { if err != nil {
return nil, err 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 // 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...) authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{ pingClient := &http.Client{
Transport: authTransport, Transport: authTransport,
@ -152,7 +158,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
Actions: actions, Actions: actions,
Class: repoInfo.Class, Class: repoInfo.Class,
} }
creds := simpleCredentialStore{auth: authConfig} creds := simpleCredentialStore{auth: *authConfig}
tokenHandlerOptions := auth.TokenHandlerOptions{ tokenHandlerOptions := auth.TokenHandlerOptions{
Transport: authTransport, Transport: authTransport,
Credentials: creds, Credentials: creds,
@ -169,18 +175,18 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI
data.GUN(repoInfo.Name.Name()), data.GUN(repoInfo.Name.Name()),
server, server,
tr, tr,
getPassphraseRetriever(streams), getPassphraseRetriever(in, out),
trustpinning.TrustPinConfig{}) trustpinning.TrustPinConfig{})
} }
func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { func getPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
aliasMap := map[string]string{ aliasMap := map[string]string{
"root": "root", "root": "root",
"snapshot": "repository", "snapshot": "repository",
"targets": "repository", "targets": "repository",
"default": "repository", "default": "repository",
} }
baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap)
env := map[string]string{ env := map[string]string{
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_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 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
}