diff --git a/docs/cmd/tkn_customrun.md b/docs/cmd/tkn_customrun.md index 99d45adb17..b2e364c413 100644 --- a/docs/cmd/tkn_customrun.md +++ b/docs/cmd/tkn_customrun.md @@ -28,5 +28,6 @@ Manage CustomRuns * [tkn](tkn.md) - CLI for tekton pipelines * [tkn customrun delete](tkn_customrun_delete.md) - Delete CustomRuns in a namespace +* [tkn customrun describe](tkn_customrun_describe.md) - Describe a CustomRun in a namespace * [tkn customrun list](tkn_customrun_list.md) - Lists CustomRuns in a namespace diff --git a/docs/cmd/tkn_customrun_describe.md b/docs/cmd/tkn_customrun_describe.md new file mode 100644 index 0000000000..be215761a2 --- /dev/null +++ b/docs/cmd/tkn_customrun_describe.md @@ -0,0 +1,53 @@ +## tkn customrun describe + +Describe a CustomRun in a namespace + +***Aliases**: desc* + +### Usage + +``` +tkn customrun describe +``` + +### Synopsis + +Describe a CustomRun in a namespace + +### Examples + +Describe a CustomRun of name 'foo' in namespace 'bar': + + tkn customrun describe foo -n bar + +or + + tkn cr desc 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) + -F, --fzf use fzf to select a CustomRun to describe + -h, --help help for describe + -L, --last show description for last CustomRun + --limit int lists number of CustomRuns when selecting a CustomRun to describe (default 5) + -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-describe.1 b/docs/man/man1/tkn-customrun-describe.1 new file mode 100644 index 0000000000..c6bbc2f6b1 --- /dev/null +++ b/docs/man/man1/tkn-customrun-describe.1 @@ -0,0 +1,102 @@ +.TH "TKN\-CUSTOMRUN\-DESCRIBE" "1" "" "Auto generated by spf13/cobra" "" +.nh +.ad l + + +.SH NAME +.PP +tkn\-customrun\-describe \- Describe a CustomRun in a namespace + + +.SH SYNOPSIS +.PP +\fBtkn customrun describe\fP + + +.SH DESCRIPTION +.PP +Describe a CustomRun 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\-F\fP, \fB\-\-fzf\fP[=false] + use fzf to select a CustomRun to describe + +.PP +\fB\-h\fP, \fB\-\-help\fP[=false] + help for describe + +.PP +\fB\-L\fP, \fB\-\-last\fP[=false] + show description for last CustomRun + +.PP +\fB\-\-limit\fP=5 + lists number of CustomRuns when selecting a CustomRun to describe + +.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 +Describe a CustomRun of name 'foo' in namespace 'bar': + +.PP +.RS + +.nf +tkn customrun describe foo \-n bar + +.fi +.RE + +.PP +or + +.PP +.RS + +.nf +tkn cr desc 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 6aa17990c5..dfa8de0ad9 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\-delete(1)\fP, \fBtkn\-customrun\-list(1)\fP +\fBtkn(1)\fP, \fBtkn\-customrun\-delete(1)\fP, \fBtkn\-customrun\-describe(1)\fP, \fBtkn\-customrun\-list(1)\fP diff --git a/pkg/cmd/customrun/customrun.go b/pkg/cmd/customrun/customrun.go index 03a50455bf..a33f136869 100644 --- a/pkg/cmd/customrun/customrun.go +++ b/pkg/cmd/customrun/customrun.go @@ -38,6 +38,7 @@ func Command(p cli.Params) *cobra.Command { flags.AddTektonOptions(cmd) cmd.AddCommand( deleteCommand(p), + describeCommand(p), listCommand(p), ) diff --git a/pkg/cmd/customrun/describe.go b/pkg/cmd/customrun/describe.go new file mode 100644 index 0000000000..5360cbdf90 --- /dev/null +++ b/pkg/cmd/customrun/describe.go @@ -0,0 +1,334 @@ +// Copyright © 2026 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 ( + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + + "github.com/jonboulle/clockwork" + "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/cli/pkg/options" + "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/schema" + cliopts "k8s.io/cli-runtime/pkg/genericclioptions" +) + +const ( + describeTemplate = `{{decorate "bold" "Name"}}: {{ .CustomRun.Name }} +{{decorate "bold" "Namespace"}}: {{ .CustomRun.Namespace }} +{{- $customRef := customRefExists .CustomRun.Spec }}{{- if ne $customRef "" }} +{{decorate "bold" "Custom Ref"}}: {{ $customRef }} +{{- end }} +{{- if ne .CustomRun.Spec.ServiceAccountName "" }} +{{decorate "bold" "Service Account"}}: {{ .CustomRun.Spec.ServiceAccountName }} +{{- end }} + +{{- $timeout := getTimeout .CustomRun -}} +{{- if and (ne $timeout "") (ne $timeout "0s") }} +{{decorate "bold" "Timeout"}}: {{ .CustomRun.Spec.Timeout.Duration.String }} +{{- end }} +{{- $l := len .CustomRun.Labels }}{{ if eq $l 0 }} +{{- else }} +{{decorate "bold" "Labels"}}: +{{- range $k, $v := .CustomRun.Labels }} + {{ $k }}={{ $v }} +{{- end }} +{{- end }} +{{- $annotations := removeLastAppliedConfig .CustomRun.Annotations -}} +{{- if $annotations }} +{{decorate "bold" "Annotations"}}: +{{- range $k, $v := $annotations }} + {{ $k }}={{ $v }} +{{- end }} +{{- end }} + +{{decorate "status" ""}}{{decorate "underline bold" "Status"}} + +STARTED DURATION STATUS +{{ formatAge .CustomRun.Status.StartTime .Time }} {{ formatDuration .CustomRun.Status.StartTime .CustomRun.Status.CompletionTime }} {{ formatCondition .CustomRun.Status.Conditions }} +{{- $msg := hasFailed .CustomRun -}} +{{- if ne $msg "" }} + +{{decorate "underline bold" "Message"}} + +{{ $msg }} +{{- end }} + +{{- if ne (len .CustomRun.Spec.Params) 0 }} + +{{decorate "params" ""}}{{decorate "underline bold" "Params"}} + + NAME VALUE +{{- range $i, $p := .CustomRun.Spec.Params }} +{{- if eq $p.Value.Type "string" }} + {{decorate "bullet" $p.Name }} {{ $p.Value.StringVal }} +{{- else if eq $p.Value.Type "array" }} + {{decorate "bullet" $p.Name }} {{ $p.Value.ArrayVal }} +{{- else }} + {{decorate "bullet" $p.Name }} {{ $p.Value.ObjectVal }} +{{- end }} +{{- end }} +{{- end }} + +{{- if ne (len .CustomRun.Status.Results) 0 }} + +{{decorate "results" ""}}{{decorate "underline bold" "Results"}} + + NAME VALUE +{{- range $result := .CustomRun.Status.Results }} + {{decorate "bullet" $result.Name }} {{ $result.Value }} +{{- end }} +{{- end }} + +{{- if ne (len .CustomRun.Spec.Workspaces) 0 }} + +{{decorate "workspaces" ""}}{{decorate "underline bold" "Workspaces"}} + + NAME SUB PATH WORKSPACE BINDING +{{- range $workspace := .CustomRun.Spec.Workspaces }} +{{- if not $workspace.SubPath }} + {{ decorate "bullet" $workspace.Name }} {{ "---" }} {{ formatWorkspace $workspace }} +{{- else }} + {{ decorate "bullet" $workspace.Name }} {{ $workspace.SubPath }} {{ formatWorkspace $workspace }} +{{- end }} +{{- end }} +{{- end }} +` + defaultCustomRunDescribeLimit = 5 +) + +func describeCommand(p cli.Params) *cobra.Command { + opts := &options.DescribeOptions{Params: p} + f := cliopts.NewPrintFlags("describe") + eg := `Describe a CustomRun of name 'foo' in namespace 'bar': + + tkn customrun describe foo -n bar + +or + + tkn cr desc foo -n bar +` + + c := &cobra.Command{ + Use: "describe", + Aliases: []string{"desc"}, + Short: "Describe a CustomRun in a namespace", + Example: eg, + SilenceUsage: true, + Annotations: map[string]string{ + "commandType": "main", + }, + ValidArgsFunction: formatted.ParentCompletion, + RunE: func(cmd *cobra.Command, args []string) error { + s := &cli.Stream{ + Out: cmd.OutOrStdout(), + Err: cmd.OutOrStderr(), + } + + output, err := cmd.LocalFlags().GetString("output") + if err != nil { + return fmt.Errorf("output option not set properly: %v", err) + } + + if !opts.Fzf { + if _, ok := os.LookupEnv("TKN_USE_FZF"); ok { + opts.Fzf = true + } + } + + cs, err := p.Clients() + if err != nil { + return err + } + + if len(args) == 0 { + lOpts := metav1.ListOptions{} + if !opts.Last { + crs, err := GetAllCustomRuns(customrunGroupResource, lOpts, cs, p.Namespace(), opts.Limit, p.Time()) + if err != nil { + return err + } + if len(crs) == 1 { + opts.CustomRunName = strings.Fields(crs[0])[0] + } else { + err = askCustomRunName(opts, crs) + if err != nil { + return err + } + } + } else { + crs, err := GetAllCustomRuns(customrunGroupResource, lOpts, cs, p.Namespace(), 1, p.Time()) + if err != nil { + return err + } + if len(crs) == 0 { + fmt.Fprintf(s.Out, "No CustomRuns present in namespace %s\n", opts.Params.Namespace()) + return nil + } + opts.CustomRunName = strings.Fields(crs[0])[0] + } + } else { + opts.CustomRunName = args[0] + } + + if output != "" { + printer, err := f.ToPrinter() + if err != nil { + return err + } + return actions.PrintObjectV1(customrunGroupResource, opts.CustomRunName, cmd.OutOrStdout(), cs, printer, p.Namespace()) + } + + return PrintCustomRunDescription(s.Out, cs, opts.Params.Namespace(), opts.CustomRunName, opts.Params.Time()) + }, + } + + c.Flags().BoolVarP(&opts.Last, "last", "L", false, "show description for last CustomRun") + c.Flags().IntVarP(&opts.Limit, "limit", "", defaultCustomRunDescribeLimit, "lists number of CustomRuns when selecting a CustomRun to describe") + c.Flags().BoolVarP(&opts.Fzf, "fzf", "F", false, "use fzf to select a CustomRun to describe") + + f.AddFlags(c) + + return c +} + +func askCustomRunName(opts *options.DescribeOptions, crs []string) error { + err := opts.ValidateOpts() + if err != nil { + return err + } + + if len(crs) == 0 { + return fmt.Errorf("no CustomRuns found") + } + + if opts.Fzf { + err = opts.FuzzyAsk(options.ResourceNameCustomRun, crs) + } else { + err = opts.Ask(options.ResourceNameCustomRun, crs) + } + if err != nil { + return err + } + + return nil +} + +// GetAllCustomRuns returns all CustomRuns as a formatted string slice +func GetAllCustomRuns(gr schema.GroupVersionResource, opts metav1.ListOptions, c *cli.Clients, ns string, limit int, time clockwork.Clock) ([]string, error) { + var customruns *v1beta1.CustomRunList + if err := actions.ListV1(gr, c, opts, ns, &customruns); err != nil { + return nil, fmt.Errorf("failed to list CustomRuns from namespace %s: %v", ns, err) + } + + var ret []string + for i, cr := range customruns.Items { + if limit > 0 && i >= limit { + break + } + ret = append(ret, fmt.Sprintf("%s\tStarted: %s\tDuration: %s\tStatus: %s", + cr.Name, + formatted.Age(cr.Status.StartTime, time), + formatted.Duration(cr.Status.StartTime, cr.Status.CompletionTime), + formatted.Condition(cr.Status.Conditions))) + } + return ret, nil +} + +// GetCustomRun retrieves a CustomRun by name +func GetCustomRun(gr schema.GroupVersionResource, c *cli.Clients, crName, ns string) (*v1beta1.CustomRun, error) { + var customrun v1beta1.CustomRun + err := actions.GetV1(gr, c, crName, ns, metav1.GetOptions{}, &customrun) + if err != nil { + return nil, err + } + return &customrun, nil +} + +// PrintCustomRunDescription prints the description of a CustomRun +func PrintCustomRunDescription(out io.Writer, c *cli.Clients, ns string, crName string, time clockwork.Clock) error { + cr, err := GetCustomRun(customrunGroupResource, c, crName, ns) + if err != nil { + return fmt.Errorf("failed to get CustomRun %s: %v", crName, err) + } + + var data = struct { + CustomRun *v1beta1.CustomRun + Time clockwork.Clock + }{ + CustomRun: cr, + Time: time, + } + + funcMap := template.FuncMap{ + "formatAge": formatted.Age, + "formatDuration": formatted.Duration, + "formatCondition": formatted.Condition, + "formatWorkspace": formatted.Workspace, + "hasFailed": hasFailed, + "customRefExists": customRefExists, + "decorate": formatted.DecorateAttr, + "getTimeout": getTimeoutValue, + "removeLastAppliedConfig": formatted.RemoveLastAppliedConfig, + } + + w := tabwriter.NewWriter(out, 0, 5, 3, ' ', tabwriter.TabIndent) + t := template.Must(template.New("Describe CustomRun").Funcs(funcMap).Parse(describeTemplate)) + + err = t.Execute(w, data) + if err != nil { + return err + } + return w.Flush() +} + +func hasFailed(cr *v1beta1.CustomRun) string { + if len(cr.Status.Conditions) == 0 { + return "" + } + + if cr.Status.Conditions[0].Status == corev1.ConditionFalse { + return cr.Status.Conditions[0].Message + } + + return "" +} + +func getTimeoutValue(cr *v1beta1.CustomRun) string { + if cr.Spec.Timeout != nil { + return cr.Spec.Timeout.Duration.String() + } + return "" +} + +func customRefExists(spec v1beta1.CustomRunSpec) string { + if spec.CustomRef != nil && spec.CustomRef.Name != "" { + return spec.CustomRef.Name + } + if spec.CustomSpec != nil { + return fmt.Sprintf("%s/%s", spec.CustomSpec.APIVersion, spec.CustomSpec.Kind) + } + return "" +} diff --git a/pkg/cmd/customrun/describe_test.go b/pkg/cmd/customrun/describe_test.go new file mode 100644 index 0000000000..298b39b8ea --- /dev/null +++ b/pkg/cmd/customrun/describe_test.go @@ -0,0 +1,603 @@ +// Copyright © 2026 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 ( + "fmt" + "strings" + "testing" + "time" + + "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" + pipelinetest "github.com/tektoncd/pipeline/test" + "gotest.tools/v3/golden" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +func TestCustomRunDescribe_not_found(t *testing.T) { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + tdc := testDynamic.Options{} + dynamic, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic} + + customrun := Command(p) + out, err := test.ExecuteCommand(customrun, "desc", "bar", "-n", "ns") + if err == nil { + t.Errorf("Expected error but did not get one") + } + expected := "Error: failed to get CustomRun bar: customruns.tekton.dev \"bar\" not found\n" + test.AssertOutput(t, expected, out) +} + +func TestCustomRunDescribe_empty_customrun(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-1", + Namespace: "ns", + Labels: map[string]string{"tekton.dev/customTask": "mytask"}, + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + Timeout: &metav1.Duration{Duration: 1 * time.Hour}, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_with_params(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-1", + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + Params: v1beta1.Params{ + { + Name: "param1", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeString, StringVal: "value1"}, + }, + { + Name: "param2", + Value: v1beta1.ParamValue{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"a", "b", "c"}}, + }, + }, + Timeout: &metav1.Duration{Duration: 1 * time.Hour}, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + CustomRunStatusFields: v1beta1.CustomRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now()}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_failed(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-1", + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + Timeout: &metav1.Duration{Duration: 1 * time.Hour}, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionFalse, + Reason: v1beta1.CustomRunReasonFailed.String(), + Message: "CustomRun failed because of some error", + }, + }, + }, + CustomRunStatusFields: v1beta1.CustomRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now().Add(2 * time.Minute)}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_last_no_customrun_present(t *testing.T) { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic} + + customrun := Command(p) + out, err := test.ExecuteCommand(customrun, "desc", "--last", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + expected := "No CustomRuns present in namespace ns\n" + test.AssertOutput(t, expected, out) +} + +func TestCustomRunDescribe_custom_output(t *testing.T) { + name := "custom-run" + expected := "customrun.tekton.dev/" + name + + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + got, err := test.ExecuteCommand(customrun, "desc", "-o", "name", "-n", "ns", name) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + got = strings.TrimSpace(got) + if got != expected { + t.Errorf("Result should be '%s' != '%s'", got, expected) + } +} + +func TestCustomRunDescribe_with_labels_and_annotations(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-with-labels", + Namespace: "ns", + Labels: map[string]string{ + "tekton.dev/customTask": "mytask", + "app": "myapp", + }, + Annotations: map[string]string{ + corev1.LastAppliedConfigAnnotation: "LastAppliedConfig", + "tekton.dev/tags": "testing", + }, + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-with-labels", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_with_results(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-with-results", + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + CustomRunStatusFields: v1beta1.CustomRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now()}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + Results: []v1beta1.CustomRunResult{ + { + Name: "result-1", + Value: "value-1", + }, + { + Name: "result-2", + Value: "value-2", + }, + }, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-with-results", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_cancelled(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-cancelled", + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + Status: v1beta1.CustomRunSpecStatusCancelled, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionFalse, + Reason: v1beta1.CustomRunReasonCancelled.String(), + Message: "CustomRun \"cr-cancelled\" was cancelled", + }, + }, + }, + CustomRunStatusFields: v1beta1.CustomRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now().Add(2 * time.Minute)}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc", "cr-cancelled", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} + +func TestCustomRunDescribe_only_one_customrun_present(t *testing.T) { + clock := test.FakeClock() + + crs := []*v1beta1.CustomRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cr-1", + Namespace: "ns", + }, + Spec: v1beta1.CustomRunSpec{ + CustomRef: &v1beta1.TaskRef{ + APIVersion: "example.dev/v0", + Kind: "MyCustomTask", + Name: "mytask", + }, + }, + Status: v1beta1.CustomRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + Reason: v1beta1.CustomRunReasonSuccessful.String(), + }, + }, + }, + CustomRunStatusFields: v1beta1.CustomRunStatusFields{ + StartTime: &metav1.Time{Time: clock.Now()}, + CompletionTime: &metav1.Time{Time: clock.Now().Add(5 * time.Minute)}, + }, + }, + }, + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + + tdc := testDynamic.Options{} + dynamic, err := tdc.Client( + cb.UnstructuredV1beta1CustomRun(crs[0], versionv1beta1), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + cs.Pipeline.Resources = cb.APIResourceList(versionv1beta1, []string{"customrun"}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dynamic, Clock: clock} + p.SetNamespace("ns") + + customrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(customrun, "desc") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + golden.Assert(t, actual, fmt.Sprintf("%s.golden", t.Name())) +} diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_cancelled.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_cancelled.golden new file mode 100644 index 0000000000..4d33ce082d --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_cancelled.golden @@ -0,0 +1,12 @@ +Name: cr-cancelled +Namespace: ns +Custom Ref: mytask + +Status + +STARTED DURATION STATUS +8 minutes ago 3m0s Failed(CustomRunCancelled) + +Message + +CustomRun "cr-cancelled" was cancelled diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_empty_customrun.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_empty_customrun.golden new file mode 100644 index 0000000000..c82cbc7d1d --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_empty_customrun.golden @@ -0,0 +1,11 @@ +Name: cr-1 +Namespace: ns +Custom Ref: mytask +Timeout: 1h0m0s +Labels: + tekton.dev/customTask=mytask + +Status + +STARTED DURATION STATUS +--- --- Succeeded diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_failed.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_failed.golden new file mode 100644 index 0000000000..623069fbee --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_failed.golden @@ -0,0 +1,13 @@ +Name: cr-1 +Namespace: ns +Custom Ref: mytask +Timeout: 1h0m0s + +Status + +STARTED DURATION STATUS +8 minutes ago 3m0s Failed + +Message + +CustomRun failed because of some error diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_only_one_customrun_present.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_only_one_customrun_present.golden new file mode 100644 index 0000000000..2dbae8f149 --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_only_one_customrun_present.golden @@ -0,0 +1,8 @@ +Name: cr-1 +Namespace: ns +Custom Ref: mytask + +Status + +STARTED DURATION STATUS +10 minutes ago 5m0s Succeeded diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_labels_and_annotations.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_labels_and_annotations.golden new file mode 100644 index 0000000000..1f061d2b5a --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_labels_and_annotations.golden @@ -0,0 +1,13 @@ +Name: cr-with-labels +Namespace: ns +Custom Ref: mytask +Labels: + app=myapp + tekton.dev/customTask=mytask +Annotations: + tekton.dev/tags=testing + +Status + +STARTED DURATION STATUS +--- --- Succeeded diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_params.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_params.golden new file mode 100644 index 0000000000..13a05bdd10 --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_params.golden @@ -0,0 +1,15 @@ +Name: cr-1 +Namespace: ns +Custom Ref: mytask +Timeout: 1h0m0s + +Status + +STARTED DURATION STATUS +10 minutes ago 5m0s Succeeded + +Params + + NAME VALUE + param1 value1 + param2 [a b c] diff --git a/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_results.golden b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_results.golden new file mode 100644 index 0000000000..6d3efc3d5d --- /dev/null +++ b/pkg/cmd/customrun/testdata/TestCustomRunDescribe_with_results.golden @@ -0,0 +1,14 @@ +Name: cr-with-results +Namespace: ns +Custom Ref: mytask + +Status + +STARTED DURATION STATUS +10 minutes ago 5m0s Succeeded + +Results + + NAME VALUE + result-1 value-1 + result-2 value-2 diff --git a/pkg/options/describe.go b/pkg/options/describe.go index 721841f9e6..4b88719842 100644 --- a/pkg/options/describe.go +++ b/pkg/options/describe.go @@ -35,6 +35,7 @@ type DescribeOptions struct { PipelineRunName string TaskName string TaskrunName string + CustomRunName string Tasks []string TriggerTemplateName string TriggerBindingName string @@ -92,6 +93,8 @@ func (opts *DescribeOptions) Ask(resource string, options []string) error { opts.TaskName = ans case ResourceNameTaskRun: opts.TaskrunName = strings.Fields(ans)[0] + case ResourceNameCustomRun: + opts.CustomRunName = strings.Fields(ans)[0] case ResourceNameTriggerTemplate: opts.TriggerTemplateName = ans case ResourceNameTriggerBinding: @@ -160,6 +163,8 @@ func (opts *DescribeOptions) FuzzyAsk(resource string, options []string) error { opts.TaskName = ans case ResourceNameTaskRun: opts.TaskrunName = strings.Fields(ans)[0] + case ResourceNameCustomRun: + opts.CustomRunName = strings.Fields(ans)[0] case ResourceNameTriggerTemplate: opts.TriggerTemplateName = ans case ResourceNameTriggerBinding: diff --git a/pkg/options/resource_names.go b/pkg/options/resource_names.go index ae017cc5ce..5202179159 100644 --- a/pkg/options/resource_names.go +++ b/pkg/options/resource_names.go @@ -5,6 +5,7 @@ const ( ResourceNamePipelineRun = "pipelinerun" ResourceNameTask = "task" ResourceNameTaskRun = "taskrun" + ResourceNameCustomRun = "customrun" ResourceNameTriggerTemplate = "triggertemplate" ResourceNameTriggerBinding = "triggerbinding" ResourceNameClusterTriggerBinding = "clustertriggerbinding"