Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce k6 cloud upload to replace --upload-only #3906

Merged
merged 3 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cmd/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,17 @@ import (
"sync"
"time"

"github.com/fatih/color"
"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/fatih/color"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

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

// cmdCloud handles the `k6 cloud` sub-command
Expand Down Expand Up @@ -335,6 +334,9 @@ func (c *cmdCloud) flagSet() *pflag.FlagSet {
"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")
if err := flags.MarkDeprecated("upload-only", "use \"k6 cloud upload\" instead"); err != nil {
joanlopez marked this conversation as resolved.
Show resolved Hide resolved
panic(err)
}

return flags
}
Expand Down Expand Up @@ -383,6 +385,7 @@ service. Be sure to run the "k6 cloud login" command prior to authenticate with
// Register `k6 cloud` subcommands
cloudCmd.AddCommand(getCmdCloudRun(gs))
cloudCmd.AddCommand(getCmdCloudLogin(gs))
cloudCmd.AddCommand(getCmdCloudUpload(c))

cloudCmd.Flags().SortFlags = false
cloudCmd.Flags().AddFlagSet(c.flagSet())
Expand Down
8 changes: 4 additions & 4 deletions cmd/cloud_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ func getCmdCloudLogin(gs *state.GlobalState) *cobra.Command {
// loginCloudCommand represents the 'cloud login' command
exampleText := getExampleText(gs, `
# Prompt for a Grafana Cloud k6 token
{{.}} cloud login
$ {{.}} cloud login
joanlopez marked this conversation as resolved.
Show resolved Hide resolved

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

# Display the stored token
{{.}} cloud login -s
$ {{.}} cloud login -s

# Reset the stored token
{{.}} cloud login -r`[1:])
$ {{.}} cloud login -r`[1:])

loginCloudCommand := &cobra.Command{
Use: cloudLoginCommandName,
Expand Down
67 changes: 67 additions & 0 deletions cmd/cloud_upload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"go.k6.io/k6/cmd/state"

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

const cloudUploadCommandName = "upload"

type cmdCloudUpload struct {
globalState *state.GlobalState

// deprecatedCloudCmd holds an instance of the k6 cloud command that we store
// in order to be able to call its run method to support the cloud upload
// feature
deprecatedCloudCmd *cmdCloud
}

func getCmdCloudUpload(cloudCmd *cmdCloud) *cobra.Command {
c := &cmdCloudUpload{
globalState: cloudCmd.gs,
deprecatedCloudCmd: cloudCmd,
}

// uploadCloudCommand represents the 'cloud upload' command
exampleText := getExampleText(cloudCmd.gs, `
# Upload the test to the Grafana Cloud k6 without actually starting a test run
$ {{.}} cloud upload script.js`[1:])

uploadCloudCommand := &cobra.Command{
Use: cloudUploadCommandName,
Short: "Upload the test to the Grafana Cloud k6",
joanlopez marked this conversation as resolved.
Show resolved Hide resolved
Long: `Upload the test to the Grafana Cloud k6.

This will upload the test to the Grafana Cloud k6 service. Using this command requires to be authenticated
against the 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"),
PreRunE: c.preRun,
RunE: c.run,
}

uploadCloudCommand.Flags().AddFlagSet(c.flagSet())

return uploadCloudCommand
}

func (c *cmdCloudUpload) preRun(cmd *cobra.Command, args []string) error {
return c.deprecatedCloudCmd.preRun(cmd, args)
}

// run is the code that runs when the user executes `k6 cloud upload`
func (c *cmdCloudUpload) run(cmd *cobra.Command, args []string) error {
c.deprecatedCloudCmd.uploadOnly = true
return c.deprecatedCloudCmd.run(cmd, args)
}

func (c *cmdCloudUpload) flagSet() *pflag.FlagSet {
flags := pflag.NewFlagSet("", pflag.ContinueOnError)
flags.SortFlags = false
flags.AddFlagSet(optionFlagSet())
flags.AddFlagSet(runtimeOptionFlagSet(false))
return flags
}
157 changes: 157 additions & 0 deletions cmd/tests/cmd_cloud_upload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package tests

import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"testing"

"go.k6.io/k6/cloudapi"
"go.k6.io/k6/cmd"
"go.k6.io/k6/lib/fsext"
"go.k6.io/k6/lib/testutils"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestK6CloudUpload(t *testing.T) {
t.Parallel()

t.Run("TestCloudUploadNotLoggedIn", func(t *testing.T) {
t.Parallel()

ts := getSimpleCloudTestState(t, nil, setupK6CloudUploadCmd, nil, nil, nil)
delete(ts.Env, "K6_CLOUD_TOKEN")
ts.ExpectedExitCode = -1 // TODO: use a more specific exit code?
joanlopez marked this conversation as resolved.
Show resolved Hide resolved
cmd.ExecuteWithGlobalState(ts.GlobalState)

stdout := ts.Stdout.String()
t.Log(stdout)
assert.Contains(t, stdout, `not logged in`)
})

t.Run("TestCloudUploadWithScript", func(t *testing.T) {
t.Parallel()

cs := func() cloudapi.TestProgressResponse {
return cloudapi.TestProgressResponse{
RunStatusText: "Archived",
RunStatus: cloudapi.RunStatusArchived,
}
}

ts := getSimpleCloudTestState(t, nil, setupK6CloudUploadCmd, nil, 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`)
})

// TestCloudUploadWithArchive 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("TestCloudUploadWithArchive", 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)

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

cs := func() cloudapi.TestProgressResponse {
return cloudapi.TestProgressResponse{
RunStatusText: "Archived",
RunStatus: cloudapi.RunStatusArchived,
}
}

srv := getMockCloud(t, testRunID, archiveUpload, cs)

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", "upload", "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, `output: https://app.k6.io/runs/123`)
assert.Contains(t, stdout, `test status: Archived`)
})
}

func setupK6CloudUploadCmd(cliFlags []string) []string {
return append([]string{"k6", "cloud", "upload"}, append(cliFlags, "test.js")...)
}