Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: tkn-results add pipelinerun list and result describe command #908

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/api/server/v1alpha2/plugin/plugin_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ func (s *LogServer) setLogPlugin() bool {
s.IsLogPluginEnabled = true
s.getLog = getBlobLogs
default:
// TODO(xinnjie) when s.config.LOGS_TYPE is File also show this error log
s.IsLogPluginEnabled = false
s.logger.Errorf("unsupported type of logs given for plugin")
}
Expand Down
127 changes: 127 additions & 0 deletions pkg/cli/cmd/pipelinerun/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package pipelinerun

import (
"context"
"encoding/json"
"fmt"
"io"
"text/tabwriter"
"text/template"

"github.com/jonboulle/clockwork"
"github.com/spf13/cobra"
"github.com/tektoncd/cli/pkg/formatted"
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/results/pkg/cli/flags"
pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto"
)

const listTemplate = `{{- $size := len .PipelineRuns -}}{{- if eq $size 0 -}}
No PipelineRuns found
{{ else -}}
NAMESPACE UID STARTED DURATION STATUS
{{- range $_, $pr := .PipelineRuns }}
{{ $pr.ObjectMeta.Namespace }} {{ $pr.ObjectMeta.Name }} {{ formatAge $pr.Status.StartTime $.Time }} {{ formatDuration $pr.Status.StartTime $pr.Status.CompletionTime }} {{ formatCondition $pr.Status.Conditions }}
{{- end -}}
{{- end -}}`

type listOptions struct {
Namespace string
Limit int
}

// listCommand initializes a cobra command to list PipelineRuns
func listCommand(params *flags.Params) *cobra.Command {
opts := &listOptions{Limit: 0, Namespace: "default"}

eg := `List all PipelineRuns in a namespace 'foo':
tkn-results pipelinerun list -n foo

List all PipelineRuns in 'default' namespace:
tkn-results pipelinerun list -n default
`
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List PipelineRuns in a namespace",
Annotations: map[string]string{
"commandType": "main",
},
Example: eg,
RunE: func(cmd *cobra.Command, _ []string) error {
if opts.Limit < 0 {
return fmt.Errorf("limit was %d, but must be greater than 0", opts.Limit)
}

resp, err := params.ResultsClient.ListRecords(cmd.Context(), &pb.ListRecordsRequest{
Parent: fmt.Sprintf("%s/results/-", opts.Namespace),
PageSize: int32(opts.Limit),
Filter: `data_type==PIPELINE_RUN`,
})
if err != nil {
return fmt.Errorf("failed to list PipelineRuns from namespace %s: %v", opts.Namespace, err)
}
return printFormatted(cmd.OutOrStdout(), resp.Records, params.Clock)
},
}
cmd.Flags().StringVarP(&opts.Namespace, "namespace", "n", "default", "Namespace to list PipelineRuns in")
cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 0, "Limit the number of PipelineRuns to return")
return cmd
}

func pipelineRunFromRecord(record *pb.Record) (*pipelinev1.PipelineRun, error) {
if record.Data == nil {
return nil, fmt.Errorf("record data is nil")
}
pr := &pipelinev1.PipelineRun{}
switch record.Data.GetType() {
case "tekton.dev/v1beta1.PipelineRun":
//nolint:staticcheck
prV1beta1 := &pipelinev1beta1.PipelineRun{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add //nolint:staticcheck adjacent to this line. It will fix the build test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed and rebased.

if err := json.Unmarshal(record.Data.Value, prV1beta1); err != nil {
return nil, fmt.Errorf("failed to unmarshal PipelineRun data: %v", err)
}
if err := pr.ConvertFrom(context.TODO(), prV1beta1); err != nil {
return nil, fmt.Errorf("failed to convert v1beta1 PipelineRun to v1: %v", err)
}
case "tekton.dev/v1.PipelineRun":
if err := json.Unmarshal(record.Data.Value, pr); err != nil {
return nil, fmt.Errorf("failed to unmarshal PipelineRun data: %v", err)
}
default:
return nil, fmt.Errorf("unsupported PipelineRun type: %s", record.Data.GetType())
}
return pr, nil
}

func printFormatted(out io.Writer, records []*pb.Record, c clockwork.Clock) error {
var data = struct {
PipelineRuns []*pipelinev1.PipelineRun
Time clockwork.Clock
}{
PipelineRuns: []*pipelinev1.PipelineRun{},
Time: c,
}

for _, record := range records {
if pr, err := pipelineRunFromRecord(record); err == nil {
data.PipelineRuns = append(data.PipelineRuns, pr)
}
}

funcMap := template.FuncMap{
"formatAge": formatted.Age,
"formatDuration": formatted.Duration,
"formatCondition": formatted.Condition,
}

w := tabwriter.NewWriter(out, 0, 5, 3, ' ', tabwriter.TabIndent)
t := template.Must(template.New("List TaskRuns").Funcs(funcMap).Parse(listTemplate))

err := t.Execute(w, data)
if err != nil {
return err
}
return w.Flush()
}
146 changes: 146 additions & 0 deletions pkg/cli/cmd/pipelinerun/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package pipelinerun

import (
"encoding/json"
"testing"
"time"

"github.com/tektoncd/results/pkg/test"

"github.com/tektoncd/results/pkg/cli/flags"
"github.com/tektoncd/results/pkg/test/fake"

"github.com/jonboulle/clockwork"

"github.com/spf13/cobra"
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
pb "github.com/tektoncd/results/proto/v1alpha2/results_go_proto"
"google.golang.org/protobuf/types/known/timestamppb"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
)

func TestListPipelineRuns_empty(t *testing.T) {
records := []*pb.Record{}
now := time.Now()
cmd := command(records, now)

output, err := test.ExecuteCommand(cmd, "list")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
test.AssertOutput(t, "No PipelineRuns found\n", output)
}

func TestListPipelineRuns(t *testing.T) {
clock := clockwork.NewFakeClock()
createTime := clock.Now().Add(time.Duration(-3) * time.Minute)
updateTime := clock.Now().Add(time.Duration(-2) * time.Minute)
startTime := clock.Now().Add(time.Duration(-3) * time.Minute)
endTime := clock.Now().Add(time.Duration(-1) * time.Minute)
records := testDataSuccessfulPipelineRun(t, createTime, updateTime, startTime, endTime)
cmd := command(records, clock.Now())
output, err := test.ExecuteCommand(cmd, "list")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
test.AssertOutput(t, `NAMESPACE UID STARTED DURATION STATUS
default hello-goodbye-run-xgkf8 3 minutes ago 2m0s Succeeded`, output)
}

func command(records []*pb.Record, now time.Time) *cobra.Command {
clock := clockwork.NewFakeClockAt(now)

param := &flags.Params{
ResultsClient: fake.NewResultsClient(nil, records),
LogsClient: nil,
PluginLogsClient: nil,
Clock: clock,
}
cmd := Command(param)
return cmd
}

func testDataSuccessfulPipelineRun(t *testing.T, createTime, updateTime, startTime, endTime time.Time) []*pb.Record {
pipelineRun := &pipelinev1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
Name: "hello-goodbye-run-xgkf8",
Namespace: "default",
UID: "d2c19786-5fb7-4577-84a4-10d43c157c5c",
ResourceVersion: "4320960",
Generation: 1,
Labels: map[string]string{
"tekton.dev/pipeline": "hello-goodbye",
},
Annotations: map[string]string{
"results.tekton.dev/log": "default/results/d2c19786-5fb7-4577-84a4-10d43c157c5c/logs/d05a16ce-f7a4-3370-8c3a-88c30067680a",
"results.tekton.dev/record": "default/results/d2c19786-5fb7-4577-84a4-10d43c157c5c/records/d2c19786-5fb7-4577-84a4-10d43c157c5c",
"results.tekton.dev/result": "default/results/d2c19786-5fb7-4577-84a4-10d43c157c5c",
},
Finalizers: []string{"results.tekton.dev/pipelinerun"},
},
Spec: pipelinev1.PipelineRunSpec{
PipelineRef: &pipelinev1.PipelineRef{
Name: "hello-goodbye",
},
Params: []pipelinev1.Param{
{
Name: "username",
Value: pipelinev1.ParamValue{
Type: pipelinev1.ParamTypeString,
StringVal: "Tekton",
},
},
},
Timeouts: &pipelinev1.TimeoutFields{
Pipeline: &metav1.Duration{Duration: time.Hour},
},
},
Status: pipelinev1.PipelineRunStatus{
PipelineRunStatusFields: pipelinev1.PipelineRunStatusFields{
StartTime: &metav1.Time{Time: startTime},
CompletionTime: &metav1.Time{Time: endTime},
ChildReferences: []pipelinev1.ChildStatusReference{
{
Name: "hello-goodbye-run-xgkf8-hello",
PipelineTaskName: "hello",
},
{
Name: "hello-goodbye-run-xgkf8-goodbye",
PipelineTaskName: "goodbye",
},
},
},
Status: duckv1.Status{
Conditions: []apis.Condition{
{
Type: apis.ConditionSucceeded,
Status: corev1.ConditionTrue,
Reason: "Succeeded",
Message: "Tasks Completed: 2 (Failed: 0, Cancelled 0), Skipped: 0",
LastTransitionTime: apis.VolatileTime{Inner: metav1.Time{Time: updateTime}},
},
},
},
},
}
prBytes, err := json.Marshal(pipelineRun)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
records := []*pb.Record{
{
Name: "default/results/e6b4b2e3-d876-4bbe-a927-95c691b6fdc7/records/e6b4b2e3-d876-4bbe-a927-95c691b6fdc7",
Uid: "095a449f-691a-4be7-9bcb-3a52bba3bc6d",
Data: &pb.Any{
Type: "tekton.dev/v1.PipelineRun",
Value: prBytes,
},
CreateTime: timestamppb.New(createTime),
UpdateTime: timestamppb.New(updateTime),
},
}
return records
}
22 changes: 22 additions & 0 deletions pkg/cli/cmd/pipelinerun/pipelinerun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package pipelinerun

import (
"github.com/spf13/cobra"
"github.com/tektoncd/results/pkg/cli/flags"
)

// Command initializes a cobra command for `pipelinerun` sub commands
func Command(params *flags.Params) *cobra.Command {
cmd := &cobra.Command{
Use: "pipelinerun",
Aliases: []string{"pr", "pipelineruns"},
Short: "Query PipelineRuns",
Annotations: map[string]string{
"commandType": "main",
},
}

cmd.AddCommand(listCommand(params))

return cmd
}
Loading