diff --git a/pkg/cmd/pipelinerun/logs.go b/pkg/cmd/pipelinerun/logs.go index fd2feb8683..5990f7b8d7 100644 --- a/pkg/cmd/pipelinerun/logs.go +++ b/pkg/cmd/pipelinerun/logs.go @@ -15,6 +15,7 @@ package pipelinerun import ( + "context" "fmt" "os" "strings" @@ -25,6 +26,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 +85,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 if there is an 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 +113,33 @@ 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 + } + ctx := context.Background() + pr, err := clients.Tekton.TektonV1().PipelineRuns(opts.Params.Namespace()).Get(ctx, opts.PipelineRunName, metav1.GetOptions{}) + 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 e9d2430b53..1528fb3f7f 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 1dce5474f6..8e353570b6 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