Skip to content

Commit

Permalink
Move cloud run and login functionalities under cloud subcommands (#3813)
Browse files Browse the repository at this point in the history
* Introduce a `k6 cloud run` command

Important notice: this commit declare a cobra sub-command holding the
logic for the `k6 cloud run` sub-command, but does not register it.

In this commit, we duplicate the logic from the existing `k6 cloud`
logic, with very little adjustments, to support the later registry of
the `k6 cloud run` command.

To simplify the collaboration on this and further reviews, we delegate
any refactoring of the original cloud command's logic, to a further
commit or Pull Request.

* Introduce a `k6 cloud login` command

Important notice: this commit declare a cobra sub-command holding the
logic for the `k6 cloud login` sub-command, but does not register it.

In this commit, we duplicate the logic from the existing `k6 login`
logic, with very little adjustments, to support the later registry of
the `k6 cloud login` command.

To simplify the collaboration on this and further reviews, we delegate
any refactoring of the original cloud command's logic, to a further
commit or Pull Request.

This new `k6 cloud login` command is notably focusing solely on
authenticating with the Grafana Cloud k6, and by design does not aim to
support InfluxDB authentication.

* Register run and login subcommands of the cloud command

* Add deprecation warning to k6 login and k6 login cloud commands

* FIXME add tests assert k6 cloud run command's arguments handling

* Apply Pull-Request suggestions

Co-authored-by: Joan López de la Franca Beltran <[email protected]>

* Improve cloud commands missing arguments handling

* Apply suggestions from code review

* Refactor cloud run command and tests to leverage existing code

* Apply suggestions from code review

Co-authored-by: Oleg Bespalov <[email protected]>

* fix lint of cmd package

---------

Co-authored-by: Joan López de la Franca Beltran <[email protected]>
Co-authored-by: Oleg Bespalov <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent d805563 commit aab3bc8
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 217 deletions.
72 changes: 62 additions & 10 deletions cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,22 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"

"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"go.k6.io/k6/cloudapi"
"go.k6.io/k6/cmd/state"
"go.k6.io/k6/errext"
"go.k6.io/k6/errext/exitcodes"
"go.k6.io/k6/lib"
"go.k6.io/k6/lib/consts"
"go.k6.io/k6/ui/pb"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

"go.k6.io/k6/cmd/state"
)

// cmdCloud handles the `k6 cloud` sub-command
Expand Down Expand Up @@ -117,7 +119,10 @@ func (c *cmdCloud) run(cmd *cobra.Command, args []string) error {
return err
}
if !cloudConfig.Token.Valid {
return errors.New("Not logged in, please use `k6 login cloud`.") //nolint:golint,revive,stylecheck
return errors.New( //nolint:golint
"not logged in, please login first to the Grafana Cloud k6 " +
"using the \"k6 cloud login\" command",
)
}

// Display config warning if needed
Expand Down Expand Up @@ -343,20 +348,67 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command {
}

exampleText := getExampleText(gs, `
{{.}} cloud script.js`[1:])
# [deprecated] Run a k6 script in the Grafana Cloud k6
$ {{.}} cloud script.js
# [deprecated] Run a k6 archive in the Grafana Cloud k6
$ {{.}} cloud archive.tar
# Authenticate with Grafana Cloud k6
$ {{.}} cloud login
# Run a k6 script in the Grafana Cloud k6
$ {{.}} cloud run script.js
# Run a k6 archive in the Grafana Cloud k6
$ {{.}} cloud run archive.tar`[1:])

cloudCmd := &cobra.Command{
Use: "cloud",
Short: "Run a test on the cloud",
Long: `Run a test on the cloud.
Long: `Run a test in the Grafana Cloud k6.
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"),
This will archive test script(s), including all necessary resources, and execute the test in the Grafana Cloud k6
service. Be sure to run the "k6 cloud login" command prior to authenticate with Grafana Cloud k6.`,
Args: exactCloudArgs(),
Deprecated: `the k6 team is in the process of modifying and deprecating the "k6 cloud" command behavior.
In the future, the "cloud" command will only display a help text, instead of running tests in the Grafana Cloud k6.
To run tests in the cloud, users are now invited to migrate to the "k6 cloud run" command instead.
`,
PreRunE: c.preRun,
RunE: c.run,
Example: exampleText,
}

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

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

return cloudCmd
}

func exactCloudArgs() cobra.PositionalArgs {
return func(_ *cobra.Command, args []string) error {
const baseErrMsg = `the "k6 cloud" command expects either a subcommand such as "run" or "login", or ` +
"a single argument consisting in a path to a script/archive, or the `-` symbol instructing " +
"the command to read the test content from stdin"

if len(args) == 0 {
return fmt.Errorf(baseErrMsg + "; " + "received no arguments")
}

hasSubcommand := args[0] == "run" || args[0] == "login"
if len(args) > 1 && !hasSubcommand {
return fmt.Errorf(
baseErrMsg+"; "+"received %d arguments %q, and %s is not a valid subcommand",
len(args), strings.Join(args, " "), args[0],
)
}

return nil
}
}
179 changes: 179 additions & 0 deletions cmd/cloud_login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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
# Reset the stored token
{{.}} cloud login -r`[1:])

loginCloudCommand := &cobra.Command{
Use: cloudLoginCommandName,
Short: "Authenticate with Grafana Cloud k6",
Long: `Authenticate with Grafana Cloud k6.
This command will authenticate you with Grafana Cloud k6.
Once authenticated you can start running tests in the cloud by using the "k6 cloud run"
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 stored 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{
Banner: "Please enter your Grafana Cloud k6 credentials",
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 Cloud k6 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
}
49 changes: 49 additions & 0 deletions cmd/cloud_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"github.com/spf13/cobra"
"go.k6.io/k6/cmd/state"
)

const cloudRunCommandName string = "run"

func getCmdCloudRun(gs *state.GlobalState) *cobra.Command {
deprecatedCloudCmd := &cmdCloud{
gs: gs,
showCloudLogs: true,
exitOnRunning: false,
uploadOnly: false,
}

exampleText := getExampleText(gs, `
# Run a test script in Grafana Cloud k6
$ {{.}} cloud run script.js
# Run a test archive in Grafana Cloud k6
$ {{.}} cloud run archive.tar
# Read a test script or archive from stdin and run it in Grafana Cloud k6
$ {{.}} cloud run - < script.js`[1:])

cloudRunCmd := &cobra.Command{
Use: cloudRunCommandName,
Short: "Run a test in Grafana Cloud k6",
Long: `Run a test in Grafana Cloud k6.
This will archive test script(s), including all necessary resources, and execute the test in the Grafana Cloud k6
service. Using this command requires to be authenticated against Grafana Cloud k6.
Use the "k6 cloud login" command to authenticate.`,
Example: exampleText,
Args: exactArgsWithMsg(1,
"the k6 cloud run command expects a single argument consisting in either a path to a script or "+
"archive file, or the \"-\" symbol indicating the script or archive should be read from stdin",
),
PreRunE: deprecatedCloudCmd.preRun,
RunE: deprecatedCloudCmd.run,
}

cloudRunCmd.Flags().SortFlags = false
cloudRunCmd.Flags().AddFlagSet(deprecatedCloudCmd.flagSet())

return cloudRunCmd
}
2 changes: 2 additions & 0 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func getCmdLogin(gs *state.GlobalState) *cobra.Command {
Logging into a service changes the default when just "-o [type]" is passed with
no parameters, you can always override the stored credentials by passing some
on the commandline.`,
Deprecated: `and will be removed in a future release. Please use the "k6 cloud login" command instead.
`,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Usage()
},
Expand Down
5 changes: 4 additions & 1 deletion cmd/login_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,14 @@ func getCmdLoginCloud(gs *state.GlobalState) *cobra.Command {
loginCloudCommand := &cobra.Command{
Use: "cloud",
Short: "Authenticate with k6 Cloud",
Long: `Authenticate with k6 Cloud",
Long: `Authenticate with Grafana Cloud k6.
This will set the default token used when just "k6 run -o cloud" is passed.`,
Example: exampleText,
Args: cobra.NoArgs,
Deprecated: `and will be removed in a future release.
Please use the "k6 cloud login" command instead.
`,
RunE: func(cmd *cobra.Command, _ []string) error {
currentDiskConf, err := readDiskConfig(gs)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions cmd/tests/cmd_cloud_run_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tests

import "testing"

func TestK6CloudRun(t *testing.T) {
t.Parallel()
runCloudTests(t, setupK6CloudRunCmd)
}

func setupK6CloudRunCmd(cliFlags []string) []string {
return append([]string{"k6", "cloud", "run"}, append(cliFlags, "test.js")...)
}
Loading

0 comments on commit aab3bc8

Please sign in to comment.