Skip to content

Commit

Permalink
feat!: force deleting a non-existent blob/manifest (#745)
Browse files Browse the repository at this point in the history
Resolves #722 

Signed-off-by: Billy Zha <[email protected]>
  • Loading branch information
qweeah authored Jan 12, 2023
1 parent dd279cd commit 9db2066
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 17 deletions.
7 changes: 6 additions & 1 deletion cmd/oras/blob/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Example - Delete a blob and print its descriptor:
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.OutputDescriptor && !opts.Confirmed {
if opts.OutputDescriptor && !opts.Force {
return errors.New("must apply --force to confirm the deletion if the descriptor is outputted")
}
return option.Parse(&opts)
Expand Down Expand Up @@ -86,6 +86,11 @@ func deleteBlob(opts deleteBlobOptions) (err error) {
desc, err := blobs.Resolve(ctx, opts.targetRef)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
if opts.Force && !opts.OutputDescriptor {
// ignore nonexistent
fmt.Println("Missing", opts.targetRef)
return nil
}
return fmt.Errorf("%s: the specified blob does not exist", opts.targetRef)
}
return err
Expand Down
6 changes: 3 additions & 3 deletions cmd/oras/internal/option/confirmation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@ import (

// Confirmation option struct.
type Confirmation struct {
Confirmed bool
Force bool
}

// ApplyFlags applies flags to a command flag set.
func (opts *Confirmation) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&opts.Confirmed, "force", "f", false, "do not prompt for confirmation")
fs.BoolVarP(&opts.Force, "force", "f", false, "ignore nonexistent references, never prompt")
}

// AskForConfirmation prints a propmt to ask for confirmation before doing an
// action and takes user input as response.
func (opts *Confirmation) AskForConfirmation(r io.Reader, prompt string) (bool, error) {
if opts.Confirmed {
if opts.Force {
return true, nil
}

Expand Down
8 changes: 4 additions & 4 deletions cmd/oras/internal/option/confirmation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ import (
func TestConfirmation_ApplyFlags(t *testing.T) {
var test struct{ Confirmation }
ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError))
if test.Confirmation.Confirmed != false {
t.Fatalf("expecting Confirmed to be false but got: %v", test.Confirmation.Confirmed)
if test.Confirmation.Force != false {
t.Fatalf("expecting Confirmed to be false but got: %v", test.Confirmation.Force)
}
}

func TestConfirmation_AskForConfirmation_forciblyConfirmed(t *testing.T) {
opts := Confirmation{
Confirmed: true,
Force: true,
}
r := strings.NewReader("")

Expand All @@ -48,7 +48,7 @@ func TestConfirmation_AskForConfirmation_forciblyConfirmed(t *testing.T) {

func TestConfirmation_AskForConfirmation_manuallyConfirmed(t *testing.T) {
opts := Confirmation{
Confirmed: false,
Force: false,
}

r := strings.NewReader("yes")
Expand Down
7 changes: 6 additions & 1 deletion cmd/oras/manifest/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.OutputDescriptor && !opts.Confirmed {
if opts.OutputDescriptor && !opts.Force {
return errors.New("must apply --force to confirm the deletion if the descriptor is outputted")
}
return option.Parse(&opts)
Expand Down Expand Up @@ -90,6 +90,11 @@ func deleteManifest(opts deleteOptions) error {
desc, err := manifests.Resolve(ctx, opts.targetRef)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
if opts.Force && !opts.OutputDescriptor {
// ignore nonexistent
fmt.Println("Missing", opts.targetRef)
return nil
}
return fmt.Errorf("%s: the specified manifest does not exist", opts.targetRef)
}
return err
Expand Down
38 changes: 30 additions & 8 deletions test/e2e/suite/command/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
const (
pushContent = "test-blob"
pushDigest = "sha256:e1ca41574914ba00e8ed5c8fc78ec8efdfd48941c7e48ad74dad8ada7f2066d8"
wrongDigest = "sha256:e1ca41574914ba00e8ed5c8fc78ec8efdfd48941c7e48ad74dad8ada7f2066d9"
invalidDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
pushDescFmt = `{"mediaType":"%s","digest":"sha256:e1ca41574914ba00e8ed5c8fc78ec8efdfd48941c7e48ad74dad8ada7f2066d8","size":9}`
deleteDigest = "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9"
deleteDescriptor = `{"mediaType":"application/octet-stream","digest":"sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9","size":3}`
Expand Down Expand Up @@ -63,7 +63,7 @@ var _ = Describe("ORAS beginners:", func() {

It("should fail to push a blob from stdin if invalid digest provided", func() {
repo := fmt.Sprintf(repoFmt, "push", "invalid-stdin-digest")
ORAS("blob", "push", Reference(Host, repo, wrongDigest), "-", "--size", strconv.Itoa(len(pushContent))).
ORAS("blob", "push", Reference(Host, repo, invalidDigest), "-", "--size", strconv.Itoa(len(pushContent))).
WithInput(strings.NewReader(pushContent)).ExpectFailure().
Exec()
})
Expand All @@ -79,7 +79,7 @@ var _ = Describe("ORAS beginners:", func() {
It("should fail to push a blob from file if invalid digest provided", func() {
repo := fmt.Sprintf(repoFmt, "push", "invalid-stdin-size")
blobPath := WriteTempFile("blob", pushContent)
ORAS("blob", "push", Reference(Host, repo, wrongDigest), blobPath, "--size", strconv.Itoa(len(pushContent))).
ORAS("blob", "push", Reference(Host, repo, invalidDigest), blobPath, "--size", strconv.Itoa(len(pushContent))).
WithInput(strings.NewReader(pushContent)).ExpectFailure().
Exec()
})
Expand Down Expand Up @@ -139,7 +139,7 @@ var _ = Describe("ORAS beginners:", func() {
ORAS("blob", "fetch", Reference(Host, dstRepo, deleteDigest), "--output", "-").MatchContent(deleteContent).Exec()
})

It("should fail if no confirmation flag and descriptor flag is provided", func() {
It("should fail if no force flag and descriptor flag is provided", func() {
dstRepo := fmt.Sprintf(repoFmt, "delete", "no-confirm")
ORAS("cp", Reference(Host, Repo, FoobarImageDigest), Reference(Host, dstRepo, FoobarImageDigest)).Exec()
ORAS("blob", "delete", Reference(Host, dstRepo, deleteDigest), "--descriptor").ExpectFailure().Exec()
Expand All @@ -152,6 +152,22 @@ var _ = Describe("ORAS beginners:", func() {
ORAS("blob", "delete", fmt.Sprintf("%s/%s:%s", Host, dstRepo, "test"), "--descriptor", "--force").ExpectFailure().Exec()
ORAS("blob", "delete", fmt.Sprintf("%s/%s@%s", Host, dstRepo, "test"), "--descriptor", "--force").ExpectFailure().Exec()
})

It("should fail to delete a non-existent blob without force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("blob", "delete", toDeleteRef).
ExpectFailure().
MatchErrKeyWords(toDeleteRef, "the specified blob does not exist").
Exec()
})

It("should fail to delete a non-existent blob and output descriptor, with force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("blob", "delete", toDeleteRef, "--force", "--descriptor").
ExpectFailure().
MatchErrKeyWords(toDeleteRef, "the specified blob does not exist").
Exec()
})
})
})

Expand All @@ -166,19 +182,25 @@ var _ = Describe("Common registry users:", func() {
WithInput(strings.NewReader("y")).
MatchKeyWords("Deleted", toDeleteRef).Exec()
ORAS("blob", "delete", toDeleteRef).
WithDescription("validate").
WithInput(strings.NewReader("y")).
ExpectFailure().
MatchErrKeyWords("Error:", toDeleteRef, "the specified blob does not exist").Exec()
})

It("should delete a blob with confirmation flag and output descriptor", func() {
It("should delete a blob with force flag and output descriptor", func() {
dstRepo := fmt.Sprintf(repoFmt, "delete", "flag-confirmation")
ORAS("cp", Reference(Host, Repo, FoobarImageDigest), Reference(Host, dstRepo, FoobarImageDigest)).Exec()
toDeleteRef := Reference(Host, dstRepo, deleteDigest)
ORAS("blob", "delete", toDeleteRef, "--force", "--descriptor").MatchContent(deleteDescriptor).Exec()
ORAS("blob", "delete", toDeleteRef, "--force", "--descriptor").
ExpectFailure().
MatchErrKeyWords("Error:", toDeleteRef, "the specified blob does not exist").Exec()
ORAS("blob", "delete", toDeleteRef).WithDescription("validate").ExpectFailure().MatchErrKeyWords("Error:", toDeleteRef, "the specified blob does not exist").Exec()
})

It("should return success when deleting a non-existent blob with force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("blob", "delete", toDeleteRef, "--force").
MatchKeyWords("Missing", toDeleteRef).
Exec()
})
})
When("running `blob push`", func() {
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/suite/command/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ var _ = Describe("ORAS beginners:", func() {
ORAS("manifest", "delete", Reference(Host, dstRepo, tempTag), "--descriptor").ExpectFailure().Exec()
})

It("should fail to delete a non-existent manifest via digest without force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("manifest", "delete", toDeleteRef).
ExpectFailure().
MatchErrKeyWords(toDeleteRef, "the specified manifest does not exist").
Exec()
})

It("should fail to delete a non-existent manifest and output descriptor via digest, with force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("manifest", "delete", toDeleteRef, "--force", "--descriptor").
ExpectFailure().
MatchErrKeyWords(toDeleteRef, "the specified manifest does not exist").
Exec()
})

It("should fail to delete a non-existent manifest and output descriptor via tag, without force flag set", func() {
toDeleteRef := Reference(Host, Repo, "this.tag.should-not.be-existed")
ORAS("manifest", "delete", toDeleteRef, "--force", "--descriptor").
ExpectFailure().
MatchErrKeyWords(toDeleteRef, "the specified manifest does not exist").
Exec()
})

It("should fail if no blob reference provided", func() {
dstRepo := fmt.Sprintf(repoFmt, "delete", "no-reference")
prepare(Reference(Host, Repo, FoobarImageTag), Reference(Host, dstRepo, tempTag))
Expand Down Expand Up @@ -320,5 +344,12 @@ var _ = Describe("Common registry users:", func() {
WithDescription("cancel without confirmation").Exec()
validate(Reference(Host, dstRepo, ""), tempTag, true)
})

It("should succeed when deleting a non-existent manifest with force flag set", func() {
toDeleteRef := Reference(Host, Repo, invalidDigest)
ORAS("manifest", "delete", toDeleteRef, "--force").
MatchKeyWords("Missing", toDeleteRef).
Exec()
})
})
})

0 comments on commit 9db2066

Please sign in to comment.