From 3bb9797bbe1ba627e2694e74c6f8cce46b35d98c Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 25 Jun 2024 17:06:15 +0200 Subject: [PATCH 01/11] 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. --- cmd/cloud_run.go | 376 ++++++++++++++++++++++++++++++++ cmd/tests/cmd_cloud_run_test.go | 248 +++++++++++++++++++++ 2 files changed, 624 insertions(+) create mode 100644 cmd/cloud_run.go create mode 100644 cmd/tests/cmd_cloud_run_test.go diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go new file mode 100644 index 00000000000..9fab789f2c5 --- /dev/null +++ b/cmd/cloud_run.go @@ -0,0 +1,376 @@ +package cmd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "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" +) + +// cmdCloudRun handles the `k6 cloud` sub-command +type cmdCloudRun struct { + gs *state.GlobalState + + showCloudLogs bool + exitOnRunning bool + uploadOnly bool +} + +func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { + c := &cmdCloudRun{ + gs: gs, + showCloudLogs: true, + exitOnRunning: false, + uploadOnly: false, + } + + exampleText := getExampleText(gs, ` + # Run a test in the Grafana k6 cloud + $ {{.}} cloud run script.js + + # Run a test in the Grafana k6 cloud with a specific token + $ {{.}} cloud run --token script.js`[1:]) + + // FIXME: when the command is "k6 cloud run" without an script/archive, we should display an error and the help + cloudRunCmd := &cobra.Command{ + Use: cloudRunCommandName, + Short: "Run a test in the Grafana k6 cloud", + Long: `Run a test in the Grafana k6 cloud. + +This will execute the test in the Grafana k6 cloud service. Using this command requires to be authenticated +against the Grafana k6 cloud. Use the "k6 cloud login" command to authenticate.`, + Example: exampleText, + Args: exactArgsWithMsg(1, "arg should either be \"-\", if reading script from stdin, or a path to a script file"), + PreRunE: c.preRun, + RunE: c.run, + } + + cloudRunCmd.Flags().SortFlags = false + cloudRunCmd.Flags().AddFlagSet(c.flagSet()) + + return cloudRunCmd +} + +//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete. +func (c *cmdCloudRun) preRun(cmd *cobra.Command, _ []string) error { + // TODO: refactor (https://github.com/loadimpact/k6/issues/883) + // + // We deliberately parse the env variables, to validate for wrong + // values, even if we don't subsequently use them (if the respective + // CLI flag was specified, since it has a higher priority). + if showCloudLogsEnv, ok := c.gs.Env["K6_SHOW_CLOUD_LOGS"]; ok { + showCloudLogsValue, err := strconv.ParseBool(showCloudLogsEnv) + if err != nil { + return fmt.Errorf("parsing K6_SHOW_CLOUD_LOGS returned an error: %w", err) + } + if !cmd.Flags().Changed("show-logs") { + c.showCloudLogs = showCloudLogsValue + } + } + + if exitOnRunningEnv, ok := c.gs.Env["K6_EXIT_ON_RUNNING"]; ok { + exitOnRunningValue, err := strconv.ParseBool(exitOnRunningEnv) + if err != nil { + return fmt.Errorf("parsing K6_EXIT_ON_RUNNING returned an error: %w", err) + } + if !cmd.Flags().Changed("exit-on-running") { + c.exitOnRunning = exitOnRunningValue + } + } + if uploadOnlyEnv, ok := c.gs.Env["K6_CLOUD_UPLOAD_ONLY"]; ok { + uploadOnlyValue, err := strconv.ParseBool(uploadOnlyEnv) + if err != nil { + return fmt.Errorf("parsing K6_CLOUD_UPLOAD_ONLY returned an error: %w", err) + } + if !cmd.Flags().Changed("upload-only") { + c.uploadOnly = uploadOnlyValue + } + } + + return nil +} + +// TODO: split apart some more +// +//nolint:funlen,gocognit,cyclop +func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error { + printBanner(c.gs) + + progressBar := pb.New( + pb.WithConstLeft("Init"), + pb.WithConstProgress(0, "Loading test script..."), + ) + printBar(c.gs, progressBar) + + test, err := loadAndConfigureLocalTest(c.gs, cmd, args, getPartialConfig) + if err != nil { + return err + } + + // It's important to NOT set the derived options back to the runner + // here, only the consolidated ones. Otherwise, if the script used + // an execution shortcut option (e.g. `iterations` or `duration`), + // we will have multiple conflicting execution options since the + // derivation will set `scenarios` as well. + testRunState, err := test.buildTestRunState(test.consolidatedConfig.Options) + if err != nil { + return err + } + + // TODO: validate for usage of execution segment + // TODO: validate for externally controlled executor (i.e. executors that aren't distributable) + // TODO: move those validations to a separate function and reuse validateConfig()? + + modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Building the archive...")) + arc := testRunState.Runner.MakeArchive() + + tmpCloudConfig, err := cloudapi.GetTemporaryCloudConfig(arc.Options.Cloud, arc.Options.External) + if err != nil { + return err + } + + // Cloud config + cloudConfig, warn, err := cloudapi.GetConsolidatedConfig( + test.derivedConfig.Collectors["cloud"], c.gs.Env, "", arc.Options.Cloud, arc.Options.External) + if err != nil { + return err + } + if !cloudConfig.Token.Valid { + return errors.New( //nolint:golint + "not logged in, please login to the Grafana k6 Cloud " + + "using the `k6 cloud login` command", + ) + } + + // Display config warning if needed + if warn != "" { + modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Warning: "+warn)) + } + + if cloudConfig.Token.Valid { + tmpCloudConfig["token"] = cloudConfig.Token + } + if cloudConfig.Name.Valid { + tmpCloudConfig["name"] = cloudConfig.Name + } + if cloudConfig.ProjectID.Valid { + tmpCloudConfig["projectID"] = cloudConfig.ProjectID + } + + if arc.Options.External == nil { + arc.Options.External = make(map[string]json.RawMessage) + } + + b, err := json.Marshal(tmpCloudConfig) + if err != nil { + return err + } + + arc.Options.Cloud = b + arc.Options.External[cloudapi.LegacyCloudConfigKey] = b + + name := cloudConfig.Name.String + if !cloudConfig.Name.Valid || cloudConfig.Name.String == "" { + name = filepath.Base(test.sourceRootPath) + } + + globalCtx, globalCancel := context.WithCancel(c.gs.Ctx) + defer globalCancel() + + logger := c.gs.Logger + + // Start cloud test run + modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Validating script options")) + client := cloudapi.NewClient( + logger, cloudConfig.Token.String, cloudConfig.Host.String, consts.Version, cloudConfig.Timeout.TimeDuration()) + if err = client.ValidateOptions(arc.Options); err != nil { + return err + } + + modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Uploading archive")) + + var cloudTestRun *cloudapi.CreateTestRunResponse + if c.uploadOnly { + cloudTestRun, err = client.UploadTestOnly(name, cloudConfig.ProjectID.Int64, arc) + } else { + cloudTestRun, err = client.StartCloudTestRun(name, cloudConfig.ProjectID.Int64, arc) + } + + if err != nil { + return err + } + + refID := cloudTestRun.ReferenceID + if cloudTestRun.ConfigOverride != nil { + cloudConfig = cloudConfig.Apply(*cloudTestRun.ConfigOverride) + } + + // Trap Interrupts, SIGINTs and SIGTERMs. + gracefulStop := func(sig os.Signal) { + logger.WithField("sig", sig).Print("Stopping cloud test run in response to signal...") + // Do this in a separate goroutine so that if it blocks, the + // second signal can still abort the process execution. + go func() { + stopErr := client.StopCloudTestRun(refID) + if stopErr != nil { + logger.WithError(stopErr).Error("Stop cloud test error") + } else { + logger.Info("Successfully sent signal to stop the cloud test, now waiting for it to actually stop...") + } + globalCancel() + }() + } + onHardStop := func(sig os.Signal) { + logger.WithField("sig", sig).Error("Aborting k6 in response to signal, we won't wait for the test to end.") + } + stopSignalHandling := handleTestAbortSignals(c.gs, gracefulStop, onHardStop) + defer stopSignalHandling() + + et, err := lib.NewExecutionTuple(test.derivedConfig.ExecutionSegment, test.derivedConfig.ExecutionSegmentSequence) + if err != nil { + return err + } + testURL := cloudapi.URLForResults(refID, cloudConfig) + executionPlan := test.derivedConfig.Scenarios.GetFullExecutionRequirements(et) + printExecutionDescription( + c.gs, "cloud", test.sourceRootPath, testURL, test.derivedConfig, et, executionPlan, nil, + ) + + modifyAndPrintBar( + c.gs, progressBar, + pb.WithConstLeft("Run "), pb.WithConstProgress(0, "Initializing the cloud test"), + ) + + progressCtx, progressCancel := context.WithCancel(globalCtx) + progressBarWG := &sync.WaitGroup{} + progressBarWG.Add(1) + defer progressBarWG.Wait() + defer progressCancel() + go func() { + showProgress(progressCtx, c.gs, []*pb.ProgressBar{progressBar}, logger) + progressBarWG.Done() + }() + + var ( + startTime time.Time + maxDuration time.Duration + ) + maxDuration, _ = lib.GetEndOffset(executionPlan) + + testProgressLock := &sync.Mutex{} + var testProgress *cloudapi.TestProgressResponse + progressBar.Modify( + pb.WithProgress(func() (float64, []string) { + testProgressLock.Lock() + defer testProgressLock.Unlock() + + if testProgress == nil { + return 0, []string{"Waiting..."} + } + + statusText := testProgress.RunStatusText + + if testProgress.RunStatus == cloudapi.RunStatusFinished { + testProgress.Progress = 1 + } else if testProgress.RunStatus == cloudapi.RunStatusRunning { + if startTime.IsZero() { + startTime = time.Now() + } + spent := time.Since(startTime) + if spent > maxDuration { + statusText = maxDuration.String() + } else { + statusText = fmt.Sprintf("%s/%s", pb.GetFixedLengthDuration(spent, maxDuration), maxDuration) + } + } + + return testProgress.Progress, []string{statusText} + }), + ) + + ticker := time.NewTicker(time.Millisecond * 2000) + if c.showCloudLogs { + go func() { + logger.Debug("Connecting to cloud logs server...") + if err := cloudConfig.StreamLogsToLogger(globalCtx, logger, refID, 0); err != nil { + logger.WithError(err).Error("error while tailing cloud logs") + } + }() + } + + for range ticker.C { + newTestProgress, progressErr := client.GetTestProgress(refID) + if progressErr != nil { + logger.WithError(progressErr).Error("Test progress error") + continue + } + + testProgressLock.Lock() + testProgress = newTestProgress + testProgressLock.Unlock() + + if (newTestProgress.RunStatus > cloudapi.RunStatusRunning) || + (c.exitOnRunning && newTestProgress.RunStatus == cloudapi.RunStatusRunning) { + globalCancel() + break + } + } + + if testProgress == nil { + //nolint:stylecheck,golint + return errext.WithExitCodeIfNone(errors.New("Test progress error"), exitcodes.CloudFailedToGetProgress) + } + + if !c.gs.Flags.Quiet { + valueColor := getColor(c.gs.Flags.NoColor || !c.gs.Stdout.IsTTY, color.FgCyan) + printToStdout(c.gs, fmt.Sprintf( + " test status: %s\n", valueColor.Sprint(testProgress.RunStatusText), + )) + } else { + logger.WithField("run_status", testProgress.RunStatusText).Debug("Test finished") + } + + if testProgress.ResultStatus == cloudapi.ResultStatusFailed { + // TODO: use different exit codes for failed thresholds vs failed test (e.g. aborted by system/limit) + //nolint:stylecheck,golint + return errext.WithExitCodeIfNone(errors.New("The test has failed"), exitcodes.CloudTestRunFailed) + } + + return nil +} + +func (c *cmdCloudRun) flagSet() *pflag.FlagSet { + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + flags.SortFlags = false + flags.AddFlagSet(optionFlagSet()) + flags.AddFlagSet(runtimeOptionFlagSet(false)) + + // TODO: Figure out a better way to handle the CLI flags + flags.BoolVar(&c.exitOnRunning, "exit-on-running", c.exitOnRunning, + "exits when test reaches the running status") + 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") + + return flags +} + +const cloudRunCommandName string = "run" diff --git a/cmd/tests/cmd_cloud_run_test.go b/cmd/tests/cmd_cloud_run_test.go new file mode 100644 index 00000000000..f41378c2884 --- /dev/null +++ b/cmd/tests/cmd_cloud_run_test.go @@ -0,0 +1,248 @@ +package tests + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.k6.io/k6/cloudapi" + "go.k6.io/k6/cmd" + "go.k6.io/k6/lib/fsext" + "go.k6.io/k6/lib/testutils" +) + +func TestCloudRunNotLoggedIn(t *testing.T) { + t.Parallel() + + ts := getSimpleCloudRunTestState(t, nil, nil, nil, nil) + delete(ts.Env, "K6_CLOUD_TOKEN") + ts.ExpectedExitCode = -1 // TODO: use a more specific exit code? + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `not logged in`) +} + +func TestCloudRunLoggedInWithScriptToken(t *testing.T) { + t.Parallel() + + script := ` + export let options = { + ext: { + loadimpact: { + token: "asdf", + name: "my load test", + projectID: 124, + note: 124, + }, + } + }; + + export default function() {}; + ` + + ts := getSimpleCloudRunTestState(t, []byte(script), nil, nil, nil) + delete(ts.Env, "K6_CLOUD_TOKEN") + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.NotContains(t, stdout, `Not logged in`) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Finished`) +} + +func TestCloudRunExitOnRunning(t *testing.T) { + t.Parallel() + + cs := func() cloudapi.TestProgressResponse { + return cloudapi.TestProgressResponse{ + RunStatusText: "Running", + RunStatus: cloudapi.RunStatusRunning, + } + } + + ts := getSimpleCloudRunTestState(t, nil, []string{"--exit-on-running", "--log-output=stdout"}, nil, cs) + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Running`) +} + +func TestCloudRunUploadOnly(t *testing.T) { + t.Parallel() + + cs := func() cloudapi.TestProgressResponse { + return cloudapi.TestProgressResponse{ + RunStatusText: "Archived", + RunStatus: cloudapi.RunStatusArchived, + } + } + + ts := getSimpleCloudRunTestState(t, nil, []string{"--upload-only", "--log-output=stdout"}, nil, cs) + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Archived`) +} + +func TestCloudRunWithConfigOverride(t *testing.T) { + t.Parallel() + + configOverride := http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { + resp.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(resp, `{ + "reference_id": "123", + "config": { + "webAppURL": "https://bogus.url", + "testRunDetails": "something from the cloud" + }, + "logs": [ + {"level": "invalid", "message": "test debug message"}, + {"level": "warning", "message": "test warning"}, + {"level": "error", "message": "test error"} + ] + }`) + assert.NoError(t, err) + }) + ts := getSimpleCloudRunTestState(t, nil, nil, configOverride, nil) + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, "execution: cloud") + assert.Contains(t, stdout, "output: something from the cloud") + assert.Contains(t, stdout, `level=debug msg="invalid message level 'invalid' for message 'test debug message'`) + assert.Contains(t, stdout, `level=error msg="test debug message" source=grafana-k6-cloud`) + assert.Contains(t, stdout, `level=warning msg="test warning" source=grafana-k6-cloud`) + assert.Contains(t, stdout, `level=error msg="test error" source=grafana-k6-cloud`) +} + +// TestCloudRunWithArchive tests that if k6 uses a static archive with the script inside that has cloud options like: +// +// export let options = { +// ext: { +// loadimpact: { +// name: "my load test", +// projectID: 124, +// note: "lorem ipsum", +// }, +// } +// }; +// +// actually sends to the cloud the archive with the correct metadata (metadata.json), like: +// +// "ext": { +// "loadimpact": { +// "name": "my load test", +// "note": "lorem ipsum", +// "projectID": 124 +// } +// } +func TestCloudRunWithArchive(t *testing.T) { + t.Parallel() + + testRunID := 123 + ts := NewGlobalTestState(t) + + archiveUpload := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + // check the archive + file, _, err := req.FormFile("file") + assert.NoError(t, err) + assert.NotNil(t, file) + + // temporary write the archive for file system + data, err := io.ReadAll(file) + assert.NoError(t, err) + + tmpPath := filepath.Join(ts.Cwd, "archive_to_cloud.tar") + require.NoError(t, fsext.WriteFile(ts.FS, tmpPath, data, 0o644)) + + // check what inside + require.NoError(t, testutils.Untar(t, ts.FS, tmpPath, "tmp/")) + + metadataRaw, err := fsext.ReadFile(ts.FS, "tmp/metadata.json") + require.NoError(t, err) + + metadata := struct { + Options struct { + Cloud struct { + Name string `json:"name"` + Note string `json:"note"` + ProjectID int `json:"projectID"` + } `json:"cloud"` + } `json:"options"` + }{} + + // then unpacked metadata should not contain any environment variables passed at the moment of archive creation + require.NoError(t, json.Unmarshal(metadataRaw, &metadata)) + require.Equal(t, "my load test", metadata.Options.Cloud.Name) + require.Equal(t, "lorem ipsum", metadata.Options.Cloud.Note) + require.Equal(t, 124, metadata.Options.Cloud.ProjectID) + + // respond with the test run ID + resp.WriteHeader(http.StatusOK) + _, err = fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) + assert.NoError(t, err) + }) + + srv := getMockCloud(t, testRunID, archiveUpload, nil) + + data, err := os.ReadFile(filepath.Join("testdata/archives", "archive_v0.46.0_with_loadimpact_option.tar")) //nolint:forbidigo // it's a test + require.NoError(t, err) + + require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "archive.tar"), data, 0o644)) + + ts.CmdArgs = []string{"k6", "cloud", "--verbose", "--log-output=stdout", "archive.tar"} + ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet + ts.Env["K6_CLOUD_HOST"] = srv.URL + ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud + + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.NotContains(t, stdout, `Not logged in`) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `hello world from archive`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Finished`) +} + +func getSimpleCloudRunTestState( + t *testing.T, script []byte, cliFlags []string, + archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse, +) *GlobalTestState { + if script == nil { + script = []byte(`export default function() {}`) + } + + if cliFlags == nil { + cliFlags = []string{"--verbose", "--log-output=stdout"} + } + + srv := getMockCloud(t, 123, archiveUpload, progressCallback) + + ts := NewGlobalTestState(t) + require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), script, 0o644)) + ts.CmdArgs = append(append([]string{"k6", "cloud", "run"}, cliFlags...), "test.js") + ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet + ts.Env["K6_CLOUD_HOST"] = srv.URL + ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud + + return ts +} From c487a7002a85d8bf3158683cb24d2436c93d7af2 Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 25 Jun 2024 17:08:07 +0200 Subject: [PATCH 02/11] 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. --- cmd/cloud_login.go | 175 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 cmd/cloud_login.go diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go new file mode 100644 index 00000000000..3ca508e6d19 --- /dev/null +++ b/cmd/cloud_login.go @@ -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 + + # 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, ¤tJSONConfig); 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 +} From 42f9de46642fe4f9ebafe7944fdc2de92d5ff803 Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 25 Jun 2024 17:10:15 +0200 Subject: [PATCH 03/11] Register run and login subcommands of the cloud command --- cmd/cloud.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 53b7139d76d..548f23f89a3 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -33,6 +33,7 @@ type cmdCloud struct { uploadOnly bool } +//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete. func (c *cmdCloud) preRun(cmd *cobra.Command, _ []string) error { // TODO: refactor (https://github.com/loadimpact/k6/issues/883) // @@ -117,7 +118,7 @@ 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("Not logged in, please use `k6 cloud login`") //nolint:golint,stylecheck } // Display config warning if needed @@ -343,20 +344,69 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { } 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 + + # [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.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 + } +} From 51427e1590ac76dd39e3914544405180f2a31a02 Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 25 Jun 2024 17:10:47 +0200 Subject: [PATCH 04/11] Add deprecation warning to k6 login and k6 login cloud commands --- cmd/login.go | 5 ++++- cmd/login_cloud.go | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index 8965998ef40..c9d6de7e507 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -11,7 +11,10 @@ func getCmdLogin(gs *state.GlobalState) *cobra.Command { loginCmd := &cobra.Command{ Use: "login", Short: "Authenticate with a service", - Long: `Authenticate with a service. + Long: `[deprecation notice] +This command is deprecated and will be removed in a future release. Please use the "k6 cloud login" command instead. + +Authenticate with a service. 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 diff --git a/cmd/login_cloud.go b/cmd/login_cloud.go index b1438f3371c..6440c63486f 100644 --- a/cmd/login_cloud.go +++ b/cmd/login_cloud.go @@ -33,7 +33,11 @@ func getCmdLoginCloud(gs *state.GlobalState) *cobra.Command { loginCloudCommand := &cobra.Command{ Use: "cloud", Short: "Authenticate with k6 Cloud", - Long: `Authenticate with k6 Cloud", + Long: `[deprecation notice] +This command is deprecated and will be removed in a future release. Please use the +"k6 cloud login" command instead. + +Authenticate with Grafana Cloud k6. This will set the default token used when just "k6 run -o cloud" is passed.`, Example: exampleText, From 40632285357adb78abdfc9d837f6e13523ceb93a Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 25 Jun 2024 17:12:36 +0200 Subject: [PATCH 05/11] FIXME add tests assert k6 cloud run command's arguments handling --- cmd/tests/cmd_cloud_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cmd/tests/cmd_cloud_test.go b/cmd/tests/cmd_cloud_test.go index a8e76e38006..1032906da56 100644 --- a/cmd/tests/cmd_cloud_test.go +++ b/cmd/tests/cmd_cloud_test.go @@ -288,3 +288,17 @@ func TestCloudWithArchive(t *testing.T) { assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) assert.Contains(t, stdout, `test status: Finished`) } + +// FIXME: This test fails because our test setup leaves stdout empty +//func TestCloudArgs(t *testing.T) { +// t.Parallel() +// +// // ts := NewGlobalTestState(t) +// ts := getSimpleCloudTestState(t, nil, nil, nil, nil) +// ts.CmdArgs = []string{"k6", "cloud", "run"} +// ts.ExpectedExitCode = -1 +// cmd.ExecuteWithGlobalState(ts.GlobalState) +// +// stdout := ts.Stdout.String() +// assert.Contains(t, stdout, `accepts 1 or 2 arg(s), received 0`) +//} From 231f50e35eb566308a3ffc9bd9460ac08d8c6d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Crevon?= Date: Wed, 17 Jul 2024 09:20:43 +0200 Subject: [PATCH 06/11] Apply Pull-Request suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joan López de la Franca Beltran <5459617+joanlopez@users.noreply.github.com> --- cmd/cloud.go | 49 ++++++++++++++----------------------- cmd/cloud_login.go | 15 +++++++----- cmd/cloud_run.go | 29 +++++++++++++--------- cmd/login.go | 3 +++ cmd/tests/cmd_cloud_test.go | 14 ----------- 5 files changed, 48 insertions(+), 62 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 548f23f89a3..93768734e03 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -33,7 +33,7 @@ type cmdCloud struct { uploadOnly bool } -//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete. +//nolint:dupl // function duplicated from the deprecated `k6 cloud` command, stmt can go when the command is remove func (c *cmdCloud) preRun(cmd *cobra.Command, _ []string) error { // TODO: refactor (https://github.com/loadimpact/k6/issues/883) // @@ -344,33 +344,34 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { } exampleText := getExampleText(gs, ` - # Authenticate with Grafana k6 Cloud + # Authenticate with Grafana Cloud k6 $ {{.}} cloud login - # Run a k6 script in the Grafana k6 cloud + # Run a k6 script in the Grafana Cloud k6 $ {{.}} cloud run script.js - # Run a k6 archive in the Grafana k6 cloud + # Run a k6 archive in the Grafana Cloud k6 $ {{.}} cloud run archive.tar - # [deprecated] Run a k6 script in the Grafana k6 cloud + # [deprecated] Run a k6 script in the Grafana Cloud k6 $ {{.}} cloud script.js - # [deprecated] Run a k6 archive in the Grafana k6 cloud + # [deprecated] Run a k6 archive in the Grafana Cloud k6 $ {{.}} cloud archive.tar`[1:]) cloudCmd := &cobra.Command{ Use: "cloud", Short: "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. + Long: `Run a test archive in the Grafana Cloud k6. -Run a test on the cloud. +This will 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. -This will execute the test on the k6 cloud service. Use "k6 login cloud" to authenticate.`, - Args: exactCloudArgs(), +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, @@ -388,23 +389,11 @@ This will execute the test on the k6 cloud service. Use "k6 login cloud" to auth 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]) + if len(args) == 0 { + return fmt.Errorf( + "the k6 cloud command accepts 1 argument consisting in either in "+ + "a subcommand such as `run` or `cloud`, or the path to a script/archive, or "+ + "the `-` symbol, received: %d arguments instead", len(args)) } return nil diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 3ca508e6d19..1981d1913dc 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -37,15 +37,18 @@ func getCmdCloudLogin(gs *state.GlobalState) *cobra.Command { {{.}} cloud login -t # Display the stored token. - {{.}} cloud login -s`[1:]) + {{.}} cloud login -s + + # Reset the stored token. + {{.}} cloud login -r`[1:]) loginCloudCommand := &cobra.Command{ Use: cloudLoginCommandName, - Short: "Authenticate with Grafana k6 Cloud", + Short: "Authenticate with Grafana Cloud k6", 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" +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. `, @@ -56,7 +59,7 @@ the "k6 run -o cloud" command. 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") + loginCloudCommand.Flags().BoolP("reset", "r", false, "reset stored token") return loginCloudCommand } @@ -144,7 +147,7 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { 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 " + + "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") } diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go index 9fab789f2c5..06adb4ef7d4 100644 --- a/cmd/cloud_run.go +++ b/cmd/cloud_run.go @@ -24,7 +24,7 @@ import ( "go.k6.io/k6/ui/pb" ) -// cmdCloudRun handles the `k6 cloud` sub-command +// cmdCloudRun handles the `k6 cloud run` sub-command type cmdCloudRun struct { gs *state.GlobalState @@ -42,22 +42,27 @@ func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { } exampleText := getExampleText(gs, ` - # Run a test in the Grafana k6 cloud + # Run a test script in Grafana Cloud k6 $ {{.}} cloud run script.js - # Run a test in the Grafana k6 cloud with a specific token - $ {{.}} cloud run --token script.js`[1:]) + # 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:]) - // FIXME: when the command is "k6 cloud run" without an script/archive, we should display an error and the help cloudRunCmd := &cobra.Command{ Use: cloudRunCommandName, - Short: "Run a test in the Grafana k6 cloud", - Long: `Run a test in the Grafana k6 cloud. + Short: "Run a test in Grafana Cloud k6", + Long: `Run a test in Grafana Cloud k6. -This will execute the test in the Grafana k6 cloud service. Using this command requires to be authenticated -against the Grafana k6 cloud. Use the "k6 cloud login" command to authenticate.`, +This will 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, "arg should either be \"-\", if reading script from stdin, or a path to a script file"), + 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: c.preRun, RunE: c.run, } @@ -68,7 +73,7 @@ against the Grafana k6 cloud. Use the "k6 cloud login" command to authenticate.` return cloudRunCmd } -//nolint:dupl // remove this statement once the migration from the `k6 cloud` to the `k6 cloud run` is complete. +//nolint:dupl // function duplicated from the deprecated `k6 cloud` command, stmt can go when the command is remove func (c *cmdCloudRun) preRun(cmd *cobra.Command, _ []string) error { // TODO: refactor (https://github.com/loadimpact/k6/issues/883) // @@ -154,7 +159,7 @@ func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error { } if !cloudConfig.Token.Valid { return errors.New( //nolint:golint - "not logged in, please login to the Grafana k6 Cloud " + + "not logged in, please login to the Grafana Cloud k6 " + "using the `k6 cloud login` command", ) } diff --git a/cmd/login.go b/cmd/login.go index c9d6de7e507..e5fda883901 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -19,6 +19,9 @@ Authenticate with a service. 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: `This command is 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() }, diff --git a/cmd/tests/cmd_cloud_test.go b/cmd/tests/cmd_cloud_test.go index 1032906da56..a8e76e38006 100644 --- a/cmd/tests/cmd_cloud_test.go +++ b/cmd/tests/cmd_cloud_test.go @@ -288,17 +288,3 @@ func TestCloudWithArchive(t *testing.T) { assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) assert.Contains(t, stdout, `test status: Finished`) } - -// FIXME: This test fails because our test setup leaves stdout empty -//func TestCloudArgs(t *testing.T) { -// t.Parallel() -// -// // ts := NewGlobalTestState(t) -// ts := getSimpleCloudTestState(t, nil, nil, nil, nil) -// ts.CmdArgs = []string{"k6", "cloud", "run"} -// ts.ExpectedExitCode = -1 -// cmd.ExecuteWithGlobalState(ts.GlobalState) -// -// stdout := ts.Stdout.String() -// assert.Contains(t, stdout, `accepts 1 or 2 arg(s), received 0`) -//} From 972219e8fecd8ee0eef60b89fe5687d71ee82ba4 Mon Sep 17 00:00:00 2001 From: oleiade Date: Fri, 19 Jul 2024 15:56:09 +0200 Subject: [PATCH 07/11] Improve cloud commands missing arguments handling --- cmd/cloud.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 93768734e03..add72fdffd3 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "sync" "time" @@ -367,8 +368,8 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { This will 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. + 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. `, @@ -389,11 +390,20 @@ To run tests in the cloud, users are now invited to migrate to the "k6 cloud run 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 := len(args) >= 1 && (args[0] == "run" || args[0] == "login") + if !hasSubcommand { return fmt.Errorf( - "the k6 cloud command accepts 1 argument consisting in either in "+ - "a subcommand such as `run` or `cloud`, or the path to a script/archive, or "+ - "the `-` symbol, received: %d arguments instead", len(args)) + baseErrMsg+"; "+"received %d arguments %q, and none of them is a subcommand", + len(args), strings.Join(args, " "), + ) } return nil From 396523935459dc48c6c38abf6e485bec8f39cee0 Mon Sep 17 00:00:00 2001 From: oleiade Date: Fri, 19 Jul 2024 16:00:23 +0200 Subject: [PATCH 08/11] Apply suggestions from code review --- cmd/cloud.go | 10 +++++----- cmd/cloud_login.go | 8 ++++---- cmd/login.go | 8 ++------ cmd/login_cloud.go | 9 ++++----- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index add72fdffd3..b04a36865bf 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -390,7 +390,7 @@ To run tests in the cloud, users are now invited to migrate to the "k6 cloud run 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 " + + 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" @@ -398,11 +398,11 @@ func exactCloudArgs() cobra.PositionalArgs { return fmt.Errorf(baseErrMsg + "; " + "received no arguments") } - hasSubcommand := len(args) >= 1 && (args[0] == "run" || args[0] == "login") - if !hasSubcommand { + hasSubcommand := args[0] == "run" || args[0] == "login" + if len(args) > 1 && !hasSubcommand { return fmt.Errorf( - baseErrMsg+"; "+"received %d arguments %q, and none of them is a subcommand", - len(args), strings.Join(args, " "), + baseErrMsg+"; "+"received %d arguments %q, and %s is not a valid subcommand", + len(args), strings.Join(args, " "), args[0], ) } diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 1981d1913dc..cbba559426f 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -30,16 +30,16 @@ func getCmdCloudLogin(gs *state.GlobalState) *cobra.Command { // loginCloudCommand represents the 'cloud login' command exampleText := getExampleText(gs, ` - # Log in with an email/password. + # Log in with an email/password {{.}} cloud login - # Store a token in k6's persistent configuration. + # Store a token in k6's persistent configuration {{.}} cloud login -t - # Display the stored token. + # Display the stored token {{.}} cloud login -s - # Reset the stored token. + # Reset the stored token {{.}} cloud login -r`[1:]) loginCloudCommand := &cobra.Command{ diff --git a/cmd/login.go b/cmd/login.go index e5fda883901..d0b1c43301d 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -11,16 +11,12 @@ func getCmdLogin(gs *state.GlobalState) *cobra.Command { loginCmd := &cobra.Command{ Use: "login", Short: "Authenticate with a service", - Long: `[deprecation notice] -This command is deprecated and will be removed in a future release. Please use the "k6 cloud login" command instead. - -Authenticate with a service. + Long: `Authenticate with a service. 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: `This command is deprecated and will be removed in a future release. -Please use the "k6 cloud login" command instead. + 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() diff --git a/cmd/login_cloud.go b/cmd/login_cloud.go index 6440c63486f..0aadf7fb749 100644 --- a/cmd/login_cloud.go +++ b/cmd/login_cloud.go @@ -33,15 +33,14 @@ func getCmdLoginCloud(gs *state.GlobalState) *cobra.Command { loginCloudCommand := &cobra.Command{ Use: "cloud", Short: "Authenticate with k6 Cloud", - Long: `[deprecation notice] -This command is deprecated and will be removed in a future release. Please use the -"k6 cloud login" command instead. - -Authenticate with Grafana Cloud k6. + 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 { From 2b0bc1f7b17712a77e0951dc41094d001dcf51af Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 23 Jul 2024 14:52:48 +0200 Subject: [PATCH 09/11] Refactor cloud run command and tests to leverage existing code --- cmd/cloud.go | 29 ++- cmd/cloud_run.go | 343 +------------------------- cmd/tests/cmd_cloud_run_test.go | 246 +----------------- cmd/tests/cmd_cloud_test.go | 424 ++++++++++++++++---------------- 4 files changed, 244 insertions(+), 798 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index b04a36865bf..640b0a7e9ed 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -13,16 +13,17 @@ import ( "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 @@ -34,7 +35,6 @@ type cmdCloud struct { uploadOnly bool } -//nolint:dupl // function duplicated from the deprecated `k6 cloud` command, stmt can go when the command is remove func (c *cmdCloud) preRun(cmd *cobra.Command, _ []string) error { // TODO: refactor (https://github.com/loadimpact/k6/issues/883) // @@ -119,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 cloud login`") //nolint:golint,stylecheck + return errors.New( //nolint:golint + "not logged in, please login to the Grafana Cloud k6 " + + "using the \"k6 cloud login\" command", + ) } // Display config warning if needed @@ -345,6 +348,12 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { } exampleText := getExampleText(gs, ` + # [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 @@ -352,13 +361,7 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { $ {{.}} cloud run script.js # Run a k6 archive in the Grafana Cloud k6 - $ {{.}} cloud run archive.tar - - # [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`[1:]) + $ {{.}} cloud run archive.tar`[1:]) cloudCmd := &cobra.Command{ Use: "cloud", diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go index 06adb4ef7d4..82bfab570a9 100644 --- a/cmd/cloud_run.go +++ b/cmd/cloud_run.go @@ -1,40 +1,14 @@ package cmd import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "strconv" - "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" ) -// cmdCloudRun handles the `k6 cloud run` sub-command -type cmdCloudRun struct { - gs *state.GlobalState - - showCloudLogs bool - exitOnRunning bool - uploadOnly bool -} +const cloudRunCommandName string = "run" func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { - c := &cmdCloudRun{ + deprecatedCloudCmd := &cmdCloud{ gs: gs, showCloudLogs: true, exitOnRunning: false, @@ -63,319 +37,12 @@ against Grafana Cloud k6. Use the "k6 cloud login" command to authenticate.`, "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: c.preRun, - RunE: c.run, + PreRunE: deprecatedCloudCmd.preRun, + RunE: deprecatedCloudCmd.run, } cloudRunCmd.Flags().SortFlags = false - cloudRunCmd.Flags().AddFlagSet(c.flagSet()) + cloudRunCmd.Flags().AddFlagSet(deprecatedCloudCmd.flagSet()) return cloudRunCmd } - -//nolint:dupl // function duplicated from the deprecated `k6 cloud` command, stmt can go when the command is remove -func (c *cmdCloudRun) preRun(cmd *cobra.Command, _ []string) error { - // TODO: refactor (https://github.com/loadimpact/k6/issues/883) - // - // We deliberately parse the env variables, to validate for wrong - // values, even if we don't subsequently use them (if the respective - // CLI flag was specified, since it has a higher priority). - if showCloudLogsEnv, ok := c.gs.Env["K6_SHOW_CLOUD_LOGS"]; ok { - showCloudLogsValue, err := strconv.ParseBool(showCloudLogsEnv) - if err != nil { - return fmt.Errorf("parsing K6_SHOW_CLOUD_LOGS returned an error: %w", err) - } - if !cmd.Flags().Changed("show-logs") { - c.showCloudLogs = showCloudLogsValue - } - } - - if exitOnRunningEnv, ok := c.gs.Env["K6_EXIT_ON_RUNNING"]; ok { - exitOnRunningValue, err := strconv.ParseBool(exitOnRunningEnv) - if err != nil { - return fmt.Errorf("parsing K6_EXIT_ON_RUNNING returned an error: %w", err) - } - if !cmd.Flags().Changed("exit-on-running") { - c.exitOnRunning = exitOnRunningValue - } - } - if uploadOnlyEnv, ok := c.gs.Env["K6_CLOUD_UPLOAD_ONLY"]; ok { - uploadOnlyValue, err := strconv.ParseBool(uploadOnlyEnv) - if err != nil { - return fmt.Errorf("parsing K6_CLOUD_UPLOAD_ONLY returned an error: %w", err) - } - if !cmd.Flags().Changed("upload-only") { - c.uploadOnly = uploadOnlyValue - } - } - - return nil -} - -// TODO: split apart some more -// -//nolint:funlen,gocognit,cyclop -func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error { - printBanner(c.gs) - - progressBar := pb.New( - pb.WithConstLeft("Init"), - pb.WithConstProgress(0, "Loading test script..."), - ) - printBar(c.gs, progressBar) - - test, err := loadAndConfigureLocalTest(c.gs, cmd, args, getPartialConfig) - if err != nil { - return err - } - - // It's important to NOT set the derived options back to the runner - // here, only the consolidated ones. Otherwise, if the script used - // an execution shortcut option (e.g. `iterations` or `duration`), - // we will have multiple conflicting execution options since the - // derivation will set `scenarios` as well. - testRunState, err := test.buildTestRunState(test.consolidatedConfig.Options) - if err != nil { - return err - } - - // TODO: validate for usage of execution segment - // TODO: validate for externally controlled executor (i.e. executors that aren't distributable) - // TODO: move those validations to a separate function and reuse validateConfig()? - - modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Building the archive...")) - arc := testRunState.Runner.MakeArchive() - - tmpCloudConfig, err := cloudapi.GetTemporaryCloudConfig(arc.Options.Cloud, arc.Options.External) - if err != nil { - return err - } - - // Cloud config - cloudConfig, warn, err := cloudapi.GetConsolidatedConfig( - test.derivedConfig.Collectors["cloud"], c.gs.Env, "", arc.Options.Cloud, arc.Options.External) - if err != nil { - return err - } - if !cloudConfig.Token.Valid { - return errors.New( //nolint:golint - "not logged in, please login to the Grafana Cloud k6 " + - "using the `k6 cloud login` command", - ) - } - - // Display config warning if needed - if warn != "" { - modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Warning: "+warn)) - } - - if cloudConfig.Token.Valid { - tmpCloudConfig["token"] = cloudConfig.Token - } - if cloudConfig.Name.Valid { - tmpCloudConfig["name"] = cloudConfig.Name - } - if cloudConfig.ProjectID.Valid { - tmpCloudConfig["projectID"] = cloudConfig.ProjectID - } - - if arc.Options.External == nil { - arc.Options.External = make(map[string]json.RawMessage) - } - - b, err := json.Marshal(tmpCloudConfig) - if err != nil { - return err - } - - arc.Options.Cloud = b - arc.Options.External[cloudapi.LegacyCloudConfigKey] = b - - name := cloudConfig.Name.String - if !cloudConfig.Name.Valid || cloudConfig.Name.String == "" { - name = filepath.Base(test.sourceRootPath) - } - - globalCtx, globalCancel := context.WithCancel(c.gs.Ctx) - defer globalCancel() - - logger := c.gs.Logger - - // Start cloud test run - modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Validating script options")) - client := cloudapi.NewClient( - logger, cloudConfig.Token.String, cloudConfig.Host.String, consts.Version, cloudConfig.Timeout.TimeDuration()) - if err = client.ValidateOptions(arc.Options); err != nil { - return err - } - - modifyAndPrintBar(c.gs, progressBar, pb.WithConstProgress(0, "Uploading archive")) - - var cloudTestRun *cloudapi.CreateTestRunResponse - if c.uploadOnly { - cloudTestRun, err = client.UploadTestOnly(name, cloudConfig.ProjectID.Int64, arc) - } else { - cloudTestRun, err = client.StartCloudTestRun(name, cloudConfig.ProjectID.Int64, arc) - } - - if err != nil { - return err - } - - refID := cloudTestRun.ReferenceID - if cloudTestRun.ConfigOverride != nil { - cloudConfig = cloudConfig.Apply(*cloudTestRun.ConfigOverride) - } - - // Trap Interrupts, SIGINTs and SIGTERMs. - gracefulStop := func(sig os.Signal) { - logger.WithField("sig", sig).Print("Stopping cloud test run in response to signal...") - // Do this in a separate goroutine so that if it blocks, the - // second signal can still abort the process execution. - go func() { - stopErr := client.StopCloudTestRun(refID) - if stopErr != nil { - logger.WithError(stopErr).Error("Stop cloud test error") - } else { - logger.Info("Successfully sent signal to stop the cloud test, now waiting for it to actually stop...") - } - globalCancel() - }() - } - onHardStop := func(sig os.Signal) { - logger.WithField("sig", sig).Error("Aborting k6 in response to signal, we won't wait for the test to end.") - } - stopSignalHandling := handleTestAbortSignals(c.gs, gracefulStop, onHardStop) - defer stopSignalHandling() - - et, err := lib.NewExecutionTuple(test.derivedConfig.ExecutionSegment, test.derivedConfig.ExecutionSegmentSequence) - if err != nil { - return err - } - testURL := cloudapi.URLForResults(refID, cloudConfig) - executionPlan := test.derivedConfig.Scenarios.GetFullExecutionRequirements(et) - printExecutionDescription( - c.gs, "cloud", test.sourceRootPath, testURL, test.derivedConfig, et, executionPlan, nil, - ) - - modifyAndPrintBar( - c.gs, progressBar, - pb.WithConstLeft("Run "), pb.WithConstProgress(0, "Initializing the cloud test"), - ) - - progressCtx, progressCancel := context.WithCancel(globalCtx) - progressBarWG := &sync.WaitGroup{} - progressBarWG.Add(1) - defer progressBarWG.Wait() - defer progressCancel() - go func() { - showProgress(progressCtx, c.gs, []*pb.ProgressBar{progressBar}, logger) - progressBarWG.Done() - }() - - var ( - startTime time.Time - maxDuration time.Duration - ) - maxDuration, _ = lib.GetEndOffset(executionPlan) - - testProgressLock := &sync.Mutex{} - var testProgress *cloudapi.TestProgressResponse - progressBar.Modify( - pb.WithProgress(func() (float64, []string) { - testProgressLock.Lock() - defer testProgressLock.Unlock() - - if testProgress == nil { - return 0, []string{"Waiting..."} - } - - statusText := testProgress.RunStatusText - - if testProgress.RunStatus == cloudapi.RunStatusFinished { - testProgress.Progress = 1 - } else if testProgress.RunStatus == cloudapi.RunStatusRunning { - if startTime.IsZero() { - startTime = time.Now() - } - spent := time.Since(startTime) - if spent > maxDuration { - statusText = maxDuration.String() - } else { - statusText = fmt.Sprintf("%s/%s", pb.GetFixedLengthDuration(spent, maxDuration), maxDuration) - } - } - - return testProgress.Progress, []string{statusText} - }), - ) - - ticker := time.NewTicker(time.Millisecond * 2000) - if c.showCloudLogs { - go func() { - logger.Debug("Connecting to cloud logs server...") - if err := cloudConfig.StreamLogsToLogger(globalCtx, logger, refID, 0); err != nil { - logger.WithError(err).Error("error while tailing cloud logs") - } - }() - } - - for range ticker.C { - newTestProgress, progressErr := client.GetTestProgress(refID) - if progressErr != nil { - logger.WithError(progressErr).Error("Test progress error") - continue - } - - testProgressLock.Lock() - testProgress = newTestProgress - testProgressLock.Unlock() - - if (newTestProgress.RunStatus > cloudapi.RunStatusRunning) || - (c.exitOnRunning && newTestProgress.RunStatus == cloudapi.RunStatusRunning) { - globalCancel() - break - } - } - - if testProgress == nil { - //nolint:stylecheck,golint - return errext.WithExitCodeIfNone(errors.New("Test progress error"), exitcodes.CloudFailedToGetProgress) - } - - if !c.gs.Flags.Quiet { - valueColor := getColor(c.gs.Flags.NoColor || !c.gs.Stdout.IsTTY, color.FgCyan) - printToStdout(c.gs, fmt.Sprintf( - " test status: %s\n", valueColor.Sprint(testProgress.RunStatusText), - )) - } else { - logger.WithField("run_status", testProgress.RunStatusText).Debug("Test finished") - } - - if testProgress.ResultStatus == cloudapi.ResultStatusFailed { - // TODO: use different exit codes for failed thresholds vs failed test (e.g. aborted by system/limit) - //nolint:stylecheck,golint - return errext.WithExitCodeIfNone(errors.New("The test has failed"), exitcodes.CloudTestRunFailed) - } - - return nil -} - -func (c *cmdCloudRun) flagSet() *pflag.FlagSet { - flags := pflag.NewFlagSet("", pflag.ContinueOnError) - flags.SortFlags = false - flags.AddFlagSet(optionFlagSet()) - flags.AddFlagSet(runtimeOptionFlagSet(false)) - - // TODO: Figure out a better way to handle the CLI flags - flags.BoolVar(&c.exitOnRunning, "exit-on-running", c.exitOnRunning, - "exits when test reaches the running status") - 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") - - return flags -} - -const cloudRunCommandName string = "run" diff --git a/cmd/tests/cmd_cloud_run_test.go b/cmd/tests/cmd_cloud_run_test.go index f41378c2884..b16ec5b1a1c 100644 --- a/cmd/tests/cmd_cloud_run_test.go +++ b/cmd/tests/cmd_cloud_run_test.go @@ -1,248 +1,12 @@ package tests -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "testing" +import "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.k6.io/k6/cloudapi" - "go.k6.io/k6/cmd" - "go.k6.io/k6/lib/fsext" - "go.k6.io/k6/lib/testutils" -) - -func TestCloudRunNotLoggedIn(t *testing.T) { +func TestK6CloudRun(t *testing.T) { t.Parallel() - - ts := getSimpleCloudRunTestState(t, nil, nil, nil, nil) - delete(ts.Env, "K6_CLOUD_TOKEN") - ts.ExpectedExitCode = -1 // TODO: use a more specific exit code? - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `not logged in`) + runCloudTests(t, setupK6CloudRunCmd) } -func TestCloudRunLoggedInWithScriptToken(t *testing.T) { - t.Parallel() - - script := ` - export let options = { - ext: { - loadimpact: { - token: "asdf", - name: "my load test", - projectID: 124, - note: 124, - }, - } - }; - - export default function() {}; - ` - - ts := getSimpleCloudRunTestState(t, []byte(script), nil, nil, nil) - delete(ts.Env, "K6_CLOUD_TOKEN") - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.NotContains(t, stdout, `Not logged in`) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Finished`) -} - -func TestCloudRunExitOnRunning(t *testing.T) { - t.Parallel() - - cs := func() cloudapi.TestProgressResponse { - return cloudapi.TestProgressResponse{ - RunStatusText: "Running", - RunStatus: cloudapi.RunStatusRunning, - } - } - - ts := getSimpleCloudRunTestState(t, nil, []string{"--exit-on-running", "--log-output=stdout"}, nil, cs) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Running`) -} - -func TestCloudRunUploadOnly(t *testing.T) { - t.Parallel() - - cs := func() cloudapi.TestProgressResponse { - return cloudapi.TestProgressResponse{ - RunStatusText: "Archived", - RunStatus: cloudapi.RunStatusArchived, - } - } - - ts := getSimpleCloudRunTestState(t, nil, []string{"--upload-only", "--log-output=stdout"}, nil, cs) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Archived`) -} - -func TestCloudRunWithConfigOverride(t *testing.T) { - t.Parallel() - - configOverride := http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { - resp.WriteHeader(http.StatusOK) - _, err := fmt.Fprint(resp, `{ - "reference_id": "123", - "config": { - "webAppURL": "https://bogus.url", - "testRunDetails": "something from the cloud" - }, - "logs": [ - {"level": "invalid", "message": "test debug message"}, - {"level": "warning", "message": "test warning"}, - {"level": "error", "message": "test error"} - ] - }`) - assert.NoError(t, err) - }) - ts := getSimpleCloudRunTestState(t, nil, nil, configOverride, nil) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, "execution: cloud") - assert.Contains(t, stdout, "output: something from the cloud") - assert.Contains(t, stdout, `level=debug msg="invalid message level 'invalid' for message 'test debug message'`) - assert.Contains(t, stdout, `level=error msg="test debug message" source=grafana-k6-cloud`) - assert.Contains(t, stdout, `level=warning msg="test warning" source=grafana-k6-cloud`) - assert.Contains(t, stdout, `level=error msg="test error" source=grafana-k6-cloud`) -} - -// TestCloudRunWithArchive tests that if k6 uses a static archive with the script inside that has cloud options like: -// -// export let options = { -// ext: { -// loadimpact: { -// name: "my load test", -// projectID: 124, -// note: "lorem ipsum", -// }, -// } -// }; -// -// actually sends to the cloud the archive with the correct metadata (metadata.json), like: -// -// "ext": { -// "loadimpact": { -// "name": "my load test", -// "note": "lorem ipsum", -// "projectID": 124 -// } -// } -func TestCloudRunWithArchive(t *testing.T) { - t.Parallel() - - testRunID := 123 - ts := NewGlobalTestState(t) - - archiveUpload := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // check the archive - file, _, err := req.FormFile("file") - assert.NoError(t, err) - assert.NotNil(t, file) - - // temporary write the archive for file system - data, err := io.ReadAll(file) - assert.NoError(t, err) - - tmpPath := filepath.Join(ts.Cwd, "archive_to_cloud.tar") - require.NoError(t, fsext.WriteFile(ts.FS, tmpPath, data, 0o644)) - - // check what inside - require.NoError(t, testutils.Untar(t, ts.FS, tmpPath, "tmp/")) - - metadataRaw, err := fsext.ReadFile(ts.FS, "tmp/metadata.json") - require.NoError(t, err) - - metadata := struct { - Options struct { - Cloud struct { - Name string `json:"name"` - Note string `json:"note"` - ProjectID int `json:"projectID"` - } `json:"cloud"` - } `json:"options"` - }{} - - // then unpacked metadata should not contain any environment variables passed at the moment of archive creation - require.NoError(t, json.Unmarshal(metadataRaw, &metadata)) - require.Equal(t, "my load test", metadata.Options.Cloud.Name) - require.Equal(t, "lorem ipsum", metadata.Options.Cloud.Note) - require.Equal(t, 124, metadata.Options.Cloud.ProjectID) - - // respond with the test run ID - resp.WriteHeader(http.StatusOK) - _, err = fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) - assert.NoError(t, err) - }) - - srv := getMockCloud(t, testRunID, archiveUpload, nil) - - data, err := os.ReadFile(filepath.Join("testdata/archives", "archive_v0.46.0_with_loadimpact_option.tar")) //nolint:forbidigo // it's a test - require.NoError(t, err) - - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "archive.tar"), data, 0o644)) - - ts.CmdArgs = []string{"k6", "cloud", "--verbose", "--log-output=stdout", "archive.tar"} - ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet - ts.Env["K6_CLOUD_HOST"] = srv.URL - ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud - - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.NotContains(t, stdout, `Not logged in`) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `hello world from archive`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Finished`) -} - -func getSimpleCloudRunTestState( - t *testing.T, script []byte, cliFlags []string, - archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse, -) *GlobalTestState { - if script == nil { - script = []byte(`export default function() {}`) - } - - if cliFlags == nil { - cliFlags = []string{"--verbose", "--log-output=stdout"} - } - - srv := getMockCloud(t, 123, archiveUpload, progressCallback) - - ts := NewGlobalTestState(t) - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), script, 0o644)) - ts.CmdArgs = append(append([]string{"k6", "cloud", "run"}, cliFlags...), "test.js") - ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet - ts.Env["K6_CLOUD_HOST"] = srv.URL - ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud - - return ts +func setupK6CloudRunCmd(cliFlags []string) []string { + return append([]string{"k6", "cloud", "run"}, append(cliFlags, "test.js")...) } diff --git a/cmd/tests/cmd_cloud_test.go b/cmd/tests/cmd_cloud_test.go index a8e76e38006..838cb60a461 100644 --- a/cmd/tests/cmd_cloud_test.go +++ b/cmd/tests/cmd_cloud_test.go @@ -10,97 +10,45 @@ import ( "path/filepath" "testing" + "go.k6.io/k6/lib/testutils" + + "go.k6.io/k6/cmd" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.k6.io/k6/cloudapi" - "go.k6.io/k6/cmd" "go.k6.io/k6/lib/fsext" - "go.k6.io/k6/lib/testutils" ) -func cloudTestStartSimple(tb testing.TB, testRunID int) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { - resp.WriteHeader(http.StatusOK) - _, err := fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) - assert.NoError(tb, err) - }) +func TestK6Cloud(t *testing.T) { + t.Parallel() + runCloudTests(t, setupK6CloudCmd) } -func getMockCloud( - t *testing.T, testRunID int, - archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse, -) *httptest.Server { - if archiveUpload == nil { - archiveUpload = cloudTestStartSimple(t, testRunID) - } - testProgressURL := fmt.Sprintf("GET ^/v1/test-progress/%d$", testRunID) - defaultProgress := cloudapi.TestProgressResponse{ - RunStatusText: "Finished", - RunStatus: cloudapi.RunStatusFinished, - ResultStatus: cloudapi.ResultStatusPassed, - Progress: 1, - } - - srv := getTestServer(t, map[string]http.Handler{ - "POST ^/v1/archive-upload$": archiveUpload, - testProgressURL: http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { - testProgress := defaultProgress - if progressCallback != nil { - testProgress = progressCallback() - } - respBody, err := json.Marshal(testProgress) - assert.NoError(t, err) - _, err = fmt.Fprint(resp, string(respBody)) - assert.NoError(t, err) - }), - }) - - t.Cleanup(srv.Close) - - return srv +func setupK6CloudCmd(cliFlags []string) []string { + return append([]string{"k6", "cloud"}, append(cliFlags, "test.js")...) } -func getSimpleCloudTestState( - t *testing.T, script []byte, cliFlags []string, - archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse, -) *GlobalTestState { - if script == nil { - script = []byte(`export default function() {}`) - } - - if cliFlags == nil { - cliFlags = []string{"--verbose", "--log-output=stdout"} - } - - srv := getMockCloud(t, 123, archiveUpload, progressCallback) - - ts := NewGlobalTestState(t) - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), script, 0o644)) - ts.CmdArgs = append(append([]string{"k6", "cloud"}, cliFlags...), "test.js") - ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet - ts.Env["K6_CLOUD_HOST"] = srv.URL - ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud - - return ts -} +type setupCommandFunc func(cliFlags []string) []string -func TestCloudNotLoggedIn(t *testing.T) { - t.Parallel() +func runCloudTests(t *testing.T, setupCmd setupCommandFunc) { + t.Run("TestCloudNotLoggedIn", func(t *testing.T) { + t.Parallel() - ts := getSimpleCloudTestState(t, nil, nil, nil, nil) - delete(ts.Env, "K6_CLOUD_TOKEN") - ts.ExpectedExitCode = -1 // TODO: use a more specific exit code? - cmd.ExecuteWithGlobalState(ts.GlobalState) + ts := getSimpleCloudTestState(t, nil, setupCmd, nil, nil, nil) + delete(ts.Env, "K6_CLOUD_TOKEN") + ts.ExpectedExitCode = -1 // TODO: use a more specific exit code? + cmd.ExecuteWithGlobalState(ts.GlobalState) - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `Not logged in`) -} + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `not logged in`) + }) -func TestCloudLoggedInWithScriptToken(t *testing.T) { - t.Parallel() + t.Run("TestCloudLoggedInWithScriptToken", func(t *testing.T) { + t.Parallel() - script := ` + script := ` export let options = { ext: { loadimpact: { @@ -114,64 +62,64 @@ func TestCloudLoggedInWithScriptToken(t *testing.T) { export default function() {}; ` - ts := getSimpleCloudTestState(t, []byte(script), nil, nil, nil) - delete(ts.Env, "K6_CLOUD_TOKEN") - cmd.ExecuteWithGlobalState(ts.GlobalState) + ts := getSimpleCloudTestState(t, []byte(script), setupCmd, nil, nil, nil) + delete(ts.Env, "K6_CLOUD_TOKEN") + cmd.ExecuteWithGlobalState(ts.GlobalState) - stdout := ts.Stdout.String() - t.Log(stdout) - assert.NotContains(t, stdout, `Not logged in`) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Finished`) -} + stdout := ts.Stdout.String() + t.Log(stdout) + assert.NotContains(t, stdout, `not logged in`) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Finished`) + }) -func TestCloudExitOnRunning(t *testing.T) { - t.Parallel() + t.Run("TestCloudExitOnRunning", func(t *testing.T) { + t.Parallel() - cs := func() cloudapi.TestProgressResponse { - return cloudapi.TestProgressResponse{ - RunStatusText: "Running", - RunStatus: cloudapi.RunStatusRunning, + cs := func() cloudapi.TestProgressResponse { + return cloudapi.TestProgressResponse{ + RunStatusText: "Running", + RunStatus: cloudapi.RunStatusRunning, + } } - } - ts := getSimpleCloudTestState(t, nil, []string{"--exit-on-running", "--log-output=stdout"}, nil, cs) - cmd.ExecuteWithGlobalState(ts.GlobalState) + ts := getSimpleCloudTestState(t, nil, setupCmd, []string{"--exit-on-running", "--log-output=stdout"}, nil, cs) + cmd.ExecuteWithGlobalState(ts.GlobalState) - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Running`) -} + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Running`) + }) -func TestCloudUploadOnly(t *testing.T) { - t.Parallel() + t.Run("TestCloudUploadOnly", func(t *testing.T) { + t.Parallel() - cs := func() cloudapi.TestProgressResponse { - return cloudapi.TestProgressResponse{ - RunStatusText: "Archived", - RunStatus: cloudapi.RunStatusArchived, + cs := func() cloudapi.TestProgressResponse { + return cloudapi.TestProgressResponse{ + RunStatusText: "Archived", + RunStatus: cloudapi.RunStatusArchived, + } } - } - ts := getSimpleCloudTestState(t, nil, []string{"--upload-only", "--log-output=stdout"}, nil, cs) - cmd.ExecuteWithGlobalState(ts.GlobalState) + ts := getSimpleCloudTestState(t, nil, setupCmd, []string{"--upload-only", "--log-output=stdout"}, nil, cs) + cmd.ExecuteWithGlobalState(ts.GlobalState) - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Archived`) -} + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Archived`) + }) -func TestCloudWithConfigOverride(t *testing.T) { - t.Parallel() + t.Run("TestCloudWithConfigOverride", func(t *testing.T) { + t.Parallel() - configOverride := http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { - resp.WriteHeader(http.StatusOK) - _, err := fmt.Fprint(resp, `{ + configOverride := http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { + resp.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(resp, `{ "reference_id": "123", "config": { "webAppURL": "https://bogus.url", @@ -183,108 +131,172 @@ func TestCloudWithConfigOverride(t *testing.T) { {"level": "error", "message": "test error"} ] }`) - assert.NoError(t, err) + assert.NoError(t, err) + }) + ts := getSimpleCloudTestState(t, nil, setupCmd, nil, configOverride, nil) + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.Contains(t, stdout, "execution: cloud") + assert.Contains(t, stdout, "output: something from the cloud") + assert.Contains(t, stdout, `level=debug msg="invalid message level 'invalid' for message 'test debug message'`) + assert.Contains(t, stdout, `level=error msg="test debug message" source=grafana-k6-cloud`) + assert.Contains(t, stdout, `level=warning msg="test warning" source=grafana-k6-cloud`) + assert.Contains(t, stdout, `level=error msg="test error" source=grafana-k6-cloud`) }) - ts := getSimpleCloudTestState(t, nil, nil, configOverride, nil) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.Contains(t, stdout, "execution: cloud") - assert.Contains(t, stdout, "output: something from the cloud") - assert.Contains(t, stdout, `level=debug msg="invalid message level 'invalid' for message 'test debug message'`) - assert.Contains(t, stdout, `level=error msg="test debug message" source=grafana-k6-cloud`) - assert.Contains(t, stdout, `level=warning msg="test warning" source=grafana-k6-cloud`) - assert.Contains(t, stdout, `level=error msg="test error" source=grafana-k6-cloud`) -} -// TestCloudWithArchive tests that if k6 uses a static archive with the script inside that has cloud options like: -// -// export let options = { -// ext: { -// loadimpact: { -// name: "my load test", -// projectID: 124, -// note: "lorem ipsum", -// }, -// } -// }; -// -// actually sends to the cloud the archive with the correct metadata (metadata.json), like: -// -// "ext": { -// "loadimpact": { -// "name": "my load test", -// "note": "lorem ipsum", -// "projectID": 124 -// } -// } -func TestCloudWithArchive(t *testing.T) { - t.Parallel() + // TestCloudWithArchive tests that if k6 uses a static archive with the script inside that has cloud options like: + // + // export let options = { + // ext: { + // loadimpact: { + // name: "my load test", + // projectID: 124, + // note: "lorem ipsum", + // }, + // } + // }; + // + // actually sends to the cloud the archive with the correct metadata (metadata.json), like: + // + // "ext": { + // "loadimpact": { + // "name": "my load test", + // "note": "lorem ipsum", + // "projectID": 124 + // } + // } + t.Run("TestCloudWithArchive", func(t *testing.T) { + t.Parallel() + + testRunID := 123 + ts := NewGlobalTestState(t) + + archiveUpload := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + // check the archive + file, _, err := req.FormFile("file") + assert.NoError(t, err) + assert.NotNil(t, file) - testRunID := 123 - ts := NewGlobalTestState(t) + // temporary write the archive for file system + data, err := io.ReadAll(file) + assert.NoError(t, err) - archiveUpload := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - // check the archive - file, _, err := req.FormFile("file") - assert.NoError(t, err) - assert.NotNil(t, file) + tmpPath := filepath.Join(ts.Cwd, "archive_to_cloud.tar") + require.NoError(t, fsext.WriteFile(ts.FS, tmpPath, data, 0o644)) + + // check what inside + require.NoError(t, testutils.Untar(t, ts.FS, tmpPath, "tmp/")) + + metadataRaw, err := fsext.ReadFile(ts.FS, "tmp/metadata.json") + require.NoError(t, err) + + metadata := struct { + Options struct { + Cloud struct { + Name string `json:"name"` + Note string `json:"note"` + ProjectID int `json:"projectID"` + } `json:"cloud"` + } `json:"options"` + }{} + + // then unpacked metadata should not contain any environment variables passed at the moment of archive creation + require.NoError(t, json.Unmarshal(metadataRaw, &metadata)) + require.Equal(t, "my load test", metadata.Options.Cloud.Name) + require.Equal(t, "lorem ipsum", metadata.Options.Cloud.Note) + require.Equal(t, 124, metadata.Options.Cloud.ProjectID) + + // respond with the test run ID + resp.WriteHeader(http.StatusOK) + _, err = fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) + assert.NoError(t, err) + }) - // temporary write the archive for file system - data, err := io.ReadAll(file) - assert.NoError(t, err) + srv := getMockCloud(t, testRunID, archiveUpload, nil) - tmpPath := filepath.Join(ts.Cwd, "archive_to_cloud.tar") - require.NoError(t, fsext.WriteFile(ts.FS, tmpPath, data, 0o644)) + data, err := os.ReadFile(filepath.Join("testdata/archives", "archive_v0.46.0_with_loadimpact_option.tar")) //nolint:forbidigo // it's a test + require.NoError(t, err) - // check what inside - require.NoError(t, testutils.Untar(t, ts.FS, tmpPath, "tmp/")) + require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "archive.tar"), data, 0o644)) - metadataRaw, err := fsext.ReadFile(ts.FS, "tmp/metadata.json") - require.NoError(t, err) + ts.CmdArgs = []string{"k6", "cloud", "--verbose", "--log-output=stdout", "archive.tar"} + ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet + ts.Env["K6_CLOUD_HOST"] = srv.URL + ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud - metadata := struct { - Options struct { - Cloud struct { - Name string `json:"name"` - Note string `json:"note"` - ProjectID int `json:"projectID"` - } `json:"cloud"` - } `json:"options"` - }{} - - // then unpacked metadata should not contain any environment variables passed at the moment of archive creation - require.NoError(t, json.Unmarshal(metadataRaw, &metadata)) - require.Equal(t, "my load test", metadata.Options.Cloud.Name) - require.Equal(t, "lorem ipsum", metadata.Options.Cloud.Note) - require.Equal(t, 124, metadata.Options.Cloud.ProjectID) - - // respond with the test run ID + cmd.ExecuteWithGlobalState(ts.GlobalState) + + stdout := ts.Stdout.String() + t.Log(stdout) + assert.NotContains(t, stdout, `not logged in`) + assert.Contains(t, stdout, `execution: cloud`) + assert.Contains(t, stdout, `hello world from archive`) + assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) + assert.Contains(t, stdout, `test status: Finished`) + }) +} + +func cloudTestStartSimple(tb testing.TB, testRunID int) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { resp.WriteHeader(http.StatusOK) - _, err = fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) - assert.NoError(t, err) + _, err := fmt.Fprintf(resp, `{"reference_id": "%d"}`, testRunID) + assert.NoError(tb, err) + }) +} + +func getMockCloud( + t *testing.T, testRunID int, + archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse, +) *httptest.Server { + if archiveUpload == nil { + archiveUpload = cloudTestStartSimple(t, testRunID) + } + testProgressURL := fmt.Sprintf("GET ^/v1/test-progress/%d$", testRunID) + defaultProgress := cloudapi.TestProgressResponse{ + RunStatusText: "Finished", + RunStatus: cloudapi.RunStatusFinished, + ResultStatus: cloudapi.ResultStatusPassed, + Progress: 1, + } + + srv := getTestServer(t, map[string]http.Handler{ + "POST ^/v1/archive-upload$": archiveUpload, + testProgressURL: http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) { + testProgress := defaultProgress + if progressCallback != nil { + testProgress = progressCallback() + } + respBody, err := json.Marshal(testProgress) + assert.NoError(t, err) + _, err = fmt.Fprint(resp, string(respBody)) + assert.NoError(t, err) + }), }) - srv := getMockCloud(t, testRunID, archiveUpload, nil) + t.Cleanup(srv.Close) + + return srv +} + +func getSimpleCloudTestState(t *testing.T, script []byte, setupCmd setupCommandFunc, cliFlags []string, archiveUpload http.Handler, progressCallback func() cloudapi.TestProgressResponse) *GlobalTestState { + if script == nil { + script = []byte(`export default function() {}`) + } - data, err := os.ReadFile(filepath.Join("testdata/archives", "archive_v0.46.0_with_loadimpact_option.tar")) //nolint:forbidigo // it's a test - require.NoError(t, err) + if cliFlags == nil { + cliFlags = []string{"--verbose", "--log-output=stdout"} + } - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "archive.tar"), data, 0o644)) + srv := getMockCloud(t, 123, archiveUpload, progressCallback) - ts.CmdArgs = []string{"k6", "cloud", "--verbose", "--log-output=stdout", "archive.tar"} + ts := NewGlobalTestState(t) + require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "test.js"), script, 0o644)) + ts.CmdArgs = setupCmd(cliFlags) ts.Env["K6_SHOW_CLOUD_LOGS"] = "false" // no mock for the logs yet ts.Env["K6_CLOUD_HOST"] = srv.URL ts.Env["K6_CLOUD_TOKEN"] = "foo" // doesn't matter, we mock the cloud - cmd.ExecuteWithGlobalState(ts.GlobalState) - - stdout := ts.Stdout.String() - t.Log(stdout) - assert.NotContains(t, stdout, `Not logged in`) - assert.Contains(t, stdout, `execution: cloud`) - assert.Contains(t, stdout, `hello world from archive`) - assert.Contains(t, stdout, `output: https://app.k6.io/runs/123`) - assert.Contains(t, stdout, `test status: Finished`) + return ts } From fd6daac399bb1c665a2be83fa8432ef1cce1a0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Crevon?= Date: Wed, 24 Jul 2024 10:42:53 +0200 Subject: [PATCH 10/11] Apply suggestions from code review Co-authored-by: Oleg Bespalov --- cmd/cloud.go | 6 +++--- cmd/cloud_login.go | 1 + cmd/cloud_run.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 640b0a7e9ed..5d913d8d745 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -120,7 +120,7 @@ func (c *cmdCloud) run(cmd *cobra.Command, args []string) error { } if !cloudConfig.Token.Valid { return errors.New( //nolint:golint - "not logged in, please login to the Grafana Cloud k6 " + + "not logged in, please login first to the Grafana Cloud k6 " + "using the \"k6 cloud login\" command", ) } @@ -366,9 +366,9 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { cloudCmd := &cobra.Command{ Use: "cloud", Short: "Run a test on the cloud", - Long: `Run a test archive in the Grafana Cloud k6. + Long: `Run a test in the Grafana Cloud k6. -This will execute the test in the Grafana Cloud k6 service. Be sure to run the "k6 cloud login" command prior to +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. diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index cbba559426f..0224839935a 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -110,6 +110,7 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { newCloudConf.Token = token default: form := ui.Form{ + Banner: "Please enter your Grafana Cloud k6 credentials", Fields: []ui.Field{ ui.StringField{ Key: "Email", diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go index 82bfab570a9..c23f3b7fbf8 100644 --- a/cmd/cloud_run.go +++ b/cmd/cloud_run.go @@ -30,7 +30,7 @@ func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { Short: "Run a test in Grafana Cloud k6", Long: `Run a test in Grafana Cloud k6. -This will execute the test in the Grafana Cloud k6 service. Using this command requires to be authenticated +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, From a569c167c8818457004339108f0d66382ddaff7f Mon Sep 17 00:00:00 2001 From: oleiade Date: Wed, 24 Jul 2024 10:55:20 +0200 Subject: [PATCH 11/11] fix lint of cmd package --- cmd/cloud.go | 4 ++-- cmd/cloud_run.go | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/cloud.go b/cmd/cloud.go index 5d913d8d745..9f529bd7064 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -368,8 +368,8 @@ func getCmdCloud(gs *state.GlobalState) *cobra.Command { Short: "Run a test on the cloud", Long: `Run a test in the Grafana Cloud k6. -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.`, +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. diff --git a/cmd/cloud_run.go b/cmd/cloud_run.go index c23f3b7fbf8..2dd81033e27 100644 --- a/cmd/cloud_run.go +++ b/cmd/cloud_run.go @@ -30,8 +30,9 @@ func getCmdCloudRun(gs *state.GlobalState) *cobra.Command { 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.`, +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 "+