diff --git a/docs/cmd/tkn_taskrun_delete.md b/docs/cmd/tkn_taskrun_delete.md index d34f16466f..8ebee9c032 100644 --- a/docs/cmd/tkn_taskrun_delete.md +++ b/docs/cmd/tkn_taskrun_delete.md @@ -28,18 +28,15 @@ or ### Options ``` - --all Delete all TaskRuns in a namespace (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 - -i, --ignore-running ignore running TaskRun (default true) - --ignore-running-pipelinerun ignore deleting taskruns of a running PipelineRun (default true) - --keep int Keep n most recent number of TaskRuns - --keep-since int When deleting all TaskRuns keep the ones that has been completed since n minutes - -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file). - --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. - -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]. + --all Delete all TaskRuns in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -i, --ignore-running ignore running TaskRun (default true) + --ignore-running-pipelinerun ignore deleting taskruns of a running PipelineRun (default true) + --keep int Keep n most recent number of TaskRuns + --keep-since int When deleting all TaskRuns keep the ones that has been completed since n minutes + -o, --output string Output format. Only "json" is supported + -t, --task string The name of a Task whose TaskRuns should be deleted (does not delete the task) ``` ### Options inherited from parent commands diff --git a/docs/man/man1/tkn-taskrun-delete.1 b/docs/man/man1/tkn-taskrun-delete.1 index 87809dc6a9..8c48577d1d 100644 --- a/docs/man/man1/tkn-taskrun-delete.1 +++ b/docs/man/man1/tkn-taskrun-delete.1 @@ -23,10 +23,6 @@ Delete TaskRuns in a namespace \fB\-\-all\fP[=false] Delete all TaskRuns in a namespace (default: false) -.PP -\fB\-\-allow\-missing\-template\-keys\fP[=true] - 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. - .PP \fB\-f\fP, \fB\-\-force\fP[=false] Whether to force deletion (default: false) @@ -53,21 +49,12 @@ Delete TaskRuns in a namespace .PP \fB\-o\fP, \fB\-\-output\fP="" - Output format. One of: (json, yaml, name, go\-template, go\-template\-file, template, templatefile, jsonpath, jsonpath\-as\-json, jsonpath\-file). - -.PP -\fB\-\-show\-managed\-fields\fP[=false] - If true, keep the managedFields when printing objects in JSON or YAML format. + Output format. Only "json" is supported .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 [ -\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]]. - .SH OPTIONS INHERITED FROM PARENT COMMANDS .PP diff --git a/pkg/cmd/taskrun/delete.go b/pkg/cmd/taskrun/delete.go index aded57b77b..3be4c1572e 100644 --- a/pkg/cmd/taskrun/delete.go +++ b/pkg/cmd/taskrun/delete.go @@ -15,6 +15,7 @@ package taskrun import ( + "encoding/json" "errors" "fmt" "strings" @@ -32,7 +33,6 @@ import ( v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) type deleteOptions struct { @@ -64,7 +64,6 @@ func trExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "TaskRun", ForceDelete: false, DeleteAllNs: false} deleteOpts := &deleteOptions{} - f := cliopts.NewPrintFlags("delete") eg := `Delete TaskRuns with names 'foo' and 'bar' in namespace 'quux': tkn taskrun delete foo bar -n quux @@ -92,6 +91,15 @@ or Err: cmd.OutOrStderr(), } + output, err := cmd.LocalFlags().GetString("output") + if err != nil { + return err + } + + if output != "" && output != "json" { + return fmt.Errorf("unsupported output format %q; supported formats: json", output) + } + if deleteOpts.TaskName != "" { opts.ParentResource = "Task" opts.ParentResourceName = deleteOpts.TaskName @@ -122,17 +130,21 @@ or return errs } - if err := opts.CheckOptions(s, availableTrs, p.Namespace()); err != nil { + checkStreams := s + if output == "json" { + checkStreams = &cli.Stream{In: strings.NewReader("y\n"), Out: &strings.Builder{}, Err: s.Err} + } + if err := opts.CheckOptions(checkStreams, availableTrs, p.Namespace()); err != nil { return err } - if err := deleteTaskRuns(s, p, availableTrs, opts); err != nil { + if err := deleteTaskRuns(s, p, availableTrs, opts, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringP("output", "o", "", `Output format. Only "json" is supported`) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().StringVarP(&deleteOpts.TaskName, "task", "t", "", "The name of a Task whose TaskRuns should be deleted (does not delete the task)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all TaskRuns in a namespace (default: false)") @@ -144,7 +156,7 @@ or return c } -func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options.DeleteOptions) error { +func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options.DeleteOptions, output string) error { var numberOfDeletedTr, numberOfKeptTr int cs, err := p.Clients() if err != nil { @@ -177,9 +189,17 @@ func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options prFinished := ownerPrFinished(cs, *tr) if !prFinished && opts.ForceDelete { - fmt.Fprintf(s.Out, "warning: Taskrun %s related pipelinerun still running.\n", tr.Name) + if output == "json" { + fmt.Fprintf(s.Err, "warning: Taskrun %s related pipelinerun still running.\n", tr.Name) + } else { + fmt.Fprintf(s.Out, "warning: Taskrun %s related pipelinerun still running.\n", tr.Name) + } } if !prFinished && !opts.ForceDelete { + if output == "json" { + return fmt.Errorf("taskrun %s is owned by a running PipelineRun; use --force to delete", tr.Name) + } + fmt.Fprintf(s.Out, "TaskRun(s): %s attached to PipelineRun is still running deleting will restart the completed taskrun. Proceed (y/n): ", tr.Name) if err := opts.TakeInput(s, ""); err != nil { continue @@ -212,14 +232,33 @@ func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options }) if opts.Keep > 0 && opts.Keep == len(trToKeep) && len(trToDelete) == 0 { - fmt.Fprintf(s.Out, "Associated %s (%d) for Task:%s is/are equal to keep (%d) \n", opts.Resource, len(trToKeep), opts.ParentResourceName, opts.Keep) - return nil + if output != "json" { + fmt.Fprintf(s.Out, "Associated %s (%d) for Task:%s is/are equal to keep (%d) \n", opts.Resource, len(trToKeep), opts.ParentResourceName, opts.Keep) + return nil + } + } else if opts.Keep > len(trToKeep) { + if output != "json" { + fmt.Fprintf(s.Out, "There is/are only %d %s(s) associated for %s: %s \n", len(trToKeep), opts.Resource, opts.ParentResource, opts.ParentResourceName) + return nil + } + } else { + d.DeleteRelated([]string{opts.ParentResourceName}) } - if opts.Keep > len(trToKeep) { - fmt.Fprintf(s.Out, "There is/are only %d %s(s) associated for %s: %s \n", len(trToKeep), opts.Resource, opts.ParentResource, opts.ParentResourceName) - return nil + } + + if output == "json" { + deleted := append(d.SuccessfulRelatedDeletes(), d.SuccessfulDeletes()...) + if deleted == nil { + deleted = []string{} + } + + result := struct { + Deleted []string `json:"deleted"` + }{ + Deleted: deleted, } - d.DeleteRelated([]string{opts.ParentResourceName}) + encodeErr := json.NewEncoder(s.Out).Encode(result) + return multierr.Append(encodeErr, d.Errors()) } if !opts.DeleteAllNs { diff --git a/pkg/cmd/taskrun/delete_test.go b/pkg/cmd/taskrun/delete_test.go index 061853d43d..98cef0e565 100644 --- a/pkg/cmd/taskrun/delete_test.go +++ b/pkg/cmd/taskrun/delete_test.go @@ -572,6 +572,55 @@ func TestTaskRunDelete_v1beta1(t *testing.T) { wantError: false, want: "All 6 TaskRuns associated with Task \"random\" deleted in namespace \"ns\"\n", }, + { + name: "With JSON output", + command: []string{"rm", "tr0-1", "-n", "ns", "-o", "json"}, + dynamic: seeds[14].dynamicClient, + input: seeds[14].pipelineClient, + inputStream: nil, + wantError: false, + want: `{"deleted":["tr0-1"]} +`, + }, + { + name: "Unsupported output format", + command: []string{"rm", "tr0-1", "-n", "ns", "-o", "yaml"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + inputStream: nil, + wantError: true, + want: "unsupported output format \"yaml\"; supported formats: json", + }, + { + name: "With JSON output for multiple TaskRuns", + command: []string{"rm", "tr0-1", "tr0-2", "-n", "ns", "-o", "json"}, + dynamic: seeds[16].dynamicClient, + input: seeds[16].pipelineClient, + inputStream: strings.NewReader("y\n"), + wantError: false, + want: `{"deleted":["tr0-1","tr0-2"]} +`, + }, + { + name: "Delete with JSON output for --task but keep meets or exceeds existing (no-op)", + command: []string{"rm", "--task", "random", "-n", "ns", "--keep", "10", "-o", "json"}, + dynamic: seeds[15].dynamicClient, + input: seeds[15].pipelineClient, + inputStream: nil, + wantError: false, + want: `{"deleted":[]} +`, + }, + { + name: "Delete with JSON output for --task", + command: []string{"rm", "--task", "random", "-n", "ns", "-o", "json"}, + dynamic: seeds[15].dynamicClient, + input: seeds[15].pipelineClient, + inputStream: nil, + wantError: false, + want: `{"deleted":["tr0-1","tr0-2","tr0-3","tr0-9"]} +`, + }, } for _, tp := range testParams { @@ -1326,6 +1375,15 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun_v1beta1(t *testing.T) { wantError bool want string }{ + { + name: "Taskrun with running pipelinerun JSON without force", + command: []string{"rm", "tr0-1", "-n", "ns", "-o", "json"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + inputStream: nil, + wantError: true, + want: "taskrun tr0-1 is owned by a running PipelineRun; use --force to delete", + }, { name: "Taskrun with running pipelinerun and answer y", command: []string{"rm", "tr0-1", "-n", "ns"}, diff --git a/pkg/deleter/deleter.go b/pkg/deleter/deleter.go index 016b33b4b1..3264e07ac3 100644 --- a/pkg/deleter/deleter.go +++ b/pkg/deleter/deleter.go @@ -104,6 +104,14 @@ func (d *Deleter) PrintSuccesses(streams *cli.Stream) { } } +func (d *Deleter) SuccessfulDeletes() []string { + return append([]string(nil), d.successfulDeletes...) +} + +func (d *Deleter) SuccessfulRelatedDeletes() []string { + return append([]string(nil), d.successfulRelatedDeletes...) +} + // appendError adds that error to the list of accumulated errors that // have occurred during execution. func (d *Deleter) appendError(err error) { diff --git a/pkg/deleter/deleter_test.go b/pkg/deleter/deleter_test.go index 391bfe9bf9..054c22324b 100644 --- a/pkg/deleter/deleter_test.go +++ b/pkg/deleter/deleter_test.go @@ -1,6 +1,7 @@ package deleter import ( + "reflect" "strings" "testing" @@ -50,6 +51,29 @@ func TestDelete(t *testing.T) { } } +func TestSuccessfulDeletes(t *testing.T) { + d := New("FooBar", successfulDeleteFunc()) + d.Delete([]string{"foo", "bar"}) + + expected := []string{"foo", "bar"} + if got := d.SuccessfulDeletes(); !reflect.DeepEqual(got, expected) { + t.Errorf("expected %v, received %v", expected, got) + } +} + +func TestSuccessfulRelatedDeletes(t *testing.T) { + d := New("FooBar", successfulDeleteFunc()) + d.WithRelated("FooBarRun", successfulListFunc("fbr1", "fbr2"), successfulDeleteFunc()) + + deletedNames := d.Delete([]string{"foo"}) + d.DeleteRelated(deletedNames) + + expected := []string{"fbr1", "fbr2"} + if got := d.SuccessfulRelatedDeletes(); !reflect.DeepEqual(got, expected) { + t.Errorf("expected %v, received %v", expected, got) + } +} + func TestDeleteRelated(t *testing.T) { for _, tc := range []struct { description string