-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add support to configure the default stack used by Cloud commands #5420
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
9c73b81
c91b1b0
1c0d291
b91bf75
bf3e61d
4d8aa50
ee18a7e
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 |
|---|---|---|
|
|
@@ -2,7 +2,9 @@ package cloudapi | |
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| "io" | ||
| "mime/multipart" | ||
| "net/http" | ||
| "strconv" | ||
|
|
@@ -70,6 +72,12 @@ type ValidateTokenResponse struct { | |
| Token string `json:"token-info"` | ||
| } | ||
|
|
||
| // AuthenticationResponse represents the response from the /cloud/v6/auth endpoint. | ||
| type AuthenticationResponse struct { | ||
| StackID int64 `json:"stack_id"` | ||
| DefaultProjectID int64 `json:"default_project_id"` | ||
| } | ||
|
|
||
| func (c *Client) handleLogEntriesFromCloud(ctrr CreateTestRunResponse) { | ||
| logger := c.logger.WithField("source", "grafana-k6-cloud") | ||
| for _, logEntry := range ctrr.Logs { | ||
|
|
@@ -85,7 +93,7 @@ func (c *Client) handleLogEntriesFromCloud(ctrr CreateTestRunResponse) { | |
| // CreateTestRun is used when a test run is being executed locally, while the | ||
| // results are streamed to the cloud, i.e. `k6 cloud run --local-execution` or `k6 run --out cloud script.js`. | ||
| func (c *Client) CreateTestRun(testRun *TestRun) (*CreateTestRunResponse, error) { | ||
| url := fmt.Sprintf("%s/tests", c.baseURL) | ||
| url := fmt.Sprintf("%s/tests", c.BaseURL(c.apiVersion)) | ||
|
|
||
| // Because the kind of request we make can vary depending on the test run configuration, we delegate | ||
| // its creation to a helper. | ||
|
|
@@ -186,7 +194,7 @@ func (c *Client) UploadTestOnly(name string, projectID int64, arc *lib.Archive) | |
| } | ||
|
|
||
| func (c *Client) uploadArchive(fields [][2]string, arc *lib.Archive) (*CreateTestRunResponse, error) { | ||
| requestURL := fmt.Sprintf("%s/archive-upload", c.baseURL) | ||
| requestURL := fmt.Sprintf("%s/archive-upload", c.BaseURL(c.apiVersion)) | ||
|
|
||
| var buf bytes.Buffer | ||
| mp := multipart.NewWriter(&buf) | ||
|
|
@@ -228,7 +236,7 @@ func (c *Client) uploadArchive(fields [][2]string, arc *lib.Archive) (*CreateTes | |
| // TestFinished sends the result and run status values to the cloud, along with | ||
| // information for the test thresholds, and marks the test run as finished. | ||
| func (c *Client) TestFinished(referenceID string, thresholds ThresholdResult, tained bool, runStatus RunStatus) error { | ||
| url := fmt.Sprintf("%s/tests/%s", c.baseURL, referenceID) | ||
| url := fmt.Sprintf("%s/tests/%s", c.BaseURL(c.apiVersion), referenceID) | ||
|
|
||
| resultStatus := ResultStatusPassed | ||
| if tained { | ||
|
|
@@ -255,7 +263,7 @@ func (c *Client) TestFinished(referenceID string, thresholds ThresholdResult, ta | |
|
|
||
| // GetTestProgress for the provided referenceID. | ||
| func (c *Client) GetTestProgress(referenceID string) (*TestProgressResponse, error) { | ||
| req, err := c.NewRequest(http.MethodGet, c.baseURL+"/test-progress/"+referenceID, nil) | ||
| req, err := c.NewRequest(http.MethodGet, c.BaseURL(c.apiVersion)+"/test-progress/"+referenceID, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -271,7 +279,7 @@ func (c *Client) GetTestProgress(referenceID string) (*TestProgressResponse, err | |
|
|
||
| // StopCloudTestRun tells the cloud to stop the test with the provided referenceID. | ||
| func (c *Client) StopCloudTestRun(referenceID string) error { | ||
| req, err := c.NewRequest("POST", c.baseURL+"/tests/"+referenceID+"/stop", nil) | ||
| req, err := c.NewRequest("POST", c.BaseURL(c.apiVersion)+"/tests/"+referenceID+"/stop", nil) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -286,7 +294,7 @@ type validateOptionsRequest struct { | |
| // ValidateOptions sends the provided options to the cloud for validation. | ||
| func (c *Client) ValidateOptions(options lib.Options) error { | ||
| data := validateOptionsRequest{Options: options} | ||
| req, err := c.NewRequest("POST", c.baseURL+"/validate-options", data) | ||
| req, err := c.NewRequest("POST", c.BaseURL(c.apiVersion)+"/validate-options", data) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -302,7 +310,7 @@ type loginRequest struct { | |
| // Login the user with the specified email and password. | ||
| func (c *Client) Login(email string, password string) (*LoginResponse, error) { | ||
| data := loginRequest{Email: email, Password: password} | ||
| req, err := c.NewRequest("POST", c.baseURL+"/login", data) | ||
| req, err := c.NewRequest("POST", c.BaseURL(c.apiVersion)+"/login", data) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -323,7 +331,7 @@ type validateTokenRequest struct { | |
| // ValidateToken calls the endpoint to validate the Client's token and returns the result. | ||
| func (c *Client) ValidateToken() (*ValidateTokenResponse, error) { | ||
| data := validateTokenRequest{Token: c.token} | ||
| req, err := c.NewRequest("POST", c.baseURL+"/validate-token", data) | ||
| req, err := c.NewRequest("POST", c.BaseURL(c.apiVersion)+"/validate-token", data) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -336,3 +344,41 @@ func (c *Client) ValidateToken() (*ValidateTokenResponse, error) { | |
|
|
||
| return &vtr, nil | ||
| } | ||
|
|
||
| // ValidateAuth validates a token and stack URL, returning the stack ID and default project ID. | ||
| // The stackURL must be a normalized full URL (e.g., https://my-team.grafana.net). | ||
| func (c *Client) ValidateAuth(stackURL string) (*AuthenticationResponse, error) { | ||
|
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. Can we use the generated go client for this operation?
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 have the same question actually. And more than that, should it be the goal for all v6 endpoints to be called via that library?
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 it would be great but would probably need a larger refactor, not directly related to what this PR does. I'm planning to use this library when working on #5009 and I can update this function then. |
||
| authURL := fmt.Sprintf("%s/auth", c.BaseURL(APIVersion6)) | ||
| req, err := http.NewRequest(http.MethodGet, authURL, nil) //nolint:noctx | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| req.Header.Set("X-Stack-Url", stackURL) | ||
| req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token)) | ||
| req.Header.Set("User-Agent", "k6cloud/"+c.version) | ||
|
|
||
| resp, err := c.client.Do(req) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("request failed: %w", err) | ||
| } | ||
| defer func() { | ||
| if resp != nil { | ||
| _, _ = io.Copy(io.Discard, resp.Body) | ||
| if cerr := resp.Body.Close(); cerr != nil && err == nil { | ||
| err = cerr | ||
| } | ||
| } | ||
| }() | ||
|
|
||
| if err := CheckResponse(resp); err != nil { | ||
|
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. The error schema in v6 AP is different from the old one. Should the error handling code be updated too? |
||
| return nil, err | ||
| } | ||
|
|
||
| authResp := AuthenticationResponse{} | ||
| if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { | ||
| return nil, fmt.Errorf("failed to decode response: %w", err) | ||
| } | ||
|
|
||
| return &authResp, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,12 +23,23 @@ const ( | |
| k6IdempotencyKeyHeader = "K6-Idempotency-Key" | ||
| ) | ||
|
|
||
| // APIVersion represents the Cloud API version. | ||
| type APIVersion string | ||
|
|
||
| const ( | ||
| // APIVersion1 represents the v1 API. | ||
| APIVersion1 APIVersion = "v1" | ||
| // APIVersion6 represents the v6 API. | ||
| APIVersion6 APIVersion = "v6" | ||
| ) | ||
|
|
||
| // Client handles communication with the k6 Cloud API. | ||
| type Client struct { | ||
| client *http.Client | ||
| token string | ||
| baseURL string | ||
| version string | ||
| client *http.Client | ||
| token string | ||
| host string | ||
| apiVersion APIVersion | ||
| version string | ||
|
|
||
| logger logrus.FieldLogger | ||
|
|
||
|
|
@@ -38,10 +49,16 @@ type Client struct { | |
|
|
||
| // NewClient return a new client for the cloud API | ||
| func NewClient(logger logrus.FieldLogger, token, host, version string, timeout time.Duration) *Client { | ||
| return NewClientWithAPIVersion(logger, token, host, version, APIVersion1, timeout) | ||
| } | ||
|
|
||
| // NewClientWithAPIVersion returns a new client for the cloud API with a specific API version. | ||
| func NewClientWithAPIVersion(logger logrus.FieldLogger, token, host, version string, apiVersion APIVersion, timeout time.Duration) *Client { | ||
| c := &Client{ | ||
| client: &http.Client{Timeout: timeout}, | ||
| token: token, | ||
| baseURL: fmt.Sprintf("%s/v1", host), | ||
| host: host, | ||
| apiVersion: apiVersion, | ||
| version: version, | ||
| retries: MaxRetries, | ||
| retryInterval: RetryInterval, | ||
|
|
@@ -50,9 +67,17 @@ func NewClient(logger logrus.FieldLogger, token, host, version string, timeout t | |
| return c | ||
| } | ||
|
|
||
| // BaseURL returns configured host. | ||
| func (c *Client) BaseURL() string { | ||
| return c.baseURL | ||
| // BaseURL returns the fully qualified base URL for the specified API version. | ||
| // It returns: | ||
| // - "{client.host}/v1" for APIVersion1 | ||
| // - "https://api.k6.io/cloud/v6" for APIVersion6 | ||
| func (c *Client) BaseURL(apiVersion APIVersion) string { | ||
| switch apiVersion { | ||
| case APIVersion6: | ||
| return "https://api.k6.io/cloud/v6" | ||
| default: | ||
| return fmt.Sprintf("%s/v1", c.host) | ||
| } | ||
|
Comment on lines
+70
to
+80
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. This change might break k6-operator code if PLZ endpoints (a mix of v1 and v4 at
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. @yorugac I'm having the same issue while working on the migration to the v6 API. If we don't want to migrate everything in one huge PR, it's not really possible to rely on @dgzlopes @grafana/k6-core Should this and the migration work be behind a feature flag so we can make the changes in different PRs until everything is ready? |
||
| } | ||
|
|
||
| // NewRequest creates new HTTP request. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,9 +18,12 @@ const LegacyCloudConfigKey = "loadimpact" | |||||
| //nolint:lll | ||||||
| type Config struct { | ||||||
| // TODO: refactor common stuff between cloud execution and output | ||||||
| Token null.String `json:"token" envconfig:"K6_CLOUD_TOKEN"` | ||||||
| ProjectID null.Int `json:"projectID" envconfig:"K6_CLOUD_PROJECT_ID"` | ||||||
| Name null.String `json:"name" envconfig:"K6_CLOUD_NAME"` | ||||||
| StackID null.Int `json:"stackID,omitempty" envconfig:"K6_CLOUD_STACK_ID"` | ||||||
| StackURL null.String `json:"stackURL,omitempty" envconfig:"K6_CLOUD_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.
Suggested change
I added URL because 2 out of 3 have it, but if the intention is to not have it then remove it from the other two. |
||||||
| DefaultProjectID null.Int `json:"defaultProjectID,omitempty"` | ||||||
| Token null.String `json:"token" envconfig:"K6_CLOUD_TOKEN"` | ||||||
| ProjectID null.Int `json:"projectID" envconfig:"K6_CLOUD_PROJECT_ID"` | ||||||
| Name null.String `json:"name" envconfig:"K6_CLOUD_NAME"` | ||||||
|
|
||||||
| Host null.String `json:"host" envconfig:"K6_CLOUD_HOST"` | ||||||
| Timeout types.NullDuration `json:"timeout" envconfig:"K6_CLOUD_TIMEOUT"` | ||||||
|
|
@@ -103,6 +106,15 @@ func NewConfig() Config { | |||||
| // | ||||||
| //nolint:cyclop | ||||||
| func (c Config) Apply(cfg Config) Config { | ||||||
| if cfg.StackID.Valid { | ||||||
| c.StackID = cfg.StackID | ||||||
| } | ||||||
| if cfg.StackURL.Valid && !c.StackURL.Valid { | ||||||
| c.StackURL = cfg.StackURL | ||||||
| } | ||||||
| if cfg.DefaultProjectID.Valid { | ||||||
| c.DefaultProjectID = cfg.DefaultProjectID | ||||||
| } | ||||||
| if cfg.Token.Valid { | ||||||
| c.Token = cfg.Token | ||||||
| } | ||||||
|
|
@@ -240,7 +252,9 @@ func mergeFromCloudOptionAndExternal( | |||||
| if err := json.Unmarshal(source, &tmpConfig); err != nil { | ||||||
| return err | ||||||
| } | ||||||
| // Only take out the ProjectID, Name and Token from the options.cloud (or legacy loadimpact struct) map: | ||||||
|
|
||||||
| // Only merge ProjectID, Name, Token, and StackID from options. | ||||||
| // StackURL and DefaultProjectID can only be set via login. | ||||||
|
Comment on lines
+255
to
+257
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. Can we get this check in a position near to the others, please? |
||||||
| if tmpConfig.ProjectID.Valid { | ||||||
| conf.ProjectID = tmpConfig.ProjectID | ||||||
| } | ||||||
|
|
@@ -250,6 +264,9 @@ func mergeFromCloudOptionAndExternal( | |||||
| if tmpConfig.Token.Valid { | ||||||
| conf.Token = tmpConfig.Token | ||||||
| } | ||||||
| if tmpConfig.StackID.Valid { | ||||||
| conf.StackID = tmpConfig.StackID | ||||||
| } | ||||||
|
|
||||||
| return nil | ||||||
| } | ||||||
|
|
||||||
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.
do we really need to export it?