diff --git a/cli/command/trust/cli_test.go b/cli/command/trust/cli_test.go new file mode 100644 index 0000000000..4ebc3ba71d --- /dev/null +++ b/cli/command/trust/cli_test.go @@ -0,0 +1,108 @@ +package trust + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/trust" + "github.com/docker/docker/client" + notaryclient "github.com/docker/notary/client" +) + +type notaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) + +// FakeCli emulates the default DockerCli +type FakeCli struct { + command.DockerCli + client client.APIClient + configfile *configfile.ConfigFile + out *command.OutStream + outBuffer *bytes.Buffer + err *bytes.Buffer + in *command.InStream + server command.ServerInfo + notaryClientFunc notaryClientFuncType +} + +// NewFakeCliWithNotaryClient returns a fake for the command.Cli interface with a specified Notary Client +func NewFakeCliWithNotaryClient(client client.APIClient, notaryClientFunc notaryClientFuncType) *FakeCli { + outBuffer := new(bytes.Buffer) + errBuffer := new(bytes.Buffer) + return &FakeCli{ + client: client, + out: command.NewOutStream(outBuffer), + outBuffer: outBuffer, + err: errBuffer, + in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + configfile: configfile.New("configfile"), + notaryClientFunc: notaryClientFunc, + } +} + +// SetIn sets the input of the cli to the specified ReadCloser +func (c *FakeCli) SetIn(in *command.InStream) { + c.in = in +} + +// SetErr sets the stderr stream for the cli to the specified io.Writer +func (c *FakeCli) SetErr(err *bytes.Buffer) { + c.err = err +} + +// SetConfigFile sets the "fake" config file +func (c *FakeCli) SetConfigFile(configfile *configfile.ConfigFile) { + c.configfile = configfile +} + +// Client returns a docker API client +func (c *FakeCli) Client() client.APIClient { + return c.client +} + +// Out returns the output stream (stdout) the cli should write on +func (c *FakeCli) Out() *command.OutStream { + return c.out +} + +// Err returns the output stream (stderr) the cli should write on +func (c *FakeCli) Err() io.Writer { + return c.err +} + +// In returns the input stream the cli will use +func (c *FakeCli) In() *command.InStream { + return c.in +} + +// ConfigFile returns the cli configfile object (to get client configuration) +func (c *FakeCli) ConfigFile() *configfile.ConfigFile { + return c.configfile +} + +// ServerInfo returns API server information for the server used by this client +func (c *FakeCli) ServerInfo() command.ServerInfo { + return c.server +} + +// OutBuffer returns the stdout buffer +func (c *FakeCli) OutBuffer() *bytes.Buffer { + return c.outBuffer +} + +// ErrBuffer Buffer returns the stderr buffer +func (c *FakeCli) ErrBuffer() *bytes.Buffer { + return c.err +} + +// NotaryClient returns an err for testing unless defined +func (c *FakeCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { + if c.notaryClientFunc != nil { + return c.notaryClientFunc(imgRefAndAuth, actions) + } + return nil, fmt.Errorf("no notary client available unless defined") +} diff --git a/cli/command/trust/inspect_test.go b/cli/command/trust/inspect_test.go index b109314bb3..62778f00fe 100644 --- a/cli/command/trust/inspect_test.go +++ b/cli/command/trust/inspect_test.go @@ -11,7 +11,10 @@ import ( dockerClient "github.com/docker/docker/client" "github.com/docker/notary" "github.com/docker/notary/client" + "github.com/docker/notary/client/changelist" + "github.com/docker/notary/storage" "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" "github.com/stretchr/testify/assert" ) @@ -39,26 +42,11 @@ func TestTrustInspectCommandErrors(t *testing.T) { args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, expectedError: "invalid repository name", }, - { - name: "nonexistent-reg", - args: []string{"nonexistent-reg-name.io/image"}, - expectedError: "No signatures or cannot access nonexistent-reg-name.io/image", - }, { name: "invalid-img-reference", args: []string{"ALPINE"}, expectedError: "invalid reference format", }, - { - name: "unsigned-img-reference", - args: []string{"riyaz/unsigned-img"}, - expectedError: "No signatures or cannot access riyaz/unsigned-img", - }, - { - name: "nonexistent-img-reference", - args: []string{"riyaz/nonexistent-img"}, - expectedError: "No signatures or cannot access riyaz/nonexistent-img", - }, } for _, tc := range testCases { cmd := newInspectCommand( @@ -69,6 +57,52 @@ func TestTrustInspectCommandErrors(t *testing.T) { } } +func TestTrustInspectCommandOfflineErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd = newInspectCommand(cli) + cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") +} + +func TestTrustInspectCommandUninitializedErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getUninitializedNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"reg/unsigned-img"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getUninitializedNotaryRepository) + cmd = newInspectCommand(cli) + cmd.SetArgs([]string{"reg/unsigned-img:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") +} + +func TestTrustInspectCommandEmptyNotaryRepoErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getEmptyTargetsNotaryRepository) + cmd := newInspectCommand(cli) + cmd.SetArgs([]string{"reg/img:unsigned-tag"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag") + assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getEmptyTargetsNotaryRepository) + cmd = newInspectCommand(cli) + cmd.SetArgs([]string{"reg/img"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "No signatures for reg/img") + assert.Contains(t, cli.OutBuffer().String(), "Administrative keys for reg/img:") +} + func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cmd := newInspectCommand(cli) @@ -434,3 +468,194 @@ func TestFormatAdminRole(t *testing.T) { targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil} assert.Equal(t, "Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs)) } + +// Sample mock CLI interfaces + +func getOfflineNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) { + return OfflineNotaryRepository{}, nil +} + +// OfflineNotaryRepository is a mock Notary repository that is offline +type OfflineNotaryRepository struct{} + +func (o OfflineNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error { + return storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error { + return storage.ErrOffline{} +} +func (o OfflineNotaryRepository) Publish() error { + return storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) AddTarget(target *client.Target, roles ...data.RoleName) error { + return nil +} +func (o OfflineNotaryRepository) RemoveTarget(targetName string, roles ...data.RoleName) error { + return nil +} +func (o OfflineNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { + return nil, storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) { + return nil, storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + return nil, storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) GetChangelist() (changelist.Changelist, error) { + return changelist.NewMemChangelist(), nil +} + +func (o OfflineNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + return nil, storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return nil, storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) AddDelegation(name data.RoleName, delegationKeys []data.PublicKey, paths []string) error { + return nil +} + +func (o OfflineNotaryRepository) AddDelegationRoleAndKeys(name data.RoleName, delegationKeys []data.PublicKey) error { + return nil +} + +func (o OfflineNotaryRepository) AddDelegationPaths(name data.RoleName, paths []string) error { + return nil +} + +func (o OfflineNotaryRepository) RemoveDelegationKeysAndPaths(name data.RoleName, keyIDs, paths []string) error { + return nil +} + +func (o OfflineNotaryRepository) RemoveDelegationRole(name data.RoleName) error { + return nil +} + +func (o OfflineNotaryRepository) RemoveDelegationPaths(name data.RoleName, paths []string) error { + return nil +} + +func (o OfflineNotaryRepository) RemoveDelegationKeys(name data.RoleName, keyIDs []string) error { + return nil +} + +func (o OfflineNotaryRepository) ClearDelegationPaths(name data.RoleName) error { + return nil +} + +func (o OfflineNotaryRepository) Witness(roles ...data.RoleName) ([]data.RoleName, error) { + return nil, nil +} + +func (o OfflineNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error { + return storage.ErrOffline{} +} + +func (o OfflineNotaryRepository) GetCryptoService() signed.CryptoService { + return nil +} + +func (o OfflineNotaryRepository) SetLegacyVersions(version int) {} + +func (o OfflineNotaryRepository) GetGUN() data.GUN { + return data.GUN("gun") +} + +func getUninitializedNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) { + return UninitializedNotaryRepository{}, nil +} + +// UninitializedNotaryRepository is a mock Notary repository that is uninintialized +// it builds on top of the OfflineNotaryRepository, instead returning ErrRepoNotInitialized +// for any online operation +type UninitializedNotaryRepository struct { + OfflineNotaryRepository +} + +func (u UninitializedNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error { + return client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error { + return client.ErrRepositoryNotExist{} +} +func (u UninitializedNotaryRepository) Publish() error { + return client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { + return nil, client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) { + return nil, client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + return nil, client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + return nil, client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return nil, client.ErrRepositoryNotExist{} +} + +func (u UninitializedNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error { + return client.ErrRepositoryNotExist{} +} + +func getEmptyTargetsNotaryRepository(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) { + return EmptyTargetsNotaryRepository{}, nil +} + +// EmptyTargetsNotaryRepository is a mock Notary repository that is initialized +// but does not have any signed targets +type EmptyTargetsNotaryRepository struct { + OfflineNotaryRepository +} + +func (e EmptyTargetsNotaryRepository) Initialize(rootKeyIDs []string, serverManagedRoles ...data.RoleName) error { + return nil +} + +func (e EmptyTargetsNotaryRepository) InitializeWithCertificate(rootKeyIDs []string, rootCerts []data.PublicKey, serverManagedRoles ...data.RoleName) error { + return nil +} +func (e EmptyTargetsNotaryRepository) Publish() error { + return nil +} + +func (e EmptyTargetsNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { + return []*client.TargetWithRole{}, nil +} + +func (e EmptyTargetsNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) { + return nil, client.ErrNoSuchTarget(name) +} + +func (e EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + return nil, client.ErrNoSuchTarget(name) +} + +func (e EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + return []client.RoleWithSignatures{}, nil +} + +func (e EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return []data.Role{}, nil +} + +func (e EmptyTargetsNotaryRepository) RotateKey(role data.RoleName, serverManagesKey bool, keyList []string) error { + return nil +} diff --git a/cli/command/trust/revoke.go b/cli/command/trust/revoke.go index 45097119a6..f53b2f8d97 100644 --- a/cli/command/trust/revoke.go +++ b/cli/command/trust/revoke.go @@ -88,6 +88,10 @@ func revokeSignature(notaryRepo client.Repository, tag string) error { func revokeSingleSig(notaryRepo client.Repository, tag string) error { releasedTargetWithRole, err := notaryRepo.GetTargetByName(tag, trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { + // if we try to remove the target and it doesn't exist, "succeed" silently + if _, ok := err.(client.ErrNoSuchTarget); ok { + return nil + } return err } releasedTarget := releasedTargetWithRole.Target diff --git a/cli/command/trust/revoke_test.go b/cli/command/trust/revoke_test.go index 330b7b1ea1..92824e70bc 100644 --- a/cli/command/trust/revoke_test.go +++ b/cli/command/trust/revoke_test.go @@ -3,7 +3,6 @@ package trust import ( "io/ioutil" "os" - "strings" "testing" "github.com/docker/cli/internal/test" @@ -34,29 +33,16 @@ func TestTrustRevokeCommandErrors(t *testing.T) { args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, expectedError: "invalid repository name", }, - { - name: "trust-data-for-tag-does-not-exist", - args: []string{"alpine:foo"}, - expectedError: "could not remove signature for alpine:foo: No valid trust data for foo", - }, { name: "invalid-img-reference", args: []string{"ALPINE"}, expectedError: "invalid reference format", }, - { - name: "unsigned-img-reference", - args: []string{"riyaz/unsigned-img:v1"}, - expectedError: strings.Join([]string{ - "could not remove signature for riyaz/unsigned-img:v1:", - "notary.docker.io does not have trust data for docker.io/riyaz/unsigned-img", - }, " "), - }, - { - name: "no-signing-keys-for-image", - args: []string{"alpine", "-y"}, - expectedError: "could not remove signature for alpine: could not find necessary signing keys", - }, + // { + // name: "no-signing-keys-for-image", + // args: []string{"alpine", "-y"}, + // expectedError: "could not remove signature for alpine: could not find necessary signing keys", + // }, { name: "digest-reference", args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, @@ -72,6 +58,71 @@ func TestTrustRevokeCommandErrors(t *testing.T) { } } +func TestTrustRevokeCommandOfflineErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd := newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image", "-y"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: client is offline") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: client is offline") +} + +func TestTrustRevokeCommandUninitializedErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getUninitializedNotaryRepository) + cmd := newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getUninitializedNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image", "-y"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: does not have trust data for") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getUninitializedNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: does not have trust data for") +} + +func TestTrustRevokeCommandEmptyNotaryRepo(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getEmptyTargetsNotaryRepository) + cmd := newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getEmptyTargetsNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image", "-y"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "Successfully deleted signature for reg-name.io/image") + + cli = NewFakeCliWithNotaryClient(&fakeClient{}, getEmptyTargetsNotaryRepository) + cmd = newRevokeCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + assert.NoError(t, cmd.Execute()) + assert.Contains(t, cli.OutBuffer().String(), "Successfully deleted signature for reg-name.io/image:tag") +} + func TestNewRevokeTrustAllSigConfirmation(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cmd := newRevokeCommand(cli) diff --git a/cli/command/trust/sign_test.go b/cli/command/trust/sign_test.go index f4c75f9b37..98b5ba58ec 100644 --- a/cli/command/trust/sign_test.go +++ b/cli/command/trust/sign_test.go @@ -51,26 +51,16 @@ func TestTrustSignCommandErrors(t *testing.T) { args: []string{"ALPINE:latest"}, expectedError: "invalid reference format", }, - { - name: "no-shell-for-passwd", - args: []string{"riyaz/unsigned-img:latest"}, - expectedError: "error during connect: Get /images/riyaz/unsigned-img:latest/json", - }, { name: "no-tag", - args: []string{"riyaz/unsigned-img"}, - expectedError: "No tag specified for riyaz/unsigned-img", + args: []string{"reg/img"}, + expectedError: "No tag specified for reg/img", }, { name: "digest-reference", args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, expectedError: "cannot use a digest reference for IMAGE:TAG", }, - { - name: "no-keys", - args: []string{"ubuntu:latest"}, - expectedError: "failed to sign \"docker.io/library/ubuntu\":latest: you are not authorized to perform this operation: server returned 401.", - }, } // change to a tmpdir tmpDir, err := ioutil.TempDir("", "docker-sign-test-") @@ -86,6 +76,15 @@ func TestTrustSignCommandErrors(t *testing.T) { } } +func TestTrustSignCommandOfflineErrors(t *testing.T) { + cli := NewFakeCliWithNotaryClient(&fakeClient{}, getOfflineNotaryRepository) + cmd := newSignCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image:tag"}) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute()) + testutil.ErrorContains(t, cmd.Execute(), "client is offline") +} + func TestGetOrGenerateNotaryKey(t *testing.T) { tmpDir, err := ioutil.TempDir("", "notary-test-") assert.NoError(t, err) @@ -274,7 +273,6 @@ func TestPrettyPrintExistingSignatureInfo(t *testing.T) { } func TestChangeList(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "docker-sign-test-") assert.NoError(t, err) defer os.RemoveAll(tmpDir)