Skip to content
Merged
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
10 changes: 0 additions & 10 deletions cmd/kosli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ func (o *listOptions) validate(cmd *cobra.Command) error {
return nil
}

func (o *listOptions) validateForListTrails(cmd *cobra.Command) error {
if o.pageNumber <= 0 {
return ErrorBeforePrintingUsage(cmd, "page number must be a positive integer")
}
if o.pageLimit < 0 {
return ErrorBeforePrintingUsage(cmd, "page limit must be a positive integer")
}
return nil
}

func newListCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Expand Down
28 changes: 25 additions & 3 deletions cmd/kosli/listTrails.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

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

const listTrailsShortDesc = `List Trails of an org.`

const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow and artifact fingerprint. The results are paginated and ordered from latest to oldest.`
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.`

const listTrailsExample = `
# get a paginated list of trails for a flow:
Expand Down Expand Up @@ -49,13 +50,30 @@ kosli list trails \
--fingerprint yourArtifactFingerprint \
--api-token yourAPIToken \
--org yourOrgName \
--output json \
--output json

# get a paginated list of trails across all flows tagged with the provided key-value pair:
kosli list trails \
--flow-tag team=backend \
--api-token yourAPIToken \
--org yourOrgName
`

type listTrailsOptions struct {
listOptions
flowName string
fingerprint string
flowTag string
}

func (o *listTrailsOptions) validate(cmd *cobra.Command) error {
if err := o.listOptions.validate(cmd); err != nil {
return err
}
if o.flowTag != "" && !strings.Contains(o.flowTag, "=") {
return ErrorBeforePrintingUsage(cmd, "flag '--flow-tag' must be in the format of key=value")
}
return nil
}

type Trail struct {
Expand Down Expand Up @@ -88,7 +106,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return o.validateForListTrails(cmd)
return o.validate(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
Expand All @@ -97,6 +115,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {

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

return cmd
Expand All @@ -110,6 +129,9 @@ func (o *listTrailsOptions) run(out io.Writer) error {
if o.fingerprint != "" {
url += fmt.Sprintf("&fingerprint=%s", o.fingerprint)
}
if o.flowTag != "" {
url += fmt.Sprintf("&flow_tag=%s", o.flowTag)
}

reqParams := &requests.RequestParams{
Method: http.MethodGet,
Expand Down
25 changes: 24 additions & 1 deletion cmd/kosli/listTrails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type ListTrailsCommandTestSuite struct {
flowName string
trailName string
fingerprint string
flowTagKey string
flowTagValue string
defaultKosliArguments string
acmeOrgKosliArguments string
}
Expand All @@ -27,18 +29,23 @@ func (suite *ListTrailsCommandTestSuite) SetupTest() {
}

suite.flowName = "list-trails"
suite.flowTagKey = "team"
suite.flowTagValue = "backend"
suite.trailName = "trail-name"
suite.fingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"

// First flow (tagged), trail and artifact for the default org
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
BeginTrail(suite.trailName, suite.flowName, "", suite.T())
CreateArtifactOnTrail(suite.flowName, suite.trailName, "artifact", suite.fingerprint, "artifact-name", suite.T())
TagFlow(suite.flowName, suite.flowTagKey, suite.flowTagValue, suite.T())

// Second flow for the acme org
global.Org = "acme-org"
global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c"
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

}

func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
Expand Down Expand Up @@ -91,6 +98,22 @@ func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
cmd: fmt.Sprintf(`list trails --fingerprint %s --output json %s`, suite.fingerprint, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "non-empty"}},
},
{
name: "10 can list trails in a flow with the provided tag",
cmd: fmt.Sprintf(`list trails --flow-tag %s=%s --output json %s`, suite.flowTagKey, suite.flowTagValue, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "non-empty"}},
},
{
name: "11 listing trails with a non-existing flow-tag returns no trails",
cmd: fmt.Sprintf(`list trails --flow-tag non=existing %s`, suite.defaultKosliArguments),
golden: "No trails were found.\n",
},
{
wantError: true,
name: "12 the value of the flow-tag flag must be a key-value pair",
cmd: fmt.Sprintf(`list trails --flow-tag %s --output json %s`, "invalid-tag", suite.defaultKosliArguments),
golden: "Error: flag '--flow-tag' must be in the format of key=value\nUsage: kosli list trails [flags]\n",
},
}

runTestCmd(suite.T(), tests)
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
flowNameFlag = "The Kosli flow name."
flowNameFlagOptional = "[optional] The Kosli flow name."
fingerprintInTrailsFlag = "[optional] The SHA256 fingerprint of the artifact to filter trails by."
flowTagFlag = "[optional] A key=value flow tag to filter trails by."
trailNameFlag = "The Kosli trail name."
trailNameFlagOptional = "[optional] The Kosli trail name."
templateArtifactName = "The name of the artifact in the yml template file."
Expand Down
12 changes: 12 additions & 0 deletions cmd/kosli/testHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -595,3 +595,15 @@ func GetAttestationId(flowName, trailName, attestationName string, t *testing.T)
require.True(t, ok, "attestation_id field not found or not a string")
return id
}

// TagFlow tags a flow with a key-value pair
func TagFlow(flowName, tagKey, tagValue string, t *testing.T) {
t.Helper()
o := &tagOptions{
payload: TagResourcePayload{
SetTags: map[string]string{tagKey: tagValue},
},
}
err := o.run([]string{"flow", flowName})
require.NoError(t, err, "flow should be tagged without error")
}