From 922af8b9b8dffca57e379fda4e3efd7029db14f0 Mon Sep 17 00:00:00 2001 From: Juan Bustamante Date: Tue, 7 May 2024 16:11:31 -0500 Subject: [PATCH] Adding acceptance test for manifest commands Signed-off-by: Juan Bustamante --- acceptance/acceptance_test.go | 144 +++++++++++++++++++++++++++ acceptance/assertions/output.go | 36 +++++++ acceptance/invoke/pack.go | 4 + pkg/client/manifest_add_test.go | 2 +- pkg/client/manifest_annotate.go | 12 +-- pkg/client/manifest_annotate_test.go | 10 +- pkg/client/manifest_create.go | 6 +- pkg/client/manifest_create_test.go | 2 +- pkg/client/manifest_push.go | 4 +- testhelpers/image_index.go | 29 +++++- 10 files changed, 229 insertions(+), 20 deletions(-) diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index a6af6bce6..a21869549 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -18,10 +18,13 @@ import ( "testing" "time" + "github.com/buildpacks/imgutil" "github.com/buildpacks/lifecycle/api" dockertypes "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pelletier/go-toml" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -623,6 +626,147 @@ func testWithoutSpecificBuilderRequirement( }) }) }) + + when("manifest", func() { + var ( + indexRepoName string + repoName1 string + repoName2 string + indexLocalPath string + tmpDir string + err error + ) + + it.Before(func() { + h.SkipIf(t, !pack.SupportsFeature(invoke.ManifestCommands), "pack manifest commands are available since 0.34.0") + + // local storage path + tmpDir, err = os.MkdirTemp("", "manifest-commands-test") + assert.Nil(err) + os.Setenv("XDG_RUNTIME_DIR", tmpDir) + + // manifest commands are experimental + pack.EnableExperimental() + + // used to avoid authentication issues with the local registry + os.Setenv("DOCKER_CONFIG", registryConfig.DockerConfigDir) + }) + + it.After(func() { + assert.Succeeds(os.RemoveAll(tmpDir)) + }) + + when("create", func() { + it.Before(func() { + it.Before(func() { + indexRepoName = registryConfig.RepoName(h.NewRandomIndexRepoName()) + + // Manifest 1 + repoName1 = fmt.Sprintf("%s:%s", indexRepoName, "busybox-amd64") + h.CreateRemoteImage(t, indexRepoName, "busybox-amd64", "busybox@sha256:a236a6469768c17ca1a6ac81a35fe6fbc1efd76b0dcdf5aebb1cf5f0774ee539") + + // Manifest 2 + repoName2 = fmt.Sprintf("%s:%s", indexRepoName, "busybox-arm64") + h.CreateRemoteImage(t, indexRepoName, "busybox-arm64", "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a") + }) + }) + when("--publish", func() { + it("creates and push the index to a remote registry", func() { + output := pack.RunSuccessfully("manifest", "create", "--publish", indexRepoName, repoName1, repoName2) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(indexRepoName) + h.AssertRemoteImageIndex(t, indexRepoName, types.OCIImageIndex, 2) + }) + }) + + when("no --publish", func() { + it("creates the index locally", func() { + output := pack.RunSuccessfully("manifest", "create", indexRepoName, repoName1, repoName2) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexLocallyCreated(indexRepoName) + + indexLocalPath = filepath.Join(tmpDir, imgutil.MakeFileSafeName(indexRepoName)) + index := h.ReadIndexManifest(t, indexLocalPath) + h.AssertEq(t, len(index.Manifests), 2) + h.AssertEq(t, index.MediaType, types.OCIImageIndex) + }) + }) + }) + + when("index is already created", func() { + var digest v1.Hash + + it.Before(func() { + indexRepoName = registryConfig.RepoName(h.NewRandomIndexRepoName()) + + // Manifest 1 + repoName1 = fmt.Sprintf("%s:%s", indexRepoName, "busybox-amd64") + image1 := h.CreateRemoteImage(t, indexRepoName, "busybox-amd64", "busybox@sha256:a236a6469768c17ca1a6ac81a35fe6fbc1efd76b0dcdf5aebb1cf5f0774ee539") + digest, err = image1.Digest() + assert.Nil(err) + + // Manifest 2 + repoName2 = fmt.Sprintf("%s:%s", indexRepoName, "busybox-arm64") + h.CreateRemoteImage(t, indexRepoName, "busybox-arm64", "busybox@sha256:0bcc1b827b855c65eaf6e031e894e682b6170160b8a676e1df7527a19d51fb1a") + + // create an index locally + pack.RunSuccessfully("manifest", "create", indexRepoName, repoName1) + indexLocalPath = filepath.Join(tmpDir, imgutil.MakeFileSafeName(indexRepoName)) + }) + + when("add", func() { + it("adds the manifest to the index", func() { + output := pack.RunSuccessfully("manifest", "add", indexRepoName, repoName2) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulManifestAddedToIndex(repoName2) + + index := h.ReadIndexManifest(t, indexLocalPath) + h.AssertEq(t, len(index.Manifests), 2) + h.AssertEq(t, index.MediaType, types.OCIImageIndex) + }) + }) + + when("remove", func() { + it("removes the index from local storage", func() { + output := pack.RunSuccessfully("manifest", "remove", indexRepoName) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexDeleted() + + h.AssertPathDoesNotExists(t, indexLocalPath) + }) + }) + + when("annotate", func() { + it("adds annotations to the manifest in the index", func() { + output := pack.RunSuccessfully("manifest", "annotate", indexRepoName, repoName1, "--annotations", "foo=bar") + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexAnnotated(repoName1, indexRepoName) + + index := h.ReadIndexManifest(t, indexLocalPath) + h.AssertEq(t, len(index.Manifests), 1) + h.AssertEq(t, len(index.Manifests[0].Annotations), 1) + }) + }) + + when("rm", func() { + it.Before(func() { + // we need to point to the manifest digest we want to delete + repoName1 = fmt.Sprintf("%s@%s", repoName1, digest.String()) + }) + + it("removes the manifest from the index", func() { + output := pack.RunSuccessfully("manifest", "rm", indexRepoName, repoName1) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulRemoveManifestFromIndex(indexRepoName) + + index := h.ReadIndexManifest(t, indexLocalPath) + h.AssertEq(t, len(index.Manifests), 0) + }) + }) + + when("push", func() { + it("pushes the index to a remote registry", func() { + output := pack.RunSuccessfully("manifest", "push", indexRepoName) + assertions.NewOutputAssertionManager(t, output).ReportsSuccessfulIndexPushed(indexRepoName) + h.AssertRemoteImageIndex(t, indexRepoName, types.OCIImageIndex, 1) + }) + }) + }) + }) } func testAcceptance( diff --git a/acceptance/assertions/output.go b/acceptance/assertions/output.go index ed7a80af2..65f206a86 100644 --- a/acceptance/assertions/output.go +++ b/acceptance/assertions/output.go @@ -32,6 +32,42 @@ func (o OutputAssertionManager) ReportsSuccessfulImageBuild(name string) { o.assert.ContainsF(o.output, "Successfully built image '%s'", name) } +func (o OutputAssertionManager) ReportsSuccessfulIndexLocallyCreated(name string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully created manifest list '%s'", name) +} + +func (o OutputAssertionManager) ReportsSuccessfulIndexPushed(name string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully pushed manifest list '%s' to registry", name) +} + +func (o OutputAssertionManager) ReportsSuccessfulManifestAddedToIndex(name string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully added image '%s' to index", name) +} + +func (o OutputAssertionManager) ReportsSuccessfulIndexDeleted() { + o.testObject.Helper() + + o.assert.Contains(o.output, "Successfully deleted manifest list(s) from local storage") +} + +func (o OutputAssertionManager) ReportsSuccessfulIndexAnnotated(name, manifest string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully annotated image '%s' in index '%s'", name, manifest) +} + +func (o OutputAssertionManager) ReportsSuccessfulRemoveManifestFromIndex(name string) { + o.testObject.Helper() + + o.assert.ContainsF(o.output, "Successfully removed image(s) from index: '%s'", name) +} + func (o OutputAssertionManager) ReportSuccessfulQuietBuild(name string) { o.testObject.Helper() o.testObject.Log("quiet mode") diff --git a/acceptance/invoke/pack.go b/acceptance/invoke/pack.go index 8c5379e60..d63170d1e 100644 --- a/acceptance/invoke/pack.go +++ b/acceptance/invoke/pack.go @@ -238,6 +238,7 @@ const ( PlatformRetries FlattenBuilderCreationV2 FixesRunImageMetadata + ManifestCommands ) var featureTests = map[Feature]func(i *PackInvoker) bool{ @@ -274,6 +275,9 @@ var featureTests = map[Feature]func(i *PackInvoker) bool{ FixesRunImageMetadata: func(i *PackInvoker) bool { return i.atLeast("v0.34.0") }, + ManifestCommands: func(i *PackInvoker) bool { + return i.atLeast("v0.34.0") + }, } func (i *PackInvoker) SupportsFeature(f Feature) bool { diff --git a/pkg/client/manifest_add_test.go b/pkg/client/manifest_add_test.go index d3f6a4581..12b20c60d 100644 --- a/pkg/client/manifest_add_test.go +++ b/pkg/client/manifest_add_test.go @@ -61,7 +61,7 @@ func testAddManifest(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) // Create a remote image to be fetched when adding to the image index - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, nil) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", nil) fakeImageFetcher.RemoteImages["index.docker.io/pack/image:latest"] = fakeImage }) it.After(func() { diff --git a/pkg/client/manifest_annotate.go b/pkg/client/manifest_annotate.go index 91d1d1845..f5d375c58 100644 --- a/pkg/client/manifest_annotate.go +++ b/pkg/client/manifest_annotate.go @@ -59,29 +59,29 @@ func (c *Client) AnnotateManifest(ctx context.Context, opts ManifestAnnotateOpti if opts.OS != "" { if err = idx.SetOS(digest, opts.OS); err != nil { - return fmt.Errorf("failed to set the 'os' for '%s': %w", opts.RepoName, err) + return fmt.Errorf("failed to set the 'os' for %s: %w", style.Symbol(opts.RepoName), err) } } if opts.OSArch != "" { if err = idx.SetArchitecture(digest, opts.OSArch); err != nil { - return fmt.Errorf("failed to set the 'arch' for '%s': %w", opts.RepoName, err) + return fmt.Errorf("failed to set the 'arch' for %s: %w", style.Symbol(opts.RepoName), err) } } if opts.OSVariant != "" { if err = idx.SetVariant(digest, opts.OSVariant); err != nil { - return fmt.Errorf("failed to set the 'os variant' for '%s': %w", opts.RepoName, err) + return fmt.Errorf("failed to set the 'os variant' for %s: %w", style.Symbol(opts.RepoName), err) } } if len(opts.Annotations) != 0 { if err = idx.SetAnnotations(digest, opts.Annotations); err != nil { - return fmt.Errorf("failed to set the 'annotations' for '%s': %w", opts.RepoName, err) + return fmt.Errorf("failed to set the 'annotations' for %s: %w", style.Symbol(opts.RepoName), err) } } if err = idx.SaveDir(); err != nil { - return fmt.Errorf("failed to save manifest list '%s' to local storage: %w", opts.RepoName, err) + return fmt.Errorf("failed to save manifest list %s to local storage: %w", style.Symbol(opts.RepoName), err) } - c.logger.Infof("Successfully annotated image '%s' in index '%s'", style.Symbol(opts.RepoName), style.Symbol(opts.IndexRepoName)) + c.logger.Infof("Successfully annotated image %s in index %s", style.Symbol(opts.RepoName), style.Symbol(opts.IndexRepoName)) return nil } diff --git a/pkg/client/manifest_annotate_test.go b/pkg/client/manifest_annotate_test.go index 3fae69295..4557accb0 100644 --- a/pkg/client/manifest_annotate_test.go +++ b/pkg/client/manifest_annotate_test.go @@ -26,7 +26,7 @@ const invalidDigest = "sha256:d4707523ce6e12afdbe9a3be5ad69027150a834870ca0933ba func TestAnnotateManifest(t *testing.T) { color.Disable(true) defer color.Disable(false) - spec.Run(t, "build", testAnnotateManifest, spec.Parallel(), spec.Report(report.Terminal{})) + spec.Run(t, "build", testAnnotateManifest, spec.Sequential(), spec.Report(report.Terminal{})) } func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { @@ -97,7 +97,7 @@ func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { indexRepoName = h.NewRandomIndexRepoName() idx, digest = h.RandomCNBIndexAndDigest(t, indexRepoName, 1, 2) mockIndexFactory.EXPECT().LoadIndex(gomock.Eq(indexRepoName), gomock.Any()).Return(idx, nil) - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, digest) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", digest) fakeImageFetcher.RemoteImages[digest.Name()] = fakeImage }) @@ -122,7 +122,7 @@ func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { indexRepoName = h.NewRandomIndexRepoName() idx, digest = h.RandomCNBIndexAndDigest(t, indexRepoName, 1, 2) mockIndexFactory.EXPECT().LoadIndex(gomock.Eq(indexRepoName), gomock.Any()).Return(idx, nil) - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, digest) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", digest) fakeImageFetcher.RemoteImages[digest.Name()] = fakeImage }) @@ -147,7 +147,7 @@ func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { indexRepoName = h.NewRandomIndexRepoName() idx, digest = h.RandomCNBIndexAndDigest(t, indexRepoName, 1, 2) mockIndexFactory.EXPECT().LoadIndex(gomock.Eq(indexRepoName), gomock.Any()).Return(idx, nil) - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, digest) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", digest) fakeImageFetcher.RemoteImages[digest.Name()] = fakeImage }) @@ -172,7 +172,7 @@ func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { indexRepoName = h.NewRandomIndexRepoName() idx, digest = h.RandomCNBIndexAndDigest(t, indexRepoName, 1, 2) mockIndexFactory.EXPECT().LoadIndex(gomock.Eq(indexRepoName), gomock.Any()).Return(idx, nil) - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, digest) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", digest) fakeImageFetcher.RemoteImages[digest.Name()] = fakeImage }) diff --git a/pkg/client/manifest_create.go b/pkg/client/manifest_create.go index 81ffe3c03..9790c5fce 100644 --- a/pkg/client/manifest_create.go +++ b/pkg/client/manifest_create.go @@ -54,15 +54,15 @@ func (c *Client) CreateManifest(ctx context.Context, opts CreateManifestOptions) return err } - c.logger.Infof("Successfully pushed manifest list '%s' to registry", style.Symbol(opts.IndexRepoName)) + c.logger.Infof("Successfully pushed manifest list %s to registry", style.Symbol(opts.IndexRepoName)) return nil } if err = index.SaveDir(); err != nil { - return fmt.Errorf("manifest list '%s' could not be saved to local storage: %w", style.Symbol(opts.IndexRepoName), err) + return fmt.Errorf("manifest list %s could not be saved to local storage: %w", style.Symbol(opts.IndexRepoName), err) } - c.logger.Infof("Successfully created manifest list '%s'", style.Symbol(opts.IndexRepoName)) + c.logger.Infof("Successfully created manifest list %s", style.Symbol(opts.IndexRepoName)) return nil } diff --git a/pkg/client/manifest_create_test.go b/pkg/client/manifest_create_test.go index d1f65bff4..f7c724a04 100644 --- a/pkg/client/manifest_create_test.go +++ b/pkg/client/manifest_create_test.go @@ -71,7 +71,7 @@ func testCreateManifest(t *testing.T, when spec.G, it spec.S) { when("remote manifest is provided", func() { it.Before(func() { - fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, nil) + fakeImage := h.NewFakeWithRandomUnderlyingV1Image(t, "pack/image", nil) fakeImageFetcher.RemoteImages["index.docker.io/library/busybox:1.36-musl"] = fakeImage }) diff --git a/pkg/client/manifest_push.go b/pkg/client/manifest_push.go index 0e2656832..07defb857 100644 --- a/pkg/client/manifest_push.go +++ b/pkg/client/manifest_push.go @@ -36,11 +36,11 @@ func (c *Client) PushManifest(opts PushManifestOptions) (err error) { } if err = idx.Push(ops...); err != nil { - return fmt.Errorf("failed to push manifest list '%s': %w", style.Symbol(opts.IndexRepoName), err) + return fmt.Errorf("failed to push manifest list %s: %w", style.Symbol(opts.IndexRepoName), err) } if !opts.Purge { - c.logger.Infof("Successfully pushed manifest list '%s'", style.Symbol(opts.IndexRepoName)) + c.logger.Infof("Successfully pushed manifest list %s to registry", style.Symbol(opts.IndexRepoName)) return nil } diff --git a/testhelpers/image_index.go b/testhelpers/image_index.go index 2c4335dfc..29b8d155e 100644 --- a/testhelpers/image_index.go +++ b/testhelpers/image_index.go @@ -11,11 +11,13 @@ import ( "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" + imgutilRemote "github.com/buildpacks/imgutil/remote" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" ) func NewRandomIndexRepoName() string { @@ -55,6 +57,29 @@ func FetchImageIndexDescriptor(t *testing.T, repoName string) v1.ImageIndex { return index } +func AssertRemoteImageIndex(t *testing.T, repoName string, mediaType types.MediaType, expectedNumberOfManifests int) { + t.Helper() + + remoteIndex := FetchImageIndexDescriptor(t, repoName) + AssertNotNil(t, remoteIndex) + remoteIndexMediaType, err := remoteIndex.MediaType() + AssertNil(t, err) + AssertEq(t, remoteIndexMediaType, mediaType) + remoteIndexManifest, err := remoteIndex.IndexManifest() + AssertNil(t, err) + AssertNotNil(t, remoteIndexManifest) + AssertEq(t, len(remoteIndexManifest.Manifests), expectedNumberOfManifests) +} + +func CreateRemoteImage(t *testing.T, repoName, tag, baseImage string) *imgutilRemote.Image { + img1RepoName := fmt.Sprintf("%s:%s", repoName, tag) + img1, err := imgutilRemote.NewImage(img1RepoName, authn.DefaultKeychain, imgutilRemote.FromBaseImage(baseImage)) + AssertNil(t, err) + err = img1.Save() + AssertNil(t, err) + return img1 +} + func ReadIndexManifest(t *testing.T, path string) *v1.IndexManifest { t.Helper() @@ -146,8 +171,8 @@ func (i *MockImageIndex) DeleteDir() error { return nil } -func NewFakeWithRandomUnderlyingV1Image(t *testing.T, identifier imgutil.Identifier) *FakeWithRandomUnderlyingImage { - fakeCNBImage := fakes.NewImage("pack/image", "", identifier) +func NewFakeWithRandomUnderlyingV1Image(t *testing.T, repoName string, identifier imgutil.Identifier) *FakeWithRandomUnderlyingImage { + fakeCNBImage := fakes.NewImage(repoName, "", identifier) underlyingImage, err := random.Image(1024, 1) AssertNil(t, err) return &FakeWithRandomUnderlyingImage{