diff --git a/docs/cmd/tkn_taskrun_delete.md b/docs/cmd/tkn_taskrun_delete.md index 1cba0d306..ccc458425 100644 --- a/docs/cmd/tkn_taskrun_delete.md +++ b/docs/cmd/tkn_taskrun_delete.md @@ -20,10 +20,23 @@ Delete TaskRuns with names 'foo' and 'bar' in namespace 'quux': tkn taskrun delete foo bar -n quux +Delete TaskRuns and print JSON: + + tkn taskrun delete foo bar -n quux -o json + +Delete all TaskRuns and print JSON: + + tkn taskrun delete --all -n quux -o json + +Delete TaskRuns associated with a Task: + + tkn taskrun delete --task my-task -n quux -o json + or tkn tr rm foo bar -n quux +Deleting a TaskRun owned by a running PipelineRun requires --force when using structured output. ### Options diff --git a/docs/man/man1/tkn-taskrun-delete.1 b/docs/man/man1/tkn-taskrun-delete.1 index 50c6697cf..31d63ea93 100644 --- a/docs/man/man1/tkn-taskrun-delete.1 +++ b/docs/man/man1/tkn-taskrun-delete.1 @@ -87,6 +87,42 @@ tkn taskrun delete foo bar \-n quux .fi .RE +.PP +Delete TaskRuns and print JSON: + +.PP +.RS + +.nf +tkn taskrun delete foo bar \-n quux \-o json + +.fi +.RE + +.PP +Delete all TaskRuns and print JSON: + +.PP +.RS + +.nf +tkn taskrun delete \-\-all \-n quux \-o json + +.fi +.RE + +.PP +Delete TaskRuns associated with a Task: + +.PP +.RS + +.nf +tkn taskrun delete \-\-task my\-task \-n quux \-o json + +.fi +.RE + .PP or @@ -99,6 +135,9 @@ tkn tr rm foo bar \-n quux .fi .RE +.PP +Deleting a TaskRun owned by a running PipelineRun requires \-\-force when using structured output. + .SH SEE ALSO .PP diff --git a/pkg/cmd/taskrun/delete.go b/pkg/cmd/taskrun/delete.go index 4a4007258..c90199fc9 100644 --- a/pkg/cmd/taskrun/delete.go +++ b/pkg/cmd/taskrun/delete.go @@ -67,10 +67,23 @@ func deleteCommand(p cli.Params) *cobra.Command { tkn taskrun delete foo bar -n quux +Delete TaskRuns and print JSON: + + tkn taskrun delete foo bar -n quux -o json + +Delete all TaskRuns and print JSON: + + tkn taskrun delete --all -n quux -o json + +Delete TaskRuns associated with a Task: + + tkn taskrun delete --task my-task -n quux -o json + or tkn tr rm foo bar -n quux -` + +Deleting a TaskRun owned by a running PipelineRun requires --force when using structured output.` c := &cobra.Command{ Use: "delete", @@ -130,12 +143,16 @@ or return errs } - checkStreams := s + checkOpts := opts if formatted.IsStructured(output) { s.Err = cmd.ErrOrStderr() - checkStreams = &cli.Stream{In: strings.NewReader("y\n"), Out: &strings.Builder{}, Err: s.Err} + + structuredOpts := *opts + structuredOpts.ForceDelete = true + checkOpts = &structuredOpts } - if err := opts.CheckOptions(checkStreams, availableTrs, p.Namespace()); err != nil { + + if err := checkOpts.CheckOptions(s, availableTrs, p.Namespace()); err != nil { return err } @@ -159,6 +176,8 @@ or func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options.DeleteOptions, output string) error { var numberOfDeletedTr, numberOfKeptTr int + var safetyErr error + cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") @@ -198,10 +217,21 @@ func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options } if !prFinished && !opts.ForceDelete { if formatted.IsStructured(output) { - return fmt.Errorf("taskrun %s is owned by a running PipelineRun; use --force to delete", tr.Name) + safetyErr = multierr.Append( + safetyErr, + fmt.Errorf( + "taskrun %s is owned by a running PipelineRun; use --force to delete", + tr.Name, + ), + ) + continue } - fmt.Fprintf(s.Out, "TaskRun(s): %s attached to PipelineRun is still running deleting will restart the completed taskrun. Proceed (y/n): ", 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 } @@ -250,8 +280,17 @@ func deleteTaskRuns(s *cli.Stream, p cli.Params, trNames []string, opts *options if formatted.IsStructured(output) { deleted := append(d.SuccessfulRelatedDeletes(), d.SuccessfulDeletes()...) - printErr := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(deleted)) - return multierr.Append(printErr, d.Errors()) + if deleted == nil { + deleted = []string{} + } + + printErr := formatted.PrintStructuredOutput( + s.Out, + output, + formatted.NewDeleteResult(deleted), + ) + + return multierr.Combine(printErr, d.Errors(), safetyErr) } if !opts.DeleteAllNs { diff --git a/pkg/cmd/taskrun/delete_test.go b/pkg/cmd/taskrun/delete_test.go index 5a2118d80..3061219b8 100644 --- a/pkg/cmd/taskrun/delete_test.go +++ b/pkg/cmd/taskrun/delete_test.go @@ -16,6 +16,7 @@ package taskrun import ( "bytes" + "context" "encoding/json" "io" "strings" @@ -31,7 +32,9 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" pipelinetest "github.com/tektoncd/pipeline/test" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" duckv1 "knative.dev/pkg/apis/duck/v1" ) @@ -230,7 +233,7 @@ func TestTaskRunDelete_v1beta1(t *testing.T) { } seeds := make([]clients, 0) - for i := 0; i < 18; i++ { + for i := 0; i < 24; i++ { trs := trdata cs, _ := test.SeedV1beta1TestData(t, test.Data{TaskRuns: trs, Tasks: tasks, Namespaces: ns}) cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"taskrun"}) @@ -250,6 +253,24 @@ func TestTaskRunDelete_v1beta1(t *testing.T) { seeds = append(seeds, clients{cs, dc}) } + emptyCS, _ := test.SeedV1beta1TestData(t, test.Data{ + Tasks: tasks, + Namespaces: ns, + }) + + emptyCS.Pipeline.Resources = cb.APIResourceList( + versionv1beta1, + []string{"taskrun"}, + ) + + emptyTDC := testDynamic.Options{} + emptyDC, err := emptyTDC.Client( + cb.UnstructuredV1beta1T(tasks[0], versionv1beta1), + ) + if err != nil { + t.Fatalf("unable to create empty dynamic client: %v", err) + } + testParams := []struct { name string command []string @@ -650,6 +671,32 @@ func TestTaskRunDelete_v1beta1(t *testing.T) { } `, }, + { + name: "Delete all TaskRuns with JSON output", + command: []string{"delete", "-n", "ns", "--all", "-o", "json"}, + dynamic: seeds[18].dynamicClient, + input: seeds[18].pipelineClient, + inputStream: nil, + wantError: false, + want: `{ + "deleted": [ + "tr0-1", + "tr0-2", + "tr0-3", + "tr0-9" + ] +} +`, + }, + { + name: "Delete all with JSON output and no TaskRuns", + command: []string{"delete", "-n", "ns", "--all", "-o", "json"}, + dynamic: emptyDC, + input: emptyCS, + inputStream: nil, + wantError: false, + want: "{\n \"deleted\": []\n}\n", + }, } for _, tp := range testParams { @@ -1406,13 +1453,13 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun_v1beta1(t *testing.T) { assertStreams bool }{ { - 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 JSON without force", + command: []string{"rm", "tr0-1", "-n", "ns", "-o", "json"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + wantError: true, + want: "taskrun tr0-1 is owned by a running PipelineRun; use --force to delete", + assertStreams: true, }, { name: "Taskrun with running pipelinerun and answer y", @@ -1574,6 +1621,29 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun(t *testing.T) { }, }, }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "tr0-3", + Labels: map[string]string{"tekton.dev/task": "random"}, + }, + Spec: v1.TaskRunSpec{ + TaskRef: &v1.TaskRef{ + Name: "random", + Kind: v1.NamespacedTaskKind, + }, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1.TaskRunReasonSuccessful.String(), + }, + }, + }, + }, + }, } prdata := []*v1.PipelineRun{ @@ -1612,7 +1682,7 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun(t *testing.T) { } seeds := make([]clients, 0) - for i := 0; i < 5; i++ { + for i := 0; i < 6; i++ { trs := trdata prs := prdata cs, _ := test.SeedTestData(t, pipelinetest.Data{TaskRuns: trs, Tasks: tasks, PipelineRuns: prs, Namespaces: ns}) @@ -1622,6 +1692,7 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun(t *testing.T) { cb.UnstructuredT(tasks[0], version), cb.UnstructuredTR(trdata[0], version), cb.UnstructuredTR(trdata[1], version), + cb.UnstructuredTR(trdata[2], version), cb.UnstructuredPR(prs[0], version), ) if err != nil { @@ -1665,6 +1736,15 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun(t *testing.T) { wantError: false, assertStreams: true, }, + { + name: "Mixed TaskRuns JSON skips running PipelineRun owner", + command: []string{"rm", "tr0-1", "tr0-3", "-n", "ns", "-o", "json"}, + dynamic: seeds[5].dynamicClient, + input: seeds[5].pipelineClient, + wantError: true, + want: "taskrun tr0-1 is owned by a running PipelineRun; use --force to delete", + assertStreams: true, + }, { name: "All taskruns with running pipelinerun ", command: []string{"delete", "-n", "ns", "--all"}, @@ -1698,22 +1778,72 @@ func Test_TaskRuns_Delete_With_Running_PipelineRun(t *testing.T) { taskrun.SetErr(&stderr) taskrun.SetArgs(tp.command) - if err := taskrun.Execute(); err != nil { + err := taskrun.Execute() + if tp.wantError { + if err == nil { + t.Fatal("expected an error") + } + if !strings.Contains(err.Error(), tp.want) { + t.Errorf("expected error containing %q, got %q", tp.want, err.Error()) + } + } else if err != nil { t.Fatalf("unexpected error: %v", err) } if !json.Valid(stdout.Bytes()) { t.Errorf("stdout is not valid JSON: %q", stdout.String()) } - + if strings.Contains(tp.name, "Mixed TaskRuns JSON") { + var result struct { + Deleted []string `json:"deleted"` + } + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + t.Fatalf("failed to decode JSON output: %v", err) + } + if len(result.Deleted) != 1 || result.Deleted[0] != "tr0-3" { + t.Errorf("expected deleted [tr0-3], got %v", result.Deleted) + } + } if strings.Contains(stdout.String(), "warning:") { t.Errorf("warning leaked into stdout: %q", stdout.String()) } + if strings.Contains(stdout.String(), "Proceed (y/n)") || + strings.Contains(stdout.String(), "Are you sure") { + t.Errorf("confirmation prompt leaked into stdout: %q", stdout.String()) + } - if !strings.Contains(stderr.String(), "warning:") { + if !tp.wantError && !strings.Contains(stderr.String(), "warning:") { t.Errorf("expected warning on stderr, got: %q", stderr.String()) } + if strings.Contains(tp.name, "Mixed TaskRuns JSON") { + ctx := context.Background() + + _, err := tp.dynamic.Resource(schema.GroupVersionResource{ + Group: "tekton.dev", + Version: version, + Resource: "taskruns", + }). + Namespace("ns"). + Get(ctx, "tr0-1", metav1.GetOptions{}) + if err != nil { + t.Errorf("expected tr0-1 to remain, got error: %v", err) + } + + _, err = tp.dynamic.Resource(schema.GroupVersionResource{ + Group: "tekton.dev", + Version: version, + Resource: "taskruns", + }). + Namespace("ns"). + Get(ctx, "tr0-3", metav1.GetOptions{}) + if err == nil { + t.Error("expected tr0-3 to be deleted, but it still exists") + } else if !apierrors.IsNotFound(err) { + t.Errorf("unexpected error checking tr0-3: %v", err) + } + } + return }