From 02bed88e6e7e720c0c8165a07f061fe98488dcf2 Mon Sep 17 00:00:00 2001 From: Arvind Date: Tue, 6 Oct 2026 01:25:40 +0530 Subject: [PATCH] feat: support structured JSON/YAML output for bulk delete operations Extend -o json/yaml support to bulk delete scenarios across task, pipeline, pipelinerun, eventlistener, triggerbinding, triggertemplate, and clustertriggerbinding delete commands. Signed-off-by: Arvind Krishnamurthy Assisted-by: Grok 4.7 (via Cursor) --- docs/cmd/tkn_clustertriggerbinding_delete.md | 8 + docs/cmd/tkn_eventlistener_delete.md | 8 + docs/cmd/tkn_pipeline_delete.md | 16 ++ docs/cmd/tkn_pipelinerun_delete.md | 24 +++ docs/cmd/tkn_task_delete.md | 16 ++ docs/cmd/tkn_triggerbinding_delete.md | 8 + docs/cmd/tkn_triggertemplate_delete.md | 8 + .../man1/tkn-clustertriggerbinding-delete.1 | 24 +++ docs/man/man1/tkn-eventlistener-delete.1 | 24 +++ docs/man/man1/tkn-pipeline-delete.1 | 48 +++++ docs/man/man1/tkn-pipelinerun-delete.1 | 72 +++++++ docs/man/man1/tkn-task-delete.1 | 48 +++++ docs/man/man1/tkn-triggerbinding-delete.1 | 24 +++ docs/man/man1/tkn-triggertemplate-delete.1 | 24 +++ pkg/cmd/clustertriggerbinding/delete.go | 24 ++- pkg/cmd/clustertriggerbinding/delete_test.go | 34 ++++ pkg/cmd/eventlistener/delete.go | 24 ++- pkg/cmd/eventlistener/delete_test.go | 35 ++++ pkg/cmd/pipeline/delete.go | 26 ++- pkg/cmd/pipeline/delete_test.go | 78 ++++++++ pkg/cmd/pipelinerun/delete.go | 60 ++++-- pkg/cmd/pipelinerun/delete_test.go | 184 ++++++++++++++++++ pkg/cmd/task/delete.go | 26 ++- pkg/cmd/task/delete_test.go | 48 +++++ pkg/cmd/triggerbinding/delete.go | 24 ++- pkg/cmd/triggerbinding/delete_test.go | 35 ++++ pkg/cmd/triggertemplate/delete.go | 24 ++- pkg/cmd/triggertemplate/delete_test.go | 35 ++++ 28 files changed, 947 insertions(+), 62 deletions(-) diff --git a/docs/cmd/tkn_clustertriggerbinding_delete.md b/docs/cmd/tkn_clustertriggerbinding_delete.md index 1cb98af5e..9a3a43e49 100644 --- a/docs/cmd/tkn_clustertriggerbinding_delete.md +++ b/docs/cmd/tkn_clustertriggerbinding_delete.md @@ -32,6 +32,14 @@ Delete a ClusterTriggerBinding and print the result as YAML: tkn clustertriggerbinding delete foo -o yaml +Delete all ClusterTriggerBindings and print the result as JSON: + + tkn clustertriggerbinding delete --all -o json + +Delete all ClusterTriggerBindings and print the result as YAML: + + tkn clustertriggerbinding delete --all -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_eventlistener_delete.md b/docs/cmd/tkn_eventlistener_delete.md index 580e23abc..68897d36b 100644 --- a/docs/cmd/tkn_eventlistener_delete.md +++ b/docs/cmd/tkn_eventlistener_delete.md @@ -32,6 +32,14 @@ Delete an EventListener and print the result as YAML: tkn eventlistener delete foo -n quux -o yaml +Delete all EventListeners in a namespace and print the result as JSON: + + tkn eventlistener delete --all -n quux -o json + +Delete all EventListeners in a namespace and print the result as YAML: + + tkn eventlistener delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_pipeline_delete.md b/docs/cmd/tkn_pipeline_delete.md index b160e946f..13dad29ca 100644 --- a/docs/cmd/tkn_pipeline_delete.md +++ b/docs/cmd/tkn_pipeline_delete.md @@ -32,6 +32,22 @@ Delete a Pipeline and print the result as YAML: tkn pipeline delete foo -n quux -o yaml +Delete all Pipelines in a namespace and print the result as JSON: + + tkn pipeline delete --all -n quux -o json + +Delete all Pipelines in a namespace and print the result as YAML: + + tkn pipeline delete --all -n quux -o yaml + +Delete a Pipeline and its PipelineRuns and print the result as JSON: + + tkn pipeline delete foo -n quux --prs -o json + +Delete a Pipeline and its PipelineRuns and print the result as YAML: + + tkn pipeline delete foo -n quux --prs -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_pipelinerun_delete.md b/docs/cmd/tkn_pipelinerun_delete.md index 4a5cc47f8..1a514a591 100644 --- a/docs/cmd/tkn_pipelinerun_delete.md +++ b/docs/cmd/tkn_pipelinerun_delete.md @@ -32,6 +32,30 @@ Delete a PipelineRun and print the result as YAML: tkn pipelinerun delete foo -n quux -o yaml +Delete all PipelineRuns in a namespace and print the result as JSON: + + tkn pipelinerun delete --all -n quux -o json + +Delete all PipelineRuns in a namespace and print the result as YAML: + + tkn pipelinerun delete --all -n quux -o yaml + +Delete all but the 2 most recent PipelineRuns and print the result as JSON: + + tkn pipelinerun delete --keep 2 -n quux -o json + +Delete all but the 2 most recent PipelineRuns and print the result as YAML: + + tkn pipelinerun delete --keep 2 -n quux -o yaml + +Delete PipelineRuns for a Pipeline and print the result as JSON: + + tkn pipelinerun delete --pipeline foo -n quux -o json + +Delete PipelineRuns for a Pipeline and print the result as YAML: + + tkn pipelinerun delete --pipeline foo -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_task_delete.md b/docs/cmd/tkn_task_delete.md index 606140428..73e3e266c 100644 --- a/docs/cmd/tkn_task_delete.md +++ b/docs/cmd/tkn_task_delete.md @@ -32,6 +32,22 @@ Delete a Task and print the result as YAML: tkn task delete foo -n quux -o yaml +Delete all Tasks in a namespace and print the result as JSON: + + tkn task delete --all -n quux -o json + +Delete all Tasks in a namespace and print the result as YAML: + + tkn task delete --all -n quux -o yaml + +Delete a Task and its TaskRuns and print the result as JSON: + + tkn task delete foo -n quux --trs -o json + +Delete a Task and its TaskRuns and print the result as YAML: + + tkn task delete foo -n quux --trs -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_triggerbinding_delete.md b/docs/cmd/tkn_triggerbinding_delete.md index 2b191c69f..41cbb719d 100644 --- a/docs/cmd/tkn_triggerbinding_delete.md +++ b/docs/cmd/tkn_triggerbinding_delete.md @@ -32,6 +32,14 @@ Delete a TriggerBinding and print the result as YAML: tkn triggerbinding delete foo -n quux -o yaml +Delete all TriggerBindings in a namespace and print the result as JSON: + + tkn triggerbinding delete --all -n quux -o json + +Delete all TriggerBindings in a namespace and print the result as YAML: + + tkn triggerbinding delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/cmd/tkn_triggertemplate_delete.md b/docs/cmd/tkn_triggertemplate_delete.md index bbd987376..5befc22ee 100644 --- a/docs/cmd/tkn_triggertemplate_delete.md +++ b/docs/cmd/tkn_triggertemplate_delete.md @@ -32,6 +32,14 @@ Delete a TriggerTemplate and print the result as YAML: tkn triggertemplate delete foo -n quux -o yaml +Delete all TriggerTemplates in a namespace and print the result as JSON: + + tkn triggertemplate delete --all -n quux -o json + +Delete all TriggerTemplates in a namespace and print the result as YAML: + + tkn triggertemplate delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-clustertriggerbinding-delete.1 b/docs/man/man1/tkn-clustertriggerbinding-delete.1 index 94a5c2508..ae4ff2c37 100644 --- a/docs/man/man1/tkn-clustertriggerbinding-delete.1 +++ b/docs/man/man1/tkn-clustertriggerbinding-delete.1 @@ -99,6 +99,30 @@ tkn clustertriggerbinding delete foo \-o yaml .fi .RE +.PP +Delete all ClusterTriggerBindings and print the result as JSON: + +.PP +.RS + +.nf +tkn clustertriggerbinding delete \-\-all \-o json + +.fi +.RE + +.PP +Delete all ClusterTriggerBindings and print the result as YAML: + +.PP +.RS + +.nf +tkn clustertriggerbinding delete \-\-all \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-eventlistener-delete.1 b/docs/man/man1/tkn-eventlistener-delete.1 index 4245b79b0..e7c6e4ae4 100644 --- a/docs/man/man1/tkn-eventlistener-delete.1 +++ b/docs/man/man1/tkn-eventlistener-delete.1 @@ -103,6 +103,30 @@ tkn eventlistener delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all EventListeners in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn eventlistener delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all EventListeners in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn eventlistener delete \-\-all \-n quux \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-pipeline-delete.1 b/docs/man/man1/tkn-pipeline-delete.1 index ea3a01373..53f1c57eb 100644 --- a/docs/man/man1/tkn-pipeline-delete.1 +++ b/docs/man/man1/tkn-pipeline-delete.1 @@ -107,6 +107,54 @@ tkn pipeline delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all Pipelines in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn pipeline delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all Pipelines in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn pipeline delete \-\-all \-n quux \-o yaml + +.fi +.RE + +.PP +Delete a Pipeline and its PipelineRuns and print the result as JSON: + +.PP +.RS + +.nf +tkn pipeline delete foo \-n quux \-\-prs \-o json + +.fi +.RE + +.PP +Delete a Pipeline and its PipelineRuns and print the result as YAML: + +.PP +.RS + +.nf +tkn pipeline delete foo \-n quux \-\-prs \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-pipelinerun-delete.1 b/docs/man/man1/tkn-pipelinerun-delete.1 index 852d4a5d6..70ed7c4f1 100644 --- a/docs/man/man1/tkn-pipelinerun-delete.1 +++ b/docs/man/man1/tkn-pipelinerun-delete.1 @@ -123,6 +123,78 @@ tkn pipelinerun delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all PipelineRuns in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all PipelineRuns in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-all \-n quux \-o yaml + +.fi +.RE + +.PP +Delete all but the 2 most recent PipelineRuns and print the result as JSON: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-keep 2 \-n quux \-o json + +.fi +.RE + +.PP +Delete all but the 2 most recent PipelineRuns and print the result as YAML: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-keep 2 \-n quux \-o yaml + +.fi +.RE + +.PP +Delete PipelineRuns for a Pipeline and print the result as JSON: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-pipeline foo \-n quux \-o json + +.fi +.RE + +.PP +Delete PipelineRuns for a Pipeline and print the result as YAML: + +.PP +.RS + +.nf +tkn pipelinerun delete \-\-pipeline foo \-n quux \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-task-delete.1 b/docs/man/man1/tkn-task-delete.1 index c8b81cddf..438610671 100644 --- a/docs/man/man1/tkn-task-delete.1 +++ b/docs/man/man1/tkn-task-delete.1 @@ -107,6 +107,54 @@ tkn task delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all Tasks in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn task delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all Tasks in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn task delete \-\-all \-n quux \-o yaml + +.fi +.RE + +.PP +Delete a Task and its TaskRuns and print the result as JSON: + +.PP +.RS + +.nf +tkn task delete foo \-n quux \-\-trs \-o json + +.fi +.RE + +.PP +Delete a Task and its TaskRuns and print the result as YAML: + +.PP +.RS + +.nf +tkn task delete foo \-n quux \-\-trs \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-triggerbinding-delete.1 b/docs/man/man1/tkn-triggerbinding-delete.1 index ca88a3226..bf863f38b 100644 --- a/docs/man/man1/tkn-triggerbinding-delete.1 +++ b/docs/man/man1/tkn-triggerbinding-delete.1 @@ -103,6 +103,30 @@ tkn triggerbinding delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all TriggerBindings in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn triggerbinding delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all TriggerBindings in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn triggerbinding delete \-\-all \-n quux \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/docs/man/man1/tkn-triggertemplate-delete.1 b/docs/man/man1/tkn-triggertemplate-delete.1 index 47abd9ec0..ea99a4eb8 100644 --- a/docs/man/man1/tkn-triggertemplate-delete.1 +++ b/docs/man/man1/tkn-triggertemplate-delete.1 @@ -103,6 +103,30 @@ tkn triggertemplate delete foo \-n quux \-o yaml .fi .RE +.PP +Delete all TriggerTemplates in a namespace and print the result as JSON: + +.PP +.RS + +.nf +tkn triggertemplate delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete all TriggerTemplates in a namespace and print the result as YAML: + +.PP +.RS + +.nf +tkn triggertemplate delete \-\-all \-n quux \-o yaml + +.fi +.RE + .PP Using \-o json or \-o yaml skips the confirmation prompt. diff --git a/pkg/cmd/clustertriggerbinding/delete.go b/pkg/cmd/clustertriggerbinding/delete.go index ce40ffada..07b40518f 100644 --- a/pkg/cmd/clustertriggerbinding/delete.go +++ b/pkg/cmd/clustertriggerbinding/delete.go @@ -66,6 +66,14 @@ Delete a ClusterTriggerBinding and print the result as YAML: tkn clustertriggerbinding delete foo -o yaml +Delete all ClusterTriggerBindings and print the result as JSON: + + tkn clustertriggerbinding delete --all -o json + +Delete all ClusterTriggerBindings and print the result as YAML: + + tkn clustertriggerbinding delete --all -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -94,10 +102,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAll { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -140,13 +144,13 @@ func deleteClusterTriggerBindings(s *cli.Stream, p cli.Params, ctbNames []string } d.Delete(ctbNames) - if !deleteAll { - if formatted.IsStructured(output) { - if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { - return err - } - return d.Errors() + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err } + return d.Errors() + } + if !deleteAll { d.PrintSuccesses(s) } else if deleteAll { if d.Errors() == nil { diff --git a/pkg/cmd/clustertriggerbinding/delete_test.go b/pkg/cmd/clustertriggerbinding/delete_test.go index 5ecb0382e..5d83e3ede 100644 --- a/pkg/cmd/clustertriggerbinding/delete_test.go +++ b/pkg/cmd/clustertriggerbinding/delete_test.go @@ -274,3 +274,37 @@ func TestClusterTriggerBindingDeleteStructuredOutput(t *testing.T) { }) } } + +func TestClusterTriggerBindingDeleteBulkStructuredOutput(t *testing.T) { + ctbs := []*v1beta1.ClusterTriggerBinding{ + {ObjectMeta: metav1.ObjectMeta{Name: "ctb-1"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "ctb-2"}}, + } + + seed := func(t *testing.T) *test.Params { + t.Helper() + cs := test.SeedTestResources(t, triggertest.Resources{ClusterTriggerBindings: ctbs}) + cs.Triggers.Resources = cb.TriggersAPIResourceList("v1beta1", []string{"clustertriggerbinding"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredV1beta1CTB(ctbs[0], "v1beta1"), + cb.UnstructuredV1beta1CTB(ctbs[1], "v1beta1"), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + } + + out, err := test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"ctb-1\",\n \"ctb-2\"\n ]\n}\n", out) + + out, err = test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- ctb-1\n- ctb-2\n", out) +} diff --git a/pkg/cmd/eventlistener/delete.go b/pkg/cmd/eventlistener/delete.go index 5eff72b4e..3c714ac6d 100644 --- a/pkg/cmd/eventlistener/delete.go +++ b/pkg/cmd/eventlistener/delete.go @@ -67,6 +67,14 @@ Delete an EventListener and print the result as YAML: tkn eventlistener delete foo -n quux -o yaml +Delete all EventListeners in a namespace and print the result as JSON: + + tkn eventlistener delete --all -n quux -o json + +Delete all EventListeners in a namespace and print the result as YAML: + + tkn eventlistener delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -95,10 +103,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -140,13 +144,13 @@ func deleteEventListeners(s *cli.Stream, p cli.Params, elNames []string, deleteA } d.Delete(elNames) - if !deleteAll { - if formatted.IsStructured(output) { - if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { - return err - } - return d.Errors() + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err } + return d.Errors() + } + if !deleteAll { d.PrintSuccesses(s) } else if deleteAll { if d.Errors() == nil { diff --git a/pkg/cmd/eventlistener/delete_test.go b/pkg/cmd/eventlistener/delete_test.go index c86b8e1ca..f6ae28127 100644 --- a/pkg/cmd/eventlistener/delete_test.go +++ b/pkg/cmd/eventlistener/delete_test.go @@ -303,3 +303,38 @@ func TestEventListenerDeleteStructuredOutput(t *testing.T) { }) } } + +func TestEventListenerDeleteBulkStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}} + els := []*v1beta1.EventListener{ + {ObjectMeta: metav1.ObjectMeta{Name: "el-1", Namespace: "ns"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "el-2", Namespace: "ns"}}, + } + + seed := func(t *testing.T) *test.Params { + t.Helper() + cs := test.SeedTestResources(t, triggertest.Resources{EventListeners: els, Namespaces: ns}) + cs.Triggers.Resources = cb.TriggersAPIResourceList("v1beta1", []string{"eventlistener"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredV1beta1EL(els[0], "v1beta1"), + cb.UnstructuredV1beta1EL(els[1], "v1beta1"), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + } + + out, err := test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"el-1\",\n \"el-2\"\n ]\n}\n", out) + + out, err = test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- el-1\n- el-2\n", out) +} diff --git a/pkg/cmd/pipeline/delete.go b/pkg/cmd/pipeline/delete.go index 8a1753e53..3c30fe74c 100644 --- a/pkg/cmd/pipeline/delete.go +++ b/pkg/cmd/pipeline/delete.go @@ -70,6 +70,22 @@ Delete a Pipeline and print the result as YAML: tkn pipeline delete foo -n quux -o yaml +Delete all Pipelines in a namespace and print the result as JSON: + + tkn pipeline delete --all -n quux -o json + +Delete all Pipelines in a namespace and print the result as YAML: + + tkn pipeline delete --all -n quux -o yaml + +Delete a Pipeline and its PipelineRuns and print the result as JSON: + + tkn pipeline delete foo -n quux --prs -o json + +Delete a Pipeline and its PipelineRuns and print the result as YAML: + + tkn pipeline delete foo -n quux --prs -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -98,10 +114,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -164,6 +176,12 @@ func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, p } d.PrintSuccesses(s) } else if opts.DeleteAllNs { + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err + } + return d.Errors() + } if d.Errors() == nil { fmt.Fprintf(s.Out, "All Pipelines deleted in namespace %q\n", p.Namespace()) } diff --git a/pkg/cmd/pipeline/delete_test.go b/pkg/cmd/pipeline/delete_test.go index 75cac81e0..0e815fb16 100644 --- a/pkg/cmd/pipeline/delete_test.go +++ b/pkg/cmd/pipeline/delete_test.go @@ -15,6 +15,7 @@ package pipeline import ( + "errors" "io" "strings" "testing" @@ -28,7 +29,9 @@ import ( pipelinetest "github.com/tektoncd/pipeline/test" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" + k8stest "k8s.io/client-go/testing" duckv1 "knative.dev/pkg/apis/duck/v1" ) @@ -839,3 +842,78 @@ func TestPipelineDeleteStructuredOutputWithRelated(t *testing.T) { } test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-1\",\n \"pipeline-run-2\",\n \"pipeline\"\n ]\n}\n", out) } + +func TestPipelineDeleteBulkStructuredOutput(t *testing.T) { + version := "v1" + pdata := []*v1.Pipeline{ + {ObjectMeta: metav1.ObjectMeta{Name: "pipeline-a", Namespace: "ns"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pipeline-b", Namespace: "ns"}}, + } + ns := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}} + + newParams := func(t *testing.T, reactors []testDynamic.PrependOpt) *test.Params { + t.Helper() + cs, _ := test.SeedTestData(t, pipelinetest.Data{Pipelines: pdata, Namespaces: ns}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipeline"}) + tdc := testDynamic.Options{PrependReactors: reactors} + dc, err := tdc.Client( + cb.UnstructuredP(pdata[0], version), + cb.UnstructuredP(pdata[1], version), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + } + + t.Run("all json skips confirmation and lists deleted names", func(t *testing.T) { + out, err := test.ExecuteCommand(Command(newParams(t, nil)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-a\",\n \"pipeline-b\"\n ]\n}\n", out) + }) + + t.Run("all yaml is equivalent", func(t *testing.T) { + out, err := test.ExecuteCommand(Command(newParams(t, nil)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- pipeline-a\n- pipeline-b\n", out) + }) + + t.Run("invalid format does not emit a success document", func(t *testing.T) { + out, err := test.ExecuteCommand(Command(newParams(t, nil)), "delete", "--all", "-n", "ns", "-o", "csv") + if err == nil { + t.Fatal("expected error") + } + test.AssertOutput(t, "invalid output format \"csv\": must be json or yaml", err.Error()) + if strings.Contains(out, "\"deleted\"") || strings.Contains(out, "pipeline-a") { + t.Fatalf("unexpected success document: %q", out) + } + }) + + t.Run("mixed failure lists successes and exits non-zero", func(t *testing.T) { + reactors := []testDynamic.PrependOpt{{ + Verb: "delete", + Resource: "pipelines", + Action: func(action k8stest.Action) (bool, runtime.Object, error) { + del := action.(k8stest.DeleteAction) + if del.GetName() == "pipeline-b" { + return true, nil, errors.New("delete failed") + } + return false, nil, nil + }, + }} + cmd := Command(newParams(t, reactors)) + cmd.SilenceErrors = true + out, err := test.ExecuteCommand(cmd, "delete", "--all", "-n", "ns", "-o", "json") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "pipeline-b") { + t.Fatalf("expected pipeline-b in error, got %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-a\"\n ]\n}\n", out) + }) +} diff --git a/pkg/cmd/pipelinerun/delete.go b/pkg/cmd/pipelinerun/delete.go index 127ec125b..ee0ad649d 100644 --- a/pkg/cmd/pipelinerun/delete.go +++ b/pkg/cmd/pipelinerun/delete.go @@ -73,6 +73,30 @@ Delete a PipelineRun and print the result as YAML: tkn pipelinerun delete foo -n quux -o yaml +Delete all PipelineRuns in a namespace and print the result as JSON: + + tkn pipelinerun delete --all -n quux -o json + +Delete all PipelineRuns in a namespace and print the result as YAML: + + tkn pipelinerun delete --all -n quux -o yaml + +Delete all but the 2 most recent PipelineRuns and print the result as JSON: + + tkn pipelinerun delete --keep 2 -n quux -o json + +Delete all but the 2 most recent PipelineRuns and print the result as YAML: + + tkn pipelinerun delete --keep 2 -n quux -o yaml + +Delete PipelineRuns for a Pipeline and print the result as JSON: + + tkn pipelinerun delete --pipeline foo -n quux -o json + +Delete PipelineRuns for a Pipeline and print the result as YAML: + + tkn pipelinerun delete --pipeline foo -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -102,10 +126,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs || opts.ParentResourceName != "" || opts.Keep > 0 || opts.KeepSince > 0 { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -204,24 +224,28 @@ func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, opts *opt }) if len(prtodelete) == 0 && opts.Keep > 0 && opts.Keep == len(prtokeep) { - fmt.Fprintf(s.Out, "Associated %s (%d) for Pipeline:%s is/are equal to keep (%d) \n", opts.Resource, len(prtokeep), opts.ParentResourceName, opts.Keep) - return nil + msg := fmt.Sprintf("Associated %s (%d) for Pipeline:%s is/are equal to keep (%d) \n", opts.Resource, len(prtokeep), opts.ParentResourceName, opts.Keep) + return printOutput(s, output, msg) } if opts.Keep > len(prtokeep) { - fmt.Fprintf(s.Out, "There is/are only %d %s(s) associated for Pipeline: %s \n", len(prtokeep), opts.Resource, opts.ParentResourceName) - return nil + msg := fmt.Sprintf("There is/are only %d %s(s) associated for Pipeline: %s \n", len(prtokeep), opts.Resource, opts.ParentResourceName) + return printOutput(s, output, msg) } d.DeleteRelated([]string{opts.ParentResourceName}) } - if !opts.DeleteAllNs { - if opts.ParentResourceName == "" && opts.Keep == 0 && opts.KeepSince == 0 && formatted.IsStructured(output) { - if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { - return err - } - return d.Errors() + if formatted.IsStructured(output) { + deleted := d.SuccessfulDeletes() + if !opts.DeleteAllNs && opts.ParentResourceName != "" { + deleted = d.SuccessfulRelatedDeletes() + } + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(deleted)); err != nil { + return err } + return d.Errors() + } + if !opts.DeleteAllNs { if d.Errors() == nil { switch { case opts.Keep > 0 && opts.KeepSince > 0 && !opts.IgnoreRunning: @@ -271,6 +295,14 @@ func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, opts *opt return d.Errors() } +func printOutput(s *cli.Stream, output, msg string) error { + if formatted.IsStructured(output) { + return formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(nil)) + } + fmt.Fprint(s.Out, msg) + return nil +} + func pipelineRunLister(cs *cli.Clients, keep, since int, ns string, ignoreRunning bool) func(string) ([]string, error) { return func(pipelineName string) ([]string, error) { labelSelector := fmt.Sprintf("tekton.dev/pipeline=%s", pipelineName) diff --git a/pkg/cmd/pipelinerun/delete_test.go b/pkg/cmd/pipelinerun/delete_test.go index 0f12b712d..427eb8200 100644 --- a/pkg/cmd/pipelinerun/delete_test.go +++ b/pkg/cmd/pipelinerun/delete_test.go @@ -1506,3 +1506,187 @@ func TestPipelineRunDeleteStructuredOutputPartialFailure(t *testing.T) { t.Fatalf("expected deleted key in structured output, got: %q", out) } } + +func completedPipelineRun(name, pipeline string, start time.Time, labels map[string]string) *v1.PipelineRun { + if labels == nil { + labels = map[string]string{"tekton.dev/pipeline": pipeline} + } + return &v1.PipelineRun{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: name, + Labels: labels, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{Name: pipeline}, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1.PipelineRunReasonSuccessful.String()}, + }, + }, + PipelineRunStatusFields: v1.PipelineRunStatusFields{ + StartTime: &metav1.Time{Time: start}, + CompletionTime: &metav1.Time{Time: start.Add(time.Minute)}, + }, + }, + } +} + +func pipelineRunDeleteParams(t *testing.T, runs []*v1.PipelineRun, reactors []testDynamic.PrependOpt) *test.Params { + t.Helper() + version := "v1" + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: runs, + Namespaces: []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}}, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipelinerun"}) + objs := make([]runtime.Object, 0, len(runs)) + for _, run := range runs { + objs = append(objs, cb.UnstructuredPR(run, version)) + } + tdc := testDynamic.Options{PrependReactors: reactors} + dc, err := tdc.Client(objs...) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} +} + +func TestPipelineRunDeleteBulkStructuredOutput(t *testing.T) { + older := time.Date(1984, time.April, 4, 0, 0, 0, 0, time.UTC) + newer := older.Add(time.Hour) + recent := time.Now() + expired := time.Now().Add(-2 * time.Hour) + + t.Run("all json skips confirmation", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "pipeline", older, nil), + completedPipelineRun("pipeline-run-2", "pipeline", newer, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-1\",\n \"pipeline-run-2\"\n ]\n}\n", out) + }) + + t.Run("all yaml is equivalent", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "pipeline", older, nil), + completedPipelineRun("pipeline-run-2", "pipeline", newer, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- pipeline-run-1\n- pipeline-run-2\n", out) + }) + + t.Run("keep omits retained runs", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-old", "pipeline", older, nil), + completedPipelineRun("pipeline-run-new", "pipeline", newer, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--keep", "1", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-old\"\n ]\n}\n", out) + }) + + t.Run("keep-since omits recent runs", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-expired", "pipeline", expired, nil), + completedPipelineRun("pipeline-run-recent", "pipeline", recent, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--keep-since", "60", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-expired\"\n ]\n}\n", out) + }) + + t.Run("pipeline keep equal emits empty document", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "foo", older, nil), + completedPipelineRun("pipeline-run-2", "foo", newer, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--pipeline", "foo", "--keep", "2", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": []\n}\n", out) + }) + + t.Run("pipeline keep greater than matches emits empty document", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "foo", older, nil), + completedPipelineRun("pipeline-run-2", "foo", newer, nil), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--pipeline", "foo", "--keep", "3", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted: []\n", out) + }) + + t.Run("pipeline selector", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "foo", older, nil), + completedPipelineRun("pipeline-run-2", "foo", newer, nil), + completedPipelineRun("pipeline-run-other", "bar", newer, map[string]string{"tekton.dev/pipeline": "bar"}), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--pipeline", "foo", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-1\",\n \"pipeline-run-2\"\n ]\n}\n", out) + }) + + t.Run("label selector omits non-matching runs", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-demo", "pipeline", older, map[string]string{"app": "demo"}), + completedPipelineRun("pipeline-run-other", "pipeline", newer, map[string]string{"app": "other"}), + } + out, err := test.ExecuteCommand(Command(pipelineRunDeleteParams(t, runs, nil)), "delete", "--all", "--label", "app=demo", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- pipeline-run-demo\n", out) + }) + + t.Run("mixed failure", func(t *testing.T) { + runs := []*v1.PipelineRun{ + completedPipelineRun("pipeline-run-1", "pipeline", older, nil), + completedPipelineRun("pipeline-run-2", "pipeline", newer, nil), + } + reactors := []testDynamic.PrependOpt{{ + Verb: "delete", + Resource: "pipelineruns", + Action: func(action k8stest.Action) (bool, runtime.Object, error) { + del := action.(k8stest.DeleteAction) + if del.GetName() == "pipeline-run-2" { + return true, nil, errors.New("delete failed") + } + return false, nil, nil + }, + }} + cmd := Command(pipelineRunDeleteParams(t, runs, reactors)) + cmd.SilenceErrors = true + out, err := test.ExecuteCommand(cmd, "delete", "--all", "-n", "ns", "-o", "json") + if err == nil { + t.Fatal("expected error") + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"pipeline-run-1\"\n ]\n}\n", out) + }) + + t.Run("invalid format", func(t *testing.T) { + _, err := test.ExecuteCommand(Command(&test.Params{}), "delete", "--all", "-n", "ns", "-o", "csv") + if err == nil { + t.Fatal("expected error") + } + test.AssertOutput(t, "invalid output format \"csv\": must be json or yaml", err.Error()) + }) +} diff --git a/pkg/cmd/task/delete.go b/pkg/cmd/task/delete.go index d0270f738..7c9bf7513 100644 --- a/pkg/cmd/task/delete.go +++ b/pkg/cmd/task/delete.go @@ -70,6 +70,22 @@ Delete a Task and print the result as YAML: tkn task delete foo -n quux -o yaml +Delete all Tasks in a namespace and print the result as JSON: + + tkn task delete --all -n quux -o json + +Delete all Tasks in a namespace and print the result as YAML: + + tkn task delete --all -n quux -o yaml + +Delete a Task and its TaskRuns and print the result as JSON: + + tkn task delete foo -n quux --trs -o json + +Delete a Task and its TaskRuns and print the result as YAML: + + tkn task delete foo -n quux --trs -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -99,10 +115,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -165,6 +177,12 @@ func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNa } d.PrintSuccesses(s) } else if opts.DeleteAllNs { + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err + } + return d.Errors() + } if d.Errors() == nil { fmt.Fprintf(s.Out, "All Tasks deleted in namespace %q\n", p.Namespace()) } diff --git a/pkg/cmd/task/delete_test.go b/pkg/cmd/task/delete_test.go index f1f024139..d12e999a8 100644 --- a/pkg/cmd/task/delete_test.go +++ b/pkg/cmd/task/delete_test.go @@ -815,3 +815,51 @@ func TestTaskDeleteStructuredOutputWithRelated(t *testing.T) { } test.AssertOutput(t, "{\n \"deleted\": [\n \"task-run-1\",\n \"task-run-2\",\n \"task\"\n ]\n}\n", out) } + +func TestTaskDeleteBulkStructuredOutput(t *testing.T) { + version := "v1" + tdata := []*v1.Task{ + {ObjectMeta: metav1.ObjectMeta{Name: "task-a", Namespace: "ns"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "task-b", Namespace: "ns"}}, + } + ns := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}} + + newParams := func(t *testing.T) *test.Params { + t.Helper() + cs, _ := test.SeedTestData(t, pipelinetest.Data{Tasks: tdata, Namespaces: ns}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredT(tdata[0], version), + cb.UnstructuredT(tdata[1], version), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + } + + t.Run("all json skips confirmation", func(t *testing.T) { + out, err := test.ExecuteCommand(Command(newParams(t)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"task-a\",\n \"task-b\"\n ]\n}\n", out) + }) + + t.Run("all yaml is equivalent", func(t *testing.T) { + out, err := test.ExecuteCommand(Command(newParams(t)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- task-a\n- task-b\n", out) + }) + + t.Run("invalid format", func(t *testing.T) { + _, err := test.ExecuteCommand(Command(newParams(t)), "delete", "--all", "-n", "ns", "-o", "csv") + if err == nil { + t.Fatal("expected error") + } + test.AssertOutput(t, "invalid output format \"csv\": must be json or yaml", err.Error()) + }) +} diff --git a/pkg/cmd/triggerbinding/delete.go b/pkg/cmd/triggerbinding/delete.go index 0444bc54c..3849f63a5 100644 --- a/pkg/cmd/triggerbinding/delete.go +++ b/pkg/cmd/triggerbinding/delete.go @@ -67,6 +67,14 @@ Delete a TriggerBinding and print the result as YAML: tkn triggerbinding delete foo -n quux -o yaml +Delete all TriggerBindings in a namespace and print the result as JSON: + + tkn triggerbinding delete --all -n quux -o json + +Delete all TriggerBindings in a namespace and print the result as YAML: + + tkn triggerbinding delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -95,10 +103,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -141,13 +145,13 @@ func deleteTriggerBindings(s *cli.Stream, p cli.Params, tbNames []string, delete } d.Delete(tbNames) - if !deleteAll { - if formatted.IsStructured(output) { - if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { - return err - } - return d.Errors() + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err } + return d.Errors() + } + if !deleteAll { d.PrintSuccesses(s) } else if deleteAll { if d.Errors() == nil { diff --git a/pkg/cmd/triggerbinding/delete_test.go b/pkg/cmd/triggerbinding/delete_test.go index da7c9b120..81c972f86 100644 --- a/pkg/cmd/triggerbinding/delete_test.go +++ b/pkg/cmd/triggerbinding/delete_test.go @@ -302,3 +302,38 @@ func TestTriggerBindingDeleteStructuredOutput(t *testing.T) { }) } } + +func TestTriggerBindingDeleteBulkStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}} + tbs := []*v1beta1.TriggerBinding{ + {ObjectMeta: metav1.ObjectMeta{Name: "tb-1", Namespace: "ns"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "tb-2", Namespace: "ns"}}, + } + + seed := func(t *testing.T) *test.Params { + t.Helper() + cs := test.SeedTestResources(t, triggertest.Resources{TriggerBindings: tbs, Namespaces: ns}) + cs.Triggers.Resources = cb.TriggersAPIResourceList("v1beta1", []string{"triggerbinding"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredV1beta1TB(tbs[0], "v1beta1"), + cb.UnstructuredV1beta1TB(tbs[1], "v1beta1"), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + } + + out, err := test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"tb-1\",\n \"tb-2\"\n ]\n}\n", out) + + out, err = test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- tb-1\n- tb-2\n", out) +} diff --git a/pkg/cmd/triggertemplate/delete.go b/pkg/cmd/triggertemplate/delete.go index 829dbdfb9..8fc7d3b7c 100644 --- a/pkg/cmd/triggertemplate/delete.go +++ b/pkg/cmd/triggertemplate/delete.go @@ -70,6 +70,14 @@ Delete a TriggerTemplate and print the result as YAML: tkn triggertemplate delete foo -n quux -o yaml +Delete all TriggerTemplates in a namespace and print the result as JSON: + + tkn triggertemplate delete --all -n quux -o json + +Delete all TriggerTemplates in a namespace and print the result as YAML: + + tkn triggertemplate delete --all -n quux -o yaml + Using -o json or -o yaml skips the confirmation prompt. ` @@ -98,10 +106,6 @@ Using -o json or -o yaml skips the confirmation prompt. if !formatted.IsStructured(output) { return fmt.Errorf("invalid output format %q: must be json or yaml", output) } - // Temporary guard: drop this once bulk delete supports -o. - if opts.DeleteAllNs { - return fmt.Errorf("structured output is not supported with bulk delete flags") - } opts.ForceDelete = true } @@ -144,13 +148,13 @@ func deleteTriggerTemplates(s *cli.Stream, p cli.Params, ttNames []string, delet } d.Delete(ttNames) - if !deleteAll { - if formatted.IsStructured(output) { - if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { - return err - } - return d.Errors() + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(d.SuccessfulDeletes())); err != nil { + return err } + return d.Errors() + } + if !deleteAll { d.PrintSuccesses(s) } else if deleteAll { if d.Errors() == nil { diff --git a/pkg/cmd/triggertemplate/delete_test.go b/pkg/cmd/triggertemplate/delete_test.go index bafbc752c..263ab6e2d 100644 --- a/pkg/cmd/triggertemplate/delete_test.go +++ b/pkg/cmd/triggertemplate/delete_test.go @@ -301,3 +301,38 @@ func TestTriggerTemplateDeleteStructuredOutput(t *testing.T) { }) } } + +func TestTriggerTemplateDeleteBulkStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{{ObjectMeta: metav1.ObjectMeta{Name: "ns"}}} + tts := []*v1beta1.TriggerTemplate{ + {ObjectMeta: metav1.ObjectMeta{Name: "tt-1", Namespace: "ns"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "tt-2", Namespace: "ns"}}, + } + + seed := func(t *testing.T) *test.Params { + t.Helper() + cs := test.SeedTestResources(t, triggertest.Resources{TriggerTemplates: tts, Namespaces: ns}) + cs.Triggers.Resources = cb.TriggersAPIResourceList("v1beta1", []string{"triggertemplate"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredV1beta1TT(tts[0], "v1beta1"), + cb.UnstructuredV1beta1TT(tts[1], "v1beta1"), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + return &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + } + + out, err := test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"tt-1\",\n \"tt-2\"\n ]\n}\n", out) + + out, err = test.ExecuteCommand(Command(seed(t)), "delete", "--all", "-n", "ns", "-o", "yaml") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "deleted:\n- tt-1\n- tt-2\n", out) +}