diff --git a/docs/cmd/tkn_pipeline_start.md b/docs/cmd/tkn_pipeline_start.md index dfdf64c21..7c3acf173 100644 --- a/docs/cmd/tkn_pipeline_start.md +++ b/docs/cmd/tkn_pipeline_start.md @@ -63,6 +63,7 @@ my-csi-template and my-volume-claim-template) ``` --dry-run preview PipelineRun without running it + -E, --exit-with-pipelinerun-error when using --showlog, exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status -f, --filename string local or remote file name containing a Pipeline definition to start a PipelineRun --finally-timeout string timeout for Finally TaskRuns -h, --help help for start diff --git a/docs/cmd/tkn_pipelinerun_logs.md b/docs/cmd/tkn_pipelinerun_logs.md index 7163cae38..1648dc4d0 100644 --- a/docs/cmd/tkn_pipelinerun_logs.md +++ b/docs/cmd/tkn_pipelinerun_logs.md @@ -30,15 +30,16 @@ Show the logs of PipelineRun named 'microservice-1' for all Tasks and steps (inc ### Options ``` - -a, --all show all logs including init steps injected by tekton - -f, --follow stream live logs - -F, --fzf use fzf to select a PipelineRun - -h, --help help for logs - -L, --last show logs for last PipelineRun - --limit int lists number of PipelineRuns (default 5) - --prefix prefix each log line with the log source (task name and step name) (default true) - -t, --task strings show logs for mentioned Tasks only - --timestamps show logs with timestamp + -a, --all show all logs including init steps injected by tekton + -E, --exit-with-pipelinerun-error exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status + -f, --follow stream live logs + -F, --fzf use fzf to select a PipelineRun + -h, --help help for logs + -L, --last show logs for last PipelineRun + --limit int lists number of PipelineRuns (default 5) + --prefix prefix each log line with the log source (task name and step name) (default true) + -t, --task strings show logs for mentioned Tasks only + --timestamps show logs with timestamp ``` ### Options inherited from parent commands diff --git a/docs/man/man1/tkn-pipeline-start.1 b/docs/man/man1/tkn-pipeline-start.1 index 5ef8ab22f..eac1634b6 100644 --- a/docs/man/man1/tkn-pipeline-start.1 +++ b/docs/man/man1/tkn-pipeline-start.1 @@ -31,6 +31,10 @@ Parameters, at least those that have no default value \fB\-\-dry\-run\fP[=false] preview PipelineRun without running it +.PP +\fB\-E\fP, \fB\-\-exit\-with\-pipelinerun\-error\fP[=false] + when using \-\-showlog, exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status + .PP \fB\-f\fP, \fB\-\-filename\fP="" local or remote file name containing a Pipeline definition to start a PipelineRun diff --git a/docs/man/man1/tkn-pipelinerun-logs.1 b/docs/man/man1/tkn-pipelinerun-logs.1 index 953eb1a7e..a5879ea6d 100644 --- a/docs/man/man1/tkn-pipelinerun-logs.1 +++ b/docs/man/man1/tkn-pipelinerun-logs.1 @@ -23,6 +23,10 @@ Show the logs of a PipelineRun \fB\-a\fP, \fB\-\-all\fP[=false] show all logs including init steps injected by tekton +.PP +\fB\-E\fP, \fB\-\-exit\-with\-pipelinerun\-error\fP[=false] + exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status + .PP \fB\-f\fP, \fB\-\-follow\fP[=false] stream live logs diff --git a/pkg/cmd/pipeline/start.go b/pkg/cmd/pipeline/start.go index 7c7a7797c..53a49326f 100644 --- a/pkg/cmd/pipeline/start.go +++ b/pkg/cmd/pipeline/start.go @@ -68,6 +68,7 @@ type startOptions struct { Labels []string ShowLog bool DryRun bool + ExitWithPrError bool Output string PrefixName string TimeOut string @@ -195,6 +196,7 @@ For passing the workspaces via flags: c.Flags().BoolVarP(&opt.UseParamDefaults, "use-param-defaults", "", false, "use default parameter values without prompting for input") c.Flags().StringVar(&opt.PodTemplate, "pod-template", "", "local or remote file containing a PodTemplate definition") c.Flags().BoolVarP(&opt.SkipOptionalWorkspace, "skip-optional-workspace", "", false, "skips the prompt for optional workspaces") + c.Flags().BoolVarP(&opt.ExitWithPrError, "exit-with-pipelinerun-error", "E", false, "when using --showlog, exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status") c.Flags().StringVarP(&opt.ServiceAccountName, "serviceaccount", "s", "", "pass the serviceaccount name") _ = c.RegisterFlagCompletionFunc("serviceaccount", @@ -413,6 +415,7 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { Prefixing: true, Params: opt.cliparams, AllSteps: false, + ExitWithPrError: opt.ExitWithPrError, } return prcmd.Run(runLogOpts) } diff --git a/pkg/cmd/pipelinerun/logs.go b/pkg/cmd/pipelinerun/logs.go index fd2feb868..235003cfc 100644 --- a/pkg/cmd/pipelinerun/logs.go +++ b/pkg/cmd/pipelinerun/logs.go @@ -25,6 +25,8 @@ import ( "github.com/tektoncd/cli/pkg/log" "github.com/tektoncd/cli/pkg/options" pipelinerunpkg "github.com/tektoncd/cli/pkg/pipelinerun" + tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -82,6 +84,7 @@ Show the logs of PipelineRun named 'microservice-1' for all Tasks and steps (inc c.Flags().BoolVarP(&opts.Follow, "follow", "f", false, "stream live logs") c.Flags().BoolVarP(&opts.Timestamps, "timestamps", "", false, "show logs with timestamp") c.Flags().BoolVarP(&opts.Prefixing, "prefix", "", true, "prefix each log line with the log source (task name and step name)") + c.Flags().BoolVarP(&opts.ExitWithPrError, "exit-with-pipelinerun-error", "E", false, "exit with pipelinerun to the unix shell, 0 if success, 1 if error, 2 on unknown status") c.Flags().StringSliceVarP(&opts.Tasks, "task", "t", []string{}, "show logs for mentioned Tasks only") c.Flags().IntVarP(&opts.Limit, "limit", "", defaultLimit, "lists number of PipelineRuns") return c @@ -109,9 +112,32 @@ func Run(opts *options.LogOptions) error { log.NewWriter(log.LogTypePipeline, opts.Prefixing).Write(opts.Stream, logC, errC) + // get pipelinerun status + if opts.ExitWithPrError { + clients, err := opts.Params.Clients() + if err != nil { + return err + } + pr, err := pipelinerunpkg.GetPipelineRun(pipelineRunGroupResource, clients, opts.PipelineRunName, opts.Params.Namespace()) + if err != nil { + return err + } + os.Exit(prStatusToUnixStatus(pr)) + } + return nil } +func prStatusToUnixStatus(pr *tektonv1.PipelineRun) int { + if len(pr.Status.Conditions) == 0 { + return 2 + } + if pr.Status.Conditions[0].Status == corev1.ConditionFalse { + return 1 + } + return 0 +} + func askRunName(opts *options.LogOptions) error { lOpts := metav1.ListOptions{} diff --git a/pkg/cmd/pipelinerun/logs_test.go b/pkg/cmd/pipelinerun/logs_test.go index e9d2430b5..1528fb3f7 100644 --- a/pkg/cmd/pipelinerun/logs_test.go +++ b/pkg/cmd/pipelinerun/logs_test.go @@ -182,6 +182,65 @@ func TestLog_no_pipelinerun_argument(t *testing.T) { } } +func TestLog_PrStatusToUnixStatus(t *testing.T) { + testCases := []struct { + name string + pr *v1.PipelineRun + expected int + }{ + { + name: "No conditions", + pr: &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{}, + }, + }, + }, + expected: 2, + }, + { + name: "Condition status is false", + pr: &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionFalse, + }, + }, + }, + }, + }, + expected: 1, + }, + { + name: "Condition status true", + pr: &v1.PipelineRun{ + Status: v1.PipelineRunStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + expected: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := prStatusToUnixStatus(tc.pr) + if result != tc.expected { + t.Errorf("Expected %d, got %d", tc.expected, result) + } + }) + } +} + func TestLog_run_found_v1beta1(t *testing.T) { clock := test.FakeClock() pdata := []*v1beta1.Pipeline{ @@ -1673,7 +1732,6 @@ func TestPipelinerunLog_follow_mode_v1beta1(t *testing.T) { cb.UnstructuredV1beta1PR(prs[0], versionv1beta1), cb.UnstructuredV1beta1P(pps[0], versionv1beta1), ) - if err != nil { t.Errorf("unable to create dynamic client: %v", err) } @@ -1862,7 +1920,6 @@ func TestPipelinerunLog_follow_mode(t *testing.T) { cb.UnstructuredPR(prs[0], version), cb.UnstructuredP(pps[0], version), ) - if err != nil { t.Errorf("unable to create dynamic client: %v", err) } @@ -2517,7 +2574,6 @@ func TestLog_pipelinerun_still_running_v1beta1(t *testing.T) { updatePRv1beta1(finalPRs, watcher) output, err := fetchLogs(prlo) - if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -2648,7 +2704,6 @@ func TestLog_pipelinerun_still_running(t *testing.T) { updatePR(finalPRs, watcher) output, err := fetchLogs(prlo) - if err != nil { t.Errorf("Unexpected error: %v", err) } @@ -3502,7 +3557,6 @@ func TestPipelinerunLog_finally_v1beta1(t *testing.T) { cb.UnstructuredV1beta1PR(prs[0], versionv1beta1), cb.UnstructuredV1beta1P(pps[0], versionv1beta1), ) - if err != nil { t.Errorf("unable to create dynamic client: %v", err) } @@ -3752,7 +3806,6 @@ func TestPipelinerunLog_finally(t *testing.T) { cb.UnstructuredPR(prs[0], version), cb.UnstructuredP(pps[0], version), ) - if err != nil { t.Errorf("unable to create dynamic client: %v", err) } diff --git a/pkg/options/logs.go b/pkg/options/logs.go index 1dce5474f..8e353570b 100644 --- a/pkg/options/logs.go +++ b/pkg/options/logs.go @@ -51,6 +51,7 @@ type LogOptions struct { Tail int64 Timestamps bool Prefixing bool + ExitWithPrError bool // ActivityTimeout is the amount of time to wait for some activity // (e.g. Pod ready) before giving up. ActivityTimeout time.Duration