diff --git a/cloudapi/api.go b/cloudapi/api.go index a7a61270fae..7d26ef14c8e 100644 --- a/cloudapi/api.go +++ b/cloudapi/api.go @@ -61,6 +61,13 @@ type LoginResponse struct { Token string `json:"token"` } +// ValidateTokenResponse is the response of a token validation. +type ValidateTokenResponse struct { + IsValid bool `json:"is_valid"` + Message string `json:"message"` + Token string `json:"token-info"` +} + func (c *Client) handleLogEntriesFromCloud(ctrr CreateTestRunResponse) { logger := c.logger.WithField("source", "grafana-k6-cloud") for _, logEntry := range ctrr.Logs { @@ -188,8 +195,7 @@ func (c *Client) TestFinished(referenceID string, thresholds ThresholdResult, ta // GetTestProgress for the provided referenceID. func (c *Client) GetTestProgress(referenceID string) (*TestProgressResponse, error) { - url := fmt.Sprintf("%s/test-progress/%s", c.baseURL, referenceID) - req, err := c.NewRequest(http.MethodGet, url, nil) + req, err := c.NewRequest(http.MethodGet, c.baseURL+"/test-progress/"+referenceID, nil) if err != nil { return nil, err } @@ -205,9 +211,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 { - url := fmt.Sprintf("%s/tests/%s/stop", c.baseURL, referenceID) - - req, err := c.NewRequest("POST", url, nil) + req, err := c.NewRequest("POST", c.baseURL+"/tests/"+referenceID+"/stop", nil) if err != nil { return err } @@ -215,17 +219,14 @@ func (c *Client) StopCloudTestRun(referenceID string) error { return c.Do(req, nil) } +type validateOptionsRequest struct { + Options lib.Options `json:"options"` +} + // ValidateOptions sends the provided options to the cloud for validation. func (c *Client) ValidateOptions(options lib.Options) error { - url := fmt.Sprintf("%s/validate-options", c.baseURL) - - data := struct { - Options lib.Options `json:"options"` - }{ - options, - } - - req, err := c.NewRequest("POST", url, data) + data := validateOptionsRequest{Options: options} + req, err := c.NewRequest("POST", c.baseURL+"/validate-options", data) if err != nil { return err } @@ -233,19 +234,15 @@ func (c *Client) ValidateOptions(options lib.Options) error { return c.Do(req, nil) } +type loginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + // Login the user with the specified email and password. func (c *Client) Login(email string, password string) (*LoginResponse, error) { - url := fmt.Sprintf("%s/login", c.baseURL) - - data := struct { - Email string `json:"email"` - Password string `json:"password"` - }{ - email, - password, - } - - req, err := c.NewRequest("POST", url, data) + data := loginRequest{Email: email, Password: password} + req, err := c.NewRequest("POST", c.baseURL+"/login", data) if err != nil { return nil, err } @@ -258,3 +255,24 @@ func (c *Client) Login(email string, password string) (*LoginResponse, error) { return &lr, nil } + +type validateTokenRequest struct { + Token string `json:"token"` +} + +// 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) + if err != nil { + return nil, err + } + + vtr := ValidateTokenResponse{} + err = c.Do(req, &vtr) + if err != nil { + return nil, err + } + + return &vtr, nil +} diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 376a404e64a..0213fcf57b4 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "fmt" "syscall" @@ -12,6 +13,7 @@ import ( "go.k6.io/k6/cloudapi" "go.k6.io/k6/cmd/state" + "go.k6.io/k6/lib/consts" "go.k6.io/k6/ui" ) @@ -90,6 +92,9 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { newCloudConf.Token = null.StringFromPtr(nil) printToStdout(c.globalState, " token reset\n") case show.Bool: + valueColor := getColor(c.globalState.Flags.NoColor || !c.globalState.Stdout.IsTTY, color.FgCyan) + printToStdout(c.globalState, fmt.Sprintf(" token: %s\n", valueColor.Sprint(newCloudConf.Token.String))) + return nil case token.Valid: newCloudConf.Token = token default: @@ -115,6 +120,13 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { newCloudConf.Token = null.StringFrom(vals["Token"]) } + if newCloudConf.Token.Valid { + err := validateToken(c.globalState, currentJSONConfigRaw, newCloudConf.Token.String) + if err != nil { + return err + } + } + if currentDiskConf.Collectors == nil { currentDiskConf.Collectors = make(map[string]json.RawMessage) } @@ -127,13 +139,45 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } if newCloudConf.Token.Valid { - valueColor := getColor(c.globalState.Flags.NoColor || !c.globalState.Stdout.IsTTY, color.FgCyan) - if !c.globalState.Flags.Quiet { - printToStdout(c.globalState, fmt.Sprintf(" token: %s\n", valueColor.Sprint(newCloudConf.Token.String))) - } printToStdout(c.globalState, fmt.Sprintf( "Logged in successfully, token saved in %s\n", c.globalState.Flags.ConfigFilePath, )) } return nil } + +func validateToken(gs *state.GlobalState, jsonRawConf json.RawMessage, token string) error { + // 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 err + } + + if warn != "" { + gs.Logger.Warn(warn) + } + + client := cloudapi.NewClient( + gs.Logger, + token, + consolidatedCurrentConfig.Host.String, + consts.Version, + consolidatedCurrentConfig.Timeout.TimeDuration(), + ) + + var res *cloudapi.ValidateTokenResponse + res, err = client.ValidateToken() + if err != nil { + return fmt.Errorf("can't validate the API token: %s", err.Error()) + } + + if !res.IsValid { + return errors.New("your API token is invalid - " + + "please, consult the Grafana Cloud k6 documentation for instructions on how to generate a new one:\n" + + "https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication") + } + + return nil +}