Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support manifest and blob deletion for OCI layout #1197

Merged
merged 9 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 54 additions & 9 deletions cmd/oras/internal/option/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
}
Expand Down
26 changes: 11 additions & 15 deletions cmd/oras/root/blob/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ type deleteBlobOptions struct {
option.Confirmation
option.Descriptor
option.Pretty
option.Remote

targetRef string
option.Target
}

func deleteCmd() *cobra.Command {
Expand All @@ -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)
},
}
Expand All @@ -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 <name@digest>", 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
}
Expand All @@ -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 {
Expand All @@ -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
}
27 changes: 11 additions & 16 deletions cmd/oras/root/manifest/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -34,9 +33,7 @@ type deleteOptions struct {
option.Confirmation
option.Descriptor
option.Pretty
option.Remote

targetRef string
option.Target
}

func deleteCmd() *cobra.Command {
Expand All @@ -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)
},
}
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
26 changes: 23 additions & 3 deletions test/e2e/suite/command/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/suite/command/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down