Skip to content

kubectl delete: Introduce new interactive flag for interactive deletion #114530

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

Merged
merged 1 commit into from
Jul 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
170 changes: 135 additions & 35 deletions staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ type DeleteOptions struct {
Quiet bool
WarnClusterScope bool
Raw string
Interactive bool

GracePeriod int
Timeout time.Duration
Expand All @@ -129,9 +130,11 @@ type DeleteOptions struct {

Output string

DynamicClient dynamic.Interface
Mapper meta.RESTMapper
Result *resource.Result
DynamicClient dynamic.Interface
Mapper meta.RESTMapper
Result *resource.Result
PreviewResult *resource.Result
previewResourceMap map[cmdwait.ResourceLocation]struct{}

genericiooptions.IOStreams
WarningPrinter *printers.WarningPrinter
Expand Down Expand Up @@ -197,8 +200,38 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
return err
}

if len(o.Raw) == 0 {
r := f.NewBuilder().
// Set default WarningPrinter if not already set.
if o.WarningPrinter == nil {
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
}

if len(o.Raw) != 0 {
return nil
}

r := f.NewBuilder().
Unstructured().
ContinueOnError().
NamespaceParam(cmdNamespace).DefaultNamespace().
FilenameParam(enforceNamespace, &o.FilenameOptions).
LabelSelectorParam(o.LabelSelector).
FieldSelectorParam(o.FieldSelector).
SelectAllParam(o.DeleteAll).
AllNamespaces(o.DeleteAllNamespaces).
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
Flatten().
Do()
err = r.Err()
if err != nil {
return err
}
o.Result = r

if o.Interactive {
// preview result will be used to list resources for confirmation prior to actual delete.
// We can not use r as result object because it can only be used once. But we need to traverse
// twice. Parameters in preview result must be equal to genuine result.
previewr := f.NewBuilder().
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit 😉

Suggested change
previewr := f.NewBuilder().
previewer := f.NewBuilder().

Unstructured().
ContinueOnError().
NamespaceParam(cmdNamespace).DefaultNamespace().
Expand All @@ -210,26 +243,22 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
Flatten().
Do()
err = r.Err()
if err != nil {
return err
}
o.Result = r

o.Mapper, err = f.ToRESTMapper()
err = previewr.Err()
if err != nil {
return err
}
o.PreviewResult = previewr
o.previewResourceMap = make(map[cmdwait.ResourceLocation]struct{})
}

o.DynamicClient, err = f.DynamicClient()
if err != nil {
return err
}
o.Mapper, err = f.ToRESTMapper()
if err != nil {
return err
}

// Set default WarningPrinter if not already set.
if o.WarningPrinter == nil {
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
o.DynamicClient, err = f.DynamicClient()
if err != nil {
return err
}

return nil
Expand Down Expand Up @@ -257,26 +286,31 @@ func (o *DeleteOptions) Validate() error {
return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
}

if len(o.Raw) > 0 {
if len(o.FilenameOptions.Filenames) > 1 {
return fmt.Errorf("--raw can only use a single local file or stdin")
} else if len(o.FilenameOptions.Filenames) == 1 {
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
return fmt.Errorf("--raw cannot read from a url")
}
}
if len(o.Raw) == 0 {
return nil
}

if o.FilenameOptions.Recursive {
return fmt.Errorf("--raw and --recursive are mutually exclusive")
}
if len(o.Output) > 0 {
return fmt.Errorf("--raw and --output are mutually exclusive")
}
if _, err := url.ParseRequestURI(o.Raw); err != nil {
return fmt.Errorf("--raw must be a valid URL path: %v", err)
if o.Interactive {
return fmt.Errorf("--interactive can not be used with --raw")
}
if len(o.FilenameOptions.Filenames) > 1 {
return fmt.Errorf("--raw can only use a single local file or stdin")
} else if len(o.FilenameOptions.Filenames) == 1 {
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
return fmt.Errorf("--raw cannot read from a url")
}
}

if o.FilenameOptions.Recursive {
return fmt.Errorf("--raw and --recursive are mutually exclusive")
}
if len(o.Output) > 0 {
return fmt.Errorf("--raw and --output are mutually exclusive")
}
if _, err := url.ParseRequestURI(o.Raw); err != nil {
return fmt.Errorf("--raw must be a valid URL path: %v", err)
}

return nil
}

Expand All @@ -291,6 +325,39 @@ func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
}
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
}

if o.Interactive {
previewInfos := []*resource.Info{}
if o.IgnoreNotFound {
o.PreviewResult = o.PreviewResult.IgnoreErrors(errors.IsNotFound)
}
err := o.PreviewResult.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
previewInfos = append(previewInfos, info)
o.previewResourceMap[cmdwait.ResourceLocation{
GroupResource: info.Mapping.Resource.GroupResource(),
Namespace: info.Namespace,
Name: info.Name,
}] = struct{}{}

return nil
})
if err != nil {
return err
}
if len(previewInfos) == 0 {
fmt.Fprintf(o.Out, "No resources found\n")
return nil
}

if !o.confirmation(previewInfos) {
fmt.Fprintf(o.Out, "deletion is cancelled\n")
return nil
}
}

return o.DeleteResult(o.Result)
}

Expand All @@ -306,6 +373,18 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
if err != nil {
return err
}

if o.Interactive {
if _, ok := o.previewResourceMap[cmdwait.ResourceLocation{
GroupResource: info.Mapping.Resource.GroupResource(),
Namespace: info.Namespace,
Name: info.Name,
}]; !ok {
// resource not in the list of previewed resources based on resourceLocation
return nil
}
}

deletedInfos = append(deletedInfos, info)
found++

Expand Down Expand Up @@ -440,3 +519,24 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
// understandable output by default
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
}

func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos))
for _, info := range infos {
groupKind := info.Mapping.GroupVersionKind
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
if len(groupKind.Group) == 0 {
kindString = strings.ToLower(groupKind.Kind)
}

fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
}
fmt.Fprintf(o.Out, i18n.T("Do you want to continue?")+" (y/n): ")
var input string
_, err := fmt.Fscan(o.In, &input)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably better to use fmt.Fscanln or r.ReadString('\n') to ensure that it doesn't hang if we don't specify anything. Currently pressing enter will be stuck until you push any button.

Can be addressed in followup.

if err != nil {
return false
}

return strings.EqualFold(input, "y")
}
11 changes: 11 additions & 0 deletions staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type DeleteFlags struct {
Wait *bool
Output *string
Raw *string
Interactive *bool
}

func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) {
Expand Down Expand Up @@ -106,6 +107,9 @@ func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams generic
if f.Raw != nil {
options.Raw = *f.Raw
}
if f.Interactive != nil {
options.Interactive = *f.Interactive
}

return options, nil
}
Expand Down Expand Up @@ -156,6 +160,11 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
if f.Raw != nil {
cmd.Flags().StringVar(f.Raw, "raw", *f.Raw, "Raw URI to DELETE to the server. Uses the transport specified by the kubeconfig file.")
}
if cmdutil.InteractiveDelete.IsEnabled() {
if f.Interactive != nil {
cmd.Flags().BoolVarP(f.Interactive, "interactive", "i", *f.Interactive, "If true, delete resource only when user confirms. This flag is in Alpha.")
}
}
}

// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
Expand All @@ -175,6 +184,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
timeout := time.Duration(0)
wait := true
raw := ""
interactive := false

filenames := []string{}
recursive := false
Expand All @@ -198,6 +208,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
Wait: &wait,
Output: &output,
Raw: &raw,
Interactive: &interactive,
}
}

Expand Down
Loading