Skip to content

Commit

Permalink
Merge bb6f6c5 into 9dbf343
Browse files Browse the repository at this point in the history
  • Loading branch information
olegbespalov authored Feb 13, 2024
2 parents 9dbf343 + bb6f6c5 commit de0815c
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 61 deletions.
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!
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:
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
// 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

0 comments on commit de0815c

Please sign in to comment.