Skip to content
Open
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
36 changes: 36 additions & 0 deletions internal/cloudapi/v6/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,47 @@ import (
"errors"
"fmt"
"io"
"math"
"net/url"

k6cloud "github.com/grafana/k6-cloud-openapi-client-go/k6"
)

// ListProjects retrieves the list of projects for the configured stack.
func (c *Client) ListProjects() (*k6cloud.ProjectListResponse, error) {
// Bound checking stackID (int64) to support using it as an int32 in calls
// to the API.
if c.stackID < math.MinInt32 || c.stackID > math.MaxInt32 {
return nil, fmt.Errorf("stack ID %d overflows int32", c.stackID)
}

ctx := context.WithValue(context.Background(), k6cloud.ContextAccessToken, c.token)
req := c.apiClient.ProjectsAPI.
ProjectsList(ctx).
XStackId(int32(c.stackID))

resp, httpRes, rerr := req.Execute()
defer func() {
if httpRes != nil {
_, _ = io.Copy(io.Discard, httpRes.Body)
_ = httpRes.Body.Close()
}
}()

if rerr != nil {
var apiErr *k6cloud.GenericOpenAPIError
if !errors.As(rerr, &apiErr) {
return nil, fmt.Errorf("failed to list projects: %w", rerr)
}
}

if err := CheckResponse(httpRes); err != nil {
return nil, fmt.Errorf("failed to list projects: %w", err)
}

return resp, nil
}

// ValidateToken calls the endpoint to validate the Client's token and returns the result.
func (c *Client) ValidateToken(stackURL string) (_ *k6cloud.AuthenticationResponse, err error) {
if stackURL == "" {
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command {
uploadCmd.SetUsageTemplate(defaultUsageTemplate)
cloudCmd.AddCommand(uploadCmd)

projectCmd := getCmdCloudProject(c)
cloudCmd.AddCommand(projectCmd)

cloudCmd.Flags().SortFlags = false
cloudCmd.Flags().AddFlagSet(c.flagSet())

Expand Down
57 changes: 57 additions & 0 deletions internal/cmd/cloud_project.go
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:
Copy link
Copy Markdown
Contributor

@joanlopez joanlopez Apr 16, 2026

Choose a reason for hiding this comment

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

I see the list command 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.

{{.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
}
141 changes: 141 additions & 0 deletions internal/cmd/cloud_project_list.go
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

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, macos-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, macos-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / lint

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / build

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, windows-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, windows-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-24.04-arm)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-24.04-arm)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

undefined: checkIfMigrationCompleted

Check failure on line 49 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

undefined: checkIfMigrationCompleted
if err := migrateLegacyConfigFileIfAny(c.globalState); err != nil {

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, macos-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, macos-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / lint

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / build

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, windows-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, windows-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-24.04-arm)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-24.04-arm)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

undefined: migrateLegacyConfigFileIfAny

Check failure on line 50 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

undefined: migrateLegacyConfigFileIfAny
return err
}
}
Comment on lines +49 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 k6 cloud ... command executes this code, as a "pre-run" or something like that, but defined in a single place. Would that make sense? Not sure if possible, tho.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, macos-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, macos-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / lint

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / build

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, windows-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, windows-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (tip, ubuntu-24.04-arm)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test (stable, ubuntu-24.04-arm)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, windows-latest)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

too many arguments in call to cloudapi.GetConsolidatedConfig

Check failure on line 63 in internal/cmd/cloud_project_list.go

View workflow job for this annotation

GitHub Actions / test-latest (1.26.x, ubuntu-24.04-arm)

too many arguments in call to cloudapi.GetConsolidatedConfig
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",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this suggestion is no longer consistent with #5833
cc/ @codebien

In other words; if I run this k6 cloud login --stack <your-stack>, it will throw an error, so it's better if we suggest something that will work from the first attempt.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
"no stack configured. Please run `k6 cloud login --stack <your-stack>` to set a default stack",
"no stack configured. Please run `k6 cloud login` to set a default stack",

I agree. We should just suggest k6 cloud login. However, we already have this logic for checking the user session. We should reuse that logic from a shared place so we don't reinvent the wheel here.

)
Comment on lines +76 to +78
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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()
}
Loading
Loading