Skip to content

Commit

Permalink
feat: support manifest and blob deletion for OCI layout (#1197)
Browse files Browse the repository at this point in the history
Signed-off-by: Billy Zha <[email protected]>
  • Loading branch information
qweeah authored Dec 11, 2023
1 parent 304c9c6 commit abe0ce5
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 44 deletions.
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

0 comments on commit abe0ce5

Please sign in to comment.