Skip to content
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
3 changes: 3 additions & 0 deletions docs/cli/tkn-results_pipelinerun_describe.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Describe a PipelineRun

Describe a PipelineRun by name or UID. If --uid is provided, then PipelineRun name is optional.

If multiple PipelineRuns match the given name, the most recent one is returned.
Use --uid to target a specific PipelineRun when needed.

```
tkn-results pipelinerun describe [pipelinerun-name]
```
Expand Down
3 changes: 3 additions & 0 deletions docs/cli/tkn-results_pipelinerun_logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Get logs for a PipelineRun

Get logs for a PipelineRun by name or UID. If --uid is provided, the PipelineRun name is optional.

If multiple PipelineRuns match the given name, the logs for the most recent one are returned.
Use --uid to target a specific PipelineRun when needed.

NOTE:
Logs are not supported for the system namespace or for the default namespace used by LokiStack.
Additionally, PipelineRun logs are not supported for S3 log storage.
Expand Down
3 changes: 3 additions & 0 deletions docs/cli/tkn-results_taskrun_describe.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Describe a TaskRun

Describe a TaskRun by name or UID. If --uid is provided, then TaskRun name is optional.

If multiple TaskRuns match the given name, the most recent one is returned.
Use --uid to target a specific TaskRun when needed.

```
tkn-results taskrun describe [taskrun-name]
```
Expand Down
3 changes: 3 additions & 0 deletions docs/cli/tkn-results_taskrun_logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Get logs for a TaskRun

Get logs for a TaskRun by name or UID. If --uid is provided, the TaskRun name is optional.

If multiple TaskRuns match the given name, the logs for the most recent one are returned.
Use --uid to target a specific TaskRun when needed.

NOTE:
Logs are not supported for the system namespace or for the default namespace used by LokiStack.
Logs are only available for completed TaskRuns. Running TaskRuns do not have logs available yet.
Expand Down
17 changes: 17 additions & 0 deletions pkg/cli/client/records/records.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

// RecordClient defines the interface for record-related operations
type RecordClient interface {
GetRecord(ctx context.Context, namespace, uid string) (*pb.Record, error)
ListRecords(ctx context.Context, in *pb.ListRecordsRequest, fields string) (*pb.ListRecordsResponse, error)
}

Expand All @@ -26,6 +27,22 @@ func NewClient(rc *client.RESTClient) RecordClient {
return &recordClient{RESTClient: rc}
}

// GetRecord fetches a single record by namespace and UID using direct primary key lookup.
// The record path follows the format: {namespace}/results/{uid}/records/{uid}
func (c *recordClient) GetRecord(ctx context.Context, namespace, uid string) (*pb.Record, error) {
out := &pb.Record{}
recordName := fmt.Sprintf("%s/results/%s/records/%s", namespace, uid, uid)
buildURL := c.BuildURL(fmt.Sprintf("parents/%s", recordName), nil)
resp, err := c.DoRequest(ctx, http.MethodGet, buildURL, nil)
if err != nil {
return nil, err
}
if err := resp.ProtoUnmarshal(out); err != nil {
return nil, err
}
return out, nil
}

// ListRecords makes request to get record list
func (c *recordClient) ListRecords(ctx context.Context, in *pb.ListRecordsRequest, fields string) (*pb.ListRecordsResponse, error) {
out := &pb.ListRecordsResponse{}
Expand Down
83 changes: 47 additions & 36 deletions pkg/cli/cmd/pipelinerun/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package pipelinerun
import (
"encoding/json"
"fmt"
"strings"
"text/tabwriter"
"text/template"

Expand Down Expand Up @@ -131,7 +130,10 @@ Describe a PipelineRun as json:
Use: "describe [pipelinerun-name]",
Aliases: []string{"desc"},
Short: "Describe a PipelineRun",
Long: "Describe a PipelineRun by name or UID. If --uid is provided, then PipelineRun name is optional.",
Long: `Describe a PipelineRun by name or UID. If --uid is provided, then PipelineRun name is optional.

If multiple PipelineRuns match the given name, the most recent one is returned.
Use --uid to target a specific PipelineRun when needed.`,
Annotations: map[string]string{
"commandType": "main",
},
Expand All @@ -157,48 +159,57 @@ Describe a PipelineRun as json:
},
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()

// Build filter string to find the PipelineRun
filter := common.BuildFilterString(opts)

// Handle namespace
parent := fmt.Sprintf("%s/results/-", p.Namespace())

// Create record client
recordClient := records.NewClient(opts.Client)

// Find the PipelineRun record
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
PageSize: 25,
}, "")
var record *pb.Record

if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
if opts.UID != "" && opts.ResourceName != "" {
return fmt.Errorf("no PipelineRun found with name %s and UID %s", opts.ResourceName, opts.UID)
} else if opts.UID != "" {
return fmt.Errorf("no PipelineRun found with UID %s", opts.UID)
if opts.UID != "" {
// Direct primary key lookup by UID
r, err := recordClient.GetRecord(ctx, p.Namespace(), opts.UID)
if err == nil {
record = r
} else {
// Fallback: filter by record name column (text, indexed)
// instead of data.metadata.uid (JSONB, unindexed).
filter := fmt.Sprintf(`name=="%s"`, opts.UID)
parent := fmt.Sprintf("%s/results/-", p.Namespace())
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
OrderBy: "create_time desc",
PageSize: 5,
}, "")
if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
if opts.ResourceName != "" {
return fmt.Errorf("no PipelineRun found with name %s and UID %s", opts.ResourceName, opts.UID)
}
return fmt.Errorf("no PipelineRun found with UID %s", opts.UID)
}
record = resp.Records[0]
}
return fmt.Errorf("no PipelineRun found with name %s", opts.ResourceName)
}

// If multiple PipelineRuns are found, return an error
if len(resp.Records) > 1 {
var uids []string
for _, record := range resp.Records {
uids = append(uids, record.Uid)
} else {
filter := common.BuildFilterString(opts)
parent := fmt.Sprintf("%s/results/-", p.Namespace())
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
OrderBy: "create_time desc",
PageSize: 5,
}, "")
if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
return fmt.Errorf("no PipelineRun found with name %s", opts.ResourceName)
}
return fmt.Errorf("multiple PipelineRuns found. Use a more specific name or UID. Available UIDs are: %s",
strings.Join(uids, ", "))
record = resp.Records[0]
}

// Parse record to PipelineRun
var pr v1.PipelineRun
if err := json.Unmarshal(resp.Records[0].Data.Value, &pr); err != nil {
if err := json.Unmarshal(record.Data.Value, &pr); err != nil {
return fmt.Errorf("failed to unmarshal PipelineRun data: %v", err)
}

Expand Down
16 changes: 8 additions & 8 deletions pkg/cli/cmd/pipelinerun/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,22 @@ func TestDescribeCommand(t *testing.T) {
wantOutput: "no PipelineRun found with name notfound",
},
{
name: "multiple found",
name: "multiple found selects most recent",
args: []string{"foo"},
mockListFunc: func(_ context.Context, _ *pb.ListRecordsRequest, _ string) (*pb.ListRecordsResponse, error) {
pr := v1.PipelineRun{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}
pr := v1.PipelineRun{
TypeMeta: metav1.TypeMeta{APIVersion: "tekton.dev/v1", Kind: "PipelineRun"},
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
}
prBytes, _ := json.Marshal(pr)
return &pb.ListRecordsResponse{
Records: []*pb.Record{
{Uid: "a", Data: &pb.Any{Value: prBytes}},
{Uid: "b", Data: &pb.Any{Value: prBytes}},
},
}, nil
},
wantErr: true,
wantOutput: "multiple PipelineRuns found",
wantErr: false,
wantOutput: "Name: foo",
},
{
name: "error from client",
Expand Down Expand Up @@ -197,9 +199,7 @@ func TestDescribeCommand(t *testing.T) {
if len(resp.Records) == 0 {
return fmt.Errorf("no PipelineRun found with name %s", args[0])
}
if len(resp.Records) > 1 {
return fmt.Errorf("multiple PipelineRuns found")
}
// selectLast: use the first record (most recent when ordered by create_time desc)
var pr v1.PipelineRun
if err := json.Unmarshal(resp.Records[0].Data.Value, &pr); err != nil {
return fmt.Errorf("failed to unmarshal PipelineRun data: %v", err)
Expand Down
82 changes: 46 additions & 36 deletions pkg/cli/cmd/pipelinerun/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"io"
"os"
"strings"

v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/results/pkg/cli/options"
Expand Down Expand Up @@ -38,6 +37,9 @@ Get logs for a PipelineRun by UID if there are multiple PipelineRuns with the sa
Short: "Get logs for a PipelineRun",
Long: `Get logs for a PipelineRun by name or UID. If --uid is provided, the PipelineRun name is optional.

If multiple PipelineRuns match the given name, the logs for the most recent one are returned.
Use --uid to target a specific PipelineRun when needed.

NOTE:
Logs are not supported for the system namespace or for the default namespace used by LokiStack.
Additionally, PipelineRun logs are not supported for S3 log storage.
Expand Down Expand Up @@ -67,47 +69,55 @@ Logs are only available for completed PipelineRuns. Running PipelineRuns do not
},
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()

// Build filter string to find the PipelineRun
filter := common.BuildFilterString(opts)

// Handle namespace
parent := fmt.Sprintf("%s/results/-", p.Namespace())

// Create record client
recordClient := records.NewClient(opts.Client)

// Find the PipelineRun record
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
PageSize: 25,
}, common.NameUIDAndDataField)
if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
if opts.UID != "" && opts.ResourceName != "" {
return fmt.Errorf("no PipelineRun found with name %s and UID %s", opts.ResourceName, opts.UID)
} else if opts.UID != "" {
return fmt.Errorf("no PipelineRun found with UID %s", opts.UID)
}
return fmt.Errorf("no PipelineRun found with name %s", opts.ResourceName)
}
var record *pb.Record

// If multiple PipelineRuns are found, return an error
if len(resp.Records) > 1 {
var uids []string
for _, record := range resp.Records {
uids = append(uids, record.Uid)
if opts.UID != "" {
// Direct primary key lookup by UID
r, err := recordClient.GetRecord(ctx, p.Namespace(), opts.UID)
if err == nil {
record = r
} else {
// Fallback: filter by record name column (text, indexed)
// instead of data.metadata.uid (JSONB, unindexed).
filter := fmt.Sprintf(`name.endsWith("records/%s")`, opts.UID)
parent := fmt.Sprintf("%s/results/-", p.Namespace())
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
OrderBy: "create_time desc",
PageSize: 5,
}, common.NameUIDAndDataField)
if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
if opts.ResourceName != "" {
return fmt.Errorf("no PipelineRun found with name %s and UID %s", opts.ResourceName, opts.UID)
}
return fmt.Errorf("no PipelineRun found with UID %s", opts.UID)
}
record = resp.Records[0]
}
return fmt.Errorf("multiple PipelineRuns found. Use a more specific name or UID. Available UIDs are: %s",
strings.Join(uids, ", "))
} else {
filter := common.BuildFilterString(opts)
parent := fmt.Sprintf("%s/results/-", p.Namespace())
resp, err := recordClient.ListRecords(ctx, &pb.ListRecordsRequest{
Parent: parent,
Filter: filter,
OrderBy: "create_time desc",
PageSize: 5,
}, common.NameUIDAndDataField)
if err != nil {
return fmt.Errorf("failed to find PipelineRun: %v", err)
}
if len(resp.Records) == 0 {
return fmt.Errorf("no PipelineRun found with name %s", opts.ResourceName)
}
record = resp.Records[0]
}

// Get the PipelineRun record
record := resp.Records[0]

// Check if the PipelineRun is completed before attempting to get logs
var pipelineRun v1.PipelineRun
if err := json.Unmarshal(record.Data.Value, &pipelineRun); err != nil {
Expand Down
Loading
Loading