Skip to content

Commit 3267dd9

Browse files
authored
kubectl delete: Introduce new interactive flag for interactive deletion (#114530)
1 parent 86038ae commit 3267dd9

File tree

6 files changed

+355
-35
lines changed

6 files changed

+355
-35
lines changed

staging/src/k8s.io/kubectl/pkg/cmd/delete/delete.go

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ type DeleteOptions struct {
121121
Quiet bool
122122
WarnClusterScope bool
123123
Raw string
124+
Interactive bool
124125

125126
GracePeriod int
126127
Timeout time.Duration
@@ -129,9 +130,11 @@ type DeleteOptions struct {
129130

130131
Output string
131132

132-
DynamicClient dynamic.Interface
133-
Mapper meta.RESTMapper
134-
Result *resource.Result
133+
DynamicClient dynamic.Interface
134+
Mapper meta.RESTMapper
135+
Result *resource.Result
136+
PreviewResult *resource.Result
137+
previewResourceMap map[cmdwait.ResourceLocation]struct{}
135138

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

200-
if len(o.Raw) == 0 {
201-
r := f.NewBuilder().
203+
// Set default WarningPrinter if not already set.
204+
if o.WarningPrinter == nil {
205+
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
206+
}
207+
208+
if len(o.Raw) != 0 {
209+
return nil
210+
}
211+
212+
r := f.NewBuilder().
213+
Unstructured().
214+
ContinueOnError().
215+
NamespaceParam(cmdNamespace).DefaultNamespace().
216+
FilenameParam(enforceNamespace, &o.FilenameOptions).
217+
LabelSelectorParam(o.LabelSelector).
218+
FieldSelectorParam(o.FieldSelector).
219+
SelectAllParam(o.DeleteAll).
220+
AllNamespaces(o.DeleteAllNamespaces).
221+
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
222+
Flatten().
223+
Do()
224+
err = r.Err()
225+
if err != nil {
226+
return err
227+
}
228+
o.Result = r
229+
230+
if o.Interactive {
231+
// preview result will be used to list resources for confirmation prior to actual delete.
232+
// We can not use r as result object because it can only be used once. But we need to traverse
233+
// twice. Parameters in preview result must be equal to genuine result.
234+
previewr := f.NewBuilder().
202235
Unstructured().
203236
ContinueOnError().
204237
NamespaceParam(cmdNamespace).DefaultNamespace().
@@ -210,26 +243,22 @@ func (o *DeleteOptions) Complete(f cmdutil.Factory, args []string, cmd *cobra.Co
210243
ResourceTypeOrNameArgs(false, args...).RequireObject(false).
211244
Flatten().
212245
Do()
213-
err = r.Err()
214-
if err != nil {
215-
return err
216-
}
217-
o.Result = r
218-
219-
o.Mapper, err = f.ToRESTMapper()
246+
err = previewr.Err()
220247
if err != nil {
221248
return err
222249
}
250+
o.PreviewResult = previewr
251+
o.previewResourceMap = make(map[cmdwait.ResourceLocation]struct{})
252+
}
223253

224-
o.DynamicClient, err = f.DynamicClient()
225-
if err != nil {
226-
return err
227-
}
254+
o.Mapper, err = f.ToRESTMapper()
255+
if err != nil {
256+
return err
228257
}
229258

230-
// Set default WarningPrinter if not already set.
231-
if o.WarningPrinter == nil {
232-
o.WarningPrinter = printers.NewWarningPrinter(o.ErrOut, printers.WarningPrinterOptions{Color: term.AllowsColorOutput(o.ErrOut)})
259+
o.DynamicClient, err = f.DynamicClient()
260+
if err != nil {
261+
return err
233262
}
234263

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

260-
if len(o.Raw) > 0 {
261-
if len(o.FilenameOptions.Filenames) > 1 {
262-
return fmt.Errorf("--raw can only use a single local file or stdin")
263-
} else if len(o.FilenameOptions.Filenames) == 1 {
264-
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
265-
return fmt.Errorf("--raw cannot read from a url")
266-
}
267-
}
289+
if len(o.Raw) == 0 {
290+
return nil
291+
}
268292

269-
if o.FilenameOptions.Recursive {
270-
return fmt.Errorf("--raw and --recursive are mutually exclusive")
271-
}
272-
if len(o.Output) > 0 {
273-
return fmt.Errorf("--raw and --output are mutually exclusive")
274-
}
275-
if _, err := url.ParseRequestURI(o.Raw); err != nil {
276-
return fmt.Errorf("--raw must be a valid URL path: %v", err)
293+
if o.Interactive {
294+
return fmt.Errorf("--interactive can not be used with --raw")
295+
}
296+
if len(o.FilenameOptions.Filenames) > 1 {
297+
return fmt.Errorf("--raw can only use a single local file or stdin")
298+
} else if len(o.FilenameOptions.Filenames) == 1 {
299+
if strings.Index(o.FilenameOptions.Filenames[0], "http://") == 0 || strings.Index(o.FilenameOptions.Filenames[0], "https://") == 0 {
300+
return fmt.Errorf("--raw cannot read from a url")
277301
}
278302
}
279303

304+
if o.FilenameOptions.Recursive {
305+
return fmt.Errorf("--raw and --recursive are mutually exclusive")
306+
}
307+
if len(o.Output) > 0 {
308+
return fmt.Errorf("--raw and --output are mutually exclusive")
309+
}
310+
if _, err := url.ParseRequestURI(o.Raw); err != nil {
311+
return fmt.Errorf("--raw must be a valid URL path: %v", err)
312+
}
313+
280314
return nil
281315
}
282316

@@ -291,6 +325,39 @@ func (o *DeleteOptions) RunDelete(f cmdutil.Factory) error {
291325
}
292326
return rawhttp.RawDelete(restClient, o.IOStreams, o.Raw, o.Filenames[0])
293327
}
328+
329+
if o.Interactive {
330+
previewInfos := []*resource.Info{}
331+
if o.IgnoreNotFound {
332+
o.PreviewResult = o.PreviewResult.IgnoreErrors(errors.IsNotFound)
333+
}
334+
err := o.PreviewResult.Visit(func(info *resource.Info, err error) error {
335+
if err != nil {
336+
return err
337+
}
338+
previewInfos = append(previewInfos, info)
339+
o.previewResourceMap[cmdwait.ResourceLocation{
340+
GroupResource: info.Mapping.Resource.GroupResource(),
341+
Namespace: info.Namespace,
342+
Name: info.Name,
343+
}] = struct{}{}
344+
345+
return nil
346+
})
347+
if err != nil {
348+
return err
349+
}
350+
if len(previewInfos) == 0 {
351+
fmt.Fprintf(o.Out, "No resources found\n")
352+
return nil
353+
}
354+
355+
if !o.confirmation(previewInfos) {
356+
fmt.Fprintf(o.Out, "deletion is cancelled\n")
357+
return nil
358+
}
359+
}
360+
294361
return o.DeleteResult(o.Result)
295362
}
296363

@@ -306,6 +373,18 @@ func (o *DeleteOptions) DeleteResult(r *resource.Result) error {
306373
if err != nil {
307374
return err
308375
}
376+
377+
if o.Interactive {
378+
if _, ok := o.previewResourceMap[cmdwait.ResourceLocation{
379+
GroupResource: info.Mapping.Resource.GroupResource(),
380+
Namespace: info.Namespace,
381+
Name: info.Name,
382+
}]; !ok {
383+
// resource not in the list of previewed resources based on resourceLocation
384+
return nil
385+
}
386+
}
387+
309388
deletedInfos = append(deletedInfos, info)
310389
found++
311390

@@ -440,3 +519,24 @@ func (o *DeleteOptions) PrintObj(info *resource.Info) {
440519
// understandable output by default
441520
fmt.Fprintf(o.Out, "%s \"%s\" %s\n", kindString, info.Name, operation)
442521
}
522+
523+
func (o *DeleteOptions) confirmation(infos []*resource.Info) bool {
524+
fmt.Fprintf(o.Out, i18n.T("You are about to delete the following %d resource(s):\n"), len(infos))
525+
for _, info := range infos {
526+
groupKind := info.Mapping.GroupVersionKind
527+
kindString := fmt.Sprintf("%s.%s", strings.ToLower(groupKind.Kind), groupKind.Group)
528+
if len(groupKind.Group) == 0 {
529+
kindString = strings.ToLower(groupKind.Kind)
530+
}
531+
532+
fmt.Fprintf(o.Out, "%s/%s\n", kindString, info.Name)
533+
}
534+
fmt.Fprintf(o.Out, i18n.T("Do you want to continue?")+" (y/n): ")
535+
var input string
536+
_, err := fmt.Fscan(o.In, &input)
537+
if err != nil {
538+
return false
539+
}
540+
541+
return strings.EqualFold(input, "y")
542+
}

staging/src/k8s.io/kubectl/pkg/cmd/delete/delete_flags.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type DeleteFlags struct {
4848
Wait *bool
4949
Output *string
5050
Raw *string
51+
Interactive *bool
5152
}
5253

5354
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
106107
if f.Raw != nil {
107108
options.Raw = *f.Raw
108109
}
110+
if f.Interactive != nil {
111+
options.Interactive = *f.Interactive
112+
}
109113

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

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

179189
filenames := []string{}
180190
recursive := false
@@ -198,6 +208,7 @@ func NewDeleteCommandFlags(usage string) *DeleteFlags {
198208
Wait: &wait,
199209
Output: &output,
200210
Raw: &raw,
211+
Interactive: &interactive,
201212
}
202213
}
203214

0 commit comments

Comments
 (0)