Skip to content

Add support to set the GC Stack the CLI should use by default #4940

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

Open
wants to merge 22 commits into
base: master
Choose a base branch
from

Conversation

dgzlopes
Copy link
Contributor

@dgzlopes dgzlopes commented Jul 17, 2025

What?

Current behaviour:

  • If you run a test or upload an archive and:
    • A: Specify the ProjectID:
      • The test/archive will end where you expected.
    • B: Don't specify a ProjectID:
      • The test/archive will end up in a seemingly random project (on the default project of your first stack/k6org).

This PR supports setting a Grafana Cloud Stack that would be used by default when running Cloud commands. It is implemented in a way that doesn't break the above behavior, but it improves the experience for all new users.

I have added a few things in this PR:

Support in the login command to pass the stack slug

Both in the interactive mode and as a flag (using --stack). In interactive mode, if the user passes a personal token, a default stack is provided, making it smoother for users with only one Stack. This default is omitted if a stack token is being used (ideally, in the future, we would set the stack this token is part of).

Screenshot from 2025-07-18 13-38-10

Test run and upload archive behave differently when no projectID is set

If the user has configured their default stack, these commands will run in the default project of the stack. A warning is shown in the CLI telling them about this behaviour.

Screenshot from 2025-07-18 13-48-02

Ability to set the stack slug/id in the script or as an environment variable

If the user wants to set it there, they can do so too. Here is a small example:

import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  vus: 10,
  duration: '30s',
  cloud: {
    stackSlug: 'daniellopes',
    note: 'Test run for k6 script',
    name: 'Test (12/03/2025-12:22:21)'
  }
};

export default function() {
  http.get('https://quickpizza.grafana.com');
  sleep(1);
}

Why?

The current experience wasn't designed around Grafana Cloud, where Stack is the higher-level concept. This PR is based on the proposal listed on #4008 but with a better UX: users inputting the slug instead of the ID, which is hard to find and identify.

Having this is a requirement for adding extra commands like a plan command as mentioned in #3733 or letting users reference their projects by their name, as proposed in #1817

Checklist

  • I have performed a self-review of my code.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests for my changes.
  • I have run linter and tests locally (make check) and all pass.

Checklist: Documentation (only for k6 maintainers and if relevant)

Please do not merge this PR until the following items are filled out.

  • I have added the correct milestone and labels to the PR.
  • I have updated the release notes: link
  • I have updated or added an issue to the k6-documentation: grafana/k6-docs#NUMBER if applicable
  • I have updated or added an issue to the TypeScript definitions: grafana/k6-DefinitelyTyped#NUMBER if applicable

Related PR(s)/Issue(s)

@CLAassistant
Copy link

CLAassistant commented Jul 17, 2025

CLA assistant check
All committers have signed the CLA.

@dgzlopes dgzlopes changed the title Add support to set default Stack Cloud commands should target Add support to set default Stack that Cloud commands should target Jul 17, 2025
@dgzlopes dgzlopes changed the title Add support to set default Stack that Cloud commands should target Add support to set default Grafana Cloud Stack info Jul 17, 2025
@dgzlopes dgzlopes changed the title Add support to set default Grafana Cloud Stack info Add support to set the Grafana Cloud Stack the CLI should use by default Jul 18, 2025
@dgzlopes dgzlopes changed the title Add support to set the Grafana Cloud Stack the CLI should use by default Add support to set the GC Stack the CLI should use by default Jul 18, 2025
@dgzlopes dgzlopes marked this pull request as ready for review July 18, 2025 11:59
@dgzlopes dgzlopes requested a review from a team as a code owner July 18, 2025 11:59
@dgzlopes dgzlopes requested review from ankur22 and codebien and removed request for a team July 18, 2025 11:59
}

// GetDefaultProject retrieves the default project for the given stack ID.
func (c *Client) GetDefaultProject(stackID int64) (int64, string, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know what the long-term plans are here.

The new REST API does a few things differently:

  • StackID is mandatory
  • Auth header is different (using Bearer instead of Token)

Should we change the existing Client, or add a new one?

What do you think about using the auto-generated Go SDK?

Moving to the new API completely is impossible until we enforce StackID (maybe in v2?). So yeah, we will be living in this mixed APIs state for a while.

Copy link
Contributor

Choose a reason for hiding this comment

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

until we enforce StackID (maybe in v2?).

Is it really a strict requirement? It seems we can get the StackID from the token just by calling /account/me, or is only available in /v3?

What do you think about using the auto-generated Go SDK?

It sounds to me a great idea to explore.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we change the existing Client, or add a new one?

As discussed above, I'd suggest adding a new client, so we keep things isolated, especially considering we're speaking about different hosts or different versions of the API.

What do you think about using the auto-generated Go SDK?

I think we should use it when possible, but I suspect most of the operations that are supported now (by k6) aren't supported by the latest version of the Public API. But yeah, if we start adding support into k6 for the operations available through the auto-generated client, then I'd suggest using it for those, for sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it really a strict requirement? It seems we can get the StackID from the token just by calling /account/me, or is only available in /v3?

A user can be in many Stacks! Which one should we pick? 👀

That's the current problem, IMO.

I'd suggest adding a new client

I'll try to do this. I'll also ask the backend about the /me endpoint and if they plan to include it in the new API.

That said, I'll do the smallest thing we can in this area to move this PR forward. We can keep adding things to that new client in future PRs (e.g., when I add k6 plan I'll need a few more methods)

@@ -127,3 +129,62 @@ func handleTestAbortSignals(gs *state.GlobalState, gracefulStopHandler, onHardSt
gs.SignalStop(sigC)
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not really sure if common.go is the right place for these shared functions.

Happy to move it around.

Copy link
Contributor

Choose a reason for hiding this comment

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

In big packages like internal/cmd I would prefer a specialized file. Can you move to a stack.go or cloud_stack.go file, please?

// TODO: Improve handling of this error. This happens when the user uses a Stack tocken instead of a Personal token.
if strings.Contains(err.Error(), "Authentication failed") {
return 0, "", nil
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we have an API to check if a token is personal or stack? If stack, from which stack?

It would be nice to be able to set it as a default and to improve this error handling.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you need someone from the backend to address this question.

@oleiade oleiade requested a review from joanlopez July 18, 2025 12:20
ankur22
ankur22 previously approved these changes Jul 18, 2025
Copy link
Contributor

@ankur22 ankur22 left a comment

Choose a reason for hiding this comment

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

Looks good. Some minor things to consider.

I'm not fully versed on this area of k6 though so someone else who has a better idea might be a better person to add their approval too.

Comment on lines 171 to 178
if cfg.StackID.Valid {
c.StackID = cfg.StackID
c.StackSlug = null.StringFromPtr(nil)
}
if cfg.StackSlug.Valid {
c.StackSlug = cfg.StackSlug
c.StackID = null.StringFromPtr(nil)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Shamefully, i don't know the difference between StackID and StackSlug. Looks like we can't work with both here. If someone did provide both, could k6 return an error (or log a warning)? The user might want to work with StackID and instead it gets overwritten by StackSlug as they forgot to undefine StackSlug.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

StackID is a number. StackSlug is a string. You can spot it easily on the URL of your Grafana Cloud instance.

e.g. .grafana.com

StackIDs can't change. StackSlugs only change on edge cases.

IDs are pretty hard to find, while Slugs are well-known. I wanted to focus the UX on Slugs because they seem way easier to understand, find, and reference. But yeah, this is quite confusing, I know 😄

@dgzlopes dgzlopes requested a review from ankur22 July 22, 2025 09:27
Copy link
Contributor

@codebien codebien left a comment

Choose a reason for hiding this comment

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

Hey @dgzlopes, thanks for your contribution here 💌

Review of the user experience's definition

I feel this is complex enough that we have to pay attention to clearly document the user's scenarios. It should serve as a future reference, detailing our decisions and their rationale. I feel the current pull request's description would benefit if we could update, improve the format and create a better structured tree of the expected user cases.

I feel the new use cases and the technical decisions/limitations of the implementation are mixed together. Separating these into distinct paragraphs would enhance readability.

Review of the code

Testing: It looks like we might be missing some tests for this logic. Perhaps you were aiming to get some initial feedback before diving deeper into testing, that's perfectly fine. We can definitely plan to incorporate them in a next step.
However, if the idea was to bypass tests altogether, I'd suggest we reconsider. The logic already has a good amount of history and complexity, and adding more on top of it without the safety net of tests would make future changes very error-prone.

HTTP clients: as soon as you will add tests then you will hit the issue with faking things with the http.DefaultClient. We should inject the clients as we are doing in other parts so we can mock their behavior for easily test the scenarios.

Sparse logic: I didn’t explore this assumption in detail but I have the impression that the source code might benefit if we add some sort of StackResolver component and we delegate to it all the logic for doing the resolution.

}

// GetDefaultProject retrieves the default project for the given stack ID.
func (c *Client) GetDefaultProject(stackID int64) (int64, string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

until we enforce StackID (maybe in v2?).

Is it really a strict requirement? It seems we can get the StackID from the token just by calling /account/me, or is only available in /v3?

What do you think about using the auto-generated Go SDK?

It sounds to me a great idea to explore.

// AccountMe retrieves the current user's account information.
func (c *Client) AccountMe() (*AccountMeResponse, error) {
// TODO: remove this hardcoded URL
req, err := c.NewRequest("GET", "https://api.k6.io/v3/account/me", nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
req, err := c.NewRequest("GET", "https://api.k6.io/v3/account/me", nil)
req, err := c.NewRequest(http.GetMethod, "https://api.k6.io/v3/account/me", nil)

Same for the others

req.Header.Set("X-Stack-Id", fmt.Sprintf("%d", stackID))
// TODO: by default the client uses Token instead of Bearer
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
req.Header.Set("User-Agent", "Go-http-client")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
req.Header.Set("User-Agent", "Go-http-client")

Is it required?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess Backend folks may want to know where the API is being consumed from, right @dgzlopes? 🤔

If we ever replace this custom implementation in favor of the auto-generated Go SDK, the User-Agent is yet another configuration attribute that you can customize.

Wrt the value, I think we might want to use something that explicitly let us know this request is coming from the k6 binary.

Comment on lines +388 to +390
// TODO: Can't use c.Do bc it messes up the headers
resp, err := http.DefaultClient.Do(req)
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Then we need a dedicated client overwriting this different logic, otherwise testing will be quite difficult

}

if len(parsed.Value) == 0 {
return 0, "", fmt.Errorf("no projects found for stack ID %d", stackID)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return 0, "", fmt.Errorf("no projects found for stack ID %d", stackID)
return 0, "", fmt.Errorf("no projects found for stack ID %d", stackSlug)

We should be consistent, if we prefer the Slug on input then we should return a slug on errors. Otherwise, they will experience the same difficulties we're carefully avoiding on inputs.

Comment on lines +147 to +151
const suffix = ".grafana.net"
if len(s) > len(suffix) && s[len(s)-len(suffix):] == suffix {
return s[:len(s)-len(suffix)]
}
return s
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +268 to +274
// We want to use this fully consolidated config for things like
// host addresses, so users can overwrite them with env vars.
consolidatedCurrentConfig, warn, err := cloudapi.GetConsolidatedConfig(
jsonRawConf, gs.Env, "", nil, nil)
if err != nil {
return nil, err
}
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to me this should be a dependency of this function instead of being part of the logic

@@ -92,18 +101,47 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error {
show := getNullBool(cmd.Flags(), "show")
reset := getNullBool(cmd.Flags(), "reset")
token := getNullString(cmd.Flags(), "token")
stackSlug := getNullString(cmd.Flags(), "stack-slug")
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the pull request's description outdated, or they are intentionally different?

// TODO: Improve handling of this error. This happens when the user uses a Stack tocken instead of a Personal token.
if strings.Contains(err.Error(), "Authentication failed") {
return 0, "", nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you need someone from the backend to address this question.

Comment on lines +175 to +177
// TODO: do this in the future? But it is a breaking change...
// gs.Logger.Error("please specify a projectID in your test or use `k6 cloud login` to set up a default stack")
// return 0, fmt.Errorf("no projectID specified and no default stack set")
Copy link
Contributor

Choose a reason for hiding this comment

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

We can do two things here:

  1. Add a warning and make the user aware of the future changes without breaking the current logic
  2. We can start having feature flags with breaking changes, we might also have some next feature flag, where we enable all the feature in the shape of v2. So, the users that don't care about breaking changes can benefit of the new features.

Comment on lines +345 to +346
GrafanaStackName string `json:"grafana_stack_name"`
GrafanaStackID int `json:"grafana_stack_id"`
Copy link
Contributor

@joanlopez joanlopez Jul 22, 2025

Choose a reason for hiding this comment

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

I have tested the endpoint, and looks like it could return a null grafana_stack_name and/or grafana_stack_id.

Is that only a particular case for my account? Or an edge case that we should handle?
Because, in such case, I think we should make this nil-able, and/or somehow explicitly document that it can be the case.

I'm wondering the wide variety of corner cases we can find once we release this to users 🤔

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
req.Header.Set("User-Agent", "Go-http-client")

// TODO: Can't use c.Do bc it messes up the headers
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is yet another indicator that we should likely have a different client implementation for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants