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
11 changes: 9 additions & 2 deletions cmd/kosli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,17 @@ func addEvidenceFlags(cmd *cobra.Command, payload *TypedEvidencePayload, ci stri
cmd.Flags().StringVar(&payload.EvidenceURL, "evidence-url", "", evidenceURLFlag)
}

func addListFlags(cmd *cobra.Command, o *listOptions) {
func addListFlags(cmd *cobra.Command, o *listOptions, customPageLimit ...int) {
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().IntVar(&o.pageNumber, "page", 1, pageNumberFlag)
cmd.Flags().IntVarP(&o.pageLimit, "page-limit", "n", 15, pageLimitFlag)

// Use customPageLimit if provided, otherwise default to 15
pageLimit := 15
if len(customPageLimit) > 0 {
pageLimit = customPageLimit[0]
}

cmd.Flags().IntVarP(&o.pageLimit, "page-limit", "n", pageLimit, pageLimitFlag)
}

func addAttestationFlags(cmd *cobra.Command, o *CommonAttestationOptions, payload *CommonAttestationPayload, ci string) {
Expand Down
10 changes: 10 additions & 0 deletions cmd/kosli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ 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
101 changes: 87 additions & 14 deletions cmd/kosli/listTrails.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,88 @@ import (
"github.com/spf13/cobra"
)

const listTrailsDesc = `List Trails for a Flow in an org.`
const listTrailsShortDesc = `List Trails for a Flow in 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 listTrailsExample = `
# list all trails for a flow:
kosli list trails \
--flow yourFlowName \
--api-token yourAPIToken \
--org yourOrgName

#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:
kosli list trails \
--flow yourFlowName \
--page-limit 30 \
--page 2 \
--api-token yourAPIToken \
--org yourOrgName

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

type listTrailsOptions struct {
listOptions
flowName string
output string
}

type Trail struct {
Name string `json:"name"`
Description string `json:"description"`
ComplianceState string `json:"compliance_state"`
}

type Pagination struct {
Page float64 `json:"page"`
PageCount float64 `json:"page_count"`
Total float64 `json:"total"`
}

type listTrailsResponse struct {
Data []Trail `json:"data"`
Pagination Pagination `json:"pagination"`
}

func newListTrailsCmd(out io.Writer) *cobra.Command {
o := new(listTrailsOptions)
cmd := &cobra.Command{
Use: "trails",
Short: listTrailsDesc,
Long: listTrailsDesc,
Args: cobra.NoArgs,
Use: "trails",
Short: listTrailsShortDesc,
Long: listTrailsLongDesc,
Example: listTrailsExample,
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return nil
return o.validateForListTrails(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out)
},
}

cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
// 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 {
Expand All @@ -49,7 +103,7 @@ func newListTrailsCmd(out io.Writer) *cobra.Command {
}

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

reqParams := &requests.RequestParams{
Method: http.MethodGet,
Expand All @@ -61,31 +115,50 @@ func (o *listTrailsOptions) run(out io.Writer) error {
return err
}

return output.FormattedPrint(response.Body, o.output, out, 0,
return output.FormattedPrint(response.Body, o.output, out, o.pageNumber,
map[string]output.FormatOutputFunc{
"table": printTrailsListAsTable,
"json": output.PrintJson,
})
}

func printTrailsListAsTable(raw string, out io.Writer, page int) error {
var trails []map[string]interface{}
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)
if err != nil {
return err
err = json.Unmarshal([]byte(raw), &response)
if err != nil {
return err
}
trails = response.Data
}

if len(trails) == 0 {
logger.Info("No trails were found.")
msg := "No trails were found"
if page != 1 {
msg = fmt.Sprintf("%s at page number %d", msg, page)
}
logger.Info(msg + ".")
return nil
}

header := []string{"NAME", "DESCRIPTION", "COMPLIANCE"}
rows := []string{}
for _, trail := range trails {
row := fmt.Sprintf("%s\t%s\t%s", trail["name"], trail["description"], trail["compliance_state"])
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)
}

tabFormattedPrint(out, header, rows)

return nil
Expand Down
17 changes: 17 additions & 0 deletions cmd/kosli/listTrails_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ func (suite *ListTrailsCommandTestSuite) TestListTrailsCmd() {
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),
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),
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),
golden: "",
},
}

runTestCmd(suite.Suite.T(), tests)
Expand Down