diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index b3c9d5d6b..95194f6ac 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -28,8 +28,10 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/pflag" "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/oci" "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/fileref" ) @@ -114,26 +116,69 @@ func parseOCILayoutReference(raw string) (path string, ref string, err error) { return } +func (opts *Target) newOCIStore() (*oci.Store, error) { + var err error + opts.Path, opts.Reference, err = parseOCILayoutReference(opts.RawReference) + if err != nil { + return nil, err + } + return oci.New(opts.Path) +} + +func (opts *Target) newRepository(common Common, logger logrus.FieldLogger) (*remote.Repository, error) { + repo, err := opts.NewRepository(opts.RawReference, common, logger) + if err != nil { + return nil, err + } + tmp := repo.Reference + tmp.Reference = "" + opts.Path = tmp.String() + opts.Reference = repo.Reference.Reference + return repo, nil +} + // NewTarget generates a new target based on opts. func (opts *Target) NewTarget(common Common, logger logrus.FieldLogger) (oras.GraphTarget, error) { switch opts.Type { case TargetTypeOCILayout: - var err error - opts.Path, opts.Reference, err = parseOCILayoutReference(opts.RawReference) + return opts.newOCIStore() + case TargetTypeRemote: + return opts.newRepository(common, logger) + } + return nil, fmt.Errorf("unknown target type: %q", opts.Type) +} + +type ResolvableDeleter interface { + content.Resolver + content.Deleter +} + +// NewBlobDeleter generates a new blob deleter based on opts. +func (opts *Target) NewBlobDeleter(common Common, logger logrus.FieldLogger) (ResolvableDeleter, error) { + switch opts.Type { + case TargetTypeOCILayout: + return opts.newOCIStore() + case TargetTypeRemote: + repo, err := opts.newRepository(common, logger) if err != nil { return nil, err } - return oci.New(opts.Path) + return repo.Blobs(), nil + } + return nil, fmt.Errorf("unknown target type: %q", opts.Type) +} + +// NewManifestDeleter generates a new blob deleter based on opts. +func (opts *Target) NewManifestDeleter(common Common, logger logrus.FieldLogger) (ResolvableDeleter, error) { + switch opts.Type { + case TargetTypeOCILayout: + return opts.newOCIStore() case TargetTypeRemote: - repo, err := opts.NewRepository(opts.RawReference, common, logger) + repo, err := opts.newRepository(common, logger) if err != nil { return nil, err } - tmp := repo.Reference - tmp.Reference = "" - opts.Path = tmp.String() - opts.Reference = repo.Reference.Reference - return repo, nil + return repo.Manifests(), nil } return nil, fmt.Errorf("unknown target type: %q", opts.Type) } diff --git a/cmd/oras/root/blob/delete.go b/cmd/oras/root/blob/delete.go index 419a1277d..50bdf7240 100644 --- a/cmd/oras/root/blob/delete.go +++ b/cmd/oras/root/blob/delete.go @@ -33,9 +33,7 @@ type deleteBlobOptions struct { option.Confirmation option.Descriptor option.Pretty - option.Remote - - targetRef string + option.Target } func deleteCmd() *cobra.Command { @@ -57,13 +55,13 @@ Example - Delete a blob and print its descriptor: `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { + opts.RawReference = args[0] if opts.OutputDescriptor && !opts.Force { return errors.New("must apply --force to confirm the deletion if the descriptor is outputted") } return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { - opts.targetRef = args[0] return deleteBlob(cmd.Context(), opts) }, } @@ -74,27 +72,25 @@ Example - Delete a blob and print its descriptor: func deleteBlob(ctx context.Context, opts deleteBlobOptions) (err error) { ctx, logger := opts.WithContext(ctx) - repo, err := opts.NewRepository(opts.targetRef, opts.Common, logger) + blobs, err := opts.NewBlobDeleter(opts.Common, logger) if err != nil { return err } - - if _, err = repo.Reference.Digest(); err != nil { - return fmt.Errorf("%s: blob reference must be of the form ", opts.targetRef) + if err := opts.EnsureReferenceNotEmpty(); err != nil { + return err } // add both pull and delete scope hints for dst repository to save potential delete-scope token requests during deleting - ctx = registryutil.WithScopeHint(ctx, repo, auth.ActionPull, auth.ActionDelete) - blobs := repo.Blobs() - desc, err := blobs.Resolve(ctx, opts.targetRef) + ctx = registryutil.WithScopeHint(ctx, blobs, auth.ActionPull, auth.ActionDelete) + desc, err := blobs.Resolve(ctx, opts.Reference) if err != nil { if errors.Is(err, errdef.ErrNotFound) { if opts.Force && !opts.OutputDescriptor { // ignore nonexistent - fmt.Println("Missing", opts.targetRef) + fmt.Println("Missing", opts.RawReference) return nil } - return fmt.Errorf("%s: the specified blob does not exist", opts.targetRef) + return fmt.Errorf("%s: the specified blob does not exist", opts.RawReference) } return err } @@ -109,7 +105,7 @@ func deleteBlob(ctx context.Context, opts deleteBlobOptions) (err error) { } if err = blobs.Delete(ctx, desc); err != nil { - return fmt.Errorf("failed to delete %s: %w", opts.targetRef, err) + return fmt.Errorf("failed to delete %s: %w", opts.RawReference, err) } if opts.OutputDescriptor { @@ -120,7 +116,7 @@ func deleteBlob(ctx context.Context, opts deleteBlobOptions) (err error) { return opts.Output(os.Stdout, descJSON) } - fmt.Println("Deleted", opts.targetRef) + fmt.Println("Deleted", opts.AnnotatedReference()) return nil } diff --git a/cmd/oras/root/manifest/delete.go b/cmd/oras/root/manifest/delete.go index 467ef04f4..8064c5ea8 100644 --- a/cmd/oras/root/manifest/delete.go +++ b/cmd/oras/root/manifest/delete.go @@ -24,7 +24,6 @@ import ( "github.com/spf13/cobra" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote/auth" - oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/registryutil" ) @@ -34,9 +33,7 @@ type deleteOptions struct { option.Confirmation option.Descriptor option.Pretty - option.Remote - - targetRef string + option.Target } func deleteCmd() *cobra.Command { @@ -61,13 +58,13 @@ Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614 `, Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { + opts.RawReference = args[0] if opts.OutputDescriptor && !opts.Force { return errors.New("must apply --force to confirm the deletion if the descriptor is outputted") } return option.Parse(&opts) }, RunE: func(cmd *cobra.Command, args []string) error { - opts.targetRef = args[0] return deleteManifest(cmd.Context(), opts) }, } @@ -79,13 +76,12 @@ Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614 func deleteManifest(ctx context.Context, opts deleteOptions) error { ctx, logger := opts.WithContext(ctx) - repo, err := opts.NewRepository(opts.targetRef, opts.Common, logger) + manifests, err := opts.NewManifestDeleter(opts.Common, logger) if err != nil { return err } - - if repo.Reference.Reference == "" { - return oerrors.NewErrEmptyTagOrDigest(repo.Reference) + if err := opts.EnsureReferenceNotEmpty(); err != nil { + return err } // add both pull and delete scope hints for dst repository to save potential delete-scope token requests during deleting @@ -94,17 +90,16 @@ func deleteManifest(ctx context.Context, opts deleteOptions) error { // possibly needed when adding a new referrers index hints = append(hints, auth.ActionPush) } - ctx = registryutil.WithScopeHint(ctx, repo, hints...) - manifests := repo.Manifests() - desc, err := manifests.Resolve(ctx, opts.targetRef) + ctx = registryutil.WithScopeHint(ctx, manifests, hints...) + desc, err := manifests.Resolve(ctx, opts.Reference) if err != nil { if errors.Is(err, errdef.ErrNotFound) { if opts.Force && !opts.OutputDescriptor { // ignore nonexistent - fmt.Println("Missing", opts.targetRef) + fmt.Println("Missing", opts.RawReference) return nil } - return fmt.Errorf("%s: the specified manifest does not exist", opts.targetRef) + return fmt.Errorf("%s: the specified manifest does not exist", opts.RawReference) } return err } @@ -119,7 +114,7 @@ func deleteManifest(ctx context.Context, opts deleteOptions) error { } if err = manifests.Delete(ctx, desc); err != nil { - return fmt.Errorf("failed to delete %s: %w", opts.targetRef, err) + return fmt.Errorf("failed to delete %s: %w", opts.RawReference, err) } if opts.OutputDescriptor { @@ -130,7 +125,7 @@ func deleteManifest(ctx context.Context, opts deleteOptions) error { return opts.Output(os.Stdout, descJSON) } - fmt.Println("Deleted", opts.targetRef) + fmt.Println("Deleted", opts.AnnotatedReference()) return nil } diff --git a/go.mod b/go.mod index f52ea8108..b716a4181 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( golang.org/x/sync v0.5.0 golang.org/x/term v0.15.0 gopkg.in/yaml.v3 v3.0.1 - oras.land/oras-go/v2 v2.3.1-0.20231026053328-062ed0e058f8 + oras.land/oras-go/v2 v2.3.1-0.20231121114731-79a08b452e76 ) require ( diff --git a/go.sum b/go.sum index 19632f6a5..063c2a68f 100644 --- a/go.sum +++ b/go.sum @@ -41,3 +41,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.3.1-0.20231026053328-062ed0e058f8 h1:MOs3GufGAl0KY0e19iZ0aYjFQFSFqY9+a/Elt8ThzdI= oras.land/oras-go/v2 v2.3.1-0.20231026053328-062ed0e058f8/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c= +oras.land/oras-go/v2 v2.3.1-0.20231121114731-79a08b452e76 h1:U3oxpjdj2zkw35OjhBaa7xLloGXPPGjwpC8Czp5+zTk= +oras.land/oras-go/v2 v2.3.1-0.20231121114731-79a08b452e76/go.mod h1:W2rj9/rGtsTh9lmU3S0uq3iwvR6wNvUuD8nmOFmWBUY= diff --git a/test/e2e/suite/command/blob.go b/test/e2e/suite/command/blob.go index 770a0a78c..34b3d8356 100644 --- a/test/e2e/suite/command/blob.go +++ b/test/e2e/suite/command/blob.go @@ -245,12 +245,32 @@ var _ = Describe("1.1 registry users:", func() { var _ = Describe("OCI image layout users:", func() { When("running `blob delete`", func() { - It("should not support deleting a blob", func() { + It("should delete a blob with interactive confirmation", func() { + // prepare toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), foobar.FooBlobDigest) + // test ORAS("blob", "delete", Flags.Layout, toDeleteRef). WithInput(strings.NewReader("y")). - MatchErrKeyWords("Error:", "unknown flag", Flags.Layout). - ExpectFailure(). + MatchKeyWords("Deleted", toDeleteRef).Exec() + // validate + ORAS("blob", "fetch", toDeleteRef, Flags.Layout, "--output", "-").ExpectFailure().Exec() + }) + + It("should delete a blob with force flag and output descriptor", func() { + // prepare + toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), foobar.FooBlobDigest) + // test + ORAS("blob", "delete", Flags.Layout, toDeleteRef, "--force", "--descriptor").MatchContent(foobar.FooBlobDescriptor).Exec() + // validate + ORAS("blob", "fetch", Flags.Layout, toDeleteRef, "--output", "-").ExpectFailure().Exec() + }) + + It("should return success when deleting a non-existent blob with force flag set", func() { + // prepare + toDeleteRef := RegistryRef(ZOTHost, ImageRepo, invalidDigest) + // test + ORAS("blob", "delete", Flags.Layout, toDeleteRef, "--force"). + MatchKeyWords("Missing", toDeleteRef). Exec() }) }) diff --git a/test/e2e/suite/command/manifest.go b/test/e2e/suite/command/manifest.go index bf8950c92..116979d0d 100644 --- a/test/e2e/suite/command/manifest.go +++ b/test/e2e/suite/command/manifest.go @@ -494,6 +494,46 @@ var _ = Describe("OCI image layout users:", func() { ORAS("manifest", "fetch-config", Flags.Layout, root).ExpectFailure().MatchErrKeyWords("Error:", "no tag or digest").Exec() }) }) + + When("running `manifest delete`", func() { + It("should do confirmed deletion via input", func() { + // prepare + toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), foobar.Tag) + // test + ORAS("manifest", "delete", Flags.Layout, toDeleteRef). + WithInput(strings.NewReader("y")).Exec() + // validate + ORAS("manifest", "fetch", Flags.Layout, toDeleteRef).ExpectFailure().MatchErrKeyWords(": not found").Exec() + }) + + It("should do confirmed deletion via flag", func() { + // prepare + toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), foobar.Tag) + // test + ORAS("manifest", "delete", Flags.Layout, toDeleteRef, "-f").Exec() + // validate + ORAS("manifest", "fetch", Flags.Layout, toDeleteRef).ExpectFailure().MatchErrKeyWords(": not found").Exec() + }) + + It("should do forced deletion and output descriptor", func() { + // prepare + toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), foobar.Tag) + // test + ORAS("manifest", "delete", Flags.Layout, toDeleteRef, "-f", "--descriptor"). + MatchContent("{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb\",\"size\":851,\"annotations\":{\"org.opencontainers.image.ref.name\":\"foobar\"}}"). + Exec() + // validate + ORAS("manifest", "fetch", Flags.Layout, toDeleteRef).MatchErrKeyWords(": not found").ExpectFailure().Exec() + }) + + It("should succeed when deleting a non-existent manifest with force flag set", func() { + // prepare + toDeleteRef := LayoutRef(PrepareTempOCI(ImageRepo), invalidDigest) + ORAS("manifest", "delete", Flags.Layout, toDeleteRef, "--force"). + MatchKeyWords("Missing", toDeleteRef). + Exec() + }) + }) }) var _ = Describe("1.0 registry users:", func() {