From ba60b995d7ba1f8cf2f1a8788b25678c60af8225 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Mon, 3 Jul 2023 16:25:26 +0800 Subject: [PATCH] feat: add flag to skip deleting obsolete referrers index (#957) Signed-off-by: Billy Zha Co-authored-by: Terry Howe --- cmd/oras/internal/errors/errors.go | 8 +++ cmd/oras/internal/option/referrers.go | 42 +++++++++++++++ cmd/oras/root/attach.go | 10 +++- cmd/oras/root/cp.go | 9 +++- cmd/oras/root/manifest/push.go | 8 ++- go.mod | 2 +- go.sum | 2 + test/e2e/README.md | 2 +- test/e2e/go.mod | 2 +- test/e2e/go.sum | 4 +- test/e2e/internal/testdata/foobar/const.go | 4 ++ test/e2e/scripts/common.sh | 5 +- test/e2e/scripts/e2e.sh | 6 ++- test/e2e/suite/command/attach.go | 60 +++++++++++++++++++++- test/e2e/suite/command/cp.go | 36 +++++++++++++ test/e2e/suite/command/manifest.go | 49 +++++++++++++++++- 16 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 cmd/oras/internal/option/referrers.go diff --git a/cmd/oras/internal/errors/errors.go b/cmd/oras/internal/errors/errors.go index e3c6e4c43..631403ac6 100644 --- a/cmd/oras/internal/errors/errors.go +++ b/cmd/oras/internal/errors/errors.go @@ -16,9 +16,11 @@ limitations under the License. package errors import ( + "errors" "fmt" "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" ) // NewErrInvalidReference creates a new error based on the reference string. @@ -30,3 +32,9 @@ func NewErrInvalidReference(ref registry.Reference) error { func NewErrInvalidReferenceStr(ref string) error { return fmt.Errorf("%s: invalid image reference, expecting ", ref) } + +// IsReferrersIndexDelete checks if err is a referrers index delete error. +func IsReferrersIndexDelete(err error) bool { + var re *remote.ReferrersError + return errors.As(err, &re) && re.IsReferrersIndexDelete() +} diff --git a/cmd/oras/internal/option/referrers.go b/cmd/oras/internal/option/referrers.go new file mode 100644 index 000000000..4920c29d0 --- /dev/null +++ b/cmd/oras/internal/option/referrers.go @@ -0,0 +1,42 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package option + +import ( + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "oras.land/oras-go/v2/registry/remote" +) + +// Referrers option struct. +type Referrers struct { + SkipDeleteReferrers bool +} + +// ApplyFlags applies flags to a command flag set. +func (opts *Referrers) ApplyFlags(fs *pflag.FlagSet) { + fs.BoolVarP(&opts.SkipDeleteReferrers, "skip-delete-referrers", "", false, "skip deleting old referrers index, only work on registry when referrers API is not supported") +} + +// SetReferrersGC sets the referrers GC option for the passed-in target. +func (opts *Referrers) SetReferrersGC(target any, logger logrus.FieldLogger) { + if repo, ok := target.(*remote.Repository); ok { + repo.SkipReferrersGC = opts.SkipDeleteReferrers + } else if opts.SkipDeleteReferrers { + // not a registry, can't skip referrers deletion + logger.Warnln("referrers deletion can only be enforced upon registry") + } +} diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index 8f0f88e89..99ed8ba14 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "os" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -26,6 +27,7 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" + oerr "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" ) @@ -35,6 +37,7 @@ type attachOptions struct { option.Packer option.ImageSpec option.Target + option.Referrers artifactType string concurrency int @@ -98,7 +101,7 @@ Example - Attach file to the manifest tagged 'v1' in an OCI image layout folder } func runAttach(ctx context.Context, opts attachOptions) error { - ctx, _ = opts.WithContext(ctx) + ctx, logger := opts.WithContext(ctx) annotations, err := opts.LoadManifestAnnotations() if err != nil { return err @@ -121,6 +124,8 @@ func runAttach(ctx context.Context, opts attachOptions) error { if err := opts.EnsureReferenceNotEmpty(); err != nil { return err } + opts.SetReferrersGC(dst, logger) + subject, err := dst.Resolve(ctx, opts.Reference) if err != nil { return err @@ -163,6 +168,9 @@ func runAttach(ctx context.Context, opts attachOptions) error { root, err := pushArtifact(dst, pack, copy) if err != nil { + if oerr.IsReferrersIndexDelete(err) { + fmt.Fprintln(os.Stderr, "attached successfully but failed to remove the outdated referrers index, please use `--skip-delete-referrers` if you want to skip the deletion") + } return err } diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index f8a953b39..20cc2a753 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -18,6 +18,7 @@ package root import ( "context" "fmt" + "os" "strings" "sync" @@ -26,6 +27,7 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2" "oras.land/oras/cmd/oras/internal/display" + oerr "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" ) @@ -34,6 +36,7 @@ type copyOptions struct { option.Common option.Platform option.BinaryTarget + option.Referrers recursive bool concurrency int @@ -96,7 +99,7 @@ Example - Copy an artifact with multiple tags with concurrency tuned: } func runCopy(ctx context.Context, opts copyOptions) error { - ctx, _ = opts.WithContext(ctx) + ctx, logger := opts.WithContext(ctx) // Prepare source src, err := opts.From.NewReadonlyTarget(ctx, opts.Common) @@ -112,6 +115,7 @@ func runCopy(ctx context.Context, opts copyOptions) error { if err != nil { return err } + opts.SetReferrersGC(dst, logger) // Prepare copy options committed := &sync.Map{} @@ -167,6 +171,9 @@ func runCopy(ctx context.Context, opts copyOptions) error { } } if err != nil { + if oerr.IsReferrersIndexDelete(err) { + fmt.Fprintln(os.Stderr, "failed to remove the outdated referrers index, please use `--skip-delete-referrers` if you want to skip the deletion") + } return err } diff --git a/cmd/oras/root/manifest/push.go b/cmd/oras/root/manifest/push.go index c932e8e3f..f6391c939 100644 --- a/cmd/oras/root/manifest/push.go +++ b/cmd/oras/root/manifest/push.go @@ -29,6 +29,7 @@ import ( "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" "oras.land/oras/cmd/oras/internal/display" + oerr "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/file" ) @@ -38,6 +39,7 @@ type pushOptions struct { option.Descriptor option.Pretty option.Target + option.Referrers concurrency int extraRefs []string @@ -104,13 +106,14 @@ Example - Push a manifest to an OCI image layout folder 'layout-dir' and tag wit } func pushManifest(ctx context.Context, opts pushOptions) error { - ctx, _ = opts.WithContext(ctx) + ctx, logger := opts.WithContext(ctx) var target oras.Target var err error target, err = opts.NewTarget(opts.Common) if err != nil { return err } + opts.SetReferrersGC(target, logger) if repo, ok := target.(*remote.Repository); ok { target = repo.Manifests() } @@ -151,6 +154,9 @@ func pushManifest(ctx context.Context, opts pushOptions) error { return err } if _, err := oras.TagBytes(ctx, target, mediaType, contentBytes, ref); err != nil { + if oerr.IsReferrersIndexDelete(err) { + fmt.Fprintln(os.Stderr, "pushed successfully but failed to remove the outdated referrers index, please use `--skip-delete-referrers` if you want to skip the deletion") + } return err } if err = display.PrintStatus(desc, "Uploaded ", verbose); err != nil { diff --git a/go.mod b/go.mod index 440585712..6aa1bf622 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/pflag v1.0.5 golang.org/x/term v0.9.0 gopkg.in/yaml.v3 v3.0.1 - oras.land/oras-go/v2 v2.2.0 + oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6 ) require ( diff --git a/go.sum b/go.sum index f52e1446e..8dddeea13 100644 --- a/go.sum +++ b/go.sum @@ -38,3 +38,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo= oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= +oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6 h1:2P1fjq1znGLo7tjy9PJsZrFF5L+qywbv28IgzKEX62E= +oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= diff --git a/test/e2e/README.md b/test/e2e/README.md index 26e7332a8..f3fa24672 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -30,7 +30,7 @@ go test oras.land/oras/test/e2e/suite/${suite_name} This is super handy when you want to do step-by-step debugging from command-line or via an IDE. If you need to debug certain specs, use [focused specs](https://onsi.github.io/ginkgo/#focused-specs) but don't check it in. ### 4. Testing Registry Services -The backend of E2E tests are two registry services: [oras-distribution](https://github.com/oras-project/distribution) and [upstream distribution](https://github.com/distribution/distribution). The former is expected to support image and artifact media types and referrer API; The latter is expected to only support image media type with subject and provide referrers via [tag schema](https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema). +The backend of E2E tests are two registry services: [oras-distribution](https://github.com/oras-project/distribution) and [upstream distribution](https://github.com/distribution/distribution). The former is expected to support image and artifact media types and referrer API; The latter is expected to only support image media type with subject and provide referrers via [tag schema](https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema), with deletion disabled. You can run scenario test suite against your own registry services via setting `ORAS_REGISTRY_HOST` or `ORAS_REGISTRY_FALLBACK_HOST` environmental variables. diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 764db24b4..e3505ea06 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -8,7 +8,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc.3 gopkg.in/yaml.v2 v2.4.0 - oras.land/oras-go/v2 v2.2.0 + oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6 ) require ( diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 2b3164b6b..a3cc76189 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -47,5 +47,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo= -oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= +oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6 h1:2P1fjq1znGLo7tjy9PJsZrFF5L+qywbv28IgzKEX62E= +oras.land/oras-go/v2 v2.2.1-0.20230531090906-7dd0378382c6/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= diff --git a/test/e2e/internal/testdata/foobar/const.go b/test/e2e/internal/testdata/foobar/const.go index 0ea5471f0..66a0591f8 100644 --- a/test/e2e/internal/testdata/foobar/const.go +++ b/test/e2e/internal/testdata/foobar/const.go @@ -16,6 +16,8 @@ limitations under the License. package foobar import ( + "fmt" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/test/e2e/internal/utils/match" @@ -24,6 +26,8 @@ import ( var ( Tag = "foobar" Digest = "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb" + Size = 851 + DescriptorStr = fmt.Sprintf(`{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"%s","size":%d}`, Digest, Size) ManifestStateKey = match.StateKey{Digest: "fd6ed2f36b54", Name: "application/vnd.oci.image.manifest.v1+json"} FileLayerNames = []string{ diff --git a/test/e2e/scripts/common.sh b/test/e2e/scripts/common.sh index 7760a6578..e1d40f919 100755 --- a/test/e2e/scripts/common.sh +++ b/test/e2e/scripts/common.sh @@ -15,13 +15,14 @@ help () { echo "Usage" - echo " run-registry " + echo " run-registry " echo "" echo "Arguments" echo " mount-root root mounting directory for pre-baked registry storage files." echo " image-name image name of the registry." echo " container-name container name of the registry service." echo " container-port port to export the registry service." + echo " delete-enabled if set to true, the registry service will be configured to allow deletion." } # run registry service for testing @@ -61,7 +62,7 @@ run_registry () { try_clean_up $ctr_name docker run --pull always -d -p $ctr_port:5000 --rm --name $ctr_name \ -u $(id -u $(whoami)) \ - --env REGISTRY_STORAGE_DELETE_ENABLED=true \ + --env REGISTRY_STORAGE_DELETE_ENABLED=$5 \ --env REGISTRY_AUTH_HTPASSWD_REALM=test-basic \ --env REGISTRY_AUTH_HTPASSWD_PATH=/etc/docker/registry/passwd \ --mount type=bind,source=$mnt_root/docker,target=/var/lib/registry/docker \ diff --git a/test/e2e/scripts/e2e.sh b/test/e2e/scripts/e2e.sh index b110e38c6..08d555a77 100755 --- a/test/e2e/scripts/e2e.sh +++ b/test/e2e/scripts/e2e.sh @@ -49,14 +49,16 @@ run_registry \ ${e2e_root}/testdata/distribution/mount \ ghcr.io/oras-project/registry:v1.0.0-rc.4 \ $oras_container_name \ - $ORAS_REGISTRY_PORT + $ORAS_REGISTRY_PORT \ + true echo " === preparing upstream distribution === " run_registry \ ${e2e_root}/testdata/distribution/mount_fallback \ registry:2.8.1 \ $upstream_container_name \ - $ORAS_REGISTRY_FALLBACK_PORT + $ORAS_REGISTRY_FALLBACK_PORT \ + false echo " === run tests === " if ! ginkgo -r -p --succinct suite; then diff --git a/test/e2e/suite/command/attach.go b/test/e2e/suite/command/attach.go index a3083e86b..499a613da 100644 --- a/test/e2e/suite/command/attach.go +++ b/test/e2e/suite/command/attach.go @@ -195,7 +195,7 @@ var _ = Describe("Fallback registry users:", func() { subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) // test - ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "--image-spec", "v1.1-image"). + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). WithWorkDir(tempDir). MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() @@ -207,6 +207,53 @@ var _ = Describe("Fallback registry users:", func() { Expect(index.Manifests[0].MediaType).To(Equal("application/vnd.oci.image.manifest.v1+json")) }) + It("should fail to attach again when cleaning referrers index", func() { + testRepo := attachTestRepo("fallback/fail-gc") + tempDir := PrepareTempFiles() + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) + // test + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). + WithWorkDir(tempDir). + MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() + + // validate + var index ocispec.Index + bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() + Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) + Expect(len(index.Manifests)).To(Equal(1)) + Expect(index.Manifests[0].MediaType).To(Equal(ocispec.MediaTypeImageManifest)) + // test + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "-a", "test.type=another.image"). + WithWorkDir(tempDir). + MatchErrKeyWords("Error: failed to delete dangling referrers index"). + ExpectFailure().Exec() + }) + + It("should attach again and skip cleanning index", func() { + testRepo := attachTestRepo("fallback/skip-gc") + tempDir := PrepareTempFiles() + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) + // test + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). + WithWorkDir(tempDir). + MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() + // validate + var index ocispec.Index + bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() + Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) + Expect(len(index.Manifests)).To(Equal(1)) + // test + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia), "-a", "test.type=another.image", "--skip-delete-referrers"). + WithWorkDir(tempDir). + MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() + // validate + bytes = ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() + Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) + Expect(len(index.Manifests)).To(Equal(2)) + }) + It("should attach a file via a OCI Image and generate referrer via tag schema", func() { testRepo := attachTestRepo("fallback/tag_schema") tempDir := PrepareTempFiles() @@ -232,6 +279,7 @@ var _ = Describe("OCI image layout users:", func() { prepare := func(root string) { ORAS("cp", RegistryRef(Host, ImageRepo, foobar.Tag), Flags.ToLayout, LayoutRef(root, foobar.Tag)).Exec() } + It("should attach a file to a subject", func() { root := PrepareTempFiles() subjectRef := LayoutRef(root, foobar.Tag) @@ -241,6 +289,16 @@ var _ = Describe("OCI image layout users:", func() { MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() }) + It("should attach and output warning for referrers deletion by default", func() { + root := PrepareTempFiles() + subjectRef := LayoutRef(root, foobar.Tag) + prepare(root) + ORAS("attach", "--artifact-type", "test.attach", "-v", Flags.Layout, subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). + MatchErrKeyWords("referrers deletion can only be enforced upon registry\n"). + WithWorkDir(root). + Exec() + }) + It("should attach a file to a subject and export the built manifest", func() { // prepare root := PrepareTempFiles() diff --git a/test/e2e/suite/command/cp.go b/test/e2e/suite/command/cp.go index 4b49187fa..5865ba0cb 100644 --- a/test/e2e/suite/command/cp.go +++ b/test/e2e/suite/command/cp.go @@ -31,6 +31,7 @@ import ( "oras.land/oras/test/e2e/internal/testdata/foobar" ma "oras.land/oras/test/e2e/internal/testdata/multi_arch" . "oras.land/oras/test/e2e/internal/utils" + "oras.land/oras/test/e2e/internal/utils/match" ) func cpTestRepo(text string) string { @@ -245,6 +246,34 @@ var _ = Describe("Common registry users:", func() { var _ = Describe("OCI spec 1.0 registry users:", func() { When("running `cp`", func() { + It("should fail to copy when cleaning referrers index", func() { + testRepo := cpTestRepo("fallback/fail-gc") + tempDir := PrepareTempFiles() + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + // prepare + prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). + WithWorkDir(tempDir). + MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() + // test + ORAS("cp", "-r", RegistryRef(FallbackHost, ArtifactRepo, foobar.FallbackSBOMImageReferrer.Digest.String()), RegistryRef(FallbackHost, testRepo, ""), "-v"). + MatchErrKeyWords("Error: failed to delete dangling referrers index"). + ExpectFailure().Exec() + }) + + It("should copy and skip cleaning referrers index", func() { + testRepo := cpTestRepo("fallback/skip-gc") + tempDir := PrepareTempFiles() + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + // prepare + prepare(RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef) + ORAS("attach", "--artifact-type", "test.attach", subjectRef, fmt.Sprintf("%s:%s", foobar.AttachFileName, foobar.AttachFileMedia)). + WithWorkDir(tempDir). + MatchStatus([]match.StateKey{foobar.AttachFileStateKey}, false, 1).Exec() + // test + ORAS("cp", "-r", RegistryRef(FallbackHost, ArtifactRepo, foobar.FallbackSBOMImageReferrer.Digest.String()), RegistryRef(FallbackHost, testRepo, ""), "--skip-delete-referrers").Exec() + }) + It("should copy an image artifact and its referrers from a registry to a fallback registry", func() { repo := cpTestRepo("to-fallback") stateKeys := append(append(foobarStates, foobar.ImageReferrersStateKeys...), foobar.ImageReferrerConfigStateKeys...) @@ -255,6 +284,7 @@ var _ = Describe("OCI spec 1.0 registry users:", func() { ORAS("discover", "-o", "tree", RegistryRef(FallbackHost, repo, foobar.Digest)). WithDescription("discover referrer via subject").MatchKeyWords(foobar.SignatureImageReferrer.Digest.String(), foobar.SBOMImageReferrer.Digest.String()).Exec() }) + It("should copy an image artifact and its referrers from a fallback registry to a registry", func() { repo := cpTestRepo("from-fallback") stateKeys := append(append(foobarStates, foobar.FallbackImageReferrersStateKeys...), foobar.ImageReferrerConfigStateKeys...) @@ -333,6 +363,12 @@ var _ = Describe("OCI layout users:", func() { Expect(srcManifest).To(Equal(dstManifest)) }) + It("should copy and output verbosed warning for Feferrers deletion by default", func() { + ORAS("cp", RegistryRef(Host, ArtifactRepo, foobar.Tag), GinkgoT().TempDir(), Flags.ToLayout, "-v"). + MatchErrKeyWords("referrers deletion can only be enforced upon registry\n"). + Exec() + }) + It("should copy an image from an OCI image layout to a registry via tag", func() { layoutDir := GinkgoT().TempDir() src := LayoutRef(layoutDir, "copied") diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index 109e4ecc3..36d30bf0c 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -518,9 +518,7 @@ var _ = Describe("OCI image layout users:", func() { ORAS("manifest", "fetch-config", Flags.Layout, root).ExpectFailure().MatchErrKeyWords("Error:", "invalid image reference").Exec() }) }) -}) -var _ = Describe("OCI image layout users:", func() { When("running `manifest push`", func() { scratchSize := 2 scratchDigest := "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a" @@ -547,6 +545,15 @@ var _ = Describe("OCI image layout users:", func() { } descriptor := "{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:f20c43161d73848408ef247f0ec7111b19fe58ffebc0cbcaa0d2c8bda4967268\",\"size\":246}" + It("should push and output verbosed warning for Feferrers deletion by default", func() { + manifestPath := WriteTempFile("manifest.json", manifest) + root := filepath.Dir(manifestPath) + prepare(root) + ORAS("manifest", "push", root, Flags.Layout, manifestPath, "--skip-delete-referrers=false"). + WithWorkDir(root). + MatchErrKeyWords("referrers deletion can only be enforced upon registry\n").Exec() + }) + It("should push a manifest from stdin", func() { root := GinkgoT().TempDir() prepare(root) @@ -555,6 +562,7 @@ var _ = Describe("OCI image layout users:", func() { WithInput(strings.NewReader(manifest)).Exec() validate(root, manifestDigest, "") }) + It("should push a manifest from stdin and tag", func() { tag := "from-stdin" root := GinkgoT().TempDir() @@ -606,3 +614,40 @@ var _ = Describe("OCI image layout users:", func() { }) }) }) + +func pushTestRepo(text string) string { + return fmt.Sprintf("command/push/%d/%s", GinkgoRandomSeed(), text) +} + +var _ = Describe("Fallback registry users:", func() { + When("running `manifest push`", func() { + manifest := fmt.Sprintf(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"oras.test","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"subject":%s,"layers":[]}`, foobar.DescriptorStr) + It("should fail to push manifest when cleaning referrers index", func() { + testRepo := pushTestRepo("fallback/fail-gc") + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + // prepare + ORAS("cp", RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef, "-r").Exec() + // test + ORAS("manifest", "push", RegistryRef(FallbackHost, testRepo, ""), "-"). + WithInput(strings.NewReader(manifest)). + ExpectFailure(). + MatchErrKeyWords("failed to delete dangling referrers index"). + Exec() + }) + It("should push manifest and skip cleaning referrers index", func() { + testRepo := pushTestRepo("fallback/skip-gc") + subjectRef := RegistryRef(FallbackHost, testRepo, foobar.Tag) + // prepare + ORAS("cp", RegistryRef(FallbackHost, ArtifactRepo, foobar.Tag), subjectRef, "-r").Exec() + // test + ORAS("manifest", "push", RegistryRef(FallbackHost, testRepo, ""), "-", "--skip-delete-referrers"). + WithInput(strings.NewReader(manifest)). + Exec() + // validate + var index ocispec.Index + bytes := ORAS("discover", subjectRef, "-o", "json").Exec().Out.Contents() + Expect(json.Unmarshal(bytes, &index)).ShouldNot(HaveOccurred()) + Expect(len(index.Manifests)).To(Equal(2)) + }) + }) +})