diff --git a/docs/cmd/tkn_pipelinerun_logs.md b/docs/cmd/tkn_pipelinerun_logs.md index cce6dd4e3..815ee43f1 100644 --- a/docs/cmd/tkn_pipelinerun_logs.md +++ b/docs/cmd/tkn_pipelinerun_logs.md @@ -32,6 +32,7 @@ Show the logs of PipelineRun -a, --all show all logs including init steps injected by tekton -f, --follow stream live logs -h, --help help for logs + --limit int lists number of pipelineruns (default 5) -t, --only-tasks strings show logs for mentioned tasks only ``` diff --git a/docs/man/man1/tkn-pipelinerun-logs.1 b/docs/man/man1/tkn-pipelinerun-logs.1 index 542a948ba..4083be890 100644 --- a/docs/man/man1/tkn-pipelinerun-logs.1 +++ b/docs/man/man1/tkn-pipelinerun-logs.1 @@ -31,6 +31,10 @@ Show the logs of PipelineRun \fB\-h\fP, \fB\-\-help\fP[=false] help for logs +.PP +\fB\-\-limit\fP=5 + lists number of pipelineruns + .PP \fB\-t\fP, \fB\-\-only\-tasks\fP=[] show logs for mentioned tasks only diff --git a/pkg/cmd/pipeline/logs.go b/pkg/cmd/pipeline/logs.go index a8f9c6974..7bb33641a 100644 --- a/pkg/cmd/pipeline/logs.go +++ b/pkg/cmd/pipeline/logs.go @@ -16,34 +16,20 @@ package pipeline import ( "fmt" - "os" "strings" - "github.com/AlecAivazis/survey/v2" - "github.com/AlecAivazis/survey/v2/terminal" "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" "github.com/tektoncd/cli/pkg/cmd/pipelinerun" "github.com/tektoncd/cli/pkg/flags" - "github.com/tektoncd/cli/pkg/formatted" + "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/pipeline" - prhsort "github.com/tektoncd/cli/pkg/helper/pipelinerun/sort" + phelper "github.com/tektoncd/cli/pkg/helper/pipeline" + prhelper "github.com/tektoncd/cli/pkg/helper/pipelinerun" validate "github.com/tektoncd/cli/pkg/helper/validate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type logOptions struct { - params cli.Params - stream *cli.Stream - askOpts survey.AskOpt - last bool - allSteps bool - follow bool - pipelineName string - runName string - limit int -} - func nameArg(args []string, p cli.Params) error { if len(args) == 1 { c, err := p.Clients() @@ -59,17 +45,7 @@ func nameArg(args []string, p cli.Params) error { } func logCommand(p cli.Params) *cobra.Command { - opts := &logOptions{params: p, - askOpts: func(opt *survey.AskOptions) error { - opt.Stdio = terminal.Stdio{ - In: os.Stdin, - Out: os.Stdout, - Err: os.Stderr, - } - - return nil - }, - } + opts := options.NewLogOptions(p) eg := ` # interactive mode: shows logs of the selected pipeline run @@ -101,7 +77,7 @@ func logCommand(p cli.Params) *cobra.Command { return nameArg(args, p) }, RunE: func(cmd *cobra.Command, args []string) error { - opts.stream = &cli.Stream{ + opts.Stream = &cli.Stream{ Out: cmd.OutOrStdout(), Err: cmd.OutOrStderr(), } @@ -110,56 +86,47 @@ func logCommand(p cli.Params) *cobra.Command { return err } - return opts.run(args) + return run(opts, args) }, } - 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", "", 5, "lists number of pipelineruns") + 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", "", 5, "lists number of pipelineruns") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_pipeline") return c } -func (opts *logOptions) run(args []string) error { - if err := opts.init(args); err != nil { +func run(opts *options.LogOptions, args []string) error { + if err := initOpts(opts, args); err != nil { return err } - if opts.pipelineName == "" || opts.runName == "" { + if opts.PipelineName == "" || opts.PipelineRunName == "" { return nil } - runLogOpts := &pipelinerun.LogOptions{ - PipelineName: opts.pipelineName, - PipelineRunName: opts.runName, - Stream: opts.stream, - Params: opts.params, - Follow: opts.follow, - AllSteps: opts.allSteps, - } - - return runLogOpts.Run() + return pipelinerun.Run(opts) } -func (opts *logOptions) init(args []string) error { +func initOpts(opts *options.LogOptions, args []string) error { // ensure the client is properly initialized - if _, err := opts.params.Clients(); err != nil { + if _, err := opts.Params.Clients(); err != nil { return err } switch len(args) { case 0: // no inputs - return opts.getAllInputs() + return getAllInputs(opts) case 1: // pipeline name provided - opts.pipelineName = args[0] - return opts.askRunName() + opts.PipelineName = args[0] + return askRunName(opts) case 2: // both pipeline and run provided - opts.pipelineName = args[0] - opts.runName = args[1] + opts.PipelineName = args[0] + opts.PipelineRunName = args[1] default: return fmt.Errorf("too many arguments") @@ -167,151 +134,68 @@ func (opts *logOptions) init(args []string) error { return nil } -func (opts *logOptions) getAllInputs() error { - if err := validateLogOpts(opts); err != nil { +func getAllInputs(opts *options.LogOptions) error { + if err := opts.ValidateOpts(); err != nil { return err } - ps, err := allPipelines(opts) + ps, err := phelper.GetAllPipelineNames(opts.Params) if err != nil { return err } if len(ps) == 0 { - fmt.Fprintln(opts.stream.Err, "No pipelines found in namespace:", opts.params.Namespace()) + fmt.Fprintln(opts.Stream.Err, "No pipelines found in namespace:", opts.Params.Namespace()) return nil } - var qs1 = []*survey.Question{{ - Name: "pipeline", - Prompt: &survey.Select{ - Message: "Select pipeline :", - Options: ps, - }, - }} - - if err = survey.Ask(qs1, &opts.pipelineName, opts.askOpts); err != nil { - fmt.Println(err.Error()) - return err + if err := opts.Ask(options.ResourceNamePipeline, ps); err != nil { + return nil } - return opts.askRunName() + return askRunName(opts) } -func (opts *logOptions) askRunName() error { - if err := validateLogOpts(opts); err != nil { +func askRunName(opts *options.LogOptions) error { + if err := opts.ValidateOpts(); err != nil { return err } - if opts.last { - return opts.initLastRunName() + if opts.Last { + return initLastRunName(opts) } - var ans string + lOpts := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("tekton.dev/pipeline=%s", opts.PipelineName), + } - prs, err := allRuns(opts.params, opts.pipelineName, opts.limit) + prs, err := prhelper.GetAllPipelineRuns(opts.Params, lOpts, opts.Limit) if err != nil { return err } + if len(prs) == 0 { - fmt.Fprintln(opts.stream.Err, "No pipelineruns found for pipeline:", opts.pipelineName) + fmt.Fprintln(opts.Stream.Err, "No pipelineruns found for pipeline:", opts.PipelineName) return nil } if len(prs) == 1 { - opts.runName = strings.Fields(prs[0])[0] + opts.PipelineRunName = strings.Fields(prs[0])[0] return nil } - var qs2 = []*survey.Question{ - { - Name: "pipelinerun", - Prompt: &survey.Select{ - Message: "Select pipelinerun :", - Options: prs, - }, - }, - } - - if err = survey.Ask(qs2, &ans, opts.askOpts); err != nil { - fmt.Println(err.Error()) - return err - } - - opts.runName = strings.Fields(ans)[0] - return nil + return opts.Ask(options.ResourceNamePipelineRun, prs) } -func (opts *logOptions) initLastRunName() error { - cs, err := opts.params.Clients() +func initLastRunName(opts *options.LogOptions) error { + cs, err := opts.Params.Clients() if err != nil { return err } - lastrun, err := pipeline.LastRun(cs.Tekton, opts.pipelineName, opts.params.Namespace()) + lastrun, err := pipeline.LastRun(cs.Tekton, opts.PipelineName, opts.Params.Namespace()) if err != nil { return err } - opts.runName = lastrun.Name - return nil -} - -func allPipelines(opts *logOptions) ([]string, error) { - cs, err := opts.params.Clients() - if err != nil { - return nil, err - } - - tkn := cs.Tekton.TektonV1alpha1() - ps, err := tkn.Pipelines(opts.params.Namespace()).List(metav1.ListOptions{}) - if err != nil { - return nil, err - } - - ret := []string{} - for _, item := range ps.Items { - ret = append(ret, item.ObjectMeta.Name) - } - return ret, nil -} - -func allRuns(p cli.Params, pName string, limit int) ([]string, error) { - cs, err := p.Clients() - if err != nil { - return nil, err - } - opts := metav1.ListOptions{ - LabelSelector: fmt.Sprintf("tekton.dev/pipeline=%s", pName), - } - - runs, err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).List(opts) - if err != nil { - return nil, err - } - - runslen := len(runs.Items) - - if runslen > 1 { - runs.Items = prhsort.SortPipelineRunsByStartTime(runs.Items) - } - - if limit > runslen { - limit = runslen - } - - ret := []string{} - for i, run := range runs.Items { - if i < limit { - ret = append(ret, run.ObjectMeta.Name+" started "+formatted.Age(run.Status.StartTime, p.Time())) - } - } - return ret, nil -} - -func validateLogOpts(opts *logOptions) error { - - if opts.limit <= 0 { - return fmt.Errorf("limit was %d but must be a positive number", opts.limit) - } - + opts.PipelineRunName = lastrun.Name return nil } diff --git a/pkg/cmd/pipeline/logs_test.go b/pkg/cmd/pipeline/logs_test.go index 0a55982cf..81290d6e2 100644 --- a/pkg/cmd/pipeline/logs_test.go +++ b/pkg/cmd/pipeline/logs_test.go @@ -24,6 +24,7 @@ import ( "github.com/AlecAivazis/survey/v2/terminal" goexpect "github.com/Netflix/go-expect" "github.com/jonboulle/clockwork" + "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/test" cb "github.com/tektoncd/cli/pkg/test/builder" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" @@ -300,7 +301,7 @@ func TestLogs_interactive_get_all_inputs(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -394,7 +395,7 @@ func TestLogs_interactive_ask_runs(t *testing.T) { opts := logOpts(prName, ns, 5, false, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -496,7 +497,7 @@ func TestLogs_interactive_limit_2(t *testing.T) { opts := logOpts(prName, ns, 2, false, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -590,7 +591,7 @@ func TestLogs_interactive_limit_1(t *testing.T) { opts := logOpts(prName, ns, 1, false, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -685,7 +686,7 @@ func TestLogs_interactive_ask_all_last_run(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -763,7 +764,7 @@ func TestLogs_interactive_ask_run_last_run(t *testing.T) { opts := logOpts(prName, ns, 5, true, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } @@ -888,22 +889,22 @@ func TestLogs_have_one_get_one(t *testing.T) { opts := logOpts(prName, ns, 5, false, cs) for _, test := range tests { t.Run(test.name, func(t *testing.T) { - opts.RunPromptTest(t, test) + RunPromptTest(t, test, opts) }) } } -func logOpts(name string, ns string, prLimit int, last bool, cs pipelinetest.Clients) *logOptions { +func logOpts(name string, ns string, prLimit int, last bool, cs pipelinetest.Clients) *options.LogOptions { p := test.Params{ Kube: cs.Kube, Tekton: cs.Pipeline, } p.SetNamespace(ns) - logOp := logOptions{ - runName: name, - limit: prLimit, - last: last, - params: &p, + logOp := options.LogOptions{ + PipelineRunName: name, + Limit: prLimit, + Last: last, + Params: &p, } return &logOp diff --git a/pkg/cmd/pipeline/logs_testutil.go b/pkg/cmd/pipeline/logs_testutil.go index c7496205f..c8cc73b9a 100644 --- a/pkg/cmd/pipeline/logs_testutil.go +++ b/pkg/cmd/pipeline/logs_testutil.go @@ -24,6 +24,7 @@ import ( "github.com/hinshun/vt10x" "github.com/stretchr/testify/require" "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/helper/options" ) type promptTest struct { @@ -32,12 +33,12 @@ type promptTest struct { procedure func(*goexpect.Console) error } -func (opts *logOptions) RunPromptTest(t *testing.T, test promptTest) { +func RunPromptTest(t *testing.T, test promptTest, opts *options.LogOptions) { test.runTest(t, test.procedure, func(stdio terminal.Stdio) error { var err error - opts.askOpts = WithStdio(stdio) - err = opts.run(test.cmdArgs) + opts.AskOpts = WithStdio(stdio) + err = run(opts, test.cmdArgs) if err != nil { return err } diff --git a/pkg/cmd/pipeline/start.go b/pkg/cmd/pipeline/start.go index fd5ebc4b7..1a1431f2c 100644 --- a/pkg/cmd/pipeline/start.go +++ b/pkg/cmd/pipeline/start.go @@ -28,6 +28,7 @@ import ( "github.com/tektoncd/cli/pkg/cmd/pipelinerun" "github.com/tektoncd/cli/pkg/flags" "github.com/tektoncd/cli/pkg/helper/labels" + "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/params" "github.com/tektoncd/cli/pkg/helper/pipeline" validate "github.com/tektoncd/cli/pkg/helper/validate" @@ -228,7 +229,6 @@ func (opt *startOptions) getInputResources(resources resourceOptionsFilter, pipe } if err := survey.Ask(qs, &ans, opt.askOpts); err != nil { - fmt.Println(err.Error()) return err } @@ -270,7 +270,6 @@ func (opt *startOptions) getInputParams(pipeline *v1alpha1.Pipeline) error { } if err := survey.Ask(qs, &ans, opt.askOpts); err != nil { - fmt.Println(err.Error()) return err } @@ -442,7 +441,7 @@ func (opt *startOptions) startPipeline(pName string) error { } fmt.Fprintf(opt.stream.Out, "Showing logs...\n") - runLogOpts := &pipelinerun.LogOptions{ + runLogOpts := &options.LogOptions{ PipelineName: pName, PipelineRunName: prCreated.Name, Stream: opt.stream, @@ -450,7 +449,7 @@ func (opt *startOptions) startPipeline(pName string) error { Params: opt.cliparams, AllSteps: false, } - return runLogOpts.Run() + return pipelinerun.Run(runLogOpts) } func mergeRes(pr *v1alpha1.PipelineRun, optRes []string) error { diff --git a/pkg/cmd/pipelinerun/log_test.go b/pkg/cmd/pipelinerun/log_test.go index a82c02275..549fb7bc2 100644 --- a/pkg/cmd/pipelinerun/log_test.go +++ b/pkg/cmd/pipelinerun/log_test.go @@ -22,6 +22,7 @@ import ( "github.com/jonboulle/clockwork" "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/helper/options" "github.com/tektoncd/cli/pkg/helper/pods/fake" "github.com/tektoncd/cli/pkg/helper/pods/stream" "github.com/tektoncd/cli/pkg/test" @@ -80,6 +81,44 @@ func TestLog_no_pipelinerun_argument(t *testing.T) { } } +func TestLog_run_found(t *testing.T) { + clock := clockwork.NewFakeClock() + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline("pipeline", "ns", + cb.PipelineCreationTimestamp(clock.Now().Add(-15*time.Minute)), + ), + }, + PipelineRuns: []*v1alpha1.PipelineRun{ + tb.PipelineRun("pipelinerun-1", "ns", + tb.PipelineRunLabel("tekton.dev/pipeline", "pipeline"), + tb.PipelineRunSpec("pipeline"), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + ), + ), + }, + Namespaces: []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + }, + }) + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube} + + c := Command(p) + _, err := test.ExecuteCommand(c, "logs", "-n", "ns") + if err != nil { + t.Errorf("Unexpected error %v", err) + } +} + func TestLog_run_not_found(t *testing.T) { pr := []*v1alpha1.PipelineRun{ tb.PipelineRun("output-pipeline-1", "ns", @@ -838,14 +877,14 @@ func updatePR(finalRuns []*v1alpha1.PipelineRun, watcher *watch.FakeWatcher) { }() } -func logOpts(name string, ns string, cs pipelinetest.Clients, streamer stream.NewStreamerFunc, allSteps bool, follow bool, onlyTasks ...string) *LogOptions { +func logOpts(name string, ns string, cs pipelinetest.Clients, streamer stream.NewStreamerFunc, allSteps bool, follow bool, onlyTasks ...string) *options.LogOptions { p := test.Params{ Kube: cs.Kube, Tekton: cs.Pipeline, } p.SetNamespace(ns) - logOptions := LogOptions{ + logOptions := options.LogOptions{ PipelineRunName: name, Tasks: onlyTasks, AllSteps: allSteps, @@ -857,11 +896,11 @@ func logOpts(name string, ns string, cs pipelinetest.Clients, streamer stream.Ne return &logOptions } -func fetchLogs(lo *LogOptions) (string, error) { +func fetchLogs(lo *options.LogOptions) (string, error) { out := new(bytes.Buffer) lo.Stream = &cli.Stream{Out: out, Err: out} - err := lo.Run() + err := Run(lo) return out.String(), err } diff --git a/pkg/cmd/pipelinerun/logs.go b/pkg/cmd/pipelinerun/logs.go index 847ae3f2f..8da35bdc8 100644 --- a/pkg/cmd/pipelinerun/logs.go +++ b/pkg/cmd/pipelinerun/logs.go @@ -16,27 +16,19 @@ package pipelinerun import ( "fmt" + "strings" "github.com/spf13/cobra" "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/helper/options" + prhelper "github.com/tektoncd/cli/pkg/helper/pipelinerun" "github.com/tektoncd/cli/pkg/helper/pods" - "github.com/tektoncd/cli/pkg/helper/pods/stream" validate "github.com/tektoncd/cli/pkg/helper/validate" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type LogOptions struct { - AllSteps bool - Follow bool - Params cli.Params - PipelineName string - PipelineRunName string - Stream *cli.Stream - Streamer stream.NewStreamerFunc - Tasks []string -} - func logCommand(p cli.Params) *cobra.Command { - opts := &LogOptions{Params: p} + opts := &options.LogOptions{Params: p} eg := ` # show the logs of PipelineRun named "foo" from the namesspace "bar" tkn pipelinerun logs foo -n bar @@ -57,9 +49,10 @@ func logCommand(p cli.Params) *cobra.Command { "commandType": "main", }, Example: eg, - Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.PipelineRunName = args[0] + if len(args) != 0 { + opts.PipelineRunName = args[0] + } opts.Stream = &cli.Stream{ Out: cmd.OutOrStdout(), @@ -70,40 +63,43 @@ func logCommand(p cli.Params) *cobra.Command { return err } - return opts.Run() + return Run(opts) }, } 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().StringSliceVarP(&opts.Tasks, "only-tasks", "t", []string{}, "show logs for mentioned tasks only") + c.Flags().IntVarP(&opts.Limit, "limit", "", 5, "lists number of pipelineruns") _ = c.MarkZshCompPositionalArgumentCustom(1, "__tkn_get_pipelinerun") return c } -func (lo *LogOptions) Run() error { - if lo.PipelineRunName == "" { - return fmt.Errorf("missing mandatory argument pipelinerun") +func Run(opts *options.LogOptions) error { + if opts.PipelineRunName == "" { + if err := askRunName(opts); err != nil { + return err + } } streamer := pods.NewStream - if lo.Streamer != nil { - streamer = lo.Streamer + if opts.Streamer != nil { + streamer = opts.Streamer } - cs, err := lo.Params.Clients() + cs, err := opts.Params.Clients() if err != nil { return err } lr := &LogReader{ - Run: lo.PipelineRunName, - Ns: lo.Params.Namespace(), + Run: opts.PipelineRunName, + Ns: opts.Params.Namespace(), Clients: cs, Streamer: streamer, - Stream: lo.Stream, - Follow: lo.Follow, - AllSteps: lo.AllSteps, - Tasks: lo.Tasks, + Stream: opts.Stream, + Follow: opts.Follow, + AllSteps: opts.AllSteps, + Tasks: opts.Tasks, } logC, errC, err := lr.Read() @@ -111,7 +107,27 @@ func (lo *LogOptions) Run() error { return err } - NewLogWriter().Write(lo.Stream, logC, errC) + NewLogWriter().Write(opts.Stream, logC, errC) return nil } + +func askRunName(opts *options.LogOptions) error { + lOpts := metav1.ListOptions{} + + prs, err := prhelper.GetAllPipelineRuns(opts.Params, lOpts, opts.Limit) + if err != nil { + return err + } + + if len(prs) == 0 { + return fmt.Errorf("No pipelineruns found") + } + + if len(prs) == 1 { + opts.PipelineRunName = strings.Fields(prs[0])[0] + return nil + } + + return opts.Ask("pipelinerun", prs) +} diff --git a/pkg/helper/options/options.go b/pkg/helper/options/delete.go similarity index 100% rename from pkg/helper/options/options.go rename to pkg/helper/options/delete.go diff --git a/pkg/helper/options/options_test.go b/pkg/helper/options/delete_test.go similarity index 100% rename from pkg/helper/options/options_test.go rename to pkg/helper/options/delete_test.go diff --git a/pkg/helper/options/logs.go b/pkg/helper/options/logs.go new file mode 100644 index 000000000..882c5ae52 --- /dev/null +++ b/pkg/helper/options/logs.go @@ -0,0 +1,91 @@ +// 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 options + +import ( + "fmt" + "os" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/helper/pods/stream" +) + +const ( + ResourceNamePipeline = "pipeline" + ResourceNamePipelineRun = "pipelinerun" +) + +type LogOptions struct { + AllSteps bool + Follow bool + Params cli.Params + PipelineName string + PipelineRunName string + Stream *cli.Stream + Streamer stream.NewStreamerFunc + Tasks []string + Last bool + Limit int + AskOpts survey.AskOpt +} + +func NewLogOptions(p cli.Params) *LogOptions { + return &LogOptions{Params: p, + AskOpts: func(opt *survey.AskOptions) error { + opt.Stdio = terminal.Stdio{ + In: os.Stdin, + Out: os.Stdout, + Err: os.Stderr, + } + return nil + }, + } +} + +func (opts *LogOptions) ValidateOpts() error { + if opts.Limit <= 0 { + return fmt.Errorf("limit was %d but must be a positive number", opts.Limit) + } + return nil +} + +func (opts *LogOptions) Ask(resource string, options []string) error { + var ans string + var qs = []*survey.Question{ + { + Name: resource, + Prompt: &survey.Select{ + Message: fmt.Sprintf("Select %s :", resource), + Options: options, + }, + }, + } + + if err := survey.Ask(qs, &ans, opts.AskOpts); err != nil { + return err + } + + switch resource { + case ResourceNamePipeline: + opts.PipelineName = ans + case ResourceNamePipelineRun: + opts.PipelineRunName = strings.Fields(ans)[0] + } + + return nil +} diff --git a/pkg/helper/options/logs_test.go b/pkg/helper/options/logs_test.go new file mode 100644 index 000000000..0ed8f86f9 --- /dev/null +++ b/pkg/helper/options/logs_test.go @@ -0,0 +1,148 @@ +// 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 options + +import ( + "testing" + + "github.com/AlecAivazis/survey/v2/terminal" + goexpect "github.com/Netflix/go-expect" + "github.com/tektoncd/cli/pkg/cli" + htest "github.com/tektoncd/cli/pkg/helper/test" + "github.com/tektoncd/cli/pkg/test" +) + +func TestLogOptions_ValidateOpts(t *testing.T) { + + testParams := []struct { + name string + limit int + wantError bool + want string + }{ + { + name: "valid limit", + limit: 1, + wantError: false, + want: "", + }, + { + name: "invalid limit", + limit: 0, + wantError: true, + want: "limit was 0 but must be a positive number", + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + opts := NewLogOptions(&cli.TektonParams{}) + opts.Limit = tp.limit + + err := opts.ValidateOpts() + if tp.wantError { + if err == nil { + t.Errorf("error expected here") + } + test.AssertOutput(t, tp.want, err.Error()) + } else { + if err != nil { + t.Errorf("unexpected Error") + } + } + }) + } +} + +func TestLogOptions_Ask(t *testing.T) { + + options := []string{ + "pipeline1", + "pipeline2", + "pipeline3", + } + options2 := []string{ + "sample-pipeline-run1 started 1 minutes ago", + "sample-pipeline-run2 started 2 minutes ago", + "sample-pipeline-run3 started 3 minutes ago", + } + + testParams := []struct { + name string + resource string + prompt htest.PromptTest + options []string + want LogOptions + }{ + { + name: "select pipeline name", + resource: ResourceNamePipeline, + prompt: htest.PromptTest{ + CmdArgs: []string{}, + Procedure: func(c *goexpect.Console) error { + if _, err := c.ExpectString("Select pipeline :"); err != nil { + return err + } + if _, err := c.SendLine(options[0]); err != nil { + return err + } + return nil + }, + }, + options: options, + want: LogOptions{ + PipelineName: "pipeline1", + PipelineRunName: "", + }, + }, + { + name: "select pipelinerun name", + resource: ResourceNamePipelineRun, + prompt: htest.PromptTest{ + CmdArgs: []string{}, + Procedure: func(c *goexpect.Console) error { + if _, err := c.ExpectString("Select pipelinerun :"); err != nil { + return err + } + if _, err := c.SendLine(options2[0]); err != nil { + return err + } + return nil + }, + }, + options: options2, + want: LogOptions{ + PipelineName: "", + PipelineRunName: "sample-pipeline-run1", + }, + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + opts := &LogOptions{} + tp.prompt.RunTest(t, tp.prompt.Procedure, func(stdio terminal.Stdio) error { + opts.AskOpts = htest.WithStdio(stdio) + return opts.Ask(tp.resource, tp.options) + }) + if opts.PipelineName != tp.want.PipelineName { + t.Errorf("unexpected PipelineName") + } + if opts.PipelineRunName != tp.want.PipelineRunName { + t.Errorf("unexpected PipelineRunName") + } + }) + } +} diff --git a/pkg/helper/pipeline/pipeline.go b/pkg/helper/pipeline/pipeline.go new file mode 100644 index 000000000..e44cbd737 --- /dev/null +++ b/pkg/helper/pipeline/pipeline.go @@ -0,0 +1,39 @@ +// 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 pipeline + +import ( + "github.com/tektoncd/cli/pkg/cli" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetAllPipelineNames(p cli.Params) ([]string, error) { + cs, err := p.Clients() + if err != nil { + return nil, err + } + + tkn := cs.Tekton.TektonV1alpha1() + ps, err := tkn.Pipelines(p.Namespace()).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + + ret := []string{} + for _, item := range ps.Items { + ret = append(ret, item.ObjectMeta.Name) + } + return ret, nil +} diff --git a/pkg/helper/pipeline/pipeline_test.go b/pkg/helper/pipeline/pipeline_test.go new file mode 100644 index 000000000..a7dbfda89 --- /dev/null +++ b/pkg/helper/pipeline/pipeline_test.go @@ -0,0 +1,90 @@ +// 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 pipeline + +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" + pipelinetest "github.com/tektoncd/pipeline/test" + tb "github.com/tektoncd/pipeline/test/builder" +) + +func TestPipelinesList_with_single_run(t *testing.T) { + clock := clockwork.NewFakeClock() + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline("pipeline", "ns", + // created 5 minutes back + cb.PipelineCreationTimestamp(clock.Now().Add(-5*time.Minute)), + ), + }, + }) + + cs2, _ := test.SeedTestData(t, pipelinetest.Data{ + Pipelines: []*v1alpha1.Pipeline{ + tb.Pipeline("pipeline", "ns", + // created 5 minutes back + cb.PipelineCreationTimestamp(clock.Now().Add(-5*time.Minute)), + ), + tb.Pipeline("pipeline2", "ns", + // created 5 minutes back + cb.PipelineCreationTimestamp(clock.Now().Add(-5*time.Minute)), + ), + }, + }) + + p := &test.Params{Tekton: cs.Pipeline, Clock: clock, Kube: cs.Kube} + p2 := &test.Params{Tekton: cs2.Pipeline, Clock: clock, Kube: cs2.Kube} + p3 := &test.Params{Tekton: cs2.Pipeline, Clock: clock, Kube: cs2.Kube} + p3.SetNamespace("unknown") + + testParams := []struct { + name string + params *test.Params + want []string + }{ + { + name: "Single Pipeline", + params: p, + want: []string{"pipeline"}, + }, + { + name: "Multi Pipelines", + params: p2, + want: []string{"pipeline", "pipeline2"}, + }, + { + name: "Unknown namespace", + params: p3, + want: []string{}, + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + got, err := GetAllPipelineNames(tp.params) + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, got) + }) + } +} diff --git a/pkg/helper/pipelinerun/pipelinerun.go b/pkg/helper/pipelinerun/pipelinerun.go new file mode 100644 index 000000000..ff2eca026 --- /dev/null +++ b/pkg/helper/pipelinerun/pipelinerun.go @@ -0,0 +1,51 @@ +// 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 pipelinerun + +import ( + "github.com/tektoncd/cli/pkg/cli" + "github.com/tektoncd/cli/pkg/formatted" + prhsort "github.com/tektoncd/cli/pkg/helper/pipelinerun/sort" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetAllPipelineRuns(p cli.Params, opts metav1.ListOptions, limit int) ([]string, error) { + cs, err := p.Clients() + if err != nil { + return nil, err + } + + runs, err := cs.Tekton.TektonV1alpha1().PipelineRuns(p.Namespace()).List(opts) + if err != nil { + return nil, err + } + + runslen := len(runs.Items) + if runslen > 1 { + runs.Items = prhsort.SortPipelineRunsByStartTime(runs.Items) + } + + if limit > runslen { + limit = runslen + } + + ret := []string{} + for i, run := range runs.Items { + if i < limit { + ret = append(ret, run.ObjectMeta.Name+" started "+formatted.Age(run.Status.StartTime, p.Time())) + } + } + return ret, nil +} diff --git a/pkg/helper/pipelinerun/pipelinerun_test.go b/pkg/helper/pipelinerun/pipelinerun_test.go new file mode 100644 index 000000000..f683cd7fe --- /dev/null +++ b/pkg/helper/pipelinerun/pipelinerun_test.go @@ -0,0 +1,116 @@ +// 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 pipelinerun + +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/pipelinerun/resources" + pipelinetest "github.com/tektoncd/pipeline/test" + tb "github.com/tektoncd/pipeline/test/builder" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "knative.dev/pkg/apis" +) + +func TestPipelinesList_with_single_run(t *testing.T) { + clock := clockwork.NewFakeClock() + pr1Started := clock.Now().Add(10 * time.Second) + runDuration := 1 * time.Minute + + cs, _ := test.SeedTestData(t, pipelinetest.Data{ + PipelineRuns: []*v1alpha1.PipelineRun{ + tb.PipelineRun("pipeline", "ns", + tb.PipelineRunLabel("tekton.dev/pipeline", "random"), + tb.PipelineRunStatus(), + ), + tb.PipelineRun("pipelinerun1", "ns", + tb.PipelineRunLabel("tekton.dev/pipeline", "pipeline"), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + tb.PipelineRunStartTime(pr1Started), + cb.PipelineRunCompletionTime(pr1Started.Add(runDuration)), + ), + ), + tb.PipelineRun("pipelinerun2", "ns", + tb.PipelineRunLabel("tekton.dev/pipeline", "pipeline"), + tb.PipelineRunStatus( + tb.PipelineRunStatusCondition(apis.Condition{ + Status: corev1.ConditionTrue, + Reason: resources.ReasonSucceeded, + }), + tb.PipelineRunStartTime(pr1Started), + cb.PipelineRunCompletionTime(pr1Started.Add(runDuration)), + ), + ), + }, + }) + + p := &test.Params{Tekton: cs.Pipeline, Clock: clock, Kube: cs.Kube} + p2 := &test.Params{Tekton: cs.Pipeline, Clock: clock, Kube: cs.Kube} + p2.SetNamespace("unknown") + + testParams := []struct { + name string + params *test.Params + listOptions metav1.ListOptions + want []string + }{ + { + name: "Specify related pipeline", + params: p, + listOptions: metav1.ListOptions{LabelSelector: "tekton.dev/pipeline=pipeline"}, + want: []string{ + "pipeline started ---", + "pipelinerun1 started -10 seconds ago", + "pipelinerun2 started -10 seconds ago", + }, + }, + { + name: "Not specify related pipeline", + params: p, + listOptions: metav1.ListOptions{}, + want: []string{ + "pipeline started ---", + "pipelinerun1 started -10 seconds ago", + "pipelinerun2 started -10 seconds ago", + }, + }, + { + name: "Specify unknown namespace", + params: p2, + listOptions: metav1.ListOptions{}, + want: []string{}, + }, + } + + for _, tp := range testParams { + t.Run(tp.name, func(t *testing.T) { + got, err := GetAllPipelineRuns(tp.params, metav1.ListOptions{}, 5) + if err != nil { + t.Errorf("unexpected Error") + } + test.AssertOutput(t, tp.want, got) + }) + } +} diff --git a/pkg/helper/test/util.go b/pkg/helper/test/util.go new file mode 100644 index 000000000..d0260f4e8 --- /dev/null +++ b/pkg/helper/test/util.go @@ -0,0 +1,75 @@ +// 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 test + +import ( + "bytes" + "testing" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + goexpect "github.com/Netflix/go-expect" + "github.com/hinshun/vt10x" + "github.com/stretchr/testify/require" +) + +type PromptTest struct { + CmdArgs []string + Procedure func(*goexpect.Console) error +} + +func (pt *PromptTest) RunTest(t *testing.T, procedure func(*goexpect.Console) error, test func(terminal.Stdio) error) { + // Multiplex output to a buffer as well for the raw bytes. + buf := new(bytes.Buffer) + c, state, err := vt10x.NewVT10XConsole(goexpect.WithStdout(buf)) + require.Nil(t, err) + defer c.Close() + + donec := make(chan struct{}) + go func() { + defer close(donec) + if err := procedure(c); err != nil { + t.Logf("procedure failed: %v", err) + } + }() + + err = test(stdio(c)) + + require.Nil(t, err) + + // Close the slave end of the pty, and read the remaining bytes from the master end. + c.Tty().Close() + <-donec + + t.Logf("Raw output: %q", buf.String()) + + // Dump the terminal's screen. + t.Logf("\n%s", goexpect.StripTrailingEmptyLines(state.String())) +} + +// WithStdio helps to test interactive command +// by setting stdio for the ask function +func WithStdio(stdio terminal.Stdio) survey.AskOpt { + return func(options *survey.AskOptions) error { + options.Stdio.In = stdio.In + options.Stdio.Out = stdio.Out + options.Stdio.Err = stdio.Err + return nil + } +} + +func stdio(c *goexpect.Console) terminal.Stdio { + return terminal.Stdio{In: c.Tty(), Out: c.Tty(), Err: c.Tty()} +}