From db6d87216d62fb45e2e3f5526cb2c277986a2df5 Mon Sep 17 00:00:00 2001 From: Christy Perez Date: Tue, 26 Sep 2017 17:15:04 -0500 Subject: [PATCH] manifest tests create, annotate, & push Signed-off-by: Christy Perez Signed-off-by: Christopher Jones --- cli/command/manifest/annotate.go | 4 +- cli/command/manifest/annotate_test.go | 78 ++++++++++++ cli/command/manifest/client_test.go | 18 +++ cli/command/manifest/create_list.go | 4 +- cli/command/manifest/create_test.go | 117 ++++++++++++++++++ cli/command/manifest/inspect_test.go | 2 +- cli/command/manifest/push.go | 3 +- cli/command/manifest/push_test.go | 73 +++++++++++ .../manifest/testdata/inspect-annotate.golden | 28 +++++ .../testdata/inspect-manifest-list.golden | 24 ++++ internal/test/cli.go | 22 ++-- 11 files changed, 357 insertions(+), 16 deletions(-) create mode 100644 cli/command/manifest/annotate_test.go create mode 100644 cli/command/manifest/create_test.go create mode 100644 cli/command/manifest/push_test.go create mode 100644 cli/command/manifest/testdata/inspect-annotate.golden create mode 100644 cli/command/manifest/testdata/inspect-manifest-list.golden diff --git a/cli/command/manifest/annotate.go b/cli/command/manifest/annotate.go index f8bd0e7590..a94fb57e2a 100644 --- a/cli/command/manifest/annotate.go +++ b/cli/command/manifest/annotate.go @@ -47,11 +47,11 @@ func newAnnotateCommand(dockerCli command.Cli) *cobra.Command { func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error { targetRef, err := normalizeReference(opts.target) if err != nil { - return errors.Wrapf(err, "annotate: Error parsing name for manifest list (%s): %s", opts.target) + return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target) } imgRef, err := normalizeReference(opts.image) if err != nil { - return errors.Wrapf(err, "annotate: Error parsing name for manifest (%s): %s:", opts.image) + return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image) } manifestStore := dockerCli.ManifestStore() diff --git a/cli/command/manifest/annotate_test.go b/cli/command/manifest/annotate_test.go new file mode 100644 index 0000000000..ad80bf6e0a --- /dev/null +++ b/cli/command/manifest/annotate_test.go @@ -0,0 +1,78 @@ +package manifest + +import ( + "io/ioutil" + "testing" + + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/gotestyourself/gotestyourself/golden" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestManifestAnnotateError(t *testing.T) { + testCases := []struct { + args []string + expectedError string + }{ + { + args: []string{"too-few-arguments"}, + expectedError: "requires exactly 2 arguments", + }, + { + args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"}, + expectedError: "error parsing name for manifest list", + }, + { + args: []string{"example.com/list:v1", "th!si'sa/fa!ke/im@ge/nam32"}, + expectedError: "error parsing name for manifest", + }, + } + + for _, tc := range testCases { + cli := test.NewFakeCli(nil) + cmd := newAnnotateCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestManifestAnnotate(t *testing.T) { + store, cleanup := newTempManifestStore(t) + defer cleanup() + + cli := test.NewFakeCli(nil) + cli.SetManifestStore(store) + namedRef := ref(t, "alpine:3.0") + imageManifest := fullImageManifest(t, namedRef) + err := store.Save(ref(t, "list:v1"), namedRef, imageManifest) + require.NoError(t, err) + + cmd := newAnnotateCommand(cli) + cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"}) + cmd.SetOutput(ioutil.Discard) + expectedError := "manifest for image example.com/fake:0.0 does not exist" + testutil.ErrorContains(t, cmd.Execute(), expectedError) + + cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) + cmd.Flags().Set("os", "freebsd") + cmd.Flags().Set("arch", "fake") + cmd.Flags().Set("os-features", "feature1") + cmd.Flags().Set("variant", "v7") + expectedError = "manifest entry for image has unsupported os/arch combination" + testutil.ErrorContains(t, cmd.Execute(), expectedError) + + cmd.Flags().Set("arch", "arm") + require.NoError(t, cmd.Execute()) + + cmd = newInspectCommand(cli) + err = cmd.Flags().Set("verbose", "true") + require.NoError(t, err) + cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) + require.NoError(t, cmd.Execute()) + actual := cli.OutBuffer() + expected := golden.Get(t, "inspect-annotate.golden") + assert.Equal(t, string(expected), actual.String()) +} diff --git a/cli/command/manifest/client_test.go b/cli/command/manifest/client_test.go index d319ea343d..e001f4aaaa 100644 --- a/cli/command/manifest/client_test.go +++ b/cli/command/manifest/client_test.go @@ -3,7 +3,9 @@ package manifest import ( manifesttypes "github.com/docker/cli/cli/manifest/types" "github.com/docker/cli/cli/registry/client" + "github.com/docker/distribution" "github.com/docker/distribution/reference" + "github.com/opencontainers/go-digest" "golang.org/x/net/context" ) @@ -11,6 +13,8 @@ type fakeRegistryClient struct { client.RegistryClient getManifestFunc func(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) + mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error + putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error) } func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { @@ -26,3 +30,17 @@ func (c *fakeRegistryClient) GetManifestList(ctx context.Context, ref reference. } return nil, nil } + +func (c *fakeRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error { + if c.mountBlobFunc != nil { + return c.mountBlobFunc(ctx, source, target) + } + return nil +} + +func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Named, mf distribution.Manifest) (digest.Digest, error) { + if c.putManifestFunc != nil { + return c.putManifestFunc(ctx, ref, mf) + } + return digest.Digest(""), nil +} diff --git a/cli/command/manifest/create_list.go b/cli/command/manifest/create_list.go index 29d244005f..bfcca77056 100644 --- a/cli/command/manifest/create_list.go +++ b/cli/command/manifest/create_list.go @@ -39,12 +39,12 @@ func createManifestList(dockerCli command.Cli, args []string, opts createOpts) e newRef := args[0] targetRef, err := normalizeReference(newRef) if err != nil { - return errors.Wrapf(err, "error parsing name for manifest list (%s): %v", newRef) + return errors.Wrapf(err, "error parsing name for manifest list %s", newRef) } _, err = registry.ParseRepositoryInfo(targetRef) if err != nil { - return errors.Wrapf(err, "error parsing repository name for manifest list (%s): %v", newRef) + return errors.Wrapf(err, "error parsing repository name for manifest list %s", newRef) } manifestStore := dockerCli.ManifestStore() diff --git a/cli/command/manifest/create_test.go b/cli/command/manifest/create_test.go new file mode 100644 index 0000000000..a16ab5a073 --- /dev/null +++ b/cli/command/manifest/create_test.go @@ -0,0 +1,117 @@ +package manifest + +import ( + "io/ioutil" + "testing" + + manifesttypes "github.com/docker/cli/cli/manifest/types" + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/docker/distribution/reference" + "github.com/gotestyourself/gotestyourself/golden" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func TestManifestCreateErrors(t *testing.T) { + testCases := []struct { + args []string + expectedError string + }{ + { + args: []string{"too-few-arguments"}, + expectedError: "requires at least 2 arguments", + }, + { + args: []string{"th!si'sa/fa!ke/li$t/name", "example.com/alpine:3.0"}, + expectedError: "error parsing name for manifest list", + }, + } + + for _, tc := range testCases { + cli := test.NewFakeCli(nil) + cmd := newCreateListCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +// create a manifest list, then overwrite it, and inspect to see if the old one is still there +func TestManifestCreateAmend(t *testing.T) { + store, cleanup := newTempManifestStore(t) + defer cleanup() + + cli := test.NewFakeCli(nil) + cli.SetManifestStore(store) + + namedRef := ref(t, "alpine:3.0") + imageManifest := fullImageManifest(t, namedRef) + err := store.Save(ref(t, "list:v1"), namedRef, imageManifest) + require.NoError(t, err) + namedRef = ref(t, "alpine:3.1") + imageManifest = fullImageManifest(t, namedRef) + err = store.Save(ref(t, "list:v1"), namedRef, imageManifest) + require.NoError(t, err) + + cmd := newCreateListCommand(cli) + cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.1"}) + cmd.Flags().Set("amend", "true") + cmd.SetOutput(ioutil.Discard) + err = cmd.Execute() + require.NoError(t, err) + + // make a new cli to clear the buffers + cli = test.NewFakeCli(nil) + cli.SetManifestStore(store) + inspectCmd := newInspectCommand(cli) + inspectCmd.SetArgs([]string{"example.com/list:v1"}) + require.NoError(t, inspectCmd.Execute()) + actual := cli.OutBuffer() + expected := golden.Get(t, "inspect-manifest-list.golden") + assert.Equal(t, string(expected), actual.String()) +} + +// attempt to overwrite a saved manifest and get refused +func TestManifestCreateRefuseAmend(t *testing.T) { + store, cleanup := newTempManifestStore(t) + defer cleanup() + + cli := test.NewFakeCli(nil) + cli.SetManifestStore(store) + namedRef := ref(t, "alpine:3.0") + imageManifest := fullImageManifest(t, namedRef) + err := store.Save(ref(t, "list:v1"), namedRef, imageManifest) + require.NoError(t, err) + + cmd := newCreateListCommand(cli) + cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) + cmd.SetOutput(ioutil.Discard) + err = cmd.Execute() + assert.EqualError(t, err, "refusing to amend an existing manifest list with no --amend flag") +} + +// attempt to make a manifest list without valid images +func TestManifestCreateNoManifest(t *testing.T) { + store, cleanup := newTempManifestStore(t) + defer cleanup() + + cli := test.NewFakeCli(nil) + cli.SetManifestStore(store) + cli.SetRegistryClient(&fakeRegistryClient{ + getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { + return manifesttypes.ImageManifest{}, errors.Errorf("No such image: %v", ref) + }, + getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { + return nil, errors.Errorf("No such manifest: %s", ref) + }, + }) + + cmd := newCreateListCommand(cli) + cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"}) + cmd.SetOutput(ioutil.Discard) + err := cmd.Execute() + assert.EqualError(t, err, "No such image: example.com/alpine:3.0") +} diff --git a/cli/command/manifest/inspect_test.go b/cli/command/manifest/inspect_test.go index ae4a7fe992..6dcf6deadd 100644 --- a/cli/command/manifest/inspect_test.go +++ b/cli/command/manifest/inspect_test.go @@ -51,7 +51,7 @@ func fullImageManifest(t *testing.T, ref reference.Named) types.ImageManifest { }) require.NoError(t, err) // TODO: include image data for verbose inspect - return types.NewImageManifest(ref, digest.Digest("abcd"), types.Image{}, man) + return types.NewImageManifest(ref, digest.Digest("sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd"), types.Image{OS: "linux", Architecture: "amd64"}, man) } func TestInspectCommandLocalManifestNotFound(t *testing.T) { diff --git a/cli/command/manifest/push.go b/cli/command/manifest/push.go index fcc9015d77..a40d62d3dc 100644 --- a/cli/command/manifest/push.go +++ b/cli/command/manifest/push.go @@ -181,7 +181,7 @@ func buildManifestDescriptor(targetRepo *registry.RepositoryInfo, imageManifest if err = manifest.Descriptor.Digest.Validate(); err != nil { return manifestlist.ManifestDescriptor{}, errors.Wrapf(err, - "digest parse of image %q failed with error: %v", imageManifest.Ref) + "digest parse of image %q failed", imageManifest.Ref) } return manifest, nil @@ -200,6 +200,7 @@ func buildBlobRequestList(imageManifest types.ImageManifest, repoName reference. return blobReqs, nil } +// nolint: interfacer func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef reference.Named) (mountRequest, error) { refWithoutTag, err := reference.WithName(targetRef.Name()) if err != nil { diff --git a/cli/command/manifest/push_test.go b/cli/command/manifest/push_test.go new file mode 100644 index 0000000000..608dd2c23b --- /dev/null +++ b/cli/command/manifest/push_test.go @@ -0,0 +1,73 @@ +package manifest + +import ( + "io/ioutil" + "testing" + + manifesttypes "github.com/docker/cli/cli/manifest/types" + "github.com/docker/cli/internal/test" + "github.com/docker/cli/internal/test/testutil" + "github.com/docker/distribution/reference" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" +) + +func newFakeRegistryClient(t *testing.T) *fakeRegistryClient { + require.NoError(t, nil) + + return &fakeRegistryClient{ + getManifestFunc: func(_ context.Context, _ reference.Named) (manifesttypes.ImageManifest, error) { + return manifesttypes.ImageManifest{}, errors.New("") + }, + getManifestListFunc: func(_ context.Context, _ reference.Named) ([]manifesttypes.ImageManifest, error) { + return nil, errors.Errorf("") + }, + } +} + +func TestManifestPushErrors(t *testing.T) { + testCases := []struct { + args []string + expectedError string + }{ + { + args: []string{"one-arg", "extra-arg"}, + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"th!si'sa/fa!ke/li$t/-name"}, + expectedError: "invalid reference format", + }, + } + + for _, tc := range testCases { + cli := test.NewFakeCli(nil) + cmd := newPushListCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + testutil.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +// store a one-image manifest list and puah it +func TestManifestPush(t *testing.T) { + store, sCleanup := newTempManifestStore(t) + defer sCleanup() + + registry := newFakeRegistryClient(t) + + cli := test.NewFakeCli(nil) + cli.SetManifestStore(store) + cli.SetRegistryClient(registry) + + namedRef := ref(t, "alpine:3.0") + imageManifest := fullImageManifest(t, namedRef) + err := store.Save(ref(t, "list:v1"), namedRef, imageManifest) + require.NoError(t, err) + + cmd := newPushListCommand(cli) + cmd.SetArgs([]string{"example.com/list:v1"}) + err = cmd.Execute() + require.NoError(t, err) +} diff --git a/cli/command/manifest/testdata/inspect-annotate.golden b/cli/command/manifest/testdata/inspect-annotate.golden new file mode 100644 index 0000000000..d39438c447 --- /dev/null +++ b/cli/command/manifest/testdata/inspect-annotate.golden @@ -0,0 +1,28 @@ +{ + "Ref": "example.com/alpine:3.0", + "Digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", + "SchemaV2Manifest": { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1520, + "digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d624560" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 1990402, + "digest": "sha256:88286f41530e93dffd4b964e1db22ce4939fffa4a4c665dab8591fbab03d4926" + } + ] + }, + "Platform": { + "architecture": "arm", + "os": "freebsd", + "os.features": [ + "feature1" + ], + "variant": "v7" + } +} diff --git a/cli/command/manifest/testdata/inspect-manifest-list.golden b/cli/command/manifest/testdata/inspect-manifest-list.golden new file mode 100644 index 0000000000..95f8c46722 --- /dev/null +++ b/cli/command/manifest/testdata/inspect-manifest-list.golden @@ -0,0 +1,24 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 428, + "digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 428, + "digest": "sha256:7328f6f8b41890597575cbaadc884e7386ae0acc53b747401ebce5cf0d62abcd", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} diff --git a/internal/test/cli.go b/internal/test/cli.go index e99e8e2fbd..ad9e1b1489 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -9,9 +9,9 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" - "github.com/docker/cli/cli/trust" manifeststore "github.com/docker/cli/cli/manifest/store" registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/trust" "github.com/docker/docker/client" notaryclient "github.com/theupdateframework/notary/client" ) @@ -22,16 +22,17 @@ type clientInfoFuncType func() command.ClientInfo // 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 + client client.APIClient + configfile *configfile.ConfigFile + out *command.OutStream + outBuffer *bytes.Buffer + err *bytes.Buffer + in *command.InStream + server command.ServerInfo + clientInfoFunc clientInfoFuncType notaryClientFunc notaryClientFuncType - manifestStore manifeststore.Store - registryClient registryclient.RegistryClient + manifestStore manifeststore.Store + registryClient registryclient.RegistryClient } // NewFakeCli returns a fake for the command.Cli interface @@ -127,6 +128,7 @@ func (c *FakeCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []st return c.notaryClientFunc(imgRefAndAuth, actions) } return nil, fmt.Errorf("no notary client available unless defined") +} // ManifestStore returns a fake store used for testing func (c *FakeCli) ManifestStore() manifeststore.Store {