diff --git a/docs/cmd/tkn_clustertriggerbinding_delete.md b/docs/cmd/tkn_clustertriggerbinding_delete.md index 19000fa49c..1cb98af5e8 100644 --- a/docs/cmd/tkn_clustertriggerbinding_delete.md +++ b/docs/cmd/tkn_clustertriggerbinding_delete.md @@ -24,17 +24,24 @@ or tkn ctb rm foo bar +Delete a ClusterTriggerBinding and print the result as JSON: + + tkn clustertriggerbinding delete foo -o json + +Delete a ClusterTriggerBinding and print the result as YAML: + + tkn clustertriggerbinding delete foo -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all ClusterTriggerBindings (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 - -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. - --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 ClusterTriggerBindings (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_customrun_delete.md b/docs/cmd/tkn_customrun_delete.md index d20471d6e2..3543a307be 100644 --- a/docs/cmd/tkn_customrun_delete.md +++ b/docs/cmd/tkn_customrun_delete.md @@ -24,15 +24,20 @@ or tkn cr rm foo -n bar +Delete a CustomRun and print the result as JSON: + + tkn customrun delete foo -n bar -o json + +Delete a CustomRun and print the result as YAML: + + tkn customrun delete foo -n bar -o yaml + ### Options ``` - --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) - -h, --help help for delete - -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. - --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]. + -h, --help help for delete + -o, --output string Output format. One of: json|yaml ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_eventlistener_delete.md b/docs/cmd/tkn_eventlistener_delete.md index 940b8fc91e..580e23abc9 100644 --- a/docs/cmd/tkn_eventlistener_delete.md +++ b/docs/cmd/tkn_eventlistener_delete.md @@ -24,17 +24,24 @@ or tkn el rm foo bar -n quux +Delete an EventListener and print the result as JSON: + + tkn eventlistener delete foo -n quux -o json + +Delete an EventListener and print the result as YAML: + + tkn eventlistener delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all EventListeners 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 - -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. - --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 EventListeners in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_pipeline_delete.md b/docs/cmd/tkn_pipeline_delete.md index 8040196593..b160e946ff 100644 --- a/docs/cmd/tkn_pipeline_delete.md +++ b/docs/cmd/tkn_pipeline_delete.md @@ -24,18 +24,25 @@ or tkn p rm foo bar -n quux +Delete a Pipeline and print the result as JSON: + + tkn pipeline delete foo -n quux -o json + +Delete a Pipeline and print the result as YAML: + + tkn pipeline delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all Pipelines 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 - -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file). - --prs Whether to delete Pipeline(s) and related resources (PipelineRuns) (default: false) - --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. - --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 Pipelines in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt + --prs Whether to delete Pipeline(s) and related resources (PipelineRuns) (default: false) ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_pipelinerun_delete.md b/docs/cmd/tkn_pipelinerun_delete.md index 9aa83acac8..4a5cc47f85 100644 --- a/docs/cmd/tkn_pipelinerun_delete.md +++ b/docs/cmd/tkn_pipelinerun_delete.md @@ -24,22 +24,29 @@ or tkn pr rm foo bar -n quux +Delete a PipelineRun and print the result as JSON: + + tkn pipelinerun delete foo -n quux -o json + +Delete a PipelineRun and print the result as YAML: + + tkn pipelinerun delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all PipelineRuns 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 PipelineRun (default true) - --keep int Keep n most recent number of PipelineRuns - --keep-since int When deleting all PipelineRuns keep the ones that has been completed since n minutes - --label string A selector (label query) to filter on when running with --all, supports '=', '==', and '!=' - -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file). - -p, --pipeline string The name of a Pipeline whose PipelineRuns should be deleted (does not delete the Pipeline) - --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. - --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 PipelineRuns in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -i, --ignore-running ignore running PipelineRun (default true) + --keep int Keep n most recent number of PipelineRuns + --keep-since int When deleting all PipelineRuns keep the ones that has been completed since n minutes + --label string A selector (label query) to filter on when running with --all, supports '=', '==', and '!=' + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt + -p, --pipeline string The name of a Pipeline whose PipelineRuns should be deleted (does not delete the Pipeline) ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_task_delete.md b/docs/cmd/tkn_task_delete.md index 917e2ec111..6061404283 100644 --- a/docs/cmd/tkn_task_delete.md +++ b/docs/cmd/tkn_task_delete.md @@ -24,18 +24,25 @@ or tkn t rm foo bar -n quux +Delete a Task and print the result as JSON: + + tkn task delete foo -n quux -o json + +Delete a Task and print the result as YAML: + + tkn task delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all Tasks 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 - -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. - --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]. - --trs Whether to delete Task(s) and related resources (TaskRuns) (default: false) + --all Delete all Tasks in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt + --trs Whether to delete Task(s) and related resources (TaskRuns) (default: false) ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_triggerbinding_delete.md b/docs/cmd/tkn_triggerbinding_delete.md index 217360cbb6..2b191c69f4 100644 --- a/docs/cmd/tkn_triggerbinding_delete.md +++ b/docs/cmd/tkn_triggerbinding_delete.md @@ -24,17 +24,24 @@ or tkn tb rm foo bar -n quux +Delete a TriggerBinding and print the result as JSON: + + tkn triggerbinding delete foo -n quux -o json + +Delete a TriggerBinding and print the result as YAML: + + tkn triggerbinding delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all TriggerBindings 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 - -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. - --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 TriggerBindings in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt ``` ### Options inherited from parent commands diff --git a/docs/cmd/tkn_triggertemplate_delete.md b/docs/cmd/tkn_triggertemplate_delete.md index 2b932957c2..bbd9873765 100644 --- a/docs/cmd/tkn_triggertemplate_delete.md +++ b/docs/cmd/tkn_triggertemplate_delete.md @@ -24,17 +24,24 @@ or tkn tt rm foo bar -n quux +Delete a TriggerTemplate and print the result as JSON: + + tkn triggertemplate delete foo -n quux -o json + +Delete a TriggerTemplate and print the result as YAML: + + tkn triggertemplate delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. + ### Options ``` - --all Delete all TriggerTemplates 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 - -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. - --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 TriggerTemplates in a namespace (default: false) + -f, --force Whether to force deletion (default: false) + -h, --help help for delete + -o, --output string Output format. One of: json|yaml. Skips the confirmation prompt ``` ### Options inherited from parent commands diff --git a/docs/man/man1/tkn-clustertriggerbinding-delete.1 b/docs/man/man1/tkn-clustertriggerbinding-delete.1 index 0baefb1551..94a5c25080 100644 --- a/docs/man/man1/tkn-clustertriggerbinding-delete.1 +++ b/docs/man/man1/tkn-clustertriggerbinding-delete.1 @@ -23,10 +23,6 @@ Delete ClusterTriggerBindings \fB\-\-all\fP[=false] Delete all ClusterTriggerBindings (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) @@ -37,16 +33,7 @@ Delete ClusterTriggerBindings .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. - -.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]]. + Output format. One of: json|yaml. Skips the confirmation prompt .SH OPTIONS INHERITED FROM PARENT COMMANDS @@ -88,6 +75,33 @@ tkn ctb rm foo bar .fi .RE +.PP +Delete a ClusterTriggerBinding and print the result as JSON: + +.PP +.RS + +.nf +tkn clustertriggerbinding delete foo \-o json + +.fi +.RE + +.PP +Delete a ClusterTriggerBinding and print the result as YAML: + +.PP +.RS + +.nf +tkn clustertriggerbinding delete foo \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-customrun-delete.1 b/docs/man/man1/tkn-customrun-delete.1 index 7ab18b9c37..9eb4af86e1 100644 --- a/docs/man/man1/tkn-customrun-delete.1 +++ b/docs/man/man1/tkn-customrun-delete.1 @@ -19,26 +19,13 @@ Delete CustomRuns in a namespace .SH OPTIONS -.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\-h\fP, \fB\-\-help\fP[=false] help for delete .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. - -.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]]. + Output format. One of: json|yaml .SH OPTIONS INHERITED FROM PARENT COMMANDS @@ -84,6 +71,30 @@ tkn cr rm foo \-n bar .fi .RE +.PP +Delete a CustomRun and print the result as JSON: + +.PP +.RS + +.nf +tkn customrun delete foo \-n bar \-o json + +.fi +.RE + +.PP +Delete a CustomRun and print the result as YAML: + +.PP +.RS + +.nf +tkn customrun delete foo \-n bar \-o yaml + +.fi +.RE + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-eventlistener-delete.1 b/docs/man/man1/tkn-eventlistener-delete.1 index 06439f66fb..4245b79b0f 100644 --- a/docs/man/man1/tkn-eventlistener-delete.1 +++ b/docs/man/man1/tkn-eventlistener-delete.1 @@ -23,10 +23,6 @@ Delete EventListeners in a namespace \fB\-\-all\fP[=false] Delete all EventListeners 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) @@ -37,16 +33,7 @@ Delete EventListeners 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. - -.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]]. + Output format. One of: json|yaml. Skips the confirmation prompt .SH OPTIONS INHERITED FROM PARENT COMMANDS @@ -92,6 +79,33 @@ tkn el rm foo bar \-n quux .fi .RE +.PP +Delete an EventListener and print the result as JSON: + +.PP +.RS + +.nf +tkn eventlistener delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete an EventListener and print the result as YAML: + +.PP +.RS + +.nf +tkn eventlistener delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-pipeline-delete.1 b/docs/man/man1/tkn-pipeline-delete.1 index 84bd46e945..ea3a01373f 100644 --- a/docs/man/man1/tkn-pipeline-delete.1 +++ b/docs/man/man1/tkn-pipeline-delete.1 @@ -23,10 +23,6 @@ Delete Pipelines in a namespace \fB\-\-all\fP[=false] Delete all Pipelines 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) @@ -37,21 +33,12 @@ Delete Pipelines 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). + Output format. One of: json|yaml. Skips the confirmation prompt .PP \fB\-\-prs\fP[=false] Whether to delete Pipeline(s) and related resources (PipelineRuns) (default: false) -.PP -\fB\-\-show\-managed\-fields\fP[=false] - If true, keep the managedFields when printing objects in JSON or YAML format. - -.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 @@ -96,6 +83,33 @@ tkn p rm foo bar \-n quux .fi .RE +.PP +Delete a Pipeline and print the result as JSON: + +.PP +.RS + +.nf +tkn pipeline delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete a Pipeline and print the result as YAML: + +.PP +.RS + +.nf +tkn pipeline delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-pipelinerun-delete.1 b/docs/man/man1/tkn-pipelinerun-delete.1 index b47dadae38..852d4a5d69 100644 --- a/docs/man/man1/tkn-pipelinerun-delete.1 +++ b/docs/man/man1/tkn-pipelinerun-delete.1 @@ -23,10 +23,6 @@ Delete PipelineRuns in a namespace \fB\-\-all\fP[=false] Delete all PipelineRuns 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 PipelineRuns 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). + Output format. One of: json|yaml. Skips the confirmation prompt .PP \fB\-p\fP, \fB\-\-pipeline\fP="" The name of a Pipeline whose PipelineRuns should be deleted (does not delete the Pipeline) -.PP -\fB\-\-show\-managed\-fields\fP[=false] - If true, keep the managedFields when printing objects in JSON or YAML format. - -.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 @@ -112,6 +99,33 @@ tkn pr rm foo bar \-n quux .fi .RE +.PP +Delete a PipelineRun and print the result as JSON: + +.PP +.RS + +.nf +tkn pipelinerun delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete a PipelineRun and print the result as YAML: + +.PP +.RS + +.nf +tkn pipelinerun delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-task-delete.1 b/docs/man/man1/tkn-task-delete.1 index 84cf5b3829..c8b81cddfe 100644 --- a/docs/man/man1/tkn-task-delete.1 +++ b/docs/man/man1/tkn-task-delete.1 @@ -23,10 +23,6 @@ Delete Tasks in a namespace \fB\-\-all\fP[=false] Delete all Tasks 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) @@ -37,16 +33,7 @@ Delete Tasks 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. - -.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]]. + Output format. One of: json|yaml. Skips the confirmation prompt .PP \fB\-\-trs\fP[=false] @@ -96,6 +83,33 @@ tkn t rm foo bar \-n quux .fi .RE +.PP +Delete a Task and print the result as JSON: + +.PP +.RS + +.nf +tkn task delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete a Task and print the result as YAML: + +.PP +.RS + +.nf +tkn task delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-triggerbinding-delete.1 b/docs/man/man1/tkn-triggerbinding-delete.1 index 58bdd96699..ca88a3226a 100644 --- a/docs/man/man1/tkn-triggerbinding-delete.1 +++ b/docs/man/man1/tkn-triggerbinding-delete.1 @@ -23,10 +23,6 @@ Delete TriggerBindings in a namespace \fB\-\-all\fP[=false] Delete all TriggerBindings 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) @@ -37,16 +33,7 @@ Delete TriggerBindings 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. - -.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]]. + Output format. One of: json|yaml. Skips the confirmation prompt .SH OPTIONS INHERITED FROM PARENT COMMANDS @@ -92,6 +79,33 @@ tkn tb rm foo bar \-n quux .fi .RE +.PP +Delete a TriggerBinding and print the result as JSON: + +.PP +.RS + +.nf +tkn triggerbinding delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete a TriggerBinding and print the result as YAML: + +.PP +.RS + +.nf +tkn triggerbinding delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/docs/man/man1/tkn-triggertemplate-delete.1 b/docs/man/man1/tkn-triggertemplate-delete.1 index 76a9698bf4..47abd9ec02 100644 --- a/docs/man/man1/tkn-triggertemplate-delete.1 +++ b/docs/man/man1/tkn-triggertemplate-delete.1 @@ -23,10 +23,6 @@ Delete TriggerTemplates in a namespace \fB\-\-all\fP[=false] Delete all TriggerTemplates 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) @@ -37,16 +33,7 @@ Delete TriggerTemplates 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. - -.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]]. + Output format. One of: json|yaml. Skips the confirmation prompt .SH OPTIONS INHERITED FROM PARENT COMMANDS @@ -92,6 +79,33 @@ tkn tt rm foo bar \-n quux .fi .RE +.PP +Delete a TriggerTemplate and print the result as JSON: + +.PP +.RS + +.nf +tkn triggertemplate delete foo \-n quux \-o json + +.fi +.RE + +.PP +Delete a TriggerTemplate and print the result as YAML: + +.PP +.RS + +.nf +tkn triggertemplate delete foo \-n quux \-o yaml + +.fi +.RE + +.PP +Using \-o json or \-o yaml skips the confirmation prompt. + .SH SEE ALSO .PP diff --git a/pkg/cmd/clustertriggerbinding/delete.go b/pkg/cmd/clustertriggerbinding/delete.go index 50dac049a7..ce40ffadab 100644 --- a/pkg/cmd/clustertriggerbinding/delete.go +++ b/pkg/cmd/clustertriggerbinding/delete.go @@ -26,7 +26,6 @@ import ( "github.com/tektoncd/cli/pkg/options" "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // clusterTriggerBindingExists validates that the arguments are valid ClusterTriggerBinding names @@ -51,7 +50,6 @@ func clusterTriggerBindingExists(args []string, p cli.Params) ([]string, error) func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "clustertriggerbinding", ForceDelete: false, DeleteAll: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete ClusterTriggerBindings with names 'foo' and 'bar' tkn clustertriggerbinding delete foo bar @@ -59,8 +57,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn ctb rm foo bar + +Delete a ClusterTriggerBinding and print the result as JSON: + + tkn clustertriggerbinding delete foo -o json + +Delete a ClusterTriggerBinding and print the result as YAML: + + tkn clustertriggerbinding delete foo -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -79,6 +89,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availbleCTBs, errs := clusterTriggerBindingExists(args, p) if len(availbleCTBs) == 0 && errs != nil { return errs @@ -88,20 +110,20 @@ or return err } - if err := deleteClusterTriggerBindings(s, p, availbleCTBs, opts.DeleteAll); err != nil { + if err := deleteClusterTriggerBindings(s, p, availbleCTBs, opts.DeleteAll, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteAll, "all", "", false, "Delete all ClusterTriggerBindings (default: false)") return c } -func deleteClusterTriggerBindings(s *cli.Stream, p cli.Params, ctbNames []string, deleteAll bool) error { +func deleteClusterTriggerBindings(s *cli.Stream, p cli.Params, ctbNames []string, deleteAll bool, output string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") @@ -119,6 +141,12 @@ 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() + } 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 4c6c6b0298..5ecb0382e7 100644 --- a/pkg/cmd/clustertriggerbinding/delete_test.go +++ b/pkg/cmd/clustertriggerbinding/delete_test.go @@ -200,3 +200,77 @@ func TestClusterTriggerBindingDelete(t *testing.T) { }) } } + +func TestClusterTriggerBindingDeleteStructuredOutput(t *testing.T) { + seeds := make([]*test.Params, 0) + for i := 0; i < 3; i++ { + ctbs := []*v1beta1.ClusterTriggerBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ctb-1", + }, + }, + } + + 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"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + seeds = append(seeds, p) + } + + testParams := []struct { + name string + command []string + input *test.Params + wantError bool + want string + }{ + { + name: "Delete clustertriggerbinding with output as json", + command: []string{"rm", "ctb-1", "-o", "json"}, + input: seeds[0], + wantError: false, + want: "{\n \"deleted\": [\n \"ctb-1\"\n ]\n}\n", + }, + { + name: "Delete clustertriggerbinding with output as yaml", + command: []string{"rm", "ctb-1", "-o", "yaml"}, + input: seeds[1], + wantError: false, + want: "deleted:\n- ctb-1\n", + }, + { + name: "Delete clustertriggerbinding with invalid output format", + command: []string{"rm", "ctb-1", "-o", "csv"}, + input: seeds[2], + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + ctb := Command(tp.input) + + out, err := test.ExecuteCommand(ctb, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} diff --git a/pkg/cmd/customrun/delete.go b/pkg/cmd/customrun/delete.go index 4eb16c0794..da972e2141 100644 --- a/pkg/cmd/customrun/delete.go +++ b/pkg/cmd/customrun/delete.go @@ -21,10 +21,10 @@ import ( "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/actions" "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/formatted" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" ) func customRunExists(client *cli.Clients, namespace string, crName string) error { @@ -40,7 +40,6 @@ func customRunExists(client *cli.Clients, namespace string, crName string) error } func deleteCommand(p cli.Params) *cobra.Command { - f := genericclioptions.NewPrintFlags("delete") eg := `Delete CustomRun with name 'foo' in namespace 'bar': tkn customrun delete foo -n bar @@ -48,14 +47,24 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn cr rm foo -n bar + +Delete a CustomRun and print the result as JSON: + + tkn customrun delete foo -n bar -o json + +Delete a CustomRun and print the result as YAML: + + tkn customrun delete foo -n bar -o yaml ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, Short: "Delete CustomRuns in a namespace", Example: eg, - Args: cobra.MinimumNArgs(1), // Requires at least one argument (customrun-name) + Args: cobra.MinimumNArgs(1), Annotations: map[string]string{ "commandType": "main", }, @@ -67,39 +76,55 @@ or Err: cmd.OutOrStderr(), } - return deleteCustomRuns(s, p, crNames) + if output != "" { + output = formatted.NormalizeOutput(output) + if !formatted.IsStructured(output) { + return fmt.Errorf("invalid output format %q: must be json or yaml", output) + } + } + + return deleteCustomRuns(s, p, crNames, output) }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.OutputFlagUsage) return c } -func deleteCustomRuns(s *cli.Stream, p cli.Params, crNames []string) error { +func deleteCustomRuns(s *cli.Stream, p cli.Params, crNames []string, output string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client: %w", err) } namespace := p.Namespace() + var deleted []string + var deleteErr error for _, crName := range crNames { - // Check if CustomRun exists before attempting deletion err := customRunExists(cs, namespace, crName) if err != nil { fmt.Fprintf(s.Err, "CustomRun %s not found in namespace %s\n", crName, namespace) continue } - // Proceed with deletion err = deleteCustomRun(cs, namespace, crName) - if err == nil { - fmt.Fprintf(s.Out, "CustomRun '%s' deleted successfully from namespace '%s'\n", crName, namespace) - } else { + if err != nil { fmt.Fprintf(s.Err, "failed to delete CustomRun %s: %v\n", crName, err) + deleteErr = err + continue + } + deleted = append(deleted, crName) + if !formatted.IsStructured(output) { + fmt.Fprintf(s.Out, "CustomRun '%s' deleted successfully from namespace '%s'\n", crName, namespace) + } + } + + if formatted.IsStructured(output) { + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(deleted)); err != nil { return err } } - return nil + return deleteErr } func deleteCustomRun(cs *cli.Clients, namespace, crName string) error { diff --git a/pkg/cmd/customrun/delete_test.go b/pkg/cmd/customrun/delete_test.go index 588a201885..1fc8ebd685 100644 --- a/pkg/cmd/customrun/delete_test.go +++ b/pkg/cmd/customrun/delete_test.go @@ -16,6 +16,8 @@ package customrun import ( "context" + "errors" + "strings" "testing" "time" @@ -27,6 +29,8 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8stest "k8s.io/client-go/testing" duckv1 "knative.dev/pkg/apis/duck/v1" ) @@ -383,3 +387,157 @@ func TestCustomRunDelete(t *testing.T) { }) } } + +func TestCustomRunDeleteStructuredOutput(t *testing.T) { + now := time.Now() + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-1", + Namespace: "ns-1", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-1", + }, + }, + } + + tdc := testDynamic.Options{} + dynamicClient, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + + tests := []struct { + name string + command *cobra.Command + args []string + wantError bool + want string + }{ + { + name: "Delete customrun with output as json", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-1", "-n", "ns-1", "-o", "json"}, + wantError: false, + want: "{\n \"deleted\": [\n \"customrun-1\"\n ]\n}\n", + }, + { + name: "Delete customrun with invalid output format", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-1", "-n", "ns-1", "-o", "csv"}, + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, td := range tests { + t.Run(td.name, func(t *testing.T) { + got, err := test.ExecuteCommand(td.command, td.args...) + + if err != nil && !td.wantError { + t.Errorf("Unexpected error: %v", err) + } + + if td.wantError { + if err != nil { + test.AssertOutput(t, td.want, err.Error()) + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + test.AssertOutput(t, td.want, got) + } + }) + } +} + +func TestCustomRunDeleteStructuredOutputPartialFailure(t *testing.T) { + now := time.Now() + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-1", + Namespace: "ns-1", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1beta1.CustomRunReasonSuccessful.String()}, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-2", + Namespace: "ns-1", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1beta1.CustomRunReasonSuccessful.String()}, + }, + }, + }, + }, + } + + ns := []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns-1"}}, + } + + tdc := testDynamic.Options{PrependReactors: []testDynamic.PrependOpt{ + { + Verb: "delete", + Resource: "customruns", + Action: func(action k8stest.Action) (bool, runtime.Object, error) { + del := action.(k8stest.DeleteAction) + if del.GetName() == "customrun-2" { + return true, nil, errors.New("delete failed") + } + return false, nil, nil + }, + }, + }} + dynamicClient, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[1], versionv1beta1), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + + out, err := test.ExecuteCommand(commandV1beta1(t, crs, now, ns, dynamicClient), + "delete", "customrun-1", "customrun-2", "-n", "ns-1", "-o", "json") + if err == nil { + t.Fatal("expected error when one delete fails") + } + if !strings.Contains(out, "\"customrun-1\"") { + t.Fatalf("expected successful delete in structured output, got: %q", out) + } + if !strings.Contains(out, "\"deleted\"") { + t.Fatalf("expected deleted key in structured output, got: %q", out) + } +} diff --git a/pkg/cmd/eventlistener/delete.go b/pkg/cmd/eventlistener/delete.go index 3b7dee8170..5eff72b4e9 100644 --- a/pkg/cmd/eventlistener/delete.go +++ b/pkg/cmd/eventlistener/delete.go @@ -26,7 +26,6 @@ import ( "github.com/tektoncd/cli/pkg/options" "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // eventListenerExists validates that the arguments are valid EventListener names @@ -52,7 +51,6 @@ func eventListenerExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "eventlistener", ForceDelete: false, DeleteAllNs: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete EventListeners with names 'foo' and 'bar' in namespace 'bar' tkn eventlistener delete foo bar -n quux @@ -60,8 +58,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn el rm foo bar -n quux + +Delete an EventListener and print the result as JSON: + + tkn eventlistener delete foo -n quux -o json + +Delete an EventListener and print the result as YAML: + + tkn eventlistener delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -80,6 +90,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availableELs, errs := eventListenerExists(args, p) if len(availableELs) == 0 && errs != nil { return errs @@ -89,20 +111,20 @@ or return err } - if err := deleteEventListeners(s, p, availableELs, opts.DeleteAllNs); err != nil { + if err := deleteEventListeners(s, p, availableELs, opts.DeleteAllNs, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all EventListeners in a namespace (default: false)") return c } -func deleteEventListeners(s *cli.Stream, p cli.Params, elNames []string, deleteAll bool) error { +func deleteEventListeners(s *cli.Stream, p cli.Params, elNames []string, deleteAll bool, output string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") @@ -119,6 +141,12 @@ 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() + } 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 b946c19e90..c86b8e1ca3 100644 --- a/pkg/cmd/eventlistener/delete_test.go +++ b/pkg/cmd/eventlistener/delete_test.go @@ -220,3 +220,86 @@ func TestEventListenerDelete(t *testing.T) { }) } } + +func TestEventListenerDeleteStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + + seeds := make([]*test.Params, 0) + for i := 0; i < 3; i++ { + els := []*v1beta1.EventListener{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "el-1", + Namespace: "ns", + }, + }, + } + + 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"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + seeds = append(seeds, p) + } + + testParams := []struct { + name string + command []string + input *test.Params + wantError bool + want string + }{ + { + name: "Delete eventlistener with output as json", + command: []string{"rm", "el-1", "-n", "ns", "-o", "json"}, + input: seeds[0], + wantError: false, + want: "{\n \"deleted\": [\n \"el-1\"\n ]\n}\n", + }, + { + name: "Delete eventlistener with output as yaml", + command: []string{"rm", "el-1", "-n", "ns", "-o", "yaml"}, + input: seeds[1], + wantError: false, + want: "deleted:\n- el-1\n", + }, + { + name: "Delete eventlistener with invalid output format", + command: []string{"rm", "el-1", "-n", "ns", "-o", "csv"}, + input: seeds[2], + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + el := Command(tp.input) + + out, err := test.ExecuteCommand(el, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} diff --git a/pkg/cmd/pipeline/delete.go b/pkg/cmd/pipeline/delete.go index 4c5a45c001..8a1753e531 100644 --- a/pkg/cmd/pipeline/delete.go +++ b/pkg/cmd/pipeline/delete.go @@ -28,7 +28,6 @@ import ( "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // pipelineExists validates that the arguments are valid pipeline names @@ -55,7 +54,6 @@ func pipelineExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "Pipeline", ForceDelete: false, DeleteRelated: false, DeleteAllNs: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete Pipelines with names 'foo' and 'bar' in namespace 'quux' tkn pipeline delete foo bar -n quux @@ -63,8 +61,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn p rm foo bar -n quux + +Delete a Pipeline and print the result as JSON: + + tkn pipeline delete foo -n quux -o json + +Delete a Pipeline and print the result as YAML: + + tkn pipeline delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -83,6 +93,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availablePNames, errs := pipelineExists(args, p) if len(availablePNames) == 0 && errs != nil { return errs @@ -92,13 +114,13 @@ or return err } - if err := deletePipelines(opts, s, p, availablePNames); err != nil { + if err := deletePipelines(opts, s, p, availablePNames, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteRelated, "prs", "", false, "Whether to delete Pipeline(s) and related resources (PipelineRuns) (default: false)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all Pipelines in a namespace (default: false)") @@ -106,7 +128,7 @@ or return c } -func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, pNames []string) error { +func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, pNames []string, output string) error { pipelinerunGroupResource := schema.GroupVersionResource{Group: "tekton.dev", Resource: "pipelineruns"} cs, err := p.Clients() @@ -133,6 +155,13 @@ func deletePipelines(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, p d.Delete(pNames) } if !opts.DeleteAllNs { + if formatted.IsStructured(output) { + deleted := append(d.SuccessfulRelatedDeletes(), d.SuccessfulDeletes()...) + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(deleted)); err != nil { + return err + } + return d.Errors() + } d.PrintSuccesses(s) } else if opts.DeleteAllNs { if d.Errors() == nil { diff --git a/pkg/cmd/pipeline/delete_test.go b/pkg/cmd/pipeline/delete_test.go index 73d2329535..75cac81e07 100644 --- a/pkg/cmd/pipeline/delete_test.go +++ b/pkg/cmd/pipeline/delete_test.go @@ -663,3 +663,179 @@ func TestPipelineDelete(t *testing.T) { }) } } + +func TestPipelineDeleteStructuredOutput(t *testing.T) { + version := "v1" + clock := test.FakeClock() + + pdata := []*v1.Pipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: clock.Now().Add(-5 * time.Minute)}, + }, + }, + } + + type clients struct { + pipelineClient pipelinetest.Clients + dynamicClient dynamic.Interface + } + + seeds := make([]clients, 0) + for i := 0; i < 3; i++ { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: pdata, + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredP(pdata[0], version), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + seeds = append(seeds, clients{cs, dc}) + } + + testParams := []struct { + name string + command []string + dynamic dynamic.Interface + input pipelinetest.Clients + wantError bool + want string + }{ + { + name: "Delete pipeline with output as json", + command: []string{"rm", "pipeline", "-n", "ns", "-o", "json"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + wantError: false, + want: "{\n \"deleted\": [\n \"pipeline\"\n ]\n}\n", + }, + { + name: "Delete pipeline with output as yaml", + command: []string{"rm", "pipeline", "-n", "ns", "-o", "yaml"}, + dynamic: seeds[1].dynamicClient, + input: seeds[1].pipelineClient, + wantError: false, + want: "deleted:\n- pipeline\n", + }, + { + name: "Delete pipeline with invalid output format", + command: []string{"rm", "pipeline", "-n", "ns", "-o", "csv"}, + dynamic: seeds[2].dynamicClient, + input: seeds[2].pipelineClient, + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + p := &test.Params{Tekton: tp.input.Pipeline, Kube: tp.input.Kube, Dynamic: tp.dynamic} + pipeline := Command(p) + + out, err := test.ExecuteCommand(pipeline, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} + +func TestPipelineDeleteStructuredOutputWithRelated(t *testing.T) { + version := "v1" + clock := test.FakeClock() + + pdata := []*v1.Pipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: clock.Now().Add(-5 * time.Minute)}, + }, + }, + } + + prdata := []*v1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-run-1", + Namespace: "ns", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + CreationTimestamp: metav1.Time{Time: clock.Now()}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{Name: "pipeline"}, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1.PipelineRunReasonSuccessful.String()}, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline-run-2", + Namespace: "ns", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + CreationTimestamp: metav1.Time{Time: clock.Now()}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{Name: "pipeline"}, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1.PipelineRunReasonSuccessful.String()}, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: pdata, + PipelineRuns: prdata, + Namespaces: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns"}}, + }, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipeline", "pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredP(pdata[0], version), + cb.UnstructuredPR(prdata[0], version), + cb.UnstructuredPR(prdata[1], version), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + out, err := test.ExecuteCommand(Command(p), "rm", "pipeline", "-n", "ns", "--prs", "-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 \"pipeline\"\n ]\n}\n", out) +} diff --git a/pkg/cmd/pipelinerun/delete.go b/pkg/cmd/pipelinerun/delete.go index 1c231c3377..127ec125bf 100644 --- a/pkg/cmd/pipelinerun/delete.go +++ b/pkg/cmd/pipelinerun/delete.go @@ -31,7 +31,6 @@ import ( "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // prExists validates that the arguments are valid PipelineRun names @@ -58,7 +57,6 @@ func prExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "PipelineRun", ForceDelete: false, ParentResource: "Pipeline", DeleteAllNs: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete PipelineRuns with names 'foo' and 'bar' in namespace 'quux': tkn pipelinerun delete foo bar -n quux @@ -66,8 +64,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn pr rm foo bar -n quux + +Delete a PipelineRun and print the result as JSON: + + tkn pipelinerun delete foo -n quux -o json + +Delete a PipelineRun and print the result as YAML: + + tkn pipelinerun delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -87,6 +97,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + if opts.Keep < 0 && opts.KeepSince < 0 { return fmt.Errorf("keep and keep-since option should not be lower than 0") } @@ -116,13 +138,13 @@ or return err } - if err := deletePipelineRuns(s, p, availablePrs, opts); err != nil { + if err := deletePipelineRuns(s, p, availablePrs, opts, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().StringVarP(&opts.ParentResourceName, "pipeline", "p", "", "The name of a Pipeline whose PipelineRuns should be deleted (does not delete the Pipeline)") c.Flags().IntVarP(&opts.Keep, "keep", "", 0, "Keep n most recent number of PipelineRuns") @@ -133,7 +155,7 @@ or return c } -func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, opts *options.DeleteOptions) error { +func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, opts *options.DeleteOptions, output string) error { var numberOfDeletedPr, numberOfKeptPr int prGroupResource := schema.GroupVersionResource{Group: "tekton.dev", Resource: "pipelineruns"} @@ -193,6 +215,13 @@ func deletePipelineRuns(s *cli.Stream, p cli.Params, prNames []string, opts *opt } 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 d.Errors() == nil { switch { case opts.Keep > 0 && opts.KeepSince > 0 && !opts.IgnoreRunning: diff --git a/pkg/cmd/pipelinerun/delete_test.go b/pkg/cmd/pipelinerun/delete_test.go index ee73dfe1b2..0f12b712d9 100644 --- a/pkg/cmd/pipelinerun/delete_test.go +++ b/pkg/cmd/pipelinerun/delete_test.go @@ -15,6 +15,7 @@ package pipelinerun 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" ) @@ -1296,3 +1299,210 @@ func TestPipelineRunDelete(t *testing.T) { }) } } + +func TestPipelineRunDeleteStructuredOutput(t *testing.T) { + version := "v1" + clock := test.FakeClock() + + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + + prdata := []*v1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "pipeline-run-1", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + CreationTimestamp: metav1.Time{Time: clock.Now()}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{ + Name: "pipeline", + }, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1.PipelineRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + type clients struct { + pipelineClient pipelinetest.Clients + dynamicClient dynamic.Interface + } + + seeds := make([]clients, 0) + for i := 0; i < 3; i++ { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: prdata, + Namespaces: ns, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipelinerun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredPR(prdata[0], version), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + seeds = append(seeds, clients{cs, dc}) + } + + testParams := []struct { + name string + command []string + dynamic dynamic.Interface + input pipelinetest.Clients + inputStream io.Reader + wantError bool + want string + }{ + { + name: "Delete pipelinerun with output as json", + command: []string{"rm", "pipeline-run-1", "-n", "ns", "-o", "json"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + wantError: false, + want: "{\n \"deleted\": [\n \"pipeline-run-1\"\n ]\n}\n", + }, + { + name: "Delete pipelinerun with output as yaml", + command: []string{"rm", "pipeline-run-1", "-n", "ns", "-o", "yaml"}, + dynamic: seeds[1].dynamicClient, + input: seeds[1].pipelineClient, + wantError: false, + want: "deleted:\n- pipeline-run-1\n", + }, + { + name: "Delete pipelinerun with invalid output format", + command: []string{"rm", "pipeline-run-1", "-n", "ns", "-o", "csv"}, + dynamic: seeds[2].dynamicClient, + input: seeds[2].pipelineClient, + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + p := &test.Params{Tekton: tp.input.Pipeline, Kube: tp.input.Kube, Dynamic: tp.dynamic} + pipelinerun := Command(p) + + if tp.inputStream != nil { + pipelinerun.SetIn(tp.inputStream) + } + + out, err := test.ExecuteCommand(pipelinerun, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} + +func TestPipelineRunDeleteStructuredOutputPartialFailure(t *testing.T) { + version := "v1" + clock := test.FakeClock() + + ns := []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns"}}, + } + + prdata := []*v1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "pipeline-run-1", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + CreationTimestamp: metav1.Time{Time: clock.Now()}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{Name: "pipeline"}, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1.PipelineRunReasonSuccessful.String()}, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "pipeline-run-2", + Labels: map[string]string{"tekton.dev/pipeline": "pipeline"}, + CreationTimestamp: metav1.Time{Time: clock.Now()}, + }, + Spec: v1.PipelineRunSpec{ + PipelineRef: &v1.PipelineRef{Name: "pipeline"}, + }, + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + {Status: corev1.ConditionTrue, Reason: v1.PipelineRunReasonSuccessful.String()}, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: prdata, + Namespaces: ns, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"pipelinerun"}) + + tdc := testDynamic.Options{PrependReactors: []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 + }, + }, + }} + dc, err := tdc.Client( + cb.UnstructuredPR(prdata[0], version), + cb.UnstructuredPR(prdata[1], version), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + out, err := test.ExecuteCommand(Command(p), "rm", "pipeline-run-1", "pipeline-run-2", "-n", "ns", "-o", "json") + if err == nil { + t.Fatal("expected error when one delete fails") + } + if !strings.Contains(out, "\"pipeline-run-1\"") { + t.Fatalf("expected successful delete in structured output, got: %q", out) + } + if !strings.Contains(out, "\"deleted\"") { + t.Fatalf("expected deleted key in structured output, got: %q", out) + } +} diff --git a/pkg/cmd/task/delete.go b/pkg/cmd/task/delete.go index 15059defd5..d0270f7388 100644 --- a/pkg/cmd/task/delete.go +++ b/pkg/cmd/task/delete.go @@ -28,7 +28,6 @@ import ( "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // taskExists validates that the arguments are valid Task names @@ -55,7 +54,6 @@ func taskExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "Task", ForceDelete: false, DeleteRelated: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete Tasks with names 'foo' and 'bar' in namespace 'quux': tkn task delete foo bar -n quux @@ -63,8 +61,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn t rm foo bar -n quux + +Delete a Task and print the result as JSON: + + tkn task delete foo -n quux -o json + +Delete a Task and print the result as YAML: + + tkn task delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -84,6 +94,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availableTaskNames, errs := taskExists(args, p) if len(availableTaskNames) == 0 && errs != nil { return errs @@ -93,13 +115,13 @@ or return err } - if err := deleteTask(opts, s, p, availableTaskNames); err != nil { + if err := deleteTask(opts, s, p, availableTaskNames, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteRelated, "trs", "", false, "Whether to delete Task(s) and related resources (TaskRuns) (default: false)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all Tasks in a namespace (default: false)") @@ -107,7 +129,7 @@ or return c } -func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNames []string) error { +func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNames []string, output string) error { taskrunGroupResource := schema.GroupVersionResource{Group: "tekton.dev", Resource: "taskruns"} cs, err := p.Clients() @@ -134,6 +156,13 @@ func deleteTask(opts *options.DeleteOptions, s *cli.Stream, p cli.Params, taskNa d.Delete(taskNames) } if !opts.DeleteAllNs { + if formatted.IsStructured(output) { + deleted := append(d.SuccessfulRelatedDeletes(), d.SuccessfulDeletes()...) + if err := formatted.PrintStructuredOutput(s.Out, output, formatted.NewDeleteResult(deleted)); err != nil { + return err + } + return d.Errors() + } d.PrintSuccesses(s) } else if opts.DeleteAllNs { if d.Errors() == nil { diff --git a/pkg/cmd/task/delete_test.go b/pkg/cmd/task/delete_test.go index f28d8e6bd9..f1f0241395 100644 --- a/pkg/cmd/task/delete_test.go +++ b/pkg/cmd/task/delete_test.go @@ -627,3 +627,191 @@ func TestTaskDelete(t *testing.T) { }) } } + +func TestTaskDeleteStructuredOutput(t *testing.T) { + version := "v1" + clock := test.FakeClock() + taskCreated := clock.Now().Add(-1 * time.Minute) + + tdata := []*v1.Task{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: taskCreated}, + }, + }, + } + + type clients struct { + pipelineClient pipelinetest.Clients + dynamicClient dynamic.Interface + } + + seeds := make([]clients, 0) + for i := 0; i < 3; i++ { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Tasks: tdata, + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task", "taskrun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredT(tdata[0], version), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + seeds = append(seeds, clients{cs, dc}) + } + + testParams := []struct { + name string + command []string + dynamic dynamic.Interface + input pipelinetest.Clients + wantError bool + want string + }{ + { + name: "Delete task with output as json", + command: []string{"rm", "task", "-n", "ns", "-o", "json"}, + dynamic: seeds[0].dynamicClient, + input: seeds[0].pipelineClient, + wantError: false, + want: "{\n \"deleted\": [\n \"task\"\n ]\n}\n", + }, + { + name: "Delete task with output as yaml", + command: []string{"rm", "task", "-n", "ns", "-o", "yaml"}, + dynamic: seeds[1].dynamicClient, + input: seeds[1].pipelineClient, + wantError: false, + want: "deleted:\n- task\n", + }, + { + name: "Delete task with invalid output format", + command: []string{"rm", "task", "-n", "ns", "-o", "csv"}, + dynamic: seeds[2].dynamicClient, + input: seeds[2].pipelineClient, + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + p := &test.Params{Tekton: tp.input.Pipeline, Kube: tp.input.Kube, Dynamic: tp.dynamic} + task := Command(p) + + out, err := test.ExecuteCommand(task, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} + +func TestTaskDeleteStructuredOutputWithRelated(t *testing.T) { + version := "v1" + clock := test.FakeClock() + taskCreated := clock.Now().Add(-1 * time.Minute) + + tdata := []*v1.Task{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "task", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: taskCreated}, + }, + }, + } + + trdata := []*v1.TaskRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "task-run-1", + Labels: map[string]string{"tekton.dev/task": "task"}, + }, + Spec: v1.TaskRunSpec{ + TaskRef: &v1.TaskRef{ + Name: "task", + Kind: v1.NamespacedTaskKind, + }, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.TaskRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "task-run-2", + Labels: map[string]string{"tekton.dev/task": "task"}, + }, + Spec: v1.TaskRunSpec{ + TaskRef: &v1.TaskRef{ + Name: "task", + Kind: v1.NamespacedTaskKind, + }, + }, + Status: v1.TaskRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.TaskRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Tasks: tdata, + TaskRuns: trdata, + Namespaces: []*corev1.Namespace{ + {ObjectMeta: metav1.ObjectMeta{Name: "ns"}}, + }, + }) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task", "taskrun"}) + tdc := testDynamic.Options{} + dc, err := tdc.Client( + cb.UnstructuredT(tdata[0], version), + cb.UnstructuredTR(trdata[0], version), + cb.UnstructuredTR(trdata[1], version), + ) + if err != nil { + t.Fatalf("unable to create dynamic client: %v", err) + } + + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + out, err := test.ExecuteCommand(Command(p), "rm", "task", "-n", "ns", "--trs", "-o", "json") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + test.AssertOutput(t, "{\n \"deleted\": [\n \"task-run-1\",\n \"task-run-2\",\n \"task\"\n ]\n}\n", out) +} diff --git a/pkg/cmd/triggerbinding/delete.go b/pkg/cmd/triggerbinding/delete.go index b9383ad250..0444bc54c0 100644 --- a/pkg/cmd/triggerbinding/delete.go +++ b/pkg/cmd/triggerbinding/delete.go @@ -26,7 +26,6 @@ import ( "github.com/tektoncd/cli/pkg/triggerbinding" "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) // triggerBindingExists validates that the arguments are valid TriggerBinding names @@ -52,7 +51,6 @@ func triggerBindingExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "triggerbinding", ForceDelete: false, DeleteAllNs: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete TriggerBindings with names 'foo' and 'bar' in namespace 'quux' tkn triggerbinding delete foo bar -n quux @@ -60,8 +58,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn tb rm foo bar -n quux + +Delete a TriggerBinding and print the result as JSON: + + tkn triggerbinding delete foo -n quux -o json + +Delete a TriggerBinding and print the result as YAML: + + tkn triggerbinding delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -80,6 +90,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availableTbs, errs := triggerBindingExists(args, p) if len(availableTbs) == 0 && errs != nil { return errs @@ -89,20 +111,20 @@ or return err } - if err := deleteTriggerBindings(s, p, availableTbs, opts.DeleteAllNs); err != nil { + if err := deleteTriggerBindings(s, p, availableTbs, opts.DeleteAllNs, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all TriggerBindings in a namespace (default: false)") return c } -func deleteTriggerBindings(s *cli.Stream, p cli.Params, tbNames []string, deleteAll bool) error { +func deleteTriggerBindings(s *cli.Stream, p cli.Params, tbNames []string, deleteAll bool, output string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") @@ -120,6 +142,12 @@ 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() + } 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 67d66eb7af..da7c9b120a 100644 --- a/pkg/cmd/triggerbinding/delete_test.go +++ b/pkg/cmd/triggerbinding/delete_test.go @@ -219,3 +219,86 @@ func TestTriggerBindingDelete(t *testing.T) { }) } } + +func TestTriggerBindingDeleteStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + + seeds := make([]*test.Params, 0) + for i := 0; i < 3; i++ { + tbs := []*v1beta1.TriggerBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "tb-1", + Namespace: "ns", + }, + }, + } + + 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"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + seeds = append(seeds, p) + } + + testParams := []struct { + name string + command []string + input *test.Params + wantError bool + want string + }{ + { + name: "Delete triggerbinding with output as json", + command: []string{"rm", "tb-1", "-n", "ns", "-o", "json"}, + input: seeds[0], + wantError: false, + want: "{\n \"deleted\": [\n \"tb-1\"\n ]\n}\n", + }, + { + name: "Delete triggerbinding with output as yaml", + command: []string{"rm", "tb-1", "-n", "ns", "-o", "yaml"}, + input: seeds[1], + wantError: false, + want: "deleted:\n- tb-1\n", + }, + { + name: "Delete triggerbinding with invalid output format", + command: []string{"rm", "tb-1", "-n", "ns", "-o", "csv"}, + input: seeds[2], + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + tb := Command(tp.input) + + out, err := test.ExecuteCommand(tb, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} diff --git a/pkg/cmd/triggertemplate/delete.go b/pkg/cmd/triggertemplate/delete.go index a2b1dd2ef2..829dbdfb93 100644 --- a/pkg/cmd/triggertemplate/delete.go +++ b/pkg/cmd/triggertemplate/delete.go @@ -27,7 +27,6 @@ import ( "go.uber.org/multierr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - cliopts "k8s.io/cli-runtime/pkg/genericclioptions" ) var triggertemplateGroupResource = schema.GroupVersionResource{Group: "triggers.tekton.dev", Resource: "triggertemplates"} @@ -55,7 +54,6 @@ func triggerTemplateExists(args []string, p cli.Params) ([]string, error) { func deleteCommand(p cli.Params) *cobra.Command { opts := &options.DeleteOptions{Resource: "triggertemplate", ForceDelete: false, DeleteAllNs: false} - f := cliopts.NewPrintFlags("delete") eg := `Delete TriggerTemplates with names 'foo' and 'bar' in namespace 'quux' tkn triggertemplate delete foo bar -n quux @@ -63,8 +61,20 @@ func deleteCommand(p cli.Params) *cobra.Command { or tkn tt rm foo bar -n quux + +Delete a TriggerTemplate and print the result as JSON: + + tkn triggertemplate delete foo -n quux -o json + +Delete a TriggerTemplate and print the result as YAML: + + tkn triggertemplate delete foo -n quux -o yaml + +Using -o json or -o yaml skips the confirmation prompt. ` + output := "" + c := &cobra.Command{ Use: "delete", Aliases: []string{"rm"}, @@ -83,6 +93,18 @@ or Err: cmd.OutOrStderr(), } + if output != "" { + output = formatted.NormalizeOutput(output) + 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 + } + availableTts, errs := triggerTemplateExists(args, p) if len(availableTts) == 0 && errs != nil { return errs @@ -92,20 +114,20 @@ or return err } - if err := deleteTriggerTemplates(s, p, availableTts, opts.DeleteAllNs); err != nil { + if err := deleteTriggerTemplates(s, p, availableTts, opts.DeleteAllNs, output); err != nil { return err } return errs }, } - f.AddFlags(c) + c.Flags().StringVarP(&output, "output", "o", "", formatted.DeleteOutputFlagUsage) c.Flags().BoolVarP(&opts.ForceDelete, "force", "f", false, "Whether to force deletion (default: false)") c.Flags().BoolVarP(&opts.DeleteAllNs, "all", "", false, "Delete all TriggerTemplates in a namespace (default: false)") return c } -func deleteTriggerTemplates(s *cli.Stream, p cli.Params, ttNames []string, deleteAll bool) error { +func deleteTriggerTemplates(s *cli.Stream, p cli.Params, ttNames []string, deleteAll bool, output string) error { cs, err := p.Clients() if err != nil { return fmt.Errorf("failed to create tekton client") @@ -123,6 +145,12 @@ 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() + } 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 17d122b42c..bafbc752cd 100644 --- a/pkg/cmd/triggertemplate/delete_test.go +++ b/pkg/cmd/triggertemplate/delete_test.go @@ -218,3 +218,86 @@ func TestTriggerTemplateDelete(t *testing.T) { }) } } + +func TestTriggerTemplateDeleteStructuredOutput(t *testing.T) { + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + + seeds := make([]*test.Params, 0) + for i := 0; i < 3; i++ { + tts := []*v1beta1.TriggerTemplate{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "tt-1", + Namespace: "ns", + }, + }, + } + + 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"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Triggers: cs.Triggers, Kube: cs.Kube, Dynamic: dc} + seeds = append(seeds, p) + } + + testParams := []struct { + name string + command []string + input *test.Params + wantError bool + want string + }{ + { + name: "Delete triggertemplate with output as json", + command: []string{"rm", "tt-1", "-n", "ns", "-o", "json"}, + input: seeds[0], + wantError: false, + want: "{\n \"deleted\": [\n \"tt-1\"\n ]\n}\n", + }, + { + name: "Delete triggertemplate with output as yaml", + command: []string{"rm", "tt-1", "-n", "ns", "-o", "yaml"}, + input: seeds[1], + wantError: false, + want: "deleted:\n- tt-1\n", + }, + { + name: "Delete triggertemplate with invalid output format", + command: []string{"rm", "tt-1", "-n", "ns", "-o", "csv"}, + input: seeds[2], + wantError: true, + want: "invalid output format \"csv\": must be json or yaml", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + tt := Command(tp.input) + + out, err := test.ExecuteCommand(tt, tp.command...) + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, out) + } + }) + } +} diff --git a/pkg/deleter/deleter.go b/pkg/deleter/deleter.go index 016b33b4b1..0f434766f2 100644 --- a/pkg/deleter/deleter.go +++ b/pkg/deleter/deleter.go @@ -104,6 +104,16 @@ func (d *Deleter) PrintSuccesses(streams *cli.Stream) { } } +// SuccessfulDeletes returns a copy of the names of successfully deleted resources. +func (d *Deleter) SuccessfulDeletes() []string { + return append([]string(nil), d.successfulDeletes...) +} + +// SuccessfulRelatedDeletes returns a copy of the names of successfully deleted related resources. +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 diff --git a/pkg/formatted/output.go b/pkg/formatted/output.go index 4b4522c5e0..ae4ecd7665 100644 --- a/pkg/formatted/output.go +++ b/pkg/formatted/output.go @@ -30,6 +30,8 @@ const ( OutputYAML = "yaml" // OutputFlagUsage is the shared --output flag help text for json/yaml commands. OutputFlagUsage = "Output format. One of: json|yaml" + // DeleteOutputFlagUsage is --output help for delete commands that skip confirmation. + DeleteOutputFlagUsage = "Output format. One of: json|yaml. Skips the confirmation prompt" // CancelledStatus is the user-facing requestedStatus for a cancel operation. CancelledStatus = "Cancelled" ) @@ -47,6 +49,19 @@ type CancelResult struct { Cancelled []CancelItem `json:"cancelled"` } +// DeleteResult is the machine-readable result of a delete operation. +type DeleteResult struct { + Deleted []string `json:"deleted"` +} + +// NewDeleteResult builds a DeleteResult from a list of successfully deleted names. +func NewDeleteResult(names []string) DeleteResult { + if names == nil { + names = []string{} + } + return DeleteResult{Deleted: names} +} + // NewCancelResult builds a CancelResult for a single cancelled resource. func NewCancelResult(kind, name, namespace, requestedStatus string) CancelResult { if requestedStatus == "" { diff --git a/pkg/formatted/output_test.go b/pkg/formatted/output_test.go index 061c0155de..b8c8a154c1 100644 --- a/pkg/formatted/output_test.go +++ b/pkg/formatted/output_test.go @@ -162,3 +162,17 @@ func TestNormalizeOutput(t *testing.T) { }) } } + +func TestNewDeleteResult(t *testing.T) { + t.Run("with names", func(t *testing.T) { + got := NewDeleteResult([]string{"foo", "bar"}) + want := DeleteResult{Deleted: []string{"foo", "bar"}} + assert.DeepEqual(t, want, got) + }) + + t.Run("nil becomes empty slice", func(t *testing.T) { + got := NewDeleteResult(nil) + assert.Assert(t, got.Deleted != nil) + assert.Equal(t, len(got.Deleted), 0) + }) +}