Skip to content

Commit

Permalink
Cloud local execution creates the test run before delegating
Browse files Browse the repository at this point in the history
  • Loading branch information
joanlopez committed Dec 5, 2024
1 parent 82400de commit e7eae06
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 79 deletions.
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 @@ import (

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 @@ func createCloudTest(gs *state.GlobalState, test *loadedAndConfiguredTest, outpu
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 @@ func createCloudTest(gs *state.GlobalState, test *loadedAndConfiguredTest, outpu
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 @@ type (
ModuleInstance struct {
vu modules.VU
obj *sobek.Object

once sync.Once
testRunID sobek.Value
}
)

Expand All @@ -37,10 +41,12 @@ func New() *RootModule {
// 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 @@ func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
}
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 @@ func (mi *ModuleInstance) Exports() modules.Exports {
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 @@ package cloud
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 setupCloudTestEnv(t *testing.T) *modulestest.Runtime {
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

0 comments on commit e7eae06

Please sign in to comment.