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

Populate __ENV.K6_CLOUDRUN_TEST_RUN_ID on local executions of k6 cloud run #4092

Merged
merged 13 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Cloud local execution creates the test run before delegating
  • Loading branch information
joanlopez committed Dec 5, 2024
commit e7eae06b19faa3546d2ad46881cc6db5c6793dc4
19 changes: 19 additions & 0 deletions cmd/cloud_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,25 @@ func (c *cmdCloudRun) preRun(cmd *cobra.Command, args []string) error {

func (c *cmdCloudRun) run(cmd *cobra.Command, args []string) error {
if c.localExecution {
// We know this execution requires a test run to be created in the Cloud.
// So, we create it before delegating the actual execution to the run command.
// To do that, we need to load the test and configure it.
test, err := loadAndConfigureLocalTest(c.runCmd.gs, cmd, args, getCloudRunLocalExecutionConfig)
if err != nil {
return fmt.Errorf("could not load and configure the test: %w", err)
}

// As we've already loaded the test, we can modify the init function to
// reuse the initialized one.
c.runCmd.loadConfiguredTest = func(*cobra.Command, []string) (*loadedAndConfiguredTest, execution.Controller, error) {
return test, local.NewController(), nil
}

// After that, we can create the remote test run.
if err := createCloudTest(c.runCmd.gs, test); err != nil {
return fmt.Errorf("could not create the cloud test run: %w", err)
}

return c.runCmd.run(cmd, args)
}

Expand Down
20 changes: 5 additions & 15 deletions cmd/outputs_cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,16 @@

const defaultTestName = "k6 test"

func findCloudOutput(outputs []string) (string, string, bool) {
for _, outFullArg := range outputs {
outType, outArg, _ := strings.Cut(outFullArg, "=")
if outType == builtinOutputCloud.String() {
return outType, outArg, true
}
}
return "", "", false
}

// createCloudTest performs some test and Cloud configuration validations and if everything
// looks good, then it creates a test run in the k6 Cloud, unless k6 is already running in the Cloud.
// It is also responsible for filling the test run id on the test options, so it can be used later.
// It returns the resulting Cloud configuration as a json.RawMessage, as expected by the Cloud output,
// or an error if something goes wrong.
func createCloudTest(gs *state.GlobalState, test *loadedAndConfiguredTest, outputType, outputArg string) error {
func createCloudTest(gs *state.GlobalState, test *loadedAndConfiguredTest) error {

Check failure on line 29 in cmd/outputs_cloud.go

View workflow job for this annotation

GitHub Actions / lint

Function 'createCloudTest' is too long (112 > 80) (funlen)
conf, warn, err := cloudapi.GetConsolidatedConfig(
test.derivedConfig.Collectors[outputType],
test.derivedConfig.Collectors[builtinOutputCloud.String()],
gs.Env,
outputArg,
"", // Historically used for -o cloud=..., no longer used (deprecated).
test.derivedConfig.Options.Cloud,
test.derivedConfig.Options.External,
)
Expand Down Expand Up @@ -124,7 +114,7 @@
Archive: testArchive,
}

logger := gs.Logger.WithFields(logrus.Fields{"output": "cloud"})
logger := gs.Logger.WithFields(logrus.Fields{"output": builtinOutputCloud.String()})

apiClient := cloudapi.NewClient(
logger, conf.Token.String, conf.Host.String, consts.Version, conf.Timeout.TimeDuration())
Expand All @@ -146,7 +136,7 @@
return fmt.Errorf("could not serialize cloud configuration: %w", err)
}

test.derivedConfig.Collectors["cloud"] = raw
test.derivedConfig.Collectors[builtinOutputCloud.String()] = raw

return nil
}
Expand Down
9 changes: 0 additions & 9 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,6 @@ func (c *cmdRun) run(cmd *cobra.Command, args []string) (err error) {
}
}

// Although outputs are created below, is at this point, before building
// the test run state, when we want to create the Cloud test run, if needed
// so that we can set the test run ID on the test options.
if outType, outArg, found := findCloudOutput(test.derivedConfig.Out); found {
if err := createCloudTest(c.gs, test, outType, outArg); err != nil {
return fmt.Errorf("could not create the '%s' output: %w", outType, err)
}
}

// Write the full consolidated *and derived* options back to the Runner.
conf := test.derivedConfig
testRunState, err := test.buildTestRunState(conf.Options)
Expand Down
64 changes: 43 additions & 21 deletions js/modules/k6/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
package cloud

import (
"errors"
"sync"

"github.com/grafana/sobek"
"github.com/mstoykov/envconfig"

"go.k6.io/k6/cloudapi"
"go.k6.io/k6/js/common"
Expand All @@ -20,6 +21,9 @@
ModuleInstance struct {
vu modules.VU
obj *sobek.Object

once sync.Once
testRunID sobek.Value
}
)

Expand All @@ -37,10 +41,12 @@
// a new instance for each VU.
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
mi := &ModuleInstance{vu: vu}

rt := vu.Runtime()
o := rt.NewObject()

mi.obj = rt.NewObject()
defProp := func(name string, getter func() (sobek.Value, error)) {
err := o.DefineAccessorProperty(name, rt.ToValue(func() sobek.Value {
err := mi.obj.DefineAccessorProperty(name, rt.ToValue(func() sobek.Value {
obj, err := getter()
if err != nil {
common.Throw(rt, err)
Expand All @@ -53,7 +59,17 @@
}
defProp("testRunId", mi.testRunId)

mi.obj = o
// By default, we try to load the test run id from the environment variables,
// which corresponds to those scenarios where the k6 binary is running in the Cloud.
var envConf cloudapi.Config
if err := envconfig.Process("", &envConf, vu.InitEnv().LookupEnv); err != nil {
common.Throw(vu.Runtime(), err)
}
if envConf.TestRunID.Valid {
mi.testRunID = mi.vu.Runtime().ToValue(envConf.TestRunID.String)
} else {
mi.testRunID = sobek.Undefined() // Default value.
}

return mi
}
Expand All @@ -63,26 +79,32 @@
return modules.Exports{Default: mi.obj}
}

var errRunInInitContext = errors.New("getting cloud information outside of the VU context is not supported")

// testRunId returns a sobek.Value(string) with the Cloud test run id.
//
// This code can be executed in two situations, either when the k6 binary is running in the Cloud, in which case
// the value of the test run id would be available in the environment, and we would have loaded at module initialization
// time; or when the k6 binary is running locally and test run id is present in the options, which we try to read at
// time of running this method, but only once for the whole execution as options won't change anymore.
func (mi *ModuleInstance) testRunId() (sobek.Value, error) {

Check warning on line 88 in js/modules/k6/cloud/cloud.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: method testRunId should be testRunID (revive)
rt := mi.vu.Runtime()
vuState := mi.vu.State()
if vuState == nil {
return sobek.Undefined(), errRunInInitContext
}

if vuState.Options.Cloud == nil {
return sobek.Undefined(), nil
// In case we have a value (e.g. loaded from env), we return it.
// If we're in the init context (where we cannot read the options), we return undefined (the default value).
if !sobek.IsUndefined(mi.testRunID) || mi.vu.State() == nil {
return mi.testRunID, nil
}

// We pass almost all values to zero/nil because here we only care about the cloud configuration present in options.
// TODO: Technically I guess we can do it only once and "cache" the value, as it shouldn't change over the test run.
conf, _, err := cloudapi.GetConsolidatedConfig(vuState.Options.Cloud, nil, "", nil, nil)
if err != nil {
return sobek.Undefined(), err
}
// Otherwise, we try to read the test run id from options.
// We only try it once for the whole execution, as options won't change.
vuState := mi.vu.State()
var err error
mi.once.Do(func() {
// We pass almost all values to zero/nil because here we only care about the Cloud configuration present in options.
var optsConf cloudapi.Config
optsConf, _, err = cloudapi.GetConsolidatedConfig(vuState.Options.Cloud, nil, "", nil, nil)

if optsConf.TestRunID.Valid {
mi.testRunID = mi.vu.Runtime().ToValue(optsConf.TestRunID.String)
}
})

return rt.ToValue(conf.TestRunID.String), nil
return mi.testRunID, err
}
76 changes: 53 additions & 23 deletions js/modules/k6/cloud/cloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
import (
"testing"

"github.com/grafana/sobek"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go.k6.io/k6/js/modulestest"
"go.k6.io/k6/lib"
)

func setupCloudTestEnv(t *testing.T) *modulestest.Runtime {
func setupCloudTestEnv(t *testing.T, env map[string]string) *modulestest.Runtime {
tRt := modulestest.NewRuntime(t)
tRt.VU.InitEnv().LookupEnv = func(key string) (string, bool) {
v, ok := env[key]
return v, ok
}
m, ok := New().NewModuleInstance(tRt.VU).(*ModuleInstance)
require.True(t, ok)
require.NoError(t, tRt.VU.Runtime().Set("cloud", m.Exports().Default))
Expand All @@ -21,34 +26,59 @@
func TestGetTestRunId(t *testing.T) {
t.Parallel()

t.Run("init context", func(t *testing.T) {
t.Run("Cloud execution", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t)
_, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)
require.ErrorIs(t, err, errRunInInitContext)
})

t.Run("undefined", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t)
tRt.MoveToVUContext(&lib.State{
Options: lib.Options{},
t.Run("Not defined", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t, nil)
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)

Check warning on line 35 in js/modules/k6/cloud/cloud_test.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var testRunId should be testRunID (revive)
require.NoError(t, err)
assert.Equal(t, sobek.Undefined(), testRunId)
})

t.Run("Defined", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t, map[string]string{"K6_CLOUD_TEST_RUN_ID": "123"})
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)

Check warning on line 43 in js/modules/k6/cloud/cloud_test.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var testRunId should be testRunID (revive)
require.NoError(t, err)
assert.Equal(t, "123", testRunId.String())
})
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)
require.NoError(t, err)
assert.Equal(t, "undefined", testRunId.String())
})

t.Run("defined", func(t *testing.T) {
t.Run("Local execution", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t)
tRt.MoveToVUContext(&lib.State{
Options: lib.Options{
Cloud: []byte(`{"testRunID": "123"}`),
},

t.Run("Init context", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t, nil)
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)

Check warning on line 55 in js/modules/k6/cloud/cloud_test.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var testRunId should be testRunID (revive)
require.NoError(t, err)
assert.Equal(t, sobek.Undefined(), testRunId)
})

t.Run("Not defined", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t, nil)
tRt.MoveToVUContext(&lib.State{
Options: lib.Options{},
})
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)

Check warning on line 66 in js/modules/k6/cloud/cloud_test.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var testRunId should be testRunID (revive)
require.NoError(t, err)
assert.Equal(t, sobek.Undefined(), testRunId)
})

t.Run("Defined", func(t *testing.T) {
t.Parallel()
tRt := setupCloudTestEnv(t, nil)
tRt.MoveToVUContext(&lib.State{
Options: lib.Options{
Cloud: []byte(`{"testRunID": "123"}`),
},
})
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)

Check warning on line 79 in js/modules/k6/cloud/cloud_test.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: var testRunId should be testRunID (revive)
require.NoError(t, err)
assert.Equal(t, "123", testRunId.String())
})
testRunId, err := tRt.VU.Runtime().RunString(`cloud.testRunId`)
require.NoError(t, err)
assert.Equal(t, "123", testRunId.String())
})
}
Loading
Loading