Skip to content

Commit 2398256

Browse files
authored
Add --flow-tag flag to list trails command
1 parent b3049d9 commit 2398256

5 files changed

Lines changed: 62 additions & 14 deletions

File tree

cmd/kosli/list.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ func (o *listOptions) validate(cmd *cobra.Command) error {
2424
return nil
2525
}
2626

27-
func (o *listOptions) validateForListTrails(cmd *cobra.Command) error {
28-
if o.pageNumber <= 0 {
29-
return ErrorBeforePrintingUsage(cmd, "page number must be a positive integer")
30-
}
31-
if o.pageLimit < 0 {
32-
return ErrorBeforePrintingUsage(cmd, "page limit must be a positive integer")
33-
}
34-
return nil
35-
}
36-
3727
func newListCmd(out io.Writer) *cobra.Command {
3828
cmd := &cobra.Command{
3929
Use: "list",

cmd/kosli/listTrails.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"strings"
89

910
"github.com/kosli-dev/cli/internal/output"
1011
"github.com/kosli-dev/cli/internal/requests"
@@ -13,7 +14,7 @@ import (
1314

1415
const listTrailsShortDesc = `List Trails of an org.`
1516

16-
const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow and artifact fingerprint. The results are paginated and ordered from latest to oldest.`
17+
const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow, flow tag and artifact fingerprint. The results are paginated and ordered from latest to oldest.`
1718

1819
const listTrailsExample = `
1920
# get a paginated list of trails for a flow:
@@ -49,13 +50,30 @@ kosli list trails \
4950
--fingerprint yourArtifactFingerprint \
5051
--api-token yourAPIToken \
5152
--org yourOrgName \
52-
--output json \
53+
--output json
54+
55+
# get a paginated list of trails across all flows tagged with the provided key-value pair:
56+
kosli list trails \
57+
--flow-tag team=backend \
58+
--api-token yourAPIToken \
59+
--org yourOrgName
5360
`
5461

5562
type listTrailsOptions struct {
5663
listOptions
5764
flowName string
5865
fingerprint string
66+
flowTag string
67+
}
68+
69+
func (o *listTrailsOptions) validate(cmd *cobra.Command) error {
70+
if err := o.listOptions.validate(cmd); err != nil {
71+
return err
72+
}
73+
if o.flowTag != "" && !strings.Contains(o.flowTag, "=") {
74+
return ErrorBeforePrintingUsage(cmd, "flag '--flow-tag' must be in the format of key=value")
75+
}
76+
return nil
5977
}
6078

6179
type Trail struct {
@@ -88,7 +106,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {
88106
if err != nil {
89107
return ErrorBeforePrintingUsage(cmd, err.Error())
90108
}
91-
return o.validateForListTrails(cmd)
109+
return o.validate(cmd)
92110
},
93111
RunE: func(cmd *cobra.Command, args []string) error {
94112
return o.run(out)
@@ -97,6 +115,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {
97115

98116
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlagOptional)
99117
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", fingerprintInTrailsFlag)
118+
cmd.Flags().StringVarP(&o.flowTag, "flow-tag", "t", "", flowTagFlag)
100119
addListFlags(cmd, &o.listOptions, 20)
101120

102121
return cmd
@@ -110,6 +129,9 @@ func (o *listTrailsOptions) run(out io.Writer) error {
110129
if o.fingerprint != "" {
111130
url += fmt.Sprintf("&fingerprint=%s", o.fingerprint)
112131
}
132+
if o.flowTag != "" {
133+
url += fmt.Sprintf("&flow_tag=%s", o.flowTag)
134+
}
113135

114136
reqParams := &requests.RequestParams{
115137
Method: http.MethodGet,

cmd/kosli/listTrails_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type ListTrailsCommandTestSuite struct {
1515
flowName string
1616
trailName string
1717
fingerprint string
18+
flowTagKey string
19+
flowTagValue string
1820
defaultKosliArguments string
1921
acmeOrgKosliArguments string
2022
}
@@ -27,18 +29,23 @@ func (suite *ListTrailsCommandTestSuite) SetupTest() {
2729
}
2830

2931
suite.flowName = "list-trails"
32+
suite.flowTagKey = "team"
33+
suite.flowTagValue = "backend"
3034
suite.trailName = "trail-name"
3135
suite.fingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"
36+
37+
// First flow (tagged), trail and artifact for the default org
3238
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
3339
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
3440
BeginTrail(suite.trailName, suite.flowName, "", suite.T())
3541
CreateArtifactOnTrail(suite.flowName, suite.trailName, "artifact", suite.fingerprint, "artifact-name", suite.T())
42+
TagFlow(suite.flowName, suite.flowTagKey, suite.flowTagValue, suite.T())
3643

44+
// Second flow for the acme org
3745
global.Org = "acme-org"
3846
global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c"
3947
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
4048
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
41-
4249
}
4350

4451
func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
@@ -91,6 +98,22 @@ func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
9198
cmd: fmt.Sprintf(`list trails --fingerprint %s --output json %s`, suite.fingerprint, suite.defaultKosliArguments),
9299
goldenJson: []jsonCheck{{"data", "non-empty"}},
93100
},
101+
{
102+
name: "10 can list trails in a flow with the provided tag",
103+
cmd: fmt.Sprintf(`list trails --flow-tag %s=%s --output json %s`, suite.flowTagKey, suite.flowTagValue, suite.defaultKosliArguments),
104+
goldenJson: []jsonCheck{{"data", "non-empty"}},
105+
},
106+
{
107+
name: "11 listing trails with a non-existing flow-tag returns no trails",
108+
cmd: fmt.Sprintf(`list trails --flow-tag non=existing %s`, suite.defaultKosliArguments),
109+
golden: "No trails were found.\n",
110+
},
111+
{
112+
wantError: true,
113+
name: "12 the value of the flow-tag flag must be a key-value pair",
114+
cmd: fmt.Sprintf(`list trails --flow-tag %s --output json %s`, "invalid-tag", suite.defaultKosliArguments),
115+
golden: "Error: flag '--flow-tag' must be in the format of key=value\nUsage: kosli list trails [flags]\n",
116+
},
94117
}
95118

96119
runTestCmd(suite.T(), tests)

cmd/kosli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
9393
flowNameFlag = "The Kosli flow name."
9494
flowNameFlagOptional = "[optional] The Kosli flow name."
9595
fingerprintInTrailsFlag = "[optional] The SHA256 fingerprint of the artifact to filter trails by."
96+
flowTagFlag = "[optional] A key=value flow tag to filter trails by."
9697
trailNameFlag = "The Kosli trail name."
9798
trailNameFlagOptional = "[optional] The Kosli trail name."
9899
templateArtifactName = "The name of the artifact in the yml template file."

cmd/kosli/testHelpers.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,3 +595,15 @@ func GetAttestationId(flowName, trailName, attestationName string, t *testing.T)
595595
require.True(t, ok, "attestation_id field not found or not a string")
596596
return id
597597
}
598+
599+
// TagFlow tags a flow with a key-value pair
600+
func TagFlow(flowName, tagKey, tagValue string, t *testing.T) {
601+
t.Helper()
602+
o := &tagOptions{
603+
payload: TagResourcePayload{
604+
SetTags: map[string]string{tagKey: tagValue},
605+
},
606+
}
607+
err := o.run([]string{"flow", flowName})
608+
require.NoError(t, err, "flow should be tagged without error")
609+
}

0 commit comments

Comments
 (0)