kubectl delete: Introduce new interactive flag for interactive deletion (#114530)
This commit is contained in:
		@@ -121,6 +121,7 @@ type DeleteOptions struct {
 | 
				
			|||||||
	Quiet               bool
 | 
						Quiet               bool
 | 
				
			||||||
	WarnClusterScope    bool
 | 
						WarnClusterScope    bool
 | 
				
			||||||
	Raw                 string
 | 
						Raw                 string
 | 
				
			||||||
 | 
						Interactive         bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	GracePeriod int
 | 
						GracePeriod int
 | 
				
			||||||
	Timeout     time.Duration
 | 
						Timeout     time.Duration
 | 
				
			||||||
@@ -132,6 +133,8 @@ type DeleteOptions struct {
 | 
				
			|||||||
	DynamicClient      dynamic.Interface
 | 
						DynamicClient      dynamic.Interface
 | 
				
			||||||
	Mapper             meta.RESTMapper
 | 
						Mapper             meta.RESTMapper
 | 
				
			||||||
	Result             *resource.Result
 | 
						Result             *resource.Result
 | 
				
			||||||
 | 
						PreviewResult      *resource.Result
 | 
				
			||||||
 | 
						previewResourceMap map[cmdwait.ResourceLocation]struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	genericiooptions.IOStreams
 | 
						genericiooptions.IOStreams
 | 
				
			||||||
	WarningPrinter *printers.WarningPrinter
 | 
						WarningPrinter *printers.WarningPrinter
 | 
				
			||||||
@@ -197,7 +200,15 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(o.Raw) == 0 {
 | 
						// 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().
 | 
						r := f.NewBuilder().
 | 
				
			||||||
		Unstructured().
 | 
							Unstructured().
 | 
				
			||||||
		ContinueOnError().
 | 
							ContinueOnError().
 | 
				
			||||||
@@ -216,6 +227,30 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	o.Result = r
 | 
						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().
 | 
				
			||||||
 | 
								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 = previewr.Err()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							o.PreviewResult = previewr
 | 
				
			||||||
 | 
							o.previewResourceMap = make(map[cmdwait.ResourceLocation]struct{})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	o.Mapper, err = f.ToRESTMapper()
 | 
						o.Mapper, err = f.ToRESTMapper()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -225,12 +260,6 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							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)})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -257,7 +286,13 @@ func (o *DeleteOptions) Validate() error {
 | 
				
			|||||||
		return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
 | 
							return fmt.Errorf("--force and --grace-period greater than 0 cannot be specified together")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(o.Raw) > 0 {
 | 
						if len(o.Raw) == 0 {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if o.Interactive {
 | 
				
			||||||
 | 
							return fmt.Errorf("--interactive can not be used with --raw")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if len(o.FilenameOptions.Filenames) > 1 {
 | 
						if len(o.FilenameOptions.Filenames) > 1 {
 | 
				
			||||||
		return fmt.Errorf("--raw can only use a single local file or stdin")
 | 
							return fmt.Errorf("--raw can only use a single local file or stdin")
 | 
				
			||||||
	} else if len(o.FilenameOptions.Filenames) == 1 {
 | 
						} else if len(o.FilenameOptions.Filenames) == 1 {
 | 
				
			||||||
@@ -275,7 +310,6 @@ func (o *DeleteOptions) Validate() error {
 | 
				
			|||||||
	if _, err := url.ParseRequestURI(o.Raw); err != nil {
 | 
						if _, err := url.ParseRequestURI(o.Raw); err != nil {
 | 
				
			||||||
		return fmt.Errorf("--raw must be a valid URL path: %v", err)
 | 
							return fmt.Errorf("--raw must be a valid URL path: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -291,6 +325,39 @@ func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
 | 
							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)
 | 
						return o.DeleteResult(o.Result)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -306,6 +373,18 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								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)
 | 
							deletedInfos = append(deletedInfos, info)
 | 
				
			||||||
		found++
 | 
							found++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -440,3 +519,24 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
 | 
				
			|||||||
	// understandable output by default
 | 
						// understandable output by default
 | 
				
			||||||
	fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
 | 
						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)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return strings.EqualFold(input, "y")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -48,6 +48,7 @@ type DeleteFlags struct {
 | 
				
			|||||||
	Wait              *bool
 | 
						Wait              *bool
 | 
				
			||||||
	Output            *string
 | 
						Output            *string
 | 
				
			||||||
	Raw               *string
 | 
						Raw               *string
 | 
				
			||||||
 | 
						Interactive       *bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) {
 | 
					func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams genericiooptions.IOStreams) (*DeleteOptions, error) {
 | 
				
			||||||
@@ -106,6 +107,9 @@ func (f *DeleteFlags) ToOptions(dynamicClient dynamic.Interface, streams generic
 | 
				
			|||||||
	if f.Raw != nil {
 | 
						if f.Raw != nil {
 | 
				
			||||||
		options.Raw = *f.Raw
 | 
							options.Raw = *f.Raw
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if f.Interactive != nil {
 | 
				
			||||||
 | 
							options.Interactive = *f.Interactive
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return options, nil
 | 
						return options, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -156,6 +160,11 @@ func (f *DeleteFlags) AddFlags(cmd *cobra.Command) {
 | 
				
			|||||||
	if f.Raw != nil {
 | 
						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.")
 | 
							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
 | 
					// NewDeleteCommandFlags provides default flags and values for use with the "delete" command
 | 
				
			||||||
@@ -175,6 +184,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
 | 
				
			|||||||
	timeout := time.Duration(0)
 | 
						timeout := time.Duration(0)
 | 
				
			||||||
	wait := true
 | 
						wait := true
 | 
				
			||||||
	raw := ""
 | 
						raw := ""
 | 
				
			||||||
 | 
						interactive := false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	filenames := []string{}
 | 
						filenames := []string{}
 | 
				
			||||||
	recursive := false
 | 
						recursive := false
 | 
				
			||||||
@@ -198,6 +208,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
 | 
				
			|||||||
		Wait:           &wait,
 | 
							Wait:           &wait,
 | 
				
			||||||
		Output:         &output,
 | 
							Output:         &output,
 | 
				
			||||||
		Raw:            &raw,
 | 
							Raw:            &raw,
 | 
				
			||||||
 | 
							Interactive:    &interactive,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ package delete
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
@@ -35,6 +36,7 @@ import (
 | 
				
			|||||||
	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
 | 
						cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
 | 
				
			||||||
	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | 
						cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | 
				
			||||||
	"k8s.io/kubectl/pkg/scheme"
 | 
						"k8s.io/kubectl/pkg/scheme"
 | 
				
			||||||
 | 
						"k8s.io/utils/pointer"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func fakecmd() *cobra.Command {
 | 
					func fakecmd() *cobra.Command {
 | 
				
			||||||
@@ -47,6 +49,49 @@ func fakecmd() *cobra.Command {
 | 
				
			|||||||
	return cmd
 | 
						return cmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDeleteFlagValidation(t *testing.T) {
 | 
				
			||||||
 | 
						f := cmdtesting.NewTestFactory()
 | 
				
			||||||
 | 
						defer f.Cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							flags        DeleteFlags
 | 
				
			||||||
 | 
							enableAlphas []cmdutil.FeatureGate
 | 
				
			||||||
 | 
							args         [][]string
 | 
				
			||||||
 | 
							expectedErr  string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								flags: DeleteFlags{
 | 
				
			||||||
 | 
									Raw:         pointer.String("test"),
 | 
				
			||||||
 | 
									Interactive: pointer.Bool(true),
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								enableAlphas: []cmdutil.FeatureGate{cmdutil.InteractiveDelete},
 | 
				
			||||||
 | 
								expectedErr:  "--interactive can not be used with --raw",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							cmd := fakecmd()
 | 
				
			||||||
 | 
							cmdtesting.WithAlphaEnvs(test.enableAlphas, t, func(t *testing.T) {
 | 
				
			||||||
 | 
								deleteOptions, err := test.flags.ToOptions(nil, genericiooptions.NewTestIOStreamsDiscard())
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("unexpected error creating delete options: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
 | 
				
			||||||
 | 
								err = deleteOptions.Complete(f, nil, cmd)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("unexpected error creating delete options: %s", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								err = deleteOptions.Validate()
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									t.Fatalf("missing expected error")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.expectedErr != err.Error() {
 | 
				
			||||||
 | 
									t.Errorf("expected error %s, got %s", test.expectedErr, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestDeleteObjectByTuple(t *testing.T) {
 | 
					func TestDeleteObjectByTuple(t *testing.T) {
 | 
				
			||||||
	cmdtesting.InitTestErrorHandler(t)
 | 
						cmdtesting.InitTestErrorHandler(t)
 | 
				
			||||||
	_, _, rc := cmdtesting.TestData()
 | 
						_, _, rc := cmdtesting.TestData()
 | 
				
			||||||
@@ -263,6 +308,90 @@ func TestDeleteObject(t *testing.T) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPreviewResultEqualToResult(t *testing.T) {
 | 
				
			||||||
 | 
						deleteFlags := NewDeleteCommandFlags("")
 | 
				
			||||||
 | 
						deleteFlags.Interactive = pointer.Bool(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tf := cmdtesting.NewTestFactory().WithNamespace("test")
 | 
				
			||||||
 | 
						defer tf.Cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						streams, _, _, _ := genericiooptions.NewTestIOStreams()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deleteOptions, err := deleteFlags.ToOptions(nil, streams)
 | 
				
			||||||
 | 
						deleteOptions.Filenames = []string{"../../../testdata/redis-master-controller.yaml"}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("unexpected error %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = deleteOptions.Complete(tf, nil, fakecmd())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("unexpected error %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						infos, err := deleteOptions.Result.Infos()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("unexpected error %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						previewInfos, err := deleteOptions.PreviewResult.Infos()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("unexpected error %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(infos) != len(previewInfos) {
 | 
				
			||||||
 | 
							t.Errorf("result and previewResult must match")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDeleteObjectWithInteractive(t *testing.T) {
 | 
				
			||||||
 | 
						cmdtesting.InitTestErrorHandler(t)
 | 
				
			||||||
 | 
						_, _, rc := cmdtesting.TestData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tf := cmdtesting.NewTestFactory().WithNamespace("test")
 | 
				
			||||||
 | 
						defer tf.Cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tf.UnstructuredClient = &fake.RESTClient{
 | 
				
			||||||
 | 
							NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
 | 
				
			||||||
 | 
							Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
								switch p, m := req.URL.Path, req.Method; {
 | 
				
			||||||
 | 
								case p == "/namespaces/test/replicationcontrollers/redis-master" && m == "DELETE":
 | 
				
			||||||
 | 
									return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &rc.Items[0])}, nil
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
 | 
				
			||||||
 | 
									return nil, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmdtesting.WithAlphaEnvs([]cmdutil.FeatureGate{cmdutil.InteractiveDelete}, t, func(t *testing.T) {
 | 
				
			||||||
 | 
							streams, in, buf, _ := genericiooptions.NewTestIOStreams()
 | 
				
			||||||
 | 
							fmt.Fprint(in, "y")
 | 
				
			||||||
 | 
							cmd := NewCmdDelete(tf, streams)
 | 
				
			||||||
 | 
							cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
 | 
				
			||||||
 | 
							cmd.Flags().Set("output", "name")
 | 
				
			||||||
 | 
							cmd.Flags().Set("interactive", "true")
 | 
				
			||||||
 | 
							cmd.Run(cmd, []string{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): replicationcontroller/redis-master\n" {
 | 
				
			||||||
 | 
								t.Errorf("unexpected output: %s", buf.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							streams, in, buf, _ = genericiooptions.NewTestIOStreams()
 | 
				
			||||||
 | 
							fmt.Fprint(in, "n")
 | 
				
			||||||
 | 
							cmd = NewCmdDelete(tf, streams)
 | 
				
			||||||
 | 
							cmd.Flags().Set("filename", "../../../testdata/redis-master-controller.yaml")
 | 
				
			||||||
 | 
							cmd.Flags().Set("output", "name")
 | 
				
			||||||
 | 
							cmd.Flags().Set("interactive", "true")
 | 
				
			||||||
 | 
							cmd.Run(cmd, []string{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if buf.String() != "You are about to delete the following 1 resource(s):\nreplicationcontroller/redis-master\nDo you want to continue? (y/n): deletion is cancelled\n" {
 | 
				
			||||||
 | 
								t.Errorf("unexpected output: %s", buf.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if buf.String() == ": replicationcontroller/redis-master\n" {
 | 
				
			||||||
 | 
								t.Errorf("unexpected output: %s", buf.String())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestGracePeriodScenarios(t *testing.T) {
 | 
					func TestGracePeriodScenarios(t *testing.T) {
 | 
				
			||||||
	pods, _, _ := cmdtesting.TestData()
 | 
						pods, _, _ := cmdtesting.TestData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -428,6 +428,7 @@ const (
 | 
				
			|||||||
	ApplySet              FeatureGate = "KUBECTL_APPLYSET"
 | 
						ApplySet              FeatureGate = "KUBECTL_APPLYSET"
 | 
				
			||||||
	ExplainOpenapiV3      FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3"
 | 
						ExplainOpenapiV3      FeatureGate = "KUBECTL_EXPLAIN_OPENAPIV3"
 | 
				
			||||||
	CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW"
 | 
						CmdPluginAsSubcommand FeatureGate = "KUBECTL_ENABLE_CMD_SHADOW"
 | 
				
			||||||
 | 
						InteractiveDelete     FeatureGate = "KUBECTL_INTERACTIVE_DELETE"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (f FeatureGate) IsEnabled() bool {
 | 
					func (f FeatureGate) IsEnabled() bool {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,3 +52,75 @@ run_kubectl_delete_allnamespaces_tests() {
 | 
				
			|||||||
  set +o nounset
 | 
					  set +o nounset
 | 
				
			||||||
  set +o errexit
 | 
					  set +o errexit
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Runs tests related to kubectl delete --confirm
 | 
				
			||||||
 | 
					run_kubectl_delete_interactive_tests() {
 | 
				
			||||||
 | 
					  set -o nounset
 | 
				
			||||||
 | 
					  set -o errexit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # enable interactivity flag feature environment variable
 | 
				
			||||||
 | 
					  export KUBECTL_INTERACTIVE_DELETE=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ns_one="namespace-$(date +%s)-${RANDOM}"
 | 
				
			||||||
 | 
					  ns_two="namespace-$(date +%s)-${RANDOM}"
 | 
				
			||||||
 | 
					  kubectl create namespace "${ns_one}"
 | 
				
			||||||
 | 
					  kubectl create namespace "${ns_two}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # create configmaps
 | 
				
			||||||
 | 
					  kubectl create configmap "one" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kubectl create configmap "two" --namespace="${ns_two}"
 | 
				
			||||||
 | 
					  kubectl label configmap "one" --namespace="${ns_one}" deletetest=true
 | 
				
			||||||
 | 
					  kubectl label configmap "two" --namespace="${ns_two}" deletetest=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # not confirm dry-run=server deletions
 | 
				
			||||||
 | 
					  output_message=$(kubectl delete configmap --dry-run=server --interactive -l deletetest=true --all-namespaces <<< $'n\n')
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'configmap/two' 'configmap/one'
 | 
				
			||||||
 | 
					  # confirm dry-run=server deletions
 | 
				
			||||||
 | 
					  kubectl delete configmap --dry-run=server --interactive -l deletetest=true --all-namespaces <<< $'y\n'
 | 
				
			||||||
 | 
					  # not confirm resource deletions
 | 
				
			||||||
 | 
					  output_message=$(kubectl delete configmap --interactive -l deletetest=true --all-namespaces <<< $'n\n')
 | 
				
			||||||
 | 
					  kube::test::if_has_string "${output_message}" 'configmap/two' 'configmap/one'
 | 
				
			||||||
 | 
					  kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kube::test::get_object_assert 'configmap -l deletetest' "{{range.items}}{{${id_field:?}}}:{{end}}" 'one:'
 | 
				
			||||||
 | 
					  kubectl config set-context "${CONTEXT}" --namespace="${ns_two}"
 | 
				
			||||||
 | 
					  kube::test::get_object_assert 'configmap -l deletetest' "{{range.items}}{{${id_field:?}}}:{{end}}" 'two:'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # clean configmaps with label deletetest=true
 | 
				
			||||||
 | 
					  kubectl delete configmap -l deletetest=true --all-namespaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # create new configmaps
 | 
				
			||||||
 | 
					  kubectl create configmap "third" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kubectl create configmap "fourth" --namespace="${ns_two}"
 | 
				
			||||||
 | 
					  kubectl label configmap "third" --namespace="${ns_one}" deletetest2=true
 | 
				
			||||||
 | 
					  kubectl label configmap "fourth" --namespace="${ns_two}" deletetest2=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # confirm all resource deletions with waiting
 | 
				
			||||||
 | 
					  kubectl delete configmaps --interactive -l deletetest2=true --all-namespaces --wait <<< $'y\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # no configmaps should be in either of those namespaces with label deletetest2
 | 
				
			||||||
 | 
					  kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kube::test::get_object_assert 'configmap -l deletetest2' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
 | 
				
			||||||
 | 
					  kubectl config set-context "${CONTEXT}" --namespace="${ns_two}"
 | 
				
			||||||
 | 
					  kube::test::get_object_assert 'configmap -l deletetest2' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # clean configmaps with label deletetest2=true
 | 
				
			||||||
 | 
					  kubectl delete configmap -l deletetest2=true --all-namespaces
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # create new configmaps in one namespace
 | 
				
			||||||
 | 
					  kubectl create configmap "fifth" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kubectl create configmap "sixth" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kubectl label configmap "fifth" --namespace="${ns_one}" deletetest3=true
 | 
				
			||||||
 | 
					  kubectl label configmap "sixth" --namespace="${ns_one}" deletetest3=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # confirm all resource deletions with forcing and waiting
 | 
				
			||||||
 | 
					  kubectl delete configmaps -l deletetest3=true --force --interactive --namespace="${ns_one}" --wait <<< $'y\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # no configmaps should be in either of those namespaces with label deletetest3
 | 
				
			||||||
 | 
					  kubectl config set-context "${CONTEXT}" --namespace="${ns_one}"
 | 
				
			||||||
 | 
					  kube::test::get_object_assert 'configmap -l deletetest3' "{{range.items}}{{${id_field:?}}}:{{end}}" ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  unset KUBECTL_INTERACTIVE_DELETE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  set +o nounset
 | 
				
			||||||
 | 
					  set +o errexit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -618,6 +618,13 @@ runTests() {
 | 
				
			|||||||
    record_command run_kubectl_delete_allnamespaces_tests
 | 
					    record_command run_kubectl_delete_allnamespaces_tests
 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ######################
 | 
				
			||||||
 | 
					  # Delete --interactive   #
 | 
				
			||||||
 | 
					  ######################
 | 
				
			||||||
 | 
					  if kube::test::if_supports_resource "${configmaps}" ; then
 | 
				
			||||||
 | 
					    record_command run_kubectl_delete_interactive_tests
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ##################
 | 
					  ##################
 | 
				
			||||||
  # Global timeout #
 | 
					  # Global timeout #
 | 
				
			||||||
  ##################
 | 
					  ##################
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user