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

Introducing options.cloud #3348

Merged
merged 2 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
140 changes: 110 additions & 30 deletions cloudapi/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloudapi

import (
"bytes"
"encoding/json"
"time"

Expand All @@ -9,6 +10,9 @@ import (
"gopkg.in/guregu/null.v3"
)

// LegacyCloudConfigKey is the key used in the JSON config for the cloud output.
const LegacyCloudConfigKey = "loadimpact"

// Config holds all the necessary data and options for sending metrics to the k6 Cloud.
//
//nolint:lll
Expand Down Expand Up @@ -165,44 +169,40 @@ func (c Config) Apply(cfg Config) Config {
return c
}

// MergeFromExternal merges three fields from the JSON in a loadimpact key of
// the provided external map. Used for options.ext.loadimpact settings.
func MergeFromExternal(external map[string]json.RawMessage, conf *Config) error {
if val, ok := external["loadimpact"]; ok {
// TODO: Important! Separate configs and fix the whole 2 configs mess!
tmpConfig := Config{}
if err := json.Unmarshal(val, &tmpConfig); err != nil {
return err
}
// Only take out the ProjectID, Name and Token from the options.ext.loadimpact map:
if tmpConfig.ProjectID.Valid {
conf.ProjectID = tmpConfig.ProjectID
}
if tmpConfig.Name.Valid {
conf.Name = tmpConfig.Name
}
if tmpConfig.Token.Valid {
conf.Token = tmpConfig.Token
}
}
return nil
}

// GetConsolidatedConfig combines the default config values with the JSON config
// values and environment variables and returns the final result.
// it also returns a warning message that could be shown to the user.
// to bring some attention to the fact that the user.
func GetConsolidatedConfig(
jsonRawConf json.RawMessage, env map[string]string, configArg string, external map[string]json.RawMessage,
) (Config, error) {
jsonRawConf json.RawMessage,
env map[string]string,
configArg string,
cloudConfig json.RawMessage,
external map[string]json.RawMessage,
) (Config, string, error) {
warn := ""

result := NewConfig()
if jsonRawConf != nil {
jsonConf := Config{}
if err := json.Unmarshal(jsonRawConf, &jsonConf); err != nil {
return result, err
return result, warn, err
}
result = result.Apply(jsonConf)
}
if err := MergeFromExternal(external, &result); err != nil {
return result, err

if err := mergeFromCloudOptionAndExternal(cloudConfig, external, &result); err != nil {
return result, warn, err
}

// We want to show a warning if the user is using the only old way of defining the config.
// Note: Since the migration to the options.cloud is a long process, this warning is planned
// to be emitted for a long time (1-2 years), after some point, and depending on the state
// of migration we could re-evaluate this warning.
if cloudConfig == nil && external != nil {
if _, ok := external[LegacyCloudConfigKey]; ok {
warn = "The options.ext.loadimpact option is deprecated, please use options.cloud instead"
}
}

envConfig := Config{}
Expand All @@ -211,13 +211,93 @@ func GetConsolidatedConfig(
return v, ok
}); err != nil {
// TODO: get rid of envconfig and actually use the env parameter...
return result, err
return result, warn, err
}
result = result.Apply(envConfig)

if configArg != "" {
result.Name = null.StringFrom(configArg)
}

return result, nil
return result, warn, nil
}

// mergeFromCloudOptionAndExternal merges three fields from the JSON in a cloud key of
// the provided external map. Used for options.cloud settings.
func mergeFromCloudOptionAndExternal(
cloudConfig json.RawMessage,
external map[string]json.RawMessage,
conf *Config,
) error {
source := pickSource(cloudConfig, external)
if source == nil {
return nil
}

// Original comment
// TODO: Important! Separate configs and fix the whole 2 configs mess!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explicit this comment a bit more, please? I'm not getting what is the suggestion here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue that I'm not getting this comment to, it's an original comment that was presented in the place around.

TBH for the case of refactoring that I did it wasn't so relevant, but on the other side I'm not comfortable removing it.

tmpConfig := Config{}
if err := json.Unmarshal(source, &tmpConfig); err != nil {
return err
}
// Only take out the ProjectID, Name and Token from the options.cloud (or legacy loadimpact struct) map:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me reading this comment that we are taking out only part of the available fields. The question now I have is why? I guess it has a reason, can you clarify it, please? As it isn't clear to me just reading the code.

And if it has then probably it can be directly part of the comment of the function explaining what should be the expected return.

As a reference I evaluate this list still valid for the as configurable options. https://k6.io/docs/cloud/creating-and-running-a-test/cloud-scripting-extras/cloud-options/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if tmpConfig.ProjectID.Valid {
conf.ProjectID = tmpConfig.ProjectID
}
if tmpConfig.Name.Valid {
conf.Name = tmpConfig.Name
}
if tmpConfig.Token.Valid {
conf.Token = tmpConfig.Token
}

return nil
}

// GetTemporaryCloudConfig returns a temporary cloud config.
// Original comment
// TODO: Fix this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep this 2-way forever, how could we really fix it in the future without the temp map? If you see a way then I would like to include the suggestion in the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a way of improving it, and to be honest, I prefer to keep the scope smaller to have a chance of delivering it.

// We reuse cloud.Config for parsing options.cloud (or legacy loadimpact struct), but this probably shouldn't be
// done, as the idea of options.ext is that they are extensible without touching k6. But in
// order for this to happen, we shouldn't actually marshal cloud.Config on top of it, because
// it will be missing some fields that aren't actually mentioned in the struct.
// So in order for use to copy the fields that we need for k6 cloud's api we unmarshal in
// map[string]interface{} and copy what we need if it isn't set already
func GetTemporaryCloudConfig(
cloudConfig json.RawMessage,
external map[string]json.RawMessage,
) (map[string]interface{}, error) {
tmpCloudConfig := make(map[string]interface{}, 3)

source := pickSource(cloudConfig, external)
if source == nil {
return tmpCloudConfig, nil
}

dec := json.NewDecoder(bytes.NewReader(source))
dec.UseNumber() // otherwise float64 are used
if err := dec.Decode(&tmpCloudConfig); err != nil {
return nil, err
}

return tmpCloudConfig, nil
}

// pickSource returns the config source to use.
func pickSource(
cloudConfig json.RawMessage,
external map[string]json.RawMessage,
) json.RawMessage {
// priority is the new way of defining the config
// via options.cloud
if cloudConfig != nil {
return cloudConfig
}

// fallback to the old way of defining the config
if val, ok := external[LegacyCloudConfigKey]; ok {
return val
}

return nil
}
67 changes: 64 additions & 3 deletions cloudapi/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,78 @@ func TestConfigApply(t *testing.T) {

func TestGetConsolidatedConfig(t *testing.T) {
t.Parallel()
config, err := GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), nil, "", nil)
config, warn, err := GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), nil, "", nil, nil)
require.NoError(t, err)
require.Equal(t, config.Token.String, "jsonraw")
require.Empty(t, warn)

config, err = GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), nil, "",
config, warn, err = GetConsolidatedConfig(
json.RawMessage(`{"token":"jsonraw"}`),
nil,
"",
json.RawMessage(`{"token":"ext"}`),
nil,
)
require.NoError(t, err)
require.Equal(t, config.Token.String, "ext")
require.Empty(t, warn)

config, warn, err = GetConsolidatedConfig(
json.RawMessage(`{"token":"jsonraw"}`),
map[string]string{"K6_CLOUD_TOKEN": "envvalue"},
"",
json.RawMessage(`{"token":"ext"}`),
nil,
)
require.NoError(t, err)
require.Equal(t, config.Token.String, "envvalue")
require.Empty(t, warn)
}

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

config, warn, err := GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), nil, "", nil,
map[string]json.RawMessage{"loadimpact": json.RawMessage(`{"token":"ext"}`)})
require.NoError(t, err)
require.Equal(t, config.Token.String, "ext")
require.NotEmpty(t, warn)

config, err = GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), map[string]string{"K6_CLOUD_TOKEN": "envvalue"}, "",
config, warn, err = GetConsolidatedConfig(json.RawMessage(`{"token":"jsonraw"}`), map[string]string{"K6_CLOUD_TOKEN": "envvalue"}, "", nil,
map[string]json.RawMessage{"loadimpact": json.RawMessage(`{"token":"ext"}`)})
require.NoError(t, err)
require.Equal(t, config.Token.String, "envvalue")
require.NotEmpty(t, warn)
}

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

config, warn, err := GetConsolidatedConfig(
json.RawMessage(`{"token":"jsonraw"}`),
nil,
"",
json.RawMessage(`{"token":"cloud"}`),
map[string]json.RawMessage{"loadimpact": json.RawMessage(`{"token":"ext"}`)},
)

require.NoError(t, err)
require.Equal(t, config.Token.String, "cloud")
require.Empty(t, warn)
}

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

config, warn, err := GetConsolidatedConfig(
json.RawMessage(`{"token":"jsonraw"}`),
map[string]string{"K6_CLOUD_TOKEN": "envvalue"},
"",
json.RawMessage(`{"token":"cloud"}`),
map[string]json.RawMessage{"loadimpact": json.RawMessage(`{"token":"ext"}`)},
)
require.NoError(t, err)

require.Equal(t, config.Token.String, "envvalue")
require.Empty(t, warn)
}
104 changes: 104 additions & 0 deletions cmd/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,110 @@ func TestArchiveContainsEnv(t *testing.T) {
require.Equal(t, "ipsum", metadata.Env["ENV2"])
}

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

// given a script with the cloud options
fileName := "script.js"
testScript := []byte(`
export let options = {
ext: {
loadimpact: {
distribution: {
one: { loadZone: 'amazon:us:ashburn', percent: 30 },
two: { loadZone: 'amazon:ie:dublin', percent: 70 },
},
},
},
};
export default function () {}
`)
ts := tests.NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, fileName), testScript, 0o644))

// when we do archiving
ts.CmdArgs = []string{"k6", "archive", fileName}

newRootCommand(ts.GlobalState).execute()
require.NoError(t, testutils.Untar(t, ts.FS, "archive.tar", "tmp/"))

data, err := fsext.ReadFile(ts.FS, "tmp/metadata.json")
require.NoError(t, err)

// we just need some basic struct
metadata := struct {
Options struct {
Ext struct {
LoadImpact struct {
Distribution map[string]struct {
LoadZone string `json:"loadZone"`
Percent float64 `json:"percent"`
} `json:"distribution"`
} `json:"loadimpact"`
} `json:"ext"`
} `json:"options"`
}{}

// then unpacked metadata should contain a ext.loadimpact struct the proper values
require.NoError(t, json.Unmarshal(data, &metadata))
require.Len(t, metadata.Options.Ext.LoadImpact.Distribution, 2)

require.Equal(t, metadata.Options.Ext.LoadImpact.Distribution["one"].LoadZone, "amazon:us:ashburn")
require.Equal(t, metadata.Options.Ext.LoadImpact.Distribution["one"].Percent, 30.)
require.Equal(t, metadata.Options.Ext.LoadImpact.Distribution["two"].LoadZone, "amazon:ie:dublin")
require.Equal(t, metadata.Options.Ext.LoadImpact.Distribution["two"].Percent, 70.)
}

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

// given a script with the cloud options
fileName := "script.js"
testScript := []byte(`
export let options = {
cloud: {
distribution: {
one: { loadZone: 'amazon:us:ashburn', percent: 30 },
two: { loadZone: 'amazon:ie:dublin', percent: 70 },
},
},
};
export default function () {}
`)
ts := tests.NewGlobalTestState(t)
require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, fileName), testScript, 0o644))

// when we do archiving
ts.CmdArgs = []string{"k6", "archive", fileName}

newRootCommand(ts.GlobalState).execute()
require.NoError(t, testutils.Untar(t, ts.FS, "archive.tar", "tmp/"))

data, err := fsext.ReadFile(ts.FS, "tmp/metadata.json")
require.NoError(t, err)

// we just need some basic struct
metadata := struct {
Options struct {
Cloud struct {
Distribution map[string]struct {
LoadZone string `json:"loadZone"`
Percent float64 `json:"percent"`
} `json:"distribution"`
} `json:"cloud"`
} `json:"options"`
}{}

// then unpacked metadata should contain options.cloud
require.NoError(t, json.Unmarshal(data, &metadata))
require.Len(t, metadata.Options.Cloud.Distribution, 2)

require.Equal(t, metadata.Options.Cloud.Distribution["one"].LoadZone, "amazon:us:ashburn")
require.Equal(t, metadata.Options.Cloud.Distribution["one"].Percent, 30.)
require.Equal(t, metadata.Options.Cloud.Distribution["two"].LoadZone, "amazon:ie:dublin")
require.Equal(t, metadata.Options.Cloud.Distribution["two"].Percent, 70.)
}

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

Expand Down
Loading