From 0beadbb86550e20869217c47dc46b34e92286f4a Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 9 Jan 2020 11:42:16 -0500 Subject: [PATCH] Refactor delete command code to make a bit more DRY While implementing multiple delete I noticed that each delete command was implementing its own error handling and messaging even though each was doing exactly the same thing. This made working on the code cumbersome - I had to double check that I'd added each of my changes correactly across every repition of the delete command. So this commit introduces a helper type, Deleter, that does most of the repetitive work and is reused across all the delete commands. --- pkg/cmd/clustertask/delete.go | 26 ++------ pkg/cmd/clustertask/delete_test.go | 2 +- pkg/cmd/condition/delete.go | 24 ++----- pkg/cmd/eventlistener/delete.go | 25 ++----- pkg/cmd/pipeline/delete.go | 58 +++++------------ pkg/cmd/pipelineresource/delete.go | 23 ++----- pkg/cmd/pipelinerun/delete.go | 25 ++----- pkg/cmd/task/delete.go | 59 +++++------------ pkg/cmd/taskrun/delete.go | 24 ++----- pkg/cmd/triggerbinding/delete.go | 25 ++----- pkg/cmd/triggertemplate/delete.go | 25 ++----- pkg/helper/deleter/deleter.go | 101 +++++++++++++++++++++++++++++ pkg/helper/deleter/deleter_test.go | 61 +++++++++++++++++ 13 files changed, 238 insertions(+), 240 deletions(-) create mode 100644 pkg/helper/deleter/deleter.go create mode 100644 pkg/helper/deleter/deleter_test.go diff --git a/pkg/cmd/clustertask/delete.go b/pkg/cmd/clustertask/delete.go index 559b55e47..898585075 100644 --- a/pkg/cmd/clustertask/delete.go +++ b/pkg/cmd/clustertask/delete.go @@ -19,9 +19,8 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -64,7 +63,6 @@ or } f.AddFlags(c) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") - _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_clustertasks") return c } @@ -74,22 +72,8 @@ func deleteClusterTasks(s *cli.Stream, p cli.Params, tNames []string) error { if err != nil { return fmt.Errorf("Failed to create tekton client") } - - var errs []error - var success []string - for _, tName := range tNames { - if err := cs.Tekton.TektonV1alpha1().ClusterTasks().Delete(tName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("Failed to delete clustertask %q: %s", tName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, tName) - } - - if len(success) > 0 { - fmt.Fprintf(s.Out, "ClusterTasks deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("ClusterTask", func(taskName string) error { + return cs.Tekton.TektonV1alpha1().ClusterTasks().Delete(taskName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, tNames) } diff --git a/pkg/cmd/clustertask/delete_test.go b/pkg/cmd/clustertask/delete_test.go index 353dc41a4..556175299 100644 --- a/pkg/cmd/clustertask/delete_test.go +++ b/pkg/cmd/clustertask/delete_test.go @@ -86,7 +86,7 @@ func TestClusterTaskDelete(t *testing.T) { input: seeds[2], inputStream: strings.NewReader("y"), wantError: true, - want: "Failed to delete clustertask \"nonexistent\": clustertasks.tekton.dev \"nonexistent\" not found", + want: "failed to delete clustertask \"nonexistent\": clustertasks.tekton.dev \"nonexistent\" not found", }, } diff --git a/pkg/cmd/condition/delete.go b/pkg/cmd/condition/delete.go index f54d17dc8..fae3759f3 100644 --- a/pkg/cmd/condition/delete.go +++ b/pkg/cmd/condition/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validate "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -78,21 +77,8 @@ func deleteConditions(s *cli.Stream, p cli.Params, condNames []string) error { if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - for _, condName := range condNames { - if err := cs.Tekton.TektonV1alpha1().Conditions(p.Namespace()).Delete(condName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete condition %q: %s", condName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, condName) - } - if len(success) > 0 { - fmt.Fprintf(s.Out, "Conditions deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("Condition", func(conditionName string) error { + return cs.Tekton.TektonV1alpha1().Conditions(p.Namespace()).Delete(conditionName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, condNames) } diff --git a/pkg/cmd/eventlistener/delete.go b/pkg/cmd/eventlistener/delete.go index 74da91615..f5662d467 100644 --- a/pkg/cmd/eventlistener/delete.go +++ b/pkg/cmd/eventlistener/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -79,22 +78,8 @@ func deleteEventListeners(s *cli.Stream, p cli.Params, elNames []string) error { if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - for _, elName := range elNames { - if err := cs.Triggers.TektonV1alpha1().EventListeners(p.Namespace()).Delete(elName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete eventlistener %q: %s", elName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, elName) - } - - if len(success) > 0 { - fmt.Fprintf(s.Out, "EventListeners deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("EventListener", func(listenerName string) error { + return cs.Triggers.TektonV1alpha1().EventListeners(p.Namespace()).Delete(listenerName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, elNames) } diff --git a/pkg/cmd/pipeline/delete.go b/pkg/cmd/pipeline/delete.go index 049e64634..cf119a473 100644 --- a/pkg/cmd/pipeline/delete.go +++ b/pkg/cmd/pipeline/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validate "github.com/tektoncd/cli/pkg/helper/validate" - multierr "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -80,53 +79,30 @@ func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, p if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - addPrintErr := func(err error) { - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) + d := deleter.New("Pipeline", func(pipelineName string) error { + return cs.Tekton.TektonV1alpha1().Pipelines(p.Namespace()).Delete(pipelineName, &metav1.DeleteOptions{}) + }) + if opts.DeleteAll { + d.WithRelated("PipelineRun", pipelineRunLister(p, cs), func(pipelineRunName string) error { + return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) + }) } + return d.Execute(s, pNames) +} - var successfulPipelines []string - var successfulPipelineRuns []string - - for _, pName := range pNames { - if err := cs.Tekton.TektonV1alpha1().Pipelines(p.Namespace()).Delete(pName, &metav1.DeleteOptions{}); err != nil { - addPrintErr(fmt.Errorf("failed to delete pipeline %q: %s", pName, err)) - continue - } - successfulPipelines = append(successfulPipelines, pName) - - if !opts.DeleteAll { - continue - } - +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", pName), + LabelSelector: fmt.Sprintf("tekton.dev/pipeline=%s", pipelineName), } - pipelineRuns, err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).List(lOpts) if err != nil { - addPrintErr(err) - continue + return nil, err } - + var names []string for _, pr := range pipelineRuns.Items { - if err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pr.Name, &metav1.DeleteOptions{}); err != nil { - addPrintErr(fmt.Errorf("failed to delete pipelinerun %q: %s", pr.Name, err)) - continue - } - - successfulPipelineRuns = append(successfulPipelineRuns, pr.Name) + names = append(names, pr.Name) } + return names, nil } - - if len(successfulPipelineRuns) > 0 { - fmt.Fprintf(s.Out, "PipelineRuns deleted: %s\n", names.QuotedList(successfulPipelineRuns)) - } - if len(successfulPipelines) > 0 { - fmt.Fprintf(s.Out, "Pipelines deleted: %s\n", names.QuotedList(successfulPipelines)) - } - - return multierr.Combine(errs...) } diff --git a/pkg/cmd/pipelineresource/delete.go b/pkg/cmd/pipelineresource/delete.go index 2e3944e43..f3ff6f731 100644 --- a/pkg/cmd/pipelineresource/delete.go +++ b/pkg/cmd/pipelineresource/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validateinput "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -80,20 +79,8 @@ func deleteResources(s *cli.Stream, p cli.Params, preNames []string) error { return fmt.Errorf("failed to create tekton client") } - var errs []error - var success []string - for _, preName := range preNames { - if err := cs.Tekton.TektonV1alpha1().PipelineResources(p.Namespace()).Delete(preName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete pipelineresource %q: %s", preName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, preName) - } - if len(success) > 0 { - fmt.Fprintf(s.Out, "PipelineResources deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("PipelineResource", func(resourceName string) error { + return cs.Tekton.TektonV1alpha1().PipelineResources(p.Namespace()).Delete(resourceName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, preNames) } diff --git a/pkg/cmd/pipelinerun/delete.go b/pkg/cmd/pipelinerun/delete.go index 8cbfa559f..0e815b9b0 100644 --- a/pkg/cmd/pipelinerun/delete.go +++ b/pkg/cmd/pipelinerun/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validate "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -78,22 +77,8 @@ func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string) error { if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - for _, prName := range prNames { - if err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(prName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete pipelinerun %q: %s", prName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, prName) - } - - if len(success) > 0 { - fmt.Fprintf(s.Out, "PipelineRuns deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("PipelineRun", func(pipelineRunName string) error { + return cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).Delete(pipelineRunName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, prNames) } diff --git a/pkg/cmd/task/delete.go b/pkg/cmd/task/delete.go index 7a0f43c79..87600e271 100644 --- a/pkg/cmd/task/delete.go +++ b/pkg/cmd/task/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validate "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -75,57 +74,35 @@ or return c } -func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, tNames []string) error { +func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNames []string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - addPrintErr := func(err error) { - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) + d := deleter.New("Task", func(taskName string) error { + return cs.Tekton.TektonV1alpha1().Tasks(p.Namespace()).Delete(taskName, &metav1.DeleteOptions{}) + }) + if opts.DeleteAll { + d.WithRelated("TaskRun", taskRunLister(p, cs), func(taskRunName string) error { + return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) + }) } + return d.Execute(s, taskNames) +} - var successfulTasks []string - var successfulTaskRuns []string - - for _, tName := range tNames { - if err := cs.Tekton.TektonV1alpha1().Tasks(p.Namespace()).Delete(tName, &metav1.DeleteOptions{}); err != nil { - addPrintErr(fmt.Errorf("failed to delete task %q: %s", tName, err)) - continue - } - successfulTasks = append(successfulTasks, tName) - - if !opts.DeleteAll { - continue - } - +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", tName), + LabelSelector: fmt.Sprintf("tekton.dev/task=%s", taskName), } - taskRuns, err := cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).List(lOpts) if err != nil { - addPrintErr(err) - continue + return nil, err } - + var names []string for _, tr := range taskRuns.Items { - if err := cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(tr.Name, &metav1.DeleteOptions{}); err != nil { - addPrintErr(fmt.Errorf("failed to delete taskrun %q: %s", tr.Name, err)) - continue - } - successfulTaskRuns = append(successfulTaskRuns, tr.Name) + names = append(names, tr.Name) } + return names, nil } - - if len(successfulTaskRuns) > 0 { - fmt.Fprintf(s.Out, "TaskRuns deleted: %s\n", names.QuotedList(successfulTaskRuns)) - } - if len(successfulTasks) > 0 { - fmt.Fprintf(s.Out, "Tasks deleted: %s\n", names.QuotedList(successfulTasks)) - } - - return multierr.Combine(errs...) } diff --git a/pkg/cmd/taskrun/delete.go b/pkg/cmd/taskrun/delete.go index e3aecb93c..b5b88b1e2 100644 --- a/pkg/cmd/taskrun/delete.go +++ b/pkg/cmd/taskrun/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" validate "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -78,21 +77,8 @@ func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string) error { if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - for _, trName := range trNames { - if err := cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(trName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete taskrun %q: %s", trName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, trName) - } - if len(success) > 0 { - fmt.Fprintf(s.Out, "TaskRuns deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("TaskRun", func(taskRunName string) error { + return cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Delete(taskRunName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, trNames) } diff --git a/pkg/cmd/triggerbinding/delete.go b/pkg/cmd/triggerbinding/delete.go index 205932e45..93476f268 100644 --- a/pkg/cmd/triggerbinding/delete.go +++ b/pkg/cmd/triggerbinding/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -79,22 +78,8 @@ func deleteTriggerBindings(s *cli.Stream, p cli.Params, tbNames []string) error if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - - for _, tbName := range tbNames { - if err := cs.Triggers.TektonV1alpha1().TriggerBindings(p.Namespace()).Delete(tbName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete triggerbinding %q: %s", tbName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, tbName) - } - if len(success) > 0 { - fmt.Fprintf(s.Out, "TriggerBindings deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("TriggerBinding", func(bindingName string) error { + return cs.Triggers.TektonV1alpha1().TriggerBindings(p.Namespace()).Delete(bindingName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, tbNames) } diff --git a/pkg/cmd/triggertemplate/delete.go b/pkg/cmd/triggertemplate/delete.go index 2b2763657..55beb237d 100644 --- a/pkg/cmd/triggertemplate/delete.go +++ b/pkg/cmd/triggertemplate/delete.go @@ -19,10 +19,9 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" - "github.com/tektoncd/cli/pkg/helper/names" + "github.com/tektoncd/cli/pkg/helper/deleter" "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/validate" - "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -79,22 +78,8 @@ func deleteTriggerTemplates(s *cli.Stream, p cli.Params, ttNames []string) error if err != nil { return fmt.Errorf("failed to create tekton client") } - - var errs []error - var success []string - - for _, ttName := range ttNames { - if err := cs.Triggers.TektonV1alpha1().TriggerTemplates(p.Namespace()).Delete(ttName, &metav1.DeleteOptions{}); err != nil { - err = fmt.Errorf("failed to delete triggertemplate %q: %s", ttName, err) - errs = append(errs, err) - fmt.Fprintf(s.Err, "%s\n", err) - continue - } - success = append(success, ttName) - } - if len(success) > 0 { - fmt.Fprintf(s.Out, "TriggerTemplates deleted: %s\n", names.QuotedList(success)) - } - - return multierr.Combine(errs...) + d := deleter.New("TriggerTemplate", func(templateName string) error { + return cs.Triggers.TektonV1alpha1().TriggerTemplates(p.Namespace()).Delete(templateName, &metav1.DeleteOptions{}) + }) + return d.Execute(s, ttNames) } diff --git a/pkg/helper/deleter/deleter.go b/pkg/helper/deleter/deleter.go new file mode 100644 index 000000000..a6534417c --- /dev/null +++ b/pkg/helper/deleter/deleter.go @@ -0,0 +1,101 @@ +package deleter + +import ( + "fmt" + "strings" + + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/helper/names" + "go.uber.org/multierr" +) + +// Deleter encapsulates behaviour around deleting resources and their relations. +// While actually performing a deletion is left to calling code, this helper +// type standardizes the sequencing, messaging and error handling related to +// deletions. +type Deleter struct { + errors []error + successfulDeletes []string + successfulRelatedDeletes []string + + kind string + relatedKind string + + delete func(string) error + + listRelated func(string) ([]string, error) + deleteRelated func(string) error +} + +// New returns a Deleter that will delete resources of kind with the given +// delete func when Execute is called. +func New(kind string, deleteFunc func(string) error) *Deleter { + return &Deleter{ + kind: kind, + delete: deleteFunc, + } +} + +// WithRelated tells this Deleter that it should also delete related resources +// when Execute is called. Related resources will be of given kind, the names of +// those resources must be provided by listFunc and each related resource will be +// passed to deleteFunc for deletion. +func (d *Deleter) WithRelated(kind string, listFunc func(string) ([]string, error), deleteFunc func(string) error) { + d.relatedKind = kind + d.listRelated = listFunc + 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 { + 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)) + } else { + d.successfulDeletes = append(d.successfulDeletes, name) + } + } + if d.relatedKind != "" && d.listRelated != nil && d.deleteRelated != nil { + for _, name := range d.successfulDeletes { + 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 { + d.printAndAddError(streams, err) + } else { + for _, subresource := range related { + if err := d.deleteRelated(subresource); err != nil { + err = fmt.Errorf("failed to delete %s %q: %s", strings.ToLower(d.relatedKind), subresource, err) + d.printAndAddError(streams, err) + } else { + d.successfulRelatedDeletes = append(d.successfulRelatedDeletes, subresource) + } + } + } +} + +// 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)) + } + if len(d.successfulDeletes) > 0 { + fmt.Fprintf(streams.Out, "%ss deleted: %s\n", d.kind, names.QuotedList(d.successfulDeletes)) + } +} + +// printAndAddError prints the given error to the given stderr stream and +// adds that error to the list of accumulated errors that have occurred +// during execution. +func (d *Deleter) printAndAddError(streams *cli.Stream, err error) { + d.errors = append(d.errors, err) + fmt.Fprintf(streams.Err, "%s\n", err) +} diff --git a/pkg/helper/deleter/deleter_test.go b/pkg/helper/deleter/deleter_test.go new file mode 100644 index 000000000..054481ae8 --- /dev/null +++ b/pkg/helper/deleter/deleter_test.go @@ -0,0 +1,61 @@ +package deleter + +import ( + "errors" + "strings" + "testing" + + "github.com/tektoncd/cli/pkg/cli" +) + +func TestExecute(t *testing.T) { + for _, tc := range []struct { + description string + names []string + expectedOut string + expectedErr string + deleteFunc func(string) error + }{{ + description: "doesnt print anything if no names provided", + names: []string{}, + expectedOut: "", + expectedErr: "", + deleteFunc: func(string) error { return nil }, + }, { + description: "prints success message if names provided", + names: []string{"foo", "bar"}, + expectedOut: "FooBars deleted: \"foo\", \"bar\"\n", + expectedErr: "", + deleteFunc: func(string) error { return nil }, + }, { + description: "prints errors if returned during delete", + 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") }, + }, { + 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") }, + }} { + 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 { + 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()) + } + }) + } +}