diff --git a/cmd/kosli/listFlows.go b/cmd/kosli/listFlows.go index 79de20cca..006d04ecb 100644 --- a/cmd/kosli/listFlows.go +++ b/cmd/kosli/listFlows.go @@ -16,7 +16,9 @@ import ( const listFlowsDesc = `List flows for an org.` type listFlowsOptions struct { - output string + output string + name string + ignoreCase bool } func newListFlowsCmd(out io.Writer) *cobra.Command { @@ -39,19 +41,34 @@ func newListFlowsCmd(out io.Writer) *cobra.Command { } cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag) + cmd.Flags().StringVarP(&o.name, "name", "n", "", searchByNameFlag) + cmd.Flags().BoolVarP(&o.ignoreCase, "ignore-case", "i", false, ignoreCaseFlag) return cmd } func (o *listFlowsOptions) run(out io.Writer) error { - url, err := url.JoinPath(global.Host, "api/v2/flows", global.Org) + base, err := url.JoinPath(global.Host, "api/v2/flows", global.Org) if err != nil { return err } + params := url.Values{} + if o.name != "" { + params.Set("search_by_name", o.name) + // case_sensitive only affects search, so only send it alongside a search term + if o.ignoreCase { + params.Set("case_sensitive", "false") + } + } + reqURL := base + if encoded := params.Encode(); encoded != "" { + reqURL = base + "?" + encoded + } + reqParams := &requests.RequestParams{ Method: http.MethodGet, - URL: url, + URL: reqURL, Token: global.ApiToken, } response, err := kosliClient.Do(reqParams) diff --git a/cmd/kosli/listFlows_test.go b/cmd/kosli/listFlows_test.go index 2999fbb7a..7135c8d94 100644 --- a/cmd/kosli/listFlows_test.go +++ b/cmd/kosli/listFlows_test.go @@ -24,6 +24,10 @@ func (suite *ListFlowsCommandTestSuite) SetupTest() { } suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) + // create flows with deterministic names so the --name / --case-sensitive tests + // can assert match / no-match behaviour reliably + CreateFlow("list-flows-search-target", suite.T()) + global.Org = "acme-org" global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c" suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken) @@ -51,6 +55,31 @@ func (suite *ListFlowsCommandTestSuite) TestListFlowsCmd() { cmd: fmt.Sprintf(`list flows --output json %s`, suite.acmeOrgKosliArguments), goldenJson: []jsonCheck{{"", "[]"}}, }, + { + name: "--name matches flows whose name contains the substring", + cmd: fmt.Sprintf(`list flows --name list-flows-search-target --output json %s`, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"", "non-empty"}, {"[0].name", "list-flows-search-target"}}, + }, + { + name: "--name with no matching substring returns an empty list", + cmd: fmt.Sprintf(`list flows --name no-such-flow-substring-xyz --output json %s`, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"", "[]"}}, + }, + { + name: "--name matching is case sensitive by default so a wrong-case substring does not match", + cmd: fmt.Sprintf(`list flows --name LIST-FLOWS-SEARCH-TARGET --output json %s`, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"", "[]"}}, + }, + { + name: "--ignore-case makes a wrong-case substring match", + cmd: fmt.Sprintf(`list flows --name LIST-FLOWS-SEARCH-TARGET --ignore-case --output json %s`, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"", "non-empty"}, {"[0].name", "list-flows-search-target"}}, + }, + { + name: "short flags -n and -i work like --name and --ignore-case", + cmd: fmt.Sprintf(`list flows -n LIST-FLOWS-SEARCH-TARGET -i --output json %s`, suite.defaultKosliArguments), + goldenJson: []jsonCheck{{"", "non-empty"}, {"[0].name", "list-flows-search-target"}}, + }, { wantError: true, name: "providing an argument causes an error", diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index c39d5d0c9..bebb8c6c0 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -107,6 +107,8 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, templateArtifactName = "The name of the artifact in the yml template file." flowNamesFlag = "[defaulted] The comma separated list of Kosli flows. Defaults to all flows of the org." outputFlag = "[defaulted] The format of the output. Valid formats are: [table, json]." + searchByNameFlag = "[optional] Only list flows whose name contains this substring. The Kosli API supports alphanumeric characters and '-'." + ignoreCaseFlag = "[optional] Perform case-insensitive matching for --name. By default matching is case sensitive." serviceAccountNameFlag = "The name of the service account whose API keys are managed." apiKeyDescriptionFlag = "A description for the API key." apiKeyExpiresAtFlag = "[optional] When the API key expires. Accepts an epoch timestamp or a date like '2026-06-04', '2026-06-04 15:04:05', or an RFC3339 timestamp. Defaults to no expiry." @@ -216,7 +218,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, excludePathsFlag = "[optional] The comma separated list of directories and files to exclude from fingerprinting. Can take glob patterns. Only applicable for --artifact-type dir." serverExcludePathsFlag = "[optional] The comma separated list of directories and files to exclude from fingerprinting. Can take glob patterns." shortFlag = "[optional] Print only the Kosli CLI version number." - reverseFlag = "[defaulted] Reverse the order of output list." + reverseFlag = "[optional] Reverse the order of output list." fingerprintFlag = "[conditional] The SHA256 fingerprint of the artifact. Only required if you don't specify '--artifact-type'." intervalFlag = "[optional] Expression to define specified snapshots range." showUnchangedArtifactsFlag = "[defaulted] Show the unchanged artifacts present in both snapshots within the diff output."