diff --git a/docs/cmd/tkn_customrun.md b/docs/cmd/tkn_customrun.md index 607a899282..e14063dcdd 100644 --- a/docs/cmd/tkn_customrun.md +++ b/docs/cmd/tkn_customrun.md @@ -27,5 +27,6 @@ Manage CustomRuns ### SEE ALSO * [tkn](tkn.md) - CLI for tekton pipelines +* [tkn customrun delete](tkn_customrun_delete.md) - Delete CustomRuns having a specific name in a namespace * [tkn customrun list](tkn_customrun_list.md) - Lists CustomRuns in a namespace diff --git a/docs/cmd/tkn_customrun_delete.md b/docs/cmd/tkn_customrun_delete.md new file mode 100644 index 0000000000..5e38f99204 --- /dev/null +++ b/docs/cmd/tkn_customrun_delete.md @@ -0,0 +1,48 @@ +## tkn customrun delete + +Delete CustomRuns having a specific name in a namespace + +### Usage + +``` +tkn customrun delete +``` + +### Synopsis + +Delete CustomRuns having a specific name in a namespace + +### Examples + +Delete CustomRun with name 'foo' in namespace 'bar': + + tkn customrun delete foo -n bar + +or + + tkn cr rm foo -n bar + + +### 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]. +``` + +### Options inherited from parent commands + +``` + -c, --context string name of the kubeconfig context to use (default: kubectl config current-context) + -k, --kubeconfig string kubectl config file (default: $HOME/.kube/config) + -n, --namespace string namespace to use (default: from $KUBECONFIG) + -C, --no-color disable coloring (default: false) +``` + +### SEE ALSO + +* [tkn customrun](tkn_customrun.md) - Manage CustomRuns + diff --git a/docs/man/man1/tkn-customrun-delete.1 b/docs/man/man1/tkn-customrun-delete.1 new file mode 100644 index 0000000000..9d8b813477 --- /dev/null +++ b/docs/man/man1/tkn-customrun-delete.1 @@ -0,0 +1,90 @@ +.TH "TKN\-CUSTOMRUN\-DELETE" "1" "" "Auto generated by spf13/cobra" "" +.nh +.ad l + + +.SH NAME +.PP +tkn\-customrun\-delete \- Delete CustomRuns having a specific name in a namespace + + +.SH SYNOPSIS +.PP +\fBtkn customrun delete\fP + + +.SH DESCRIPTION +.PP +Delete CustomRuns having a specific name 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]]. + + +.SH OPTIONS INHERITED FROM PARENT COMMANDS +.PP +\fB\-c\fP, \fB\-\-context\fP="" + name of the kubeconfig context to use (default: kubectl config current\-context) + +.PP +\fB\-k\fP, \fB\-\-kubeconfig\fP="" + kubectl config file (default: $HOME/.kube/config) + +.PP +\fB\-n\fP, \fB\-\-namespace\fP="" + namespace to use (default: from $KUBECONFIG) + +.PP +\fB\-C\fP, \fB\-\-no\-color\fP[=false] + disable coloring (default: false) + + +.SH EXAMPLE +.PP +Delete CustomRun with name 'foo' in namespace 'bar': + +.PP +.RS + +.nf +tkn customrun delete foo \-n bar + +.fi +.RE + +.PP +or + +.PP +.RS + +.nf +tkn cr rm foo \-n bar + +.fi +.RE + + +.SH SEE ALSO +.PP +\fBtkn\-customrun(1)\fP diff --git a/docs/man/man1/tkn-customrun.1 b/docs/man/man1/tkn-customrun.1 index 361f9d0589..6aa17990c5 100644 --- a/docs/man/man1/tkn-customrun.1 +++ b/docs/man/man1/tkn-customrun.1 @@ -42,4 +42,4 @@ Manage CustomRuns .SH SEE ALSO .PP -\fBtkn(1)\fP, \fBtkn\-customrun\-list(1)\fP +\fBtkn(1)\fP, \fBtkn\-customrun\-delete(1)\fP, \fBtkn\-customrun\-list(1)\fP diff --git a/pkg/cmd/customrun/customrun.go b/pkg/cmd/customrun/customrun.go index 0b270266c0..03a50455bf 100644 --- a/pkg/cmd/customrun/customrun.go +++ b/pkg/cmd/customrun/customrun.go @@ -37,6 +37,7 @@ func Command(p cli.Params) *cobra.Command { flags.AddTektonOptions(cmd) cmd.AddCommand( + deleteCommand(p), listCommand(p), ) diff --git a/pkg/cmd/customrun/delete.go b/pkg/cmd/customrun/delete.go new file mode 100644 index 0000000000..cdac709eaa --- /dev/null +++ b/pkg/cmd/customrun/delete.go @@ -0,0 +1,127 @@ +// Copyright © 2024 The Tekton Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customrun + +import ( + "context" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/tektoncd/cli/pkg/actions" + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "go.uber.org/multierr" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func crExists(args []string, p cli.Params) ([]string, error) { + availableCrs := make([]string, 0) + c, err := p.Clients() + if err != nil { + return availableCrs, err + } + var errorList error + ns := p.Namespace() + for _, name := range args { + var cr *v1beta1.CustomRun + err := actions.GetV1(customrunGroupResource, c, name, ns, metav1.GetOptions{}, &cr) + if err != nil { + errorList = multierr.Append(errorList, err) + if !errors.IsNotFound(err) { + // Log the error but don't stop execution + continue + } + // CustomRun not found, skip to the next + fmt.Fprintf(os.Stderr, "CustomRun %s not found in namespace %s\n", name, ns) + continue + } + availableCrs = append(availableCrs, name) + } + return availableCrs, nil +} + +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 + +or + + tkn cr rm foo -n bar +` + + 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) + Annotations: map[string]string{ + "commandType": "main", + }, + RunE: func(cmd *cobra.Command, args []string) error { + crNames := args + s := &cli.Stream{ + In: cmd.InOrStdin(), + Out: cmd.OutOrStdout(), + Err: cmd.OutOrStderr(), + } + + return deleteCustomRuns(s, p, crNames) + + }, + } + + f.AddFlags(c) + return c +} + +func deleteCustomRuns(s *cli.Stream, p cli.Params, crNames []string) error { + cs, err := p.Clients() + if err != nil { + return fmt.Errorf("failed to create tekton client: %w", err) + } + namespace := p.Namespace() + for _, crName := range crNames { + // Check if CustomRun exists before attempting deletion + exists, _ := crExists([]string{crName}, p) + if len(exists) == 0 { + 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 { + fmt.Fprintf(s.Err, "failed to delete CustomRun %s: %v\n", crName, err) + return err + } + } + return nil +} + +func deleteCustomRun(cs *cli.Clients, namespace, crName string) error { + err := cs.Dynamic.Resource(customrunGroupResource).Namespace(namespace).Delete(context.TODO(), crName, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete CustomRun %s: %w", crName, err) + } + return nil +} diff --git a/pkg/cmd/customrun/delete_test.go b/pkg/cmd/customrun/delete_test.go new file mode 100644 index 0000000000..3f884300cb --- /dev/null +++ b/pkg/cmd/customrun/delete_test.go @@ -0,0 +1,390 @@ +// Copyright © 2024 The Tekton Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package customrun + +import ( + "context" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/test" + cb "github.com/tektoncd/cli/pkg/test/builder" + testDynamic "github.com/tektoncd/cli/pkg/test/dynamic" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + corev1 "k8s.io/api/core/v1" + + //apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +func TestDeleteSingleCustomRun(t *testing.T) { + // Create a sample CustomRun + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-1", + Namespace: "foo", + }, + 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: "foo", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + tdc := testDynamic.Options{} + dynamicClient, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[1], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + + // Create cli.Clients with the dynamic client + clients := &cli.Clients{ + Dynamic: dynamicClient, + } + + // Namespace and CustomRun name for the test + namespace := "foo" + customRunName := "customrun-1" + + // Check that the CustomRun exists before deletion + _, err = clients.Dynamic.Resource(customrunGroupResource).Namespace(namespace).Get(context.TODO(), customRunName, metav1.GetOptions{}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Call the function + err = deleteCustomRun(clients, namespace, customRunName) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // Verify that the CustomRun has been deleted + _, err = clients.Dynamic.Resource(customrunGroupResource).Namespace(namespace).Get(context.TODO(), customRunName, metav1.GetOptions{}) + if err == nil { + t.Errorf("expected error but got none") + } + + // Attempt to delete a non-existent CustomRun + err = deleteCustomRun(clients, namespace, "non-existent-customrun") + if err == nil { + t.Errorf("expected error but got none") + } +} +func TestCustomRunDelete(t *testing.T) { + now := time.Now() + // Define the CustomRuns for testing + 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(), + }, + }, + }, + }, + }, + // Additional CustomRuns for ns-1 and ns-2 + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-3", + 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-4", + 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-5", + 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-6", + Namespace: "ns-2", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-7", + Namespace: "ns-2", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-8", + Namespace: "ns-2", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-9", + Namespace: "ns-2", + }, + Spec: v1beta1.CustomRunSpec{}, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "customrun-10", + Namespace: "ns-2", + }, + 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", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns-2", + }, + }, + } + + tdc := testDynamic.Options{} + dynamicClient, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[1], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[2], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[3], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[4], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[5], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[6], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[7], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[8], versionv1beta1), + cb.UnstructuredV1beta1CustomRun(crs[9], 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 non-existent customrun", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-xyz", "-n", "ns-1"}, + wantError: true, + want: "failed to delete CustomRun customrun-xyz: customruns.tekton.dev customrun-xyz not found\n", + }, + { + name: "Delete one customrun without namespace", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-1"}, + wantError: false, + want: "CustomRun customrun-1 not found in namespace \n", + }, + { + name: "Delete multiple customruns without namespace", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-2", "customrun-3"}, + wantError: false, + want: "CustomRun customrun-2 not found in namespace \nCustomRun customrun-3 not found in namespace \n", + }, + { + name: "Delete one customrun with namespace", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-4", "-n", "ns-1"}, + wantError: false, + want: "CustomRun 'customrun-4' deleted successfully from namespace 'ns-1'\n", + }, + { + name: "Delete multiple customruns with namespace", + command: commandV1beta1(t, crs, now, ns, dynamicClient), + args: []string{"delete", "customrun-6", "customrun-7", "-n", "ns-2"}, + wantError: false, + want: "CustomRun 'customrun-6' deleted successfully from namespace 'ns-2'\nCustomRun 'customrun-7' deleted successfully from namespace 'ns-2'\n", + }, + } + + 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 { + //t.Errorf("expected error but got none") + //} else { + 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) + } + }) + } +}