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
66 changes: 31 additions & 35 deletions cmd/kosli/listTrails.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,46 +11,51 @@ import (
"github.com/spf13/cobra"
)

const listTrailsShortDesc = `List Trails for a Flow in an org.`
const listTrailsShortDesc = `List Trails of an org.`

const listTrailsLongDesc = listTrailsShortDesc + `The results are ordered from latest to oldest.
If the ^page-limit^ flag is provided, the results will be paginated, otherwise all results will be
returned.
If ^page-limit^ is set to 0, all results will be returned.`
const listTrailsLongDesc = listTrailsShortDesc + `The list can be filtered by flow and artifact fingerprint. The results are paginated and ordered from latest to oldest.`

const listTrailsExample = `
# list all trails for a flow:
# get a paginated list of trails for a flow:
kosli list trails \
--flow yourFlowName \
--api-token yourAPIToken \
--org yourOrgName

#list the most recent 30 trails for a flow:
# list the most recent 30 trails for a flow:
kosli list trails \
--flow yourFlowName \
--page-limit 30 \
--api-token yourAPIToken \
--org yourOrgName

#show the second page of trails for a flow:
# show the second page of trails for a flow:
kosli list trails \
--flow yourFlowName \
--page-limit 30 \
--page 2 \
--api-token yourAPIToken \
--org yourOrgName

# list all trails for a flow (in JSON):
# get a paginated list of trails for a flow (in JSON):
kosli list trails \
--flow yourFlowName \
--api-token yourAPIToken \
--org yourOrgName \
--output json

# get a paginated list of trails across all flows that contain an artifact with the provided fingerprint (in JSON):
kosli list trails \
--fingerprint yourArtifactFingerprint \
--api-token yourAPIToken \
--org yourOrgName \
--output json \
`

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

type Trail struct {
Expand Down Expand Up @@ -90,20 +95,21 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {
},
}

cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
// We set the defauly page limit to 0 so that all results are returned if the flag is not provided
addListFlags(cmd, &o.listOptions, 0)

err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlagOptional)
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", fingerprintInTrailsFlag)
addListFlags(cmd, &o.listOptions, 20)

return cmd
}

func (o *listTrailsOptions) run(out io.Writer) error {
url := fmt.Sprintf("%s/api/v2/trails/%s/%s?per_page=%d&page=%d", global.Host, global.Org, o.flowName, o.pageLimit, o.pageNumber)
url := fmt.Sprintf("%s/api/v2/trails/%s?per_page=%d&page=%d", global.Host, global.Org, o.pageLimit, o.pageNumber)
if o.flowName != "" {
url += fmt.Sprintf("&flow=%s", o.flowName)
}
if o.fingerprint != "" {
url += fmt.Sprintf("&fingerprint=%s", o.fingerprint)
}

reqParams := &requests.RequestParams{
Method: http.MethodGet,
Expand All @@ -124,19 +130,11 @@ func (o *listTrailsOptions) run(out io.Writer) error {

func printTrailsListAsTable(raw string, out io.Writer, page int) error {
response := &listTrailsResponse{}
trails := []Trail{}

// If using pagination, the response will have the format {data: [], pagination: {}}
// and therefore will not unmarshal into an array of Trail structs; instead, we need
// to unmarshal into a listTrailsResponse struct and extract the data field.
err := json.Unmarshal([]byte(raw), &trails)
err := json.Unmarshal([]byte(raw), response)
if err != nil {
err = json.Unmarshal([]byte(raw), &response)
if err != nil {
return err
}
trails = response.Data
return err
}
trails := response.Data

if len(trails) == 0 {
msg := "No trails were found"
Expand All @@ -153,11 +151,9 @@ func printTrailsListAsTable(raw string, out io.Writer, page int) error {
row := fmt.Sprintf("%s\t%s\t%s", trail.Name, trail.Description, trail.ComplianceState)
rows = append(rows, row)
}
if len(response.Data) > 0 {
pagination := response.Pagination
paginationInfo := fmt.Sprintf("\nShowing page %.0f of %.0f, total %.0f items", pagination.Page, pagination.PageCount, pagination.Total)
rows = append(rows, paginationInfo)
}
pagination := response.Pagination
paginationInfo := fmt.Sprintf("\nShowing page %.0f of %.0f, total %.0f items", pagination.Page, pagination.PageCount, pagination.Total)
rows = append(rows, paginationInfo)

tabFormattedPrint(out, header, rows)

Expand Down
55 changes: 33 additions & 22 deletions cmd/kosli/listTrails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,84 @@ import (
type ListTrailsCommandTestSuite struct {
suite.Suite
flowName string
trailName string
fingerprint string
defaultKosliArguments string
acmeOrgKosliArguments string
}

func (suite *ListTrailsCommandTestSuite) SetupTest() {
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Org: `docs-cmd-test-user`,
Host: "http://localhost:8001",
}

suite.flowName = "list-trails"
suite.defaultKosliArguments = fmt.Sprintf(" --flow %s --host %s --org %s --api-token %s", suite.flowName, global.Host, global.Org, global.ApiToken)
suite.trailName = "trail-name"
suite.fingerprint = "7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9"
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("trail-name", suite.flowName, "", suite.T())
BeginTrail(suite.trailName, suite.flowName, "", suite.T())
CreateArtifactOnTrail(suite.flowName, suite.trailName, "artifact", suite.fingerprint, "artifact-name", suite.T())

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

}

func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
tests := []cmdTestCase{
{
name: "listing trails works when there are trails",
cmd: fmt.Sprintf(`list trails %s`, suite.defaultKosliArguments),
golden: "",
name: "1 listing trails works when there are trails",
cmd: fmt.Sprintf(`list trails --flow %s %s`, suite.flowName, suite.defaultKosliArguments),
goldenFile: "output/list/list-trails.txt",
},
{
name: "listing trails works when there are no trails",
cmd: fmt.Sprintf(`list trails %s`, suite.acmeOrgKosliArguments),
name: "2 listing trails works when there are no trails",
cmd: fmt.Sprintf(`list trails --flow %s %s`, suite.flowName, suite.acmeOrgKosliArguments),
golden: "No trails were found.\n",
},
{
name: "listing trails with --output json works when there are trails",
cmd: fmt.Sprintf(`list trails --output json %s`, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "non-empty"}},
name: "3 listing trails with --output json works when there are trails",
cmd: fmt.Sprintf(`list trails --flow %s --output json %s`, suite.flowName, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "non-empty"}},
},
{
name: "listing trails with --output json works when there are no trails",
cmd: fmt.Sprintf(`list trails --output json %s`, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"", "[]"}},
name: "4 listing trails with --output json works when there are no trails",
cmd: fmt.Sprintf(`list trails --flow %s --output json %s`, suite.flowName, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"data", "[]"}},
},
{
wantError: true,
name: "providing an argument causes an error",
name: "5 providing an argument causes an error",
cmd: fmt.Sprintf(`list trails xxx %s`, suite.defaultKosliArguments),
golden: "Error: unknown command \"xxx\" for \"kosli list trails\"\n",
},
{
wantError: true,
name: "negative page limit causes an error",
cmd: fmt.Sprintf(`list trails --page-limit -1 %s`, suite.defaultKosliArguments),
name: "6 negative page limit causes an error",
cmd: fmt.Sprintf(`list trails --flow %s --page-limit -1 %s`, suite.flowName, suite.defaultKosliArguments),
golden: "Error: flag '--page-limit' has value '-1' which is illegal\n",
},
{
wantError: true,
name: "negative page number causes an error",
cmd: fmt.Sprintf(`list trails --page -1 %s`, suite.defaultKosliArguments),
name: "7 negative page number causes an error",
cmd: fmt.Sprintf(`list trails --flow %s --page -1 %s`, suite.flowName, suite.defaultKosliArguments),
golden: "Error: flag '--page' has value '-1' which is illegal\n",
},
{
name: "can list trails with pagination",
cmd: fmt.Sprintf(`list trails --page-limit 15 --page 2 %s`, suite.defaultKosliArguments),
name: "8 can list trails with pagination",
Copy link
Contributor

Choose a reason for hiding this comment

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

Since all the tests now include pagination, this test feels superfluous

cmd: fmt.Sprintf(`list trails --flow %s --page-limit 15 --page 2 %s`, suite.flowName, suite.defaultKosliArguments),
golden: "",
},
{
name: "9 can list trails that contain an artifact with the provided fingerprint",
cmd: fmt.Sprintf(`list trails --fingerprint %s --output json %s`, suite.fingerprint, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "non-empty"}},
},
}

runTestCmd(suite.T(), tests)
Expand Down
2 changes: 2 additions & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
debugFlag = "[optional] Print debug logs to stdout. A boolean flag https://docs.kosli.com/faq/#boolean-flags (default false)"
artifactTypeFlag = "The type of the artifact to calculate its SHA256 fingerprint. One of: [oci, docker, file, dir]. Only required if you want Kosli to calculate the fingerprint for you (i.e. when you don't specify '--fingerprint' on commands that allow it)."
flowNameFlag = "The Kosli flow name."
flowNameFlagOptional = "[optional] The Kosli flow name."
fingerprintInTrailsFlag = "[optional] The SHA256 fingerprint of the artifact 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
4 changes: 4 additions & 0 deletions cmd/kosli/testdata/output/list/list-trails.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NAME DESCRIPTION COMPLIANCE
trail-name test trail INCOMPLETE

Showing page 1 of 1, total 1 items