-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add k6 cloud project and k6 cloud project list commands
#5650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "go.k6.io/k6/cmd/state" | ||
| ) | ||
|
|
||
| type cmdCloudProject struct { | ||
| globalState *state.GlobalState | ||
| } | ||
|
|
||
| func getCmdCloudProject(cloudCmd *cmdCloud) *cobra.Command { | ||
| c := &cmdCloudProject{ | ||
| globalState: cloudCmd.gs, | ||
| } | ||
|
|
||
| exampleText := getExampleText(cloudCmd.gs, ` | ||
| # List all projects in the configured stack | ||
| $ {{.}} cloud project list | ||
|
|
||
| # List projects in JSON format | ||
| $ {{.}} cloud project list --json`[1:]) | ||
|
|
||
| cloudProjectCommand := &cobra.Command{ | ||
| Use: "project", | ||
| Short: "Work with Grafana Cloud k6 projects", | ||
| Long: `Work with Grafana Cloud k6 projects.`, | ||
|
|
||
| Example: exampleText, | ||
| } | ||
|
|
||
| defaultUsageTemplate := (&cobra.Command{}).UsageTemplate() | ||
| defaultUsageTemplate = strings.ReplaceAll(defaultUsageTemplate, "FlagUsages", "FlagUsagesWrapped 120") | ||
|
|
||
| listCmd := getCmdCloudProjectList(c) | ||
| listCmd.SetUsageTemplate(defaultUsageTemplate) | ||
| cloudProjectCommand.AddCommand(listCmd) | ||
|
|
||
| cloudProjectCommand.SetUsageTemplate(`Usage: | ||
| {{.CommandPath}} <command> [flags] | ||
|
|
||
| Available Commands:{{range .Commands}}{{if .IsAvailableCommand}} | ||
| {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}} | ||
|
|
||
| Examples: | ||
| {{.Example}} | ||
| Flags: | ||
| -h, --help Show help | ||
| {{if .HasExample}} | ||
| {{end}} | ||
| Use "{{.CommandPath}} <command> --help" for more information about a command. | ||
| `) | ||
|
|
||
| return cloudProjectCommand | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,141 @@ | ||||||
| package cmd | ||||||
|
|
||||||
| import ( | ||||||
| "bytes" | ||||||
| "encoding/json" | ||||||
| "errors" | ||||||
| "fmt" | ||||||
| "strings" | ||||||
| "text/tabwriter" | ||||||
|
|
||||||
| k6cloud "github.com/grafana/k6-cloud-openapi-client-go/k6" | ||||||
| "github.com/spf13/cobra" | ||||||
|
|
||||||
| "go.k6.io/k6/cloudapi" | ||||||
| "go.k6.io/k6/cmd/state" | ||||||
| "go.k6.io/k6/internal/build" | ||||||
| v6cloudapi "go.k6.io/k6/internal/cloudapi/v6" | ||||||
| ) | ||||||
|
|
||||||
| type cmdCloudProjectList struct { | ||||||
| globalState *state.GlobalState | ||||||
| isJSON bool | ||||||
| } | ||||||
|
|
||||||
| func getCmdCloudProjectList(projectCmd *cmdCloudProject) *cobra.Command { | ||||||
| c := &cmdCloudProjectList{ | ||||||
| globalState: projectCmd.globalState, | ||||||
| } | ||||||
|
|
||||||
| exampleText := getExampleText(projectCmd.globalState, ` | ||||||
| # List all projects in the configured stack | ||||||
| $ {{.}} cloud project list`[1:]) | ||||||
|
|
||||||
| listCmd := &cobra.Command{ | ||||||
| Use: "list", | ||||||
| Short: "List Grafana Cloud k6 projects", | ||||||
| Long: `List all projects in the configured Grafana Cloud k6 stack.`, | ||||||
| Example: exampleText, | ||||||
| Args: cobra.NoArgs, | ||||||
| RunE: c.run, | ||||||
| } | ||||||
|
|
||||||
| listCmd.Flags().BoolVar(&c.isJSON, "json", false, "output project list in JSON format") | ||||||
|
|
||||||
| return listCmd | ||||||
| } | ||||||
|
|
||||||
| func (c *cmdCloudProjectList) run(_ *cobra.Command, _ []string) error { | ||||||
| if !checkIfMigrationCompleted(c.globalState) { | ||||||
|
Check failure on line 49 in internal/cmd/cloud_project_list.go
|
||||||
| if err := migrateLegacyConfigFileIfAny(c.globalState); err != nil { | ||||||
|
Check failure on line 50 in internal/cmd/cloud_project_list.go
|
||||||
| return err | ||||||
| } | ||||||
| } | ||||||
|
Comment on lines
+49
to
+53
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally, I'd prefer if we could do this in a single place. If not, it's okay. I haven't digged into Cobra details since a while ago, so I forgot almost all my knowledge about it, but I was wondering if there's a way so any
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess the same reasoning could apply to the lines below, again, if possible, as ideally I'd like to not end up with the same code in ~20 places if we start adding support for more project operations or support for other types of resources (e.g., load tests). |
||||||
|
|
||||||
| currentDiskConf, err := readDiskConfig(c.globalState) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| currentJSONConfigRaw := currentDiskConf.Collectors["cloud"] | ||||||
|
|
||||||
| cloudConfig, warn, err := cloudapi.GetConsolidatedConfig( | ||||||
| currentJSONConfigRaw, c.globalState.Env, "", nil, nil) | ||||||
|
Check failure on line 63 in internal/cmd/cloud_project_list.go
|
||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| if warn != "" { | ||||||
| c.globalState.Logger.Warn(warn) | ||||||
| } | ||||||
|
|
||||||
| if !cloudConfig.Token.Valid || cloudConfig.Token.String == "" { | ||||||
| return errUserUnauthenticated | ||||||
| } | ||||||
|
|
||||||
| if !cloudConfig.StackID.Valid || cloudConfig.StackID.Int64 == 0 { | ||||||
| return errors.New( | ||||||
| "no stack configured. Please run `k6 cloud login --stack <your-stack>` to set a default stack", | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I agree. We should just suggest |
||||||
| ) | ||||||
|
Comment on lines
+76
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be worth creating a constant for this error, we'll probably use it a lot in other parts of the codebase in the future. |
||||||
| } | ||||||
|
|
||||||
| client, err := v6cloudapi.NewClient( | ||||||
| c.globalState.Logger, | ||||||
| cloudConfig.Token.String, | ||||||
| cloudConfig.Hostv6.String, | ||||||
| build.Version, | ||||||
| cloudConfig.Timeout.TimeDuration(), | ||||||
| ) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
| client.SetStackID(cloudConfig.StackID.Int64) | ||||||
|
|
||||||
| resp, err := client.ListProjects() | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| if c.isJSON { | ||||||
| return c.outputJSON(resp.Value) | ||||||
| } | ||||||
|
|
||||||
| stackHeader := fmt.Sprintf("Projects for stack %d:\n\n", cloudConfig.StackID.Int64) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we have the StackURL field non-empty, we should use it instead of the stack ID (but it might be empty). |
||||||
|
|
||||||
| if len(resp.Value) == 0 { | ||||||
| printToStdout(c.globalState, stackHeader+ | ||||||
| "No projects found.\n"+ | ||||||
| "To create a project, visit https://grafana.com/docs/grafana-cloud/testing/k6/projects/\n") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| printToStdout(c.globalState, stackHeader+formatProjectTable(resp.Value)) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func (c *cmdCloudProjectList) outputJSON(projects []k6cloud.ProjectApiModel) error { | ||||||
| buf := &bytes.Buffer{} | ||||||
| enc := json.NewEncoder(buf) | ||||||
| enc.SetEscapeHTML(false) | ||||||
| enc.SetIndent("", " ") | ||||||
| if err := enc.Encode(projects); err != nil { | ||||||
| return fmt.Errorf("failed to encode project list: %w", err) | ||||||
| } | ||||||
|
|
||||||
| printToStdout(c.globalState, buf.String()) | ||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
| func formatProjectTable(projects []k6cloud.ProjectApiModel) string { | ||||||
| var buf strings.Builder | ||||||
| w := tabwriter.NewWriter(&buf, 0, 0, 3, ' ', 0) | ||||||
| _, _ = fmt.Fprintln(w, "ID\tNAME\tDEFAULT") | ||||||
| for _, p := range projects { | ||||||
| def := "no" | ||||||
| if p.IsDefault { | ||||||
| def = "yes" | ||||||
| } | ||||||
| _, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", p.Id, p.Name, def) | ||||||
| } | ||||||
| _ = w.Flush() | ||||||
| return buf.String() | ||||||
| } | ||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the
listcommand help is convoluted, would make sense to use something like this there as well? I'm wondering if we should even define this as a function/const to be reused more widely.