diff --git a/docs/cmd/tkn_pipeline_logs.md b/docs/cmd/tkn_pipeline_logs.md index f31b0f2c1..4bb3be356 100644 --- a/docs/cmd/tkn_pipeline_logs.md +++ b/docs/cmd/tkn_pipeline_logs.md @@ -32,10 +32,11 @@ Show pipeline logs ### Options ``` - -a, --all show all logs including init steps injected by tekton - -f, --follow stream live logs - -h, --help help for logs - -l, --last show logs for last run + -a, --all show all logs including init steps injected by tekton + -f, --follow stream live logs + -h, --help help for logs + -l, --last show logs for last run + -L, --limit int lists number of pipelineruns (default 5) ``` ### Options inherited from parent commands diff --git a/docs/man/man1/tkn-pipeline-logs.1 b/docs/man/man1/tkn-pipeline-logs.1 index ab2871fe3..3efe79863 100644 --- a/docs/man/man1/tkn-pipeline-logs.1 +++ b/docs/man/man1/tkn-pipeline-logs.1 @@ -35,6 +35,10 @@ Show pipeline logs \fB\-l\fP, \fB\-\-last\fP[=false] show logs for last run +.PP +\fB\-L\fP, \fB\-\-limit\fP=5 + lists number of pipelineruns + .SH OPTIONS INHERITED FROM PARENT COMMANDS .PP diff --git a/pkg/cmd/pipeline/logs.go b/pkg/cmd/pipeline/logs.go index fcf504719..d3cf29eda 100644 --- a/pkg/cmd/pipeline/logs.go +++ b/pkg/cmd/pipeline/logs.go @@ -17,6 +17,7 @@ package pipeline import ( "fmt" "os" + "sort" "strings" "github.com/AlecAivazis/survey/v2" @@ -38,6 +39,7 @@ type logOptions struct { follow bool pipelineName string runName string + limit int } func nameArg(args []string, p cli.Params) error { @@ -103,6 +105,7 @@ func logCommand(p cli.Params) *cobra.Command { c.Flags().BoolVarP(&opts.last, "last", "l", false, "show logs for last run") c.Flags().BoolVarP(&opts.allSteps, "all", "a", false, "show all logs including init steps injected by tekton") c.Flags().BoolVarP(&opts.follow, "follow", "f", false, "stream live logs") + c.Flags().IntVarP(&opts.limit, "limit", "L", 5, "lists number of pipelineruns") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_pipeline") return c @@ -157,6 +160,12 @@ func (opts *logOptions) init(args []string) error { } func (opts *logOptions) getAllInputs() error { + err := validate(opts) + + if err != nil { + return err + } + ps, err := allPipelines(opts) if err != nil { return err @@ -185,9 +194,14 @@ func (opts *logOptions) getAllInputs() error { } func (opts *logOptions) askRunName() error { + err := validate(opts) + if err != nil { + return err + } + var ans string - prs, err := allRuns(opts.params, opts.pipelineName) + prs, err := allRuns(opts.params, opts.pipelineName, opts.limit) if err != nil { return err } @@ -246,7 +260,7 @@ func allPipelines(opts *logOptions) ([]string, error) { return ret, nil } -func allRuns(p cli.Params, pName string) ([]string, error) { +func allRuns(p cli.Params, pName string, limit int) ([]string, error) { cs, err := p.Clients() if err != nil { return nil, err @@ -260,9 +274,19 @@ func allRuns(p cli.Params, pName string) ([]string, error) { return nil, err } + runslen := len(runs.Items) + + if runslen > 1 { + sort.Sort(byStartTime(runs.Items)) + } + + if limit > runslen { + limit = runslen + } + ret := []string{} for i, run := range runs.Items { - if i < 5 { + if i < limit { ret = append(ret, run.ObjectMeta.Name+" started "+formatted.Age(run.Status.StartTime, p.Time())) } } @@ -291,3 +315,28 @@ func lastRun(cs *cli.Clients, ns, pName string) (string, error) { } return latest.ObjectMeta.Name, nil } + +type byStartTime []v1alpha1.PipelineRun + +func (s byStartTime) Len() int { return len(s) } +func (s byStartTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byStartTime) Less(i, j int) bool { + if s[j].Status.StartTime == nil { + return false + } + + if s[i].Status.StartTime == nil { + return true + } + + return s[j].Status.StartTime.Before(s[i].Status.StartTime) +} + +func validate(opts *logOptions) error { + + if opts.limit <= 0 { + return fmt.Errorf("limit was %d but must be a positive number", opts.limit) + } + + return nil +} diff --git a/pkg/cmd/pipeline/logs_test.go b/pkg/cmd/pipeline/logs_test.go index cc888bc89..7c8c50ef6 100644 --- a/pkg/cmd/pipeline/logs_test.go +++ b/pkg/cmd/pipeline/logs_test.go @@ -15,6 +15,7 @@ package pipeline import ( + "fmt" "testing" "time" @@ -106,6 +107,20 @@ func TestLogs_wrong_run(t *testing.T) { test.AssertOutput(t, expected, err.Error()) } +func TestLogs_negative_limit(t *testing.T) { + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline(pipelineName, ns), + }}) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube} + + c := Command(p) + _, err := test.ExecuteCommand(c, "logs", pipelineName, "-n", ns, "-L", fmt.Sprintf("%d", -1)) + + expected := "limit was -1 but must be a positive number" + test.AssertOutput(t, expected, err.Error()) +} + func TestLogs_interactive_get_all_inputs(t *testing.T) { clock := clockwork.NewFakeClock() @@ -165,6 +180,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { if _, err := c.SendLine(string(terminal.KeyArrowDown)); err != nil { return err } + if _, err := c.ExpectString("output-pipeline"); err != nil { return err } @@ -181,7 +197,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { return err } - if _, err := c.ExpectString(prName + " started 5 minutes ago"); err != nil { + if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { return err } @@ -189,7 +205,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { return err } - if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { + if _, err := c.ExpectString(prName + " started 2 minutes ago"); err != nil { return err } @@ -197,7 +213,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { return err } - if _, err := c.ExpectString(prName + " started 5 minutes ago"); err != nil { + if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { return err } @@ -209,7 +225,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { }, }, } - opts := logOpts(prName, ns, cs) + opts := logOpts(prName, ns, 5, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -277,15 +293,197 @@ func TestLogs_interactive_ask_runs(t *testing.T) { return err } + if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyArrowDown)); err != nil { + return err + } + + if _, err := c.ExpectString(prName + "started 5 minutes ago"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyEnter)); err != nil { + return err + } + + return nil + }, + }, + } + opts := logOpts(prName, ns, 5, cs) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts.RunPromptTest(t, test) + }) + } +} + +func TestLogs_interactive_limit_2(t *testing.T) { + clock := clockwork.NewFakeClock() + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline(pipelineName, ns, + // created 15 minutes back + cb.PipelineCreationTimestamp(clock.Now().Add(-15*time.Minute)), + ), + }, + PipelineRuns: []*v1alpha1.PipelineRun{ + + tb.PipelineRun(prName, ns, + cb.PipelineRunCreationTimestamp(clock.Now().Add(-10*time.Minute)), + tb.PipelineRunLabel("tekton.dev/pipeline", pipelineName), + tb.PipelineRunSpec(pipelineName), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + // pipeline run started 5 minutes ago + tb.PipelineRunStartTime(clock.Now().Add(-5*time.Minute)), + // takes 10 minutes to complete + cb.PipelineRunCompletionTime(clock.Now().Add(10*time.Minute)), + ), + ), + tb.PipelineRun(prName2, ns, + cb.PipelineRunCreationTimestamp(clock.Now().Add(-8*time.Minute)), + tb.PipelineRunLabel("tekton.dev/pipeline", pipelineName), + tb.PipelineRunSpec(pipelineName), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + // pipeline run started 3 minutes ago + tb.PipelineRunStartTime(clock.Now().Add(-3*time.Minute)), + // takes 10 minutes to complete + cb.PipelineRunCompletionTime(clock.Now().Add(10*time.Minute)), + ), + ), + }, + }) + + tests := []promptTest{ + { + name: "basic interaction", + cmdArgs: []string{pipelineName}, + + procedure: func(c *expect.Console) error { + if _, err := c.ExpectString("output-pipeline"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyEnter)); err != nil { + return err + } + + if _, err := c.ExpectString("Select pipelinerun :"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyArrowDown)); err != nil { + return err + } + + if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyArrowDown)); err != nil { + return err + } + if _, err := c.ExpectString(prName + " started 5 minutes ago"); err != nil { return err } + if _, err := c.SendLine(string(terminal.KeyEnter)); err != nil { + return err + } + + return nil + }, + }, + } + opts := logOpts(prName, ns, 2, cs) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + opts.RunPromptTest(t, test) + }) + } +} + +func TestLogs_interactive_limit_1(t *testing.T) { + clock := clockwork.NewFakeClock() + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline(pipelineName, ns, + // created 15 minutes back + cb.PipelineCreationTimestamp(clock.Now().Add(-15*time.Minute)), + ), + }, + PipelineRuns: []*v1alpha1.PipelineRun{ + + tb.PipelineRun(prName, ns, + cb.PipelineRunCreationTimestamp(clock.Now().Add(-10*time.Minute)), + tb.PipelineRunLabel("tekton.dev/pipeline", pipelineName), + tb.PipelineRunSpec(pipelineName), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + // pipeline run started 5 minutes ago + tb.PipelineRunStartTime(clock.Now().Add(-5*time.Minute)), + // takes 10 minutes to complete + cb.PipelineRunCompletionTime(clock.Now().Add(10*time.Minute)), + ), + ), + tb.PipelineRun(prName2, ns, + cb.PipelineRunCreationTimestamp(clock.Now().Add(-8*time.Minute)), + tb.PipelineRunLabel("tekton.dev/pipeline", pipelineName), + tb.PipelineRunSpec(pipelineName), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + // pipeline run started 3 minutes ago + tb.PipelineRunStartTime(clock.Now().Add(-3*time.Minute)), + // takes 10 minutes to complete + cb.PipelineRunCompletionTime(clock.Now().Add(10*time.Minute)), + ), + ), + }, + }) + + tests := []promptTest{ + { + name: "basic interaction", + cmdArgs: []string{pipelineName}, + + procedure: func(c *expect.Console) error { + if _, err := c.ExpectString("output-pipeline"); err != nil { + return err + } + + if _, err := c.SendLine(string(terminal.KeyEnter)); err != nil { + return err + } + + if _, err := c.ExpectString("Select pipelinerun :"); err != nil { + return err + } + if _, err := c.SendLine(string(terminal.KeyArrowDown)); err != nil { return err } - if _, err := c.ExpectString(prName2 + "started 3 minutes ago"); err != nil { + if _, err := c.ExpectString(prName2 + " started 3 minutes ago"); err != nil { return err } @@ -297,7 +495,7 @@ func TestLogs_interactive_ask_runs(t *testing.T) { }, }, } - opts := logOpts(prName, ns, cs) + opts := logOpts(prName, ns, 1, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { opts.RunPromptTest(t, test) @@ -305,7 +503,7 @@ func TestLogs_interactive_ask_runs(t *testing.T) { } } -func logOpts(name string, ns string, cs pipelinetest.Clients) *logOptions { +func logOpts(name string, ns string, prLimit int, cs pipelinetest.Clients) *logOptions { p := test.Params{ Kube: cs.Kube, Tekton: cs.Pipeline, @@ -313,6 +511,7 @@ func logOpts(name string, ns string, cs pipelinetest.Clients) *logOptions { p.SetNamespace(ns) logOp := logOptions{ runName: name, + limit: prLimit, params: &p, } diff --git a/pkg/cmd/pipelinerun/list.go b/pkg/cmd/pipelinerun/list.go index 44ae7a513..676fd56e9 100644 --- a/pkg/cmd/pipelinerun/list.go +++ b/pkg/cmd/pipelinerun/list.go @@ -183,7 +183,9 @@ func (s byStartTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s byStartTime) Less(i, j int) bool { if s[j].Status.StartTime == nil { return false - } else if s[i].Status.StartTime == nil { + } + + if s[i].Status.StartTime == nil { return true } diff --git a/pkg/cmd/taskrun/list.go b/pkg/cmd/taskrun/list.go index e84a7a187..1cb6964a9 100644 --- a/pkg/cmd/taskrun/list.go +++ b/pkg/cmd/taskrun/list.go @@ -184,7 +184,9 @@ func (s byStartTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s byStartTime) Less(i, j int) bool { if s[j].Status.StartTime == nil { return false - } else if s[i].Status.StartTime == nil { + } + + if s[i].Status.StartTime == nil { return true }