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

Cloud subcommand 'upload' to replace --upload-only #3850

Closed
wants to merge 8 commits into from
66 changes: 60 additions & 6 deletions cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
uploadOnly bool
}

//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete.

Check failure on line 36 in cmd/cloud.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete.` is unused for linter "dupl" (nolintlint)
func (c *cmdCloud) preRun(cmd *cobra.Command, _ []string) error {
// TODO: refactor (https:/loadimpact/k6/issues/883)
//
Expand Down Expand Up @@ -117,7 +118,7 @@
return err
}
if !cloudConfig.Token.Valid {
return errors.New("Not logged in, please use `k6 login cloud`.") //nolint:golint,revive,stylecheck
return errors.New("Not logged in, please use `k6 cloud login`") //nolint:golint,stylecheck
}

// Display config warning if needed
Expand Down Expand Up @@ -329,7 +330,7 @@
flags.BoolVar(&c.showCloudLogs, "show-logs", c.showCloudLogs,
"enable showing of logs when a test is executed in the cloud")
flags.BoolVar(&c.uploadOnly, "upload-only", c.uploadOnly,
"only upload the test to the cloud without actually starting a test run")
"(deprecated) only upload the test to the cloud without actually starting a test run")

return flags
}
Expand All @@ -343,20 +344,73 @@
}

exampleText := getExampleText(gs, `
{{.}} cloud script.js`[1:])
# Authenticate with Grafana k6 Cloud
$ {{.}} cloud login

# Run a k6 script in the Grafana k6 cloud
$ {{.}} cloud run script.js

# Run a k6 archive in the Grafana k6 cloud
$ {{.}} cloud run archive.tar

# Upload the test script to the k6 Cloud (without starting a test run)
$ {{.}} cloud upload script.js

# [deprecated] Run a k6 script in the Grafana k6 cloud
$ {{.}} cloud script.js

# [deprecated] Run a k6 archive in the Grafana k6 cloud
$ {{.}} cloud archive.tar`[1:])

cloudCmd := &cobra.Command{
Use: "cloud",
Short: "Run a test on the cloud",
Long: `Run a test on the cloud.
Long: `[deprecation notice]
The k6 team is in the process of modifying and deprecating the cloud command behavior. In the future, the "cloud"
command will only display a help text, instead of running tests in the cloud.
To run tests in the cloud, users are now invited to migrate to the "k6 cloud run" command instead.

Run a test on the cloud.

This will execute the test on the k6 cloud service. Use "k6 login cloud" to authenticate.`,
Example: exampleText,
Args: exactArgsWithMsg(1, "arg should either be \"-\", if reading script from stdin, or a path to a script file"),
Args: exactCloudArgs(),
PreRunE: c.preRun,
RunE: c.run,
Example: exampleText,
}

// Register `k6 cloud` subcommands
cloudCmd.AddCommand(getCmdCloudRun(gs))
cloudCmd.AddCommand(getCmdCloudLogin(gs))
cloudCmd.AddCommand(getCmdCloudUpload(gs))

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

return cloudCmd
}

func exactCloudArgs() cobra.PositionalArgs {
return func(_ *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 2 {
return fmt.Errorf("accepts 1 or 2 arg(s), received %d", len(args))
}

var (
isRunSubcommand = args[0] == "run"
isLoginSubcommand = args[0] == "login"
isScript = filepath.Ext(args[0]) == ".js"
isArchive = filepath.Ext(args[0]) == ".tar"
)

if len(args) == 1 && !isScript && !isArchive {
return fmt.Errorf("unexpected argument: %s", args[0])
}

if len(args) == 2 && !isRunSubcommand && !isLoginSubcommand {
return fmt.Errorf("unexpected argument: %s", args[0])
}

return nil
}
}
175 changes: 175 additions & 0 deletions cmd/cloud_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cmd

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

"github.com/fatih/color"
"github.com/spf13/cobra"
"golang.org/x/term"
"gopkg.in/guregu/null.v3"

"go.k6.io/k6/cloudapi"
"go.k6.io/k6/cmd/state"
"go.k6.io/k6/lib/consts"
"go.k6.io/k6/ui"
)

const cloudLoginCommandName = "login"

type cmdCloudLogin struct {
globalState *state.GlobalState
}

func getCmdCloudLogin(gs *state.GlobalState) *cobra.Command {
c := &cmdCloudLogin{
globalState: gs,
}

// loginCloudCommand represents the 'cloud login' command
exampleText := getExampleText(gs, `
# Log in with an email/password.
{{.}} cloud login

# Store a token in k6's persistent configuration.
{{.}} cloud login -t <YOUR_TOKEN>

# Display the stored token.
{{.}} cloud login -s`[1:])

loginCloudCommand := &cobra.Command{
Use: cloudLoginCommandName,
Short: "Authenticate with Grafana k6 Cloud",
Long: `Authenticate with Grafana Cloud k6.

This command will authenticate you with Grafana Cloud k6. Once authenticated,
Once authenticated you can start running tests in the cloud by using the "k6 cloud"
command, or by executing a test locally and outputting samples to the cloud using
the "k6 run -o cloud" command.
`,
Example: exampleText,
Args: cobra.NoArgs,
RunE: c.run,
}

loginCloudCommand.Flags().StringP("token", "t", "", "specify `token` to use")
loginCloudCommand.Flags().BoolP("show", "s", false, "display saved token and exit")
loginCloudCommand.Flags().BoolP("reset", "r", false, "reset token")

return loginCloudCommand
}

// 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 {
return err
}

currentJSONConfig := cloudapi.Config{}
currentJSONConfigRaw := currentDiskConf.Collectors["cloud"]
if currentJSONConfigRaw != nil {
// We only want to modify this config, see comment below
if jsonerr := json.Unmarshal(currentJSONConfigRaw, &currentJSONConfig); jsonerr != nil {
return jsonerr
}
}

// 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

show := getNullBool(cmd.Flags(), "show")
reset := getNullBool(cmd.Flags(), "reset")
token := getNullString(cmd.Flags(), "token")
switch {
case reset.Valid:
newCloudConf.Token = null.StringFromPtr(nil)
printToStdout(c.globalState, " token reset\n")
case show.Bool:
case token.Valid:
newCloudConf.Token = token
default:
form := ui.Form{
Fields: []ui.Field{
ui.StringField{
Key: "Email",
Label: "Email",
},
ui.PasswordField{
Key: "Password",
Label: "Password",
},
},
}
if !term.IsTerminal(int(syscall.Stdin)) { //nolint:unconvert
c.globalState.Logger.Warn("Stdin is not a terminal, falling back to plain text input")
}
var vals map[string]string
vals, err = form.Run(c.globalState.Stdin, c.globalState.Stdout)
if err != nil {
return err
}
email := vals["Email"]
password := vals["Password"]

client := cloudapi.NewClient(
c.globalState.Logger,
"",
consolidatedCurrentConfig.Host.String,
consts.Version,
consolidatedCurrentConfig.Timeout.TimeDuration())

var res *cloudapi.LoginResponse
res, err = client.Login(email, password)
if err != nil {
return err
}

if res.Token == "" {
return errors.New("your account does not appear to have an active API token, please consult the " +
"Grafana k6 cloud documentation for instructions on how to generate " +
"one: https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication")
}

newCloudConf.Token = null.StringFrom(res.Token)
}

if currentDiskConf.Collectors == nil {
currentDiskConf.Collectors = make(map[string]json.RawMessage)
}
currentDiskConf.Collectors["cloud"], err = json.Marshal(newCloudConf)
if err != nil {
return err
}
if err := writeDiskConfig(c.globalState, currentDiskConf); err != nil {
return err
}

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
}
Loading
Loading