diff --git a/docs/cmd/tkn_pipeline_delete.md b/docs/cmd/tkn_pipeline_delete.md index 18992ac61..c683b3404 100644 --- a/docs/cmd/tkn_pipeline_delete.md +++ b/docs/cmd/tkn_pipeline_delete.md @@ -28,7 +28,7 @@ or ### Options ``` - -a, --all Whether to delete related resources (pipelineruns) (default: false) + -a, --all Whether to also delete related resources (pipelineruns) (default: false) --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) -f, --force Whether to force deletion (default: false) -h, --help help for delete diff --git a/docs/cmd/tkn_pipelinerun_delete.md b/docs/cmd/tkn_pipelinerun_delete.md index b904a5715..e6e943fd5 100644 --- a/docs/cmd/tkn_pipelinerun_delete.md +++ b/docs/cmd/tkn_pipelinerun_delete.md @@ -32,6 +32,7 @@ or -f, --force Whether to force deletion (default: false) -h, --help help for delete -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. + -p, --pipeline string The name of a pipeline whose pipelineruns should be deleted (does not delete the pipeline) --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. ``` diff --git a/docs/cmd/tkn_task_delete.md b/docs/cmd/tkn_task_delete.md index 87f3825a1..16d3d85b4 100644 --- a/docs/cmd/tkn_task_delete.md +++ b/docs/cmd/tkn_task_delete.md @@ -28,7 +28,7 @@ or ### Options ``` - -a, --all Whether to delete related resources (taskruns) (default: false) + -a, --all Whether to also delete related resources (taskruns) (default: false) --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) -f, --force Whether to force deletion (default: false) -h, --help help for delete diff --git a/docs/cmd/tkn_taskrun_delete.md b/docs/cmd/tkn_taskrun_delete.md index cfc1bf888..67f9d927a 100644 --- a/docs/cmd/tkn_taskrun_delete.md +++ b/docs/cmd/tkn_taskrun_delete.md @@ -32,6 +32,7 @@ or -f, --force Whether to force deletion (default: false) -h, --help help for delete -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. + -t, --task string The name of a task whose taskruns should be deleted (does not delete the task) --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. ``` diff --git a/docs/man/man1/tkn-pipeline-delete.1 b/docs/man/man1/tkn-pipeline-delete.1 index 8a0da79a4..49d526585 100644 --- a/docs/man/man1/tkn-pipeline-delete.1 +++ b/docs/man/man1/tkn-pipeline-delete.1 @@ -21,7 +21,7 @@ Delete pipelines in a namespace .SH OPTIONS .PP \fB\-a\fP, \fB\-\-all\fP[=false] - Whether to delete related resources (pipelineruns) (default: false) + Whether to also delete related resources (pipelineruns) (default: false) .PP \fB\-\-allow\-missing\-template\-keys\fP[=true] diff --git a/docs/man/man1/tkn-pipelinerun-delete.1 b/docs/man/man1/tkn-pipelinerun-delete.1 index fb3ec24e3..c2da3af79 100644 --- a/docs/man/man1/tkn-pipelinerun-delete.1 +++ b/docs/man/man1/tkn-pipelinerun-delete.1 @@ -35,6 +35,10 @@ Delete pipelineruns in a namespace \fB\-o\fP, \fB\-\-output\fP="" Output format. One of: json|yaml|name|go\-template|go\-template\-file|template|templatefile|jsonpath|jsonpath\-file. +.PP +\fB\-p\fP, \fB\-\-pipeline\fP="" + The name of a pipeline whose pipelineruns should be deleted (does not delete the pipeline) + .PP \fB\-\-template\fP="" Template string or path to template file to use when \-o=go\-template, \-o=go\-template\-file. The template format is golang templates [ diff --git a/docs/man/man1/tkn-task-delete.1 b/docs/man/man1/tkn-task-delete.1 index 54145d7e8..c2d69dc09 100644 --- a/docs/man/man1/tkn-task-delete.1 +++ b/docs/man/man1/tkn-task-delete.1 @@ -21,7 +21,7 @@ Delete task resources in a namespace .SH OPTIONS .PP \fB\-a\fP, \fB\-\-all\fP[=false] - Whether to delete related resources (taskruns) (default: false) + Whether to also delete related resources (taskruns) (default: false) .PP \fB\-\-allow\-missing\-template\-keys\fP[=true] diff --git a/docs/man/man1/tkn-taskrun-delete.1 b/docs/man/man1/tkn-taskrun-delete.1 index 1342805ab..52b3bb422 100644 --- a/docs/man/man1/tkn-taskrun-delete.1 +++ b/docs/man/man1/tkn-taskrun-delete.1 @@ -35,6 +35,10 @@ Delete taskruns in a namespace \fB\-o\fP, \fB\-\-output\fP="" Output format. One of: json|yaml|name|go\-template|go\-template\-file|template|templatefile|jsonpath|jsonpath\-file. +.PP +\fB\-t\fP, \fB\-\-task\fP="" + The name of a task whose taskruns should be deleted (does not delete the task) + .PP \fB\-\-template\fP="" Template string or path to template file to use when \-o=go\-template, \-o=go\-template\-file. The template format is golang templates [ diff --git a/pkg/cmd/clustertask/delete.go b/pkg/cmd/clustertask/delete.go index 898585075..a1417935d 100644 --- a/pkg/cmd/clustertask/delete.go +++ b/pkg/cmd/clustertask/delete.go @@ -75,5 +75,7 @@ func deleteClusterTasks(s *cli.Stream, p cli.Params, tNames []string) error { d := deleter.New("ClusterTask", func(taskName string) error { return cs.Tekton.TektonV1alpha1().ClusterTasks().Delete(taskName, &metav1.DeleteOptions{}) }) - return d.Execute(s, tNames) + d.Delete(s, tNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/cmd/condition/delete.go b/pkg/cmd/condition/delete.go index fae3759f3..17b5b9a06 100644 --- a/pkg/cmd/condition/delete.go +++ b/pkg/cmd/condition/delete.go @@ -80,5 +80,7 @@ func deleteConditions(s *cli.Stream, p cli.Params, condNames []string) error { d := deleter.New("Condition", func(conditionName string) error { return cs.Tekton.TektonV1alpha1().Conditions(p.Namespace()).Delete(conditionName, &metav1.DeleteOptions{}) }) - return d.Execute(s, condNames) + d.Delete(s, condNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/cmd/eventlistener/delete.go b/pkg/cmd/eventlistener/delete.go index f5662d467..370ad41a7 100644 --- a/pkg/cmd/eventlistener/delete.go +++ b/pkg/cmd/eventlistener/delete.go @@ -81,5 +81,7 @@ func deleteEventListeners(s *cli.Stream, p cli.Params, elNames []string) error { d := deleter.New("EventListener", func(listenerName string) error { return cs.Triggers.TektonV1alpha1().EventListeners(p.Namespace()).Delete(listenerName, &metav1.DeleteOptions{}) }) - return d.Execute(s, elNames) + d.Delete(s, elNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/cmd/pipeline/delete.go b/pkg/cmd/pipeline/delete.go index cf119a473..73d5d6732 100644 --- a/pkg/cmd/pipeline/delete.go +++ b/pkg/cmd/pipeline/delete.go @@ -68,7 +68,7 @@ or } f.AddFlags(c) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") - c.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Whether to delete related resources (pipelineruns) (default: false)") + c.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Whether to also delete related resources (pipelineruns) (default: false)") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_pipeline") return c @@ -82,12 +82,15 @@ func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, p d := deleter.New("Pipeline", func(pipelineName string) error { return cs.Tekton.TektonV1alpha1().Pipelines(p.Namespace()).Delete(pipelineName, &metav1.DeleteOptions{}) }) + d.WithRelated("PipelineRun", pipelineRunLister(p, cs), func(pipelineRunName string) error { + return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) + }) + deletedPipelineNames := d.Delete(s, pNames) if opts.DeleteAll { - d.WithRelated("PipelineRun", pipelineRunLister(p, cs), func(pipelineRunName string) error { - return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) - }) + d.DeleteRelated(s, deletedPipelineNames) } - return d.Execute(s, pNames) + d.PrintSuccesses(s) + return d.Errors() } func pipelineRunLister(p cli.Params, cs *cli.Clients) func(string) ([]string, error) { diff --git a/pkg/cmd/pipeline/delete_test.go b/pkg/cmd/pipeline/delete_test.go index 55a36a495..f6144a17f 100644 --- a/pkg/cmd/pipeline/delete_test.go +++ b/pkg/cmd/pipeline/delete_test.go @@ -36,7 +36,7 @@ func TestPipelineDelete(t *testing.T) { clock := clockwork.NewFakeClock() seeds := make([]pipelinetest.Clients, 0) - for i := 0; i < 5; i++ { + for i := 0; i < 7; i++ { cs, _ := test.SeedTestData(t, pipelinetest.Data{ Pipelines: []*v1alpha1.Pipeline{ tb.Pipeline("pipeline", "ns", diff --git a/pkg/cmd/pipelineresource/delete.go b/pkg/cmd/pipelineresource/delete.go index f3ff6f731..c4393847f 100644 --- a/pkg/cmd/pipelineresource/delete.go +++ b/pkg/cmd/pipelineresource/delete.go @@ -82,5 +82,7 @@ func deleteResources(s *cli.Stream, p cli.Params, preNames []string) error { d := deleter.New("PipelineResource", func(resourceName string) error { return cs.Tekton.TektonV1alpha1().PipelineResources(p.Namespace()).Delete(resourceName, &metav1.DeleteOptions{}) }) - return d.Execute(s, preNames) + d.Delete(s, preNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/cmd/pipelinerun/delete.go b/pkg/cmd/pipelinerun/delete.go index 0e815b9b0..e0d615073 100644 --- a/pkg/cmd/pipelinerun/delete.go +++ b/pkg/cmd/pipelinerun/delete.go @@ -15,6 +15,7 @@ package pipelinerun import ( + "errors" "fmt" "github.com/spf13/cobra" @@ -27,7 +28,7 @@ import ( ) func deleteCommand(p cli.Params) *cobra.Command { - opts := &options.DeleteOptions{Resource: "pipelinerun", ForceDelete: false} + opts := &options.DeleteOptions{Resource: "pipelinerun", ForceDelete: false, ParentResource: "pipeline"} f := cliopts.NewPrintFlags("delete") eg := `Delete PipelineRuns with names 'foo' and 'bar' in namespace 'quux': @@ -43,7 +44,7 @@ or Aliases: []string{"rm"}, Short: "Delete pipelineruns in a namespace", Example: eg, - Args: cobra.MinimumNArgs(1), + Args: cobra.MinimumNArgs(0), SilenceUsage: true, Annotations: map[string]string{ "commandType": "main", @@ -63,22 +64,53 @@ or return err } - return deletePipelineRuns(s, p, args) + return deletePipelineRuns(s, p, args, opts.ParentResourceName) }, } f.AddFlags(c) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") + c.Flags().StringVarP(&opts.ParentResourceName, "pipeline", "p", "", "The name of a pipeline whose pipelineruns should be deleted (does not delete the pipeline)") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_pipelinerun") return c } -func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string) error { +func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, parentPipeline string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") } - d := deleter.New("PipelineRun", func(pipelineRunName string) error { - return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) - }) - return d.Execute(s, prNames) + var d *deleter.Deleter + if parentPipeline == "" { + d = deleter.New("PipelineRun", func(pipelineRunName string) error { + return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) + }) + d.Delete(s, prNames) + } else { + d = deleter.New("Pipeline", func(_ string) error { + return errors.New("the pipeline should not be deleted") + }) + d.WithRelated("PipelineRun", pipelineRunLister(p, cs), func(pipelineRunName string) error { + return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) + }) + d.DeleteRelated(s, []string{parentPipeline}) + } + d.PrintSuccesses(s) + return d.Errors() +} + +func pipelineRunLister(p cli.Params, cs *cli.Clients) func(string) ([]string, error) { + return func(pipelineName string) ([]string, error) { + lOpts := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("tekton.dev/pipeline=%s", pipelineName), + } + pipelineRuns, err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).List(lOpts) + if err != nil { + return nil, err + } + var names []string + for _, pr := range pipelineRuns.Items { + names = append(names, pr.Name) + } + return names, nil + } } diff --git a/pkg/cmd/pipelinerun/delete_test.go b/pkg/cmd/pipelinerun/delete_test.go index 1e096d234..4c1151e4f 100644 --- a/pkg/cmd/pipelinerun/delete_test.go +++ b/pkg/cmd/pipelinerun/delete_test.go @@ -130,6 +130,22 @@ func TestPipelineRunDelete(t *testing.T) { wantError: true, want: "failed to delete pipelinerun \"nonexistent\": pipelineruns.tekton.dev \"nonexistent\" not found", }, + { + name: "Attempt remove forgetting to include pipelinerun names", + command: []string{"rm", "-n", "ns"}, + input: seeds[2], + inputStream: nil, + wantError: true, + want: "must provide pipelineruns to delete or --pipeline flag", + }, + { + name: "Remove pipelineruns of a pipeline", + command: []string{"rm", "--pipeline", "pipeline", "-n", "ns"}, + input: seeds[0], + inputStream: strings.NewReader("y"), + wantError: false, + want: `Are you sure you want to delete all pipelineruns related to pipeline "pipeline" (y/n): `, + }, } for _, tp := range testParams { @@ -144,12 +160,13 @@ func TestPipelineRunDelete(t *testing.T) { out, err := test.ExecuteCommand(pipelinerun, tp.command...) if tp.wantError { if err == nil { - t.Errorf("Error expected here") + t.Errorf("error expected here") + } else { + test.AssertOutput(t, tp.want, err.Error()) } - test.AssertOutput(t, tp.want, err.Error()) } else { if err != nil { - t.Errorf("Unexpected Error") + t.Errorf("unexpected Error") } test.AssertOutput(t, tp.want, out) } diff --git a/pkg/cmd/task/delete.go b/pkg/cmd/task/delete.go index 87600e271..21d90521b 100644 --- a/pkg/cmd/task/delete.go +++ b/pkg/cmd/task/delete.go @@ -68,7 +68,7 @@ or } f.AddFlags(c) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") - c.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Whether to delete related resources (taskruns) (default: false)") + c.Flags().BoolVarP(&opts.DeleteAll, "all", "a", false, "Whether to also delete related resources (taskruns) (default: false)") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_task") return c @@ -82,12 +82,15 @@ func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNa d := deleter.New("Task", func(taskName string) error { return cs.Tekton.TektonV1alpha1().Tasks(p.Namespace()).Delete(taskName, &metav1.DeleteOptions{}) }) + d.WithRelated("TaskRun", taskRunLister(p, cs), func(taskRunName string) error { + return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) + }) + deletedTaskNames := d.Delete(s, taskNames) if opts.DeleteAll { - d.WithRelated("TaskRun", taskRunLister(p, cs), func(taskRunName string) error { - return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) - }) + d.DeleteRelated(s, deletedTaskNames) } - return d.Execute(s, taskNames) + d.PrintSuccesses(s) + return d.Errors() } func taskRunLister(p cli.Params, cs *cli.Clients) func(string) ([]string, error) { diff --git a/pkg/cmd/taskrun/delete.go b/pkg/cmd/taskrun/delete.go index b5b88b1e2..e5ab4d18d 100644 --- a/pkg/cmd/taskrun/delete.go +++ b/pkg/cmd/taskrun/delete.go @@ -15,6 +15,7 @@ package taskrun import ( + "errors" "fmt" "github.com/spf13/cobra" @@ -27,7 +28,7 @@ import ( ) func deleteCommand(p cli.Params) *cobra.Command { - opts := &options.DeleteOptions{Resource: "taskrun", ForceDelete: false} + opts := &options.DeleteOptions{Resource: "taskrun", ForceDelete: false, ParentResource: "task"} f := cliopts.NewPrintFlags("delete") eg := `Delete TaskRuns with names 'foo' and 'bar' in namespace 'quux': @@ -43,7 +44,7 @@ or Aliases: []string{"rm"}, Short: "Delete taskruns in a namespace", Example: eg, - Args: cobra.MinimumNArgs(1), + Args: cobra.MinimumNArgs(0), SilenceUsage: true, Annotations: map[string]string{ "commandType": "main", @@ -63,22 +64,53 @@ or return err } - return deleteTaskRuns(s, p, args) + return deleteTaskRuns(s, p, args, opts.ParentResourceName) }, } f.AddFlags(c) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") + c.Flags().StringVarP(&opts.ParentResourceName, "task", "t", "", "The name of a task whose taskruns should be deleted (does not delete the task)") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_taskrun") return c } -func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string) error { +func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, parentTask string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") } - d := deleter.New("TaskRun", func(taskRunName string) error { - return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) - }) - return d.Execute(s, trNames) + var d *deleter.Deleter + if parentTask == "" { + d = deleter.New("TaskRun", func(taskRunName string) error { + return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) + }) + d.Delete(s, trNames) + } else { + d = deleter.New("Task", func(_ string) error { + return errors.New("the task should not be deleted") + }) + d.WithRelated("TaskRun", taskRunLister(p, cs), func(taskRunName string) error { + return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) + }) + d.DeleteRelated(s, []string{parentTask}) + } + d.PrintSuccesses(s) + return d.Errors() +} + +func taskRunLister(p cli.Params, cs *cli.Clients) func(string) ([]string, error) { + return func(taskName string) ([]string, error) { + lOpts := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("tekton.dev/task=%s", taskName), + } + taskRuns, err := cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).List(lOpts) + if err != nil { + return nil, err + } + var names []string + for _, tr := range taskRuns.Items { + names = append(names, tr.Name) + } + return names, nil + } } diff --git a/pkg/cmd/taskrun/delete_test.go b/pkg/cmd/taskrun/delete_test.go index 9d2f1733f..c6cf30e2d 100644 --- a/pkg/cmd/taskrun/delete_test.go +++ b/pkg/cmd/taskrun/delete_test.go @@ -112,6 +112,22 @@ func TestTaskRunDelete(t *testing.T) { wantError: true, want: "failed to delete taskrun \"nonexistent\": taskruns.tekton.dev \"nonexistent\" not found", }, + { + name: "Attempt remove forgetting to include taskrun names", + command: []string{"rm", "-n", "ns"}, + input: seeds[2], + inputStream: nil, + wantError: true, + want: "must provide taskruns to delete or --task flag", + }, + { + name: "Remove taskruns of a task", + command: []string{"rm", "--task", "task", "-n", "ns"}, + input: seeds[0], + inputStream: strings.NewReader("y"), + wantError: false, + want: `Are you sure you want to delete all taskruns related to task "task" (y/n): `, + }, } for _, tp := range testParams { @@ -127,8 +143,9 @@ func TestTaskRunDelete(t *testing.T) { if tp.wantError { if err == nil { t.Errorf("error expected here") + } else { + test.AssertOutput(t, tp.want, err.Error()) } - test.AssertOutput(t, tp.want, err.Error()) } else { if err != nil { t.Errorf("unexpected Error") diff --git a/pkg/cmd/triggerbinding/delete.go b/pkg/cmd/triggerbinding/delete.go index 93476f268..35b9c53a8 100644 --- a/pkg/cmd/triggerbinding/delete.go +++ b/pkg/cmd/triggerbinding/delete.go @@ -81,5 +81,7 @@ func deleteTriggerBindings(s *cli.Stream, p cli.Params, tbNames []string) error d := deleter.New("TriggerBinding", func(bindingName string) error { return cs.Triggers.TektonV1alpha1().TriggerBindings(p.Namespace()).Delete(bindingName, &metav1.DeleteOptions{}) }) - return d.Execute(s, tbNames) + d.Delete(s, tbNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/cmd/triggertemplate/delete.go b/pkg/cmd/triggertemplate/delete.go index 55beb237d..02f58ca7e 100644 --- a/pkg/cmd/triggertemplate/delete.go +++ b/pkg/cmd/triggertemplate/delete.go @@ -81,5 +81,7 @@ func deleteTriggerTemplates(s *cli.Stream, p cli.Params, ttNames []string) error d := deleter.New("TriggerTemplate", func(templateName string) error { return cs.Triggers.TektonV1alpha1().TriggerTemplates(p.Namespace()).Delete(templateName, &metav1.DeleteOptions{}) }) - return d.Execute(s, ttNames) + d.Delete(s, ttNames) + d.PrintSuccesses(s) + return d.Errors() } diff --git a/pkg/helper/deleter/deleter.go b/pkg/helper/deleter/deleter.go index a6534417c..05027c423 100644 --- a/pkg/helper/deleter/deleter.go +++ b/pkg/helper/deleter/deleter.go @@ -46,9 +46,11 @@ func (d *Deleter) WithRelated(kind string, listFunc func(string) ([]string, erro d.deleteRelated = deleteFunc } -// Execute performs the deletion of resources and relations. Errors are aggregated -// and returned at the end of the func. -func (d *Deleter) Execute(streams *cli.Stream, resourceNames []string) error { +// Delete performs the deletion of resources. Errors are printed to stderr of +// the passed in streams struct and are also aggregated for later access +// with d.Errors(). The names of successfully deleted resources are +// returned. +func (d *Deleter) Delete(streams *cli.Stream, resourceNames []string) []string { for _, name := range resourceNames { if err := d.delete(name); err != nil { d.printAndAddError(streams, fmt.Errorf("failed to delete %s %q: %s", strings.ToLower(d.kind), name, err)) @@ -56,19 +58,24 @@ func (d *Deleter) Execute(streams *cli.Stream, resourceNames []string) error { d.successfulDeletes = append(d.successfulDeletes, name) } } + return d.successfulDeletes +} + +// DeleteRelated performs the deletion of resources related to d's kind. Errors are +// aggregated and can be accessed with d.Errors(). +func (d *Deleter) DeleteRelated(streams *cli.Stream, resourceNames []string) { if d.relatedKind != "" && d.listRelated != nil && d.deleteRelated != nil { - for _, name := range d.successfulDeletes { + for _, name := range resourceNames { d.deleteRelatedList(streams, name) } } - d.printSuccesses(streams) - return multierr.Combine(d.errors...) } // deleteRelatedList gets the list of resources related to resourceName using the // provided listFunc and then calls the deleteRelated func for each relation. func (d *Deleter) deleteRelatedList(streams *cli.Stream, resourceName string) { if related, err := d.listRelated(resourceName); err != nil { + err = fmt.Errorf("failed to list %ss: %s", strings.ToLower(d.relatedKind), err) d.printAndAddError(streams, err) } else { for _, subresource := range related { @@ -82,8 +89,8 @@ func (d *Deleter) deleteRelatedList(streams *cli.Stream, resourceName string) { } } -// printSuccesses writes success messages to the provided stdout stream. -func (d *Deleter) printSuccesses(streams *cli.Stream) { +// PrintSuccesses writes success messages to the provided stdout stream. +func (d *Deleter) PrintSuccesses(streams *cli.Stream) { if len(d.successfulRelatedDeletes) > 0 { fmt.Fprintf(streams.Out, "%ss deleted: %s\n", d.relatedKind, names.QuotedList(d.successfulRelatedDeletes)) } @@ -99,3 +106,8 @@ func (d *Deleter) printAndAddError(streams *cli.Stream, err error) { d.errors = append(d.errors, err) fmt.Fprintf(streams.Err, "%s\n", err) } + +// Errors returns any accumulated errors in the operation of this deleter. +func (d *Deleter) Errors() error { + return multierr.Combine(d.errors...) +} diff --git a/pkg/helper/deleter/deleter_test.go b/pkg/helper/deleter/deleter_test.go index 054481ae8..f85510cbf 100644 --- a/pkg/helper/deleter/deleter_test.go +++ b/pkg/helper/deleter/deleter_test.go @@ -8,7 +8,7 @@ import ( "github.com/tektoncd/cli/pkg/cli" ) -func TestExecute(t *testing.T) { +func TestDelete(t *testing.T) { for _, tc := range []struct { description string names []string @@ -32,20 +32,22 @@ func TestExecute(t *testing.T) { names: []string{"baz"}, expectedOut: "", expectedErr: "failed to delete foobar \"baz\": there was an unfortunate incident\n", - deleteFunc: func(string) error { return errors.New("there was an unfortunate incident") }, + deleteFunc: unsuccessfulDeleteFunc("there was an unfortunate incident"), }, { description: "prints multiple errors if multiple deletions fail", names: []string{"baz", "quux"}, expectedOut: "", expectedErr: "failed to delete foobar \"baz\": there was an unfortunate incident\nfailed to delete foobar \"quux\": there was an unfortunate incident\n", - deleteFunc: func(string) error { return errors.New("there was an unfortunate incident") }, + deleteFunc: unsuccessfulDeleteFunc("there was an unfortunate incident"), }} { t.Run(tc.description, func(t *testing.T) { stdout := &strings.Builder{} stderr := &strings.Builder{} streams := &cli.Stream{Out: stdout, Err: stderr} d := New("FooBar", tc.deleteFunc) - if err := d.Execute(streams, tc.names); err != nil { + d.Delete(streams, tc.names) + d.PrintSuccesses(streams) + if err := d.Errors(); err != nil { if tc.expectedErr == "" { t.Errorf("unexpected error: %v", err) } @@ -59,3 +61,137 @@ func TestExecute(t *testing.T) { }) } } + +func TestDeleteRelated(t *testing.T) { + for _, tc := range []struct { + description string + relatedKind string + listFunc func(string) ([]string, error) + deleteFunc func(string) error + expectedOut string + expectedErr string + }{{ + description: "doesnt print anything if no related are configured", + }, { + description: "prints success message with deleted relations", + relatedKind: "FooBarRun", + listFunc: successfulListFunc("fbr1", "fbr2"), + deleteFunc: successfulDeleteFunc(), + expectedOut: "FooBarRuns deleted: \"fbr1\", \"fbr2\"\n", + }, { + description: "prints error message with problems encountered during deletes", + relatedKind: "FooBarRun", + listFunc: successfulListFunc("fbr1"), + deleteFunc: unsuccessfulDeleteFunc("bad times"), + expectedErr: "failed to delete foobarrun \"fbr1\": bad times\n", + }, { + description: "prints error message with problems encountered during list", + relatedKind: "FooBarRun", + listFunc: unsuccessfulListFunc("bad times"), + deleteFunc: successfulDeleteFunc(), + expectedErr: "failed to list foobarruns: bad times\n", + }} { + t.Run(tc.description, func(t *testing.T) { + stdout := &strings.Builder{} + stderr := &strings.Builder{} + streams := &cli.Stream{Out: stdout, Err: stderr} + d := New("FooBar", successfulDeleteFunc()) + if tc.relatedKind != "" { + d.WithRelated(tc.relatedKind, tc.listFunc, tc.deleteFunc) + d.DeleteRelated(streams, []string{"foo"}) + } + d.PrintSuccesses(streams) + if err := d.Errors(); err != nil { + if tc.expectedErr == "" { + t.Errorf("unexpected error: %v", err) + } + } + if stdout.String() != tc.expectedOut { + t.Errorf("expected stdout %q received %q", tc.expectedOut, stdout.String()) + } + if stderr.String() != tc.expectedErr { + t.Errorf("expected stderr %q received %q", tc.expectedErr, stderr.String()) + } + }) + } +} + +func TestDeleteAndDeleteRelated(t *testing.T) { + for _, tc := range []struct { + description string + kind string + names []string + relatedKind string + deleteFunc func(string) error + listRelatedFunc func(string) ([]string, error) + deleteRelatedFunc func(string) error + expectedOut string + expectedErr string + }{{ + description: "doesnt print anything if no related are configured", + }, { + description: "prints success message with deleted and deleted relations", + kind: "FooBar", + names: []string{"fb1"}, + relatedKind: "FooBarRun", + deleteFunc: successfulDeleteFunc(), + listRelatedFunc: successfulListFunc("fbr1", "fbr2"), + deleteRelatedFunc: successfulDeleteFunc(), + expectedOut: "FooBarRuns deleted: \"fbr1\", \"fbr2\"\nFooBars deleted: \"fb1\"\n", + }, { + description: "prints error message with errors encountered during deletes and does not attempt to delete related", + kind: "FooBar", + names: []string{"fb1"}, + relatedKind: "FooBarRun", + deleteFunc: unsuccessfulDeleteFunc("bad times"), + listRelatedFunc: successfulListFunc("fbr1"), + deleteRelatedFunc: successfulDeleteFunc(), + expectedErr: "failed to delete foobar \"fb1\": bad times\n", + }} { + t.Run(tc.description, func(t *testing.T) { + stdout := &strings.Builder{} + stderr := &strings.Builder{} + streams := &cli.Stream{Out: stdout, Err: stderr} + d := New(tc.kind, tc.deleteFunc) + d.WithRelated(tc.relatedKind, tc.listRelatedFunc, tc.deleteRelatedFunc) + deletedNames := d.Delete(streams, tc.names) + d.DeleteRelated(streams, deletedNames) + d.PrintSuccesses(streams) + if err := d.Errors(); err != nil { + if tc.expectedErr == "" { + t.Errorf("unexpected error: %v", err) + } + } + if stdout.String() != tc.expectedOut { + t.Errorf("expected stdout %q received %q", tc.expectedOut, stdout.String()) + } + if stderr.String() != tc.expectedErr { + t.Errorf("expected stderr %q received %q", tc.expectedErr, stderr.String()) + } + }) + } +} + +func successfulDeleteFunc() func(string) error { + return func(string) error { + return nil + } +} + +func unsuccessfulDeleteFunc(message string) func(string) error { + return func(string) error { + return errors.New(message) + } +} + +func successfulListFunc(returnedNames ...string) func(string) ([]string, error) { + return func(string) ([]string, error) { + return returnedNames, nil + } +} + +func unsuccessfulListFunc(message string) func(string) ([]string, error) { + return func(string) ([]string, error) { + return nil, errors.New(message) + } +} diff --git a/pkg/helper/options/delete.go b/pkg/helper/options/delete.go index 6648f3660..23adf41bf 100644 --- a/pkg/helper/options/delete.go +++ b/pkg/helper/options/delete.go @@ -24,9 +24,11 @@ import ( ) type DeleteOptions struct { - Resource string - ForceDelete bool - DeleteAll bool + Resource string + ParentResource string + ParentResourceName string + ForceDelete bool + DeleteAll bool } func (o *DeleteOptions) CheckOptions(s *cli.Stream, resourceNames []string) error { @@ -35,9 +37,17 @@ func (o *DeleteOptions) CheckOptions(s *cli.Stream, resourceNames []string) erro } formattedNames := names.QuotedList(resourceNames) - if o.DeleteAll { + + if len(resourceNames) == 0 && o.ParentResource != "" && o.ParentResourceName == "" { + return fmt.Errorf("must provide %ss to delete or --%s flag", o.Resource, o.ParentResource) + } + + switch { + case o.ParentResource != "" && o.ParentResourceName != "": + fmt.Fprintf(s.Out, "Are you sure you want to delete all %ss related to %s %q (y/n): ", o.Resource, o.ParentResource, o.ParentResourceName) + case o.DeleteAll: fmt.Fprintf(s.Out, "Are you sure you want to delete %s and related resources %s (y/n): ", o.Resource, formattedNames) - } else { + default: fmt.Fprintf(s.Out, "Are you sure you want to delete %s %s (y/n): ", o.Resource, formattedNames) } diff --git a/pkg/helper/options/delete_test.go b/pkg/helper/options/delete_test.go index de27727dc..b870093af 100644 --- a/pkg/helper/options/delete_test.go +++ b/pkg/helper/options/delete_test.go @@ -89,6 +89,13 @@ func TestDeleteOptions(t *testing.T) { wantError: true, want: "canceled deleting testRes \"test1\", \"test2\"", }, + { + name: "Specify parent resource, answer y", + opt: &DeleteOptions{Resource: "testRes", ParentResource: "testParentRes", ParentResourceName: "my-test-resource-parent"}, + stream: &cli.Stream{In: strings.NewReader("y"), Out: os.Stdout}, + resourcesNames: []string{""}, + wantError: false, + }, } for _, tp := range testParams { @@ -100,7 +107,7 @@ func TestDeleteOptions(t *testing.T) { } test.AssertOutput(t, tp.want, err.Error()) } else if err != nil { - t.Fatal("unexpected Error") + t.Fatalf("unexpected Error: %v", err) } }) }