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 deleting a manifest from a remote registry #506

Merged
merged 19 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from 17 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
54 changes: 54 additions & 0 deletions cmd/oras/internal/option/confirmation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
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 (
"fmt"
"io"
"strings"

"github.com/spf13/pflag"
)

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

// ApplyFlags applies flags to a command flag set.
func (opts *Confirmation) ApplyFlags(fs *pflag.FlagSet) {
fs.BoolVarP(&opts.Confirmed, "yes", "y", false, "do not prompt for confirmation")
}

func (opts *Confirmation) AskForConfirmation(r io.Reader, prompt string) (bool, error) {
yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
if opts.Confirmed {
return true, nil
}

fmt.Print(prompt, "[y/N]")

var response string
if _, err := fmt.Fscanln(r, &response); err != nil {
return false, err
}

switch strings.ToLower(response) {
case "y", "yes":
return true, nil
default:
return false, nil
}
}
82 changes: 82 additions & 0 deletions cmd/oras/internal/option/confirmation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
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 (
"reflect"
"strings"
"testing"

"github.com/spf13/pflag"
)

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)
}
}

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

got, err := opts.AskForConfirmation(r, "")
if err != nil {
t.Fatal("Confirmation.AskForConfirmation() error =", err)
}
if !reflect.DeepEqual(got, true) {
t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, true)
}
}

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

r := strings.NewReader("yes")
got, err := opts.AskForConfirmation(r, "")
if err != nil {
t.Fatal("Confirmation.AskForConfirmation() error =", err)
}
if !reflect.DeepEqual(got, true) {
t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, true)
}

r = strings.NewReader("no")
got, err = opts.AskForConfirmation(r, "")
if err != nil {
t.Fatal("Confirmation.AskForConfirmation() error =", err)
}
if !reflect.DeepEqual(got, false) {
t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, false)
}
}

func TestConfirmation_AskForConfirmation_FscanlnErr(t *testing.T) {
opts := Confirmation{
Confirmed: false,
}
r := strings.NewReader("yes no")

_, err := opts.AskForConfirmation(r, "")
expected := "expected newline"
if err.Error() != expected {
t.Fatalf("AskForConfirmation() error = %v, wantErr %v", err, expected)
}
}
3 changes: 2 additions & 1 deletion cmd/oras/manifest/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ import (

func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "manifest [fetch]",
Use: "manifest [command]",
Short: "[Preview] Manifest operations",
}

cmd.AddCommand(
fetchCmd(),
deleteCmd(),
)
return cmd
}
119 changes: 119 additions & 0 deletions cmd/oras/manifest/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
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 manifest

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
"oras.land/oras-go/v2/errdef"
oerrors "oras.land/oras/cmd/oras/internal/errors"
"oras.land/oras/cmd/oras/internal/option"
)

type deleteOptions struct {
option.Common
option.Confirmation
option.Descriptor
option.Pretty
option.Remote

targetRef string
}

func deleteCmd() *cobra.Command {
var opts deleteOptions
cmd := &cobra.Command{
Use: "delete [flags] name[:tag|@digest]",
Short: "[Preview] Delete a manifest from remote registry",
Long: `[Preview] Delete a manifest from remote registry

** This command is in preview and under development. **

Example - Delete a manifest tagged with 'latest' from repository 'locahost:5000/hello':
oras manifest delete localhost:5000/hello:latest

Example - Delete a manifest without prompting confirmation:
oras manifest delete --yes localhost:5000/hello:latest

yuehaoliang marked this conversation as resolved.
Show resolved Hide resolved
Example - Delete a manifest and print its descriptor:
oras manifest delete --descriptor localhost:5000/hello:latest

Example - Delete a manifest by digest 'sha256:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9' from repository 'locahost:5000/hello':
oras manifest delete localhost:5000/hello@sha:99e4703fbf30916f549cd6bfa9cdbab614b5392fbe64fdee971359a77073cdf9
`,
Args: cobra.ExactArgs(1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Can we delete multiple manifests at the same time?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuehaoliang-microsoft Please create an issue for this question, and only address it in another PR after being well discussed.

Copy link
Contributor Author

@yuehaoliang yuehaoliang Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuehaoliang-microsoft Please create an issue for this question, and only address it in another PR after being well discussed.

Created issue #560 to follow up deleting multiple manifest in a single command.

PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.OutputDescriptor && !opts.Confirmed {
return errors.New("must apply --yes to confirm the deletion if the descriptor is outputted")
}
return opts.ReadPassword()
},
RunE: func(_ *cobra.Command, args []string) error {
opts.targetRef = args[0]
return deleteManifest(opts)
},
}

option.ApplyFlags(&opts, cmd.Flags())
return cmd
}

func deleteManifest(opts deleteOptions) error {
ctx, _ := opts.SetLoggerLevel()
repo, err := opts.NewRepository(opts.targetRef, opts.Common)
if err != nil {
return err
}

if repo.Reference.Reference == "" {
return oerrors.NewErrInvalidReference(repo.Reference)
}

manifests := repo.Manifests()
desc, err := manifests.Resolve(ctx, opts.targetRef)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
return fmt.Errorf("%s: the specified manifest does not exist", opts.targetRef)
}
return err
}

prompt := fmt.Sprintf("Are you sure you want to delete the manifest %q and all tags associated with it?", desc.Digest)
confirmed, err := opts.AskForConfirmation(os.Stdin, prompt)
if err != nil {
return err
}
if !confirmed {
return nil
}

if err = manifests.Delete(ctx, desc); err != nil {
return fmt.Errorf("failed to delete %s: %w", opts.targetRef, err)
}

if opts.OutputDescriptor {
descJSON, err := opts.Marshal(desc)
if err != nil {
return err
}
return opts.Output(os.Stdout, descJSON)
}

fmt.Println("Deleted", opts.targetRef)

return nil
}