Skip to content
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

Perform token validation as part of 'k6 cloud login' #3930

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
68 changes: 43 additions & 25 deletions cloudapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -205,47 +211,38 @@ 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
}

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
}

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
}
Expand All @@ -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
}
45 changes: 41 additions & 4 deletions cmd/cloud_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"encoding/json"
"errors"
"fmt"
"syscall"

Expand All @@ -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"
)

Expand Down Expand Up @@ -63,6 +65,8 @@ the "k6 run -o cloud" command.
}

// run is the code that runs when the user executes `k6 cloud login`
//
//nolint:funlen
func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error {
currentDiskConf, err := readDiskConfig(c.globalState)
if err != nil {
Expand All @@ -78,6 +82,18 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []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(
currentJSONConfigRaw, c.globalState.Env, "", nil, nil)
if err != nil {
return err
}

if warn != "" {
c.globalState.Logger.Warn(warn)
}

// But we don't want to save them back to the JSON file, we only
// want to save what already existed there and the login details.
newCloudConf := currentJSONConfig
Expand All @@ -90,6 +106,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)
Copy link
Contributor Author

@joanlopez joanlopez Sep 4, 2024

Choose a reason for hiding this comment

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

I moved these lines here, from the bottom of the file, as I think it only makes sense to print the token when providing the -s flag, as also discussed at #3886.

Otherwise, it doesn't, because:

  • When resetting, with -r, there's no token.
  • When providing it with -t, it's already there.
  • When using the form, we define it as a sensitive field (so it's not displayed while being written).

If we want to show it in the last scenario, I'd just switch the form input type to regular text, otherwise (as it was until now) it is a bit incoherent.

printToStdout(c.globalState, fmt.Sprintf(" token: %s\n", valueColor.Sprint(newCloudConf.Token.String)))
return nil
case token.Valid:
newCloudConf.Token = token
default:
Expand All @@ -115,6 +134,28 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error {
newCloudConf.Token = null.StringFrom(vals["Token"])
}

if newCloudConf.Token.Valid {
client := cloudapi.NewClient(
c.globalState.Logger,
newCloudConf.Token.String,
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")
}
}

joanlopez marked this conversation as resolved.
Show resolved Hide resolved
if currentDiskConf.Collectors == nil {
currentDiskConf.Collectors = make(map[string]json.RawMessage)
}
Expand All @@ -127,10 +168,6 @@ 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,
))
Expand Down
Loading