diff --git a/docs/cmd/tkn_taskrun.md b/docs/cmd/tkn_taskrun.md index 4bc5d65a1e..7e0b92f232 100644 --- a/docs/cmd/tkn_taskrun.md +++ b/docs/cmd/tkn_taskrun.md @@ -20,6 +20,7 @@ Manage taskruns * [tkn](tkn.md) - CLI for tekton pipelines * [tkn taskrun delete](tkn_taskrun_delete.md) - Delete a taskrun in a namespace +* [tkn taskrun describe](tkn_taskrun_describe.md) - Describe a taskrun in a namespace * [tkn taskrun list](tkn_taskrun_list.md) - Lists taskruns in a namespace * [tkn taskrun logs](tkn_taskrun_logs.md) - Show taskruns logs diff --git a/docs/cmd/tkn_taskrun_describe.md b/docs/cmd/tkn_taskrun_describe.md new file mode 100644 index 0000000000..17f1a7f54f --- /dev/null +++ b/docs/cmd/tkn_taskrun_describe.md @@ -0,0 +1,45 @@ +## tkn taskrun describe + +Describe a taskrun in a namespace + +***Aliases**: desc* + +### Usage + +``` +tkn taskrun describe +``` + +### Synopsis + +Describe a taskrun in a namespace + +### Examples + + +# Describe a TaskRun of name 'foo' in namespace 'bar' +tkn taskrun describe foo -n bar + +tkn tr 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) + -h, --help help for describe + -o, --output string Output format. One of: json|yaml|name|go-template|go-template-file|template|templatefile|jsonpath|jsonpath-file. + --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 + +``` + -k, --kubeconfig string kubectl config file (default: $HOME/.kube/config) + -n, --namespace string namespace to use (default: from $KUBECONFIG) +``` + +### SEE ALSO + +* [tkn taskrun](tkn_taskrun.md) - Manage taskruns + diff --git a/docs/man/man1/tkn-taskrun-describe.1 b/docs/man/man1/tkn-taskrun-describe.1 new file mode 100644 index 0000000000..268afcfda1 --- /dev/null +++ b/docs/man/man1/tkn-taskrun-describe.1 @@ -0,0 +1,62 @@ +.TH "TKN\-TASKRUN\-DESCRIBE" "1" "Sep 2019" "Auto generated by spf13/cobra" "" +.nh +.ad l + + +.SH NAME +.PP +tkn\-taskrun\-describe \- Describe a taskrun in a namespace + + +.SH SYNOPSIS +.PP +\fBtkn taskrun describe\fP + + +.SH DESCRIPTION +.PP +Describe a taskrun 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 describe + +.PP +\fB\-o\fP, \fB\-\-output\fP="" + Output format. One of: json|yaml|name|go\-template|go\-template\-file|template|templatefile|jsonpath|jsonpath\-file. + +.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\-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) + + +.SH EXAMPLE + +.SH Describe a TaskRun of name 'foo' in namespace 'bar' +.PP +tkn taskrun describe foo \-n bar + +.PP +tkn tr desc foo \-n bar", + + +.SH SEE ALSO +.PP +\fBtkn\-taskrun(1)\fP diff --git a/pkg/cmd/taskrun/describe.go b/pkg/cmd/taskrun/describe.go new file mode 100644 index 0000000000..0760e5ce2a --- /dev/null +++ b/pkg/cmd/taskrun/describe.go @@ -0,0 +1,174 @@ +// Copyright © 2019 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 taskrun + +import ( + "fmt" + + "text/tabwriter" + "text/template" + + "github.com/spf13/cobra" + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/formatted" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cliopts "k8s.io/cli-runtime/pkg/genericclioptions" +) + +const templ = `Name: {{ .TaskRun.Name }} +Namespace: {{ .TaskRun.Namespace }} +Task Ref: {{ .TaskRun.Spec.TaskRef.Name }} +{{- if ne .TaskRun.Spec.ServiceAccount "" }} +Service Account: {{ .TaskRun.Spec.ServiceAccount }} +{{- end }} + +Status +STARTED DURATION STATUS +{{ formatAge .TaskRun.Status.StartTime .Params.Time }} {{ formatDuration .TaskRun.Status.StartTime .TaskRun.Status.CompletionTime }} {{ formatCondition .TaskRun.Status.Conditions }} +{{- $msg := hasFailed .TaskRun -}} +{{- if ne $msg "" }} + +Message +{{ $msg }} +{{- end }} + +Inputs +{{- $l := len .TaskRun.Spec.Inputs.Resources }}{{ if eq $l 0 }} +No resources +{{- else }} +NAME RESOURCE REF +{{- range $i, $r := .TaskRun.Spec.Inputs.Resources }} +{{$r.Name }} {{ $r.ResourceRef.Name }} +{{- end }} +{{- end }} + +Outputs +{{- $l := len .TaskRun.Spec.Outputs.Resources }}{{ if eq $l 0 }} +No resources +{{- else }} +NAME RESOURCE REF +{{- range $i, $r := .TaskRun.Spec.Outputs.Resources }} +{{$r.Name }} {{ $r.ResourceRef.Name }} +{{- end }} +{{- end }} + +Params +{{- $l := len .TaskRun.Spec.Inputs.Params }}{{ if eq $l 0 }} +No params +{{- else }} +NAME VALUE +{{- range $i, $p := .TaskRun.Spec.Inputs.Params }} +{{- if eq $p.Value.Type "string" }} +{{ $p.Name }} {{ $p.Value.StringVal }} +{{- else }} +{{ $p.Name }} {{ $p.Value.ArrayVal }} +{{- end }} +{{- end }} +{{- end }} + +Steps +{{- $l := len .TaskRun.Status.Steps }}{{ if eq $l 0 }} +No steps +{{- else }} +STEP NAME +{{- range $steps := .TaskRun.Status.Steps }} +{{ $steps.Name }} +{{- end }} +{{- end }} +` + +func describeCommand(p cli.Params) *cobra.Command { + f := cliopts.NewPrintFlags("describe") + eg := ` +# Describe a TaskRun of name 'foo' in namespace 'bar' +tkn taskrun describe foo -n bar + +tkn tr desc foo -n bar", +` + + c := &cobra.Command{ + Use: "describe", + Aliases: []string{"desc"}, + Short: "Describe a taskrun in a namespace", + Example: eg, + Args: cobra.MinimumNArgs(1), + SilenceUsage: true, + Annotations: map[string]string{ + "commandType": "main", + }, + RunE: func(cmd *cobra.Command, args []string) error { + s := &cli.Stream{ + Out: cmd.OutOrStdout(), + Err: cmd.OutOrStderr(), + } + return printTaskRunDescription(s, args[0], p) + }, + } + + _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_taskrun") + f.AddFlags(c) + + return c +} + +func printTaskRunDescription(s *cli.Stream, trName string, p cli.Params) error { + cs, err := p.Clients() + if err != nil { + return fmt.Errorf("failed to create tekton client") + } + + tr, err := cs.Tekton.TektonV1alpha1().TaskRuns(p.Namespace()).Get(trName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to find taskrun %q", trName) + } + + var data = struct { + TaskRun *v1alpha1.TaskRun + Params cli.Params + }{ + TaskRun: tr, + Params: p, + } + + funcMap := template.FuncMap{ + "formatAge": formatted.Age, + "formatDuration": formatted.Duration, + "formatCondition": formatted.Condition, + "hasFailed": hasFailed, + } + + w := tabwriter.NewWriter(s.Out, 0, 5, 3, ' ', tabwriter.TabIndent) + t := template.Must(template.New("Describe taskrun").Funcs(funcMap).Parse(templ)) + + err = t.Execute(w, data) + if err != nil { + fmt.Fprintf(s.Err, "Failed to execute template") + return err + } + return w.Flush() +} + +func hasFailed(tr *v1alpha1.TaskRun) string { + if len(tr.Status.Conditions) == 0 { + return "" + } + + if tr.Status.Conditions[0].Status == corev1.ConditionFalse { + return tr.Status.Conditions[0].Message + } + return "" +} diff --git a/pkg/cmd/taskrun/describe_test.go b/pkg/cmd/taskrun/describe_test.go new file mode 100644 index 0000000000..0f8b12306b --- /dev/null +++ b/pkg/cmd/taskrun/describe_test.go @@ -0,0 +1,230 @@ +// Copyright © 2019 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 taskrun + +import ( + "testing" + "time" + + "github.com/jonboulle/clockwork" + "github.com/tektoncd/cli/pkg/test" + cb "github.com/tektoncd/cli/pkg/test/builder" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + "github.com/tektoncd/pipeline/pkg/reconciler/v1alpha1/pipelinerun/resources" + pipelinetest "github.com/tektoncd/pipeline/test" + tb "github.com/tektoncd/pipeline/test/builder" + corev1 "k8s.io/api/core/v1" + "knative.dev/pkg/apis" +) + +func TestTaskRunDescribe_not_found(t *testing.T) { + cs, _ := test.SeedTestData(t, pipelinetest.Data{}) + p := &test.Params{Tekton: cs.Pipeline} + + taskrun := Command(p) + _, err := test.ExecuteCommand(taskrun, "desc", "bar", "-n", "ns") + if err == nil { + t.Errorf("Expected error, did not get any") + } + expected := "failed to find taskrun \"bar\"" + test.AssertOutput(t, expected, err.Error()) +} + +func TestTaskRunDescribe_empty_taskrun(t *testing.T) { + clock := clockwork.NewFakeClock() + + trs := []*v1alpha1.TaskRun{ + tb.TaskRun("tr-1", "ns", + tb.TaskRunLabel("tekton.dev/task", "t1"), + tb.TaskRunSpec(tb.TaskRunTaskRef("t1")), + tb.TaskRunStatus( + tb.StatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + ), + ), + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + TaskRuns: trs, + }) + + p := &test.Params{Tekton: cs.Pipeline, Clock: clock} + + taskrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(taskrun, "desc", "tr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + expected := `Name: tr-1 +Namespace: ns +Task Ref: t1 + +Status +STARTED DURATION STATUS +--- --- Succeeded + +Inputs +No resources + +Outputs +No resources + +Params +No params + +Steps +No steps +` + + test.AssertOutput(t, expected, actual) +} + +func TestTaskRunDescribe_only_taskrun(t *testing.T) { + clock := clockwork.NewFakeClock() + + trs := []*v1alpha1.TaskRun{ + tb.TaskRun("tr-1", "ns", + tb.TaskRunStatus( + tb.TaskRunStartTime(clockwork.NewFakeClock().Now().Add(20*time.Second)), + tb.StatusCondition(apis.Condition{ + Type: apis.ConditionSucceeded, + Status: corev1.ConditionTrue, + }), + tb.StepState( + cb.StepName("step1"), + tb.StateTerminated(0), + ), + tb.StepState( + cb.StepName("step2"), + tb.StateTerminated(0), + ), + ), + tb.TaskRunSpec( + tb.TaskRunTaskRef("t1"), + tb.TaskRunInputs(tb.TaskRunInputsParam("input", "param")), + tb.TaskRunInputs(tb.TaskRunInputsParam("input2", "param2")), + tb.TaskRunInputs(tb.TaskRunInputsResource("git", tb.TaskResourceBindingRef("git"))), + tb.TaskRunInputs(tb.TaskRunInputsResource("image-input", tb.TaskResourceBindingRef("image"))), + tb.TaskRunOutputs(tb.TaskRunOutputsResource("image-output", tb.TaskResourceBindingRef("image"))), + tb.TaskRunOutputs(tb.TaskRunOutputsResource("image-output2", tb.TaskResourceBindingRef("image"))), + ), + ), + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + TaskRuns: trs, + }) + + p := &test.Params{Tekton: cs.Pipeline, Clock: clock} + + taskrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(taskrun, "desc", "tr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + expected := `Name: tr-1 +Namespace: ns +Task Ref: t1 + +Status +STARTED DURATION STATUS +9 minutes ago --- Succeeded + +Inputs +NAME RESOURCE REF +git git +image-input image + +Outputs +NAME RESOURCE REF +image-output image +image-output2 image + +Params +NAME VALUE +input param +input2 param2 + +Steps +STEP NAME +step1 +step2 +` + + test.AssertOutput(t, expected, actual) +} + +func TestTaskRunDescribe_failed(t *testing.T) { + clock := clockwork.NewFakeClock() + + trs := []*v1alpha1.TaskRun{ + tb.TaskRun("tr-1", "ns", + tb.TaskRunStatus( + tb.TaskRunStartTime(clock.Now().Add(2*time.Minute)), + cb.TaskRunCompletionTime(clock.Now().Add(5*time.Minute)), + tb.StatusCondition(apis.Condition{ + Status: corev1.ConditionFalse, + Reason: resources.ReasonFailed, + Message: "Testing tr failed", + }), + ), + tb.TaskRunSpec( + tb.TaskRunTaskRef("t1"), + ), + ), + } + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + TaskRuns: trs, + }) + + p := &test.Params{Tekton: cs.Pipeline, Clock: clock} + + taskrun := Command(p) + clock.Advance(10 * time.Minute) + actual, err := test.ExecuteCommand(taskrun, "desc", "tr-1", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + expected := `Name: tr-1 +Namespace: ns +Task Ref: t1 + +Status +STARTED DURATION STATUS +8 minutes ago 3 minutes Failed + +Message +Testing tr failed + +Inputs +No resources + +Outputs +No resources + +Params +No params + +Steps +No steps +` + + test.AssertOutput(t, expected, actual) +} diff --git a/pkg/cmd/taskrun/taskrun.go b/pkg/cmd/taskrun/taskrun.go index 56559d3dc3..a153e03c4f 100644 --- a/pkg/cmd/taskrun/taskrun.go +++ b/pkg/cmd/taskrun/taskrun.go @@ -38,6 +38,7 @@ func Command(p cli.Params) *cobra.Command { listCommand(p), logCommand(p), deleteCommand(p), + describeCommand(p), ) return cmd