diff --git a/cmd/state-svc/internal/notifications/notifications.go b/cmd/state-svc/internal/notifications/notifications.go index 19136e29a9..b2555ea723 100644 --- a/cmd/state-svc/internal/notifications/notifications.go +++ b/cmd/state-svc/internal/notifications/notifications.go @@ -25,7 +25,7 @@ import ( ) func init() { - configMediator.RegisterOption(constants.NotificationsURLConfig, configMediator.String, "") + configMediator.RegisterOption(constants.NotificationsURLConfig, configMediator.String, "", constants.NotificationsOverrideEnvVarName) } const ConfigKeyLastReport = "notifications.last_reported" diff --git a/cmd/state/main.go b/cmd/state/main.go index 8ddde10de9..64b17ad955 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -97,7 +97,7 @@ func main() { // Configuration options // This should only be used if the config option is not exclusive to one package. configMediator.RegisterOption(constants.OptinBuildscriptsConfig, configMediator.Bool, false) - configMediator.RegisterOption(constants.NotificationsURLConfig, configMediator.String, "") + configMediator.RegisterOption(constants.NotificationsURLConfig, configMediator.String, "", constants.NotificationsOverrideEnvVarName) // Set up our output formatter/writer outFlags := parseOutputFlags(os.Args) diff --git a/internal/analytics/analytics.go b/internal/analytics/analytics.go index 05641be0d1..a1bdfdabf5 100644 --- a/internal/analytics/analytics.go +++ b/internal/analytics/analytics.go @@ -7,7 +7,7 @@ import ( ) func init() { - configMediator.RegisterOption(constants.AnalyticsPixelOverrideConfig, configMediator.String, "") + configMediator.RegisterOption(constants.AnalyticsPixelOverrideConfig, configMediator.String, "", constants.AnalyticsPixelOverrideEnv) } // Dispatcher describes a struct that can send analytics event in the background diff --git a/internal/config/instance.go b/internal/config/instance.go index bfc3979bc4..4d666fa850 100644 --- a/internal/config/instance.go +++ b/internal/config/instance.go @@ -178,11 +178,20 @@ func (i *Instance) rawGet(key string) interface{} { } func (i *Instance) Get(key string) interface{} { + opt := mediator.GetOption(key) + + // An environment variable override takes precedence over any stored or default value. + if mediator.KnownOption(opt) { + if value, _, ok := mediator.EnvOverride(opt); ok { + return value + } + } + result := i.rawGet(key) if result != nil { return result } - if opt := mediator.GetOption(key); mediator.KnownOption(opt) { + if mediator.KnownOption(opt) { return mediator.GetDefault(opt) } return nil diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index d7a6d8e8c3..3c1f136aa0 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -1545,6 +1545,12 @@ value: other: Value default: other: Default +source: + other: Source +config_source_local: + other: local +config_source_default: + other: default vulnerabilities: other: Vulnerabilities (CVEs) dependency_row: diff --git a/internal/mediators/config/registry.go b/internal/mediators/config/registry.go index f6f327f53d..80f7695afc 100644 --- a/internal/mediators/config/registry.go +++ b/internal/mediators/config/registry.go @@ -1,6 +1,24 @@ package config -import "sort" +import ( + "os" + "sort" + "strings" + + "github.com/spf13/cast" +) + +// EnvVarPrefix is prepended to a config key to derive its canonical environment-variable override. +const EnvVarPrefix = "ACTIVESTATE_CONFIG_" + +var envVarReplacer = strings.NewReplacer(".", "_", "-", "_") + +// CanonicalEnvVarName returns the environment variable that overrides the given config key. Every +// registered config option can be overridden this way, e.g. "api.host" maps to +// "ACTIVESTATE_CONFIG_API_HOST". +func CanonicalEnvVarName(key string) string { + return EnvVarPrefix + strings.ToUpper(envVarReplacer.Replace(key)) +} type Type int @@ -25,9 +43,13 @@ var EmptyEvent = func(value interface{}) (interface{}, error) { // Option defines what a config value's name and type should be, along with any get/set events type Option struct { - Name string - Type Type - Default interface{} + Name string + Type Type + Default interface{} + // EnvAliases are additional (legacy/bespoke) environment variables that override this option, in + // addition to its canonical CanonicalEnvVarName. They are kept for backwards compatibility with + // env vars that predate the canonical ACTIVESTATE_CONFIG_* scheme (e.g. ACTIVESTATE_API_HOST). + EnvAliases []string GetEvent Event SetEvent Event isRegistered bool @@ -52,28 +74,92 @@ func NewEnum(options []string, default_ string) *Enums { func GetOption(key string) Option { rule, ok := registry[key] if !ok { - return Option{key, String, "", EmptyEvent, EmptyEvent, false, false} + return Option{key, String, "", nil, EmptyEvent, EmptyEvent, false, false} } return rule } -// Registers a config option without get/set events. -func RegisterOption(key string, t Type, defaultValue interface{}) { - registerOption(key, t, defaultValue, EmptyEvent, EmptyEvent, false) +// RegisterOption registers a config option without get/set events. Any envAliases are additional +// legacy/bespoke environment variables that override the option, in addition to its canonical +// ACTIVESTATE_CONFIG_* variable. They exist only for env vars that predate and do not use the +// canonical prefix (e.g. ACTIVESTATE_API_HOST). +func RegisterOption(key string, t Type, defaultValue interface{}, envAliases ...string) { + registerOption(key, t, defaultValue, envAliases, EmptyEvent, EmptyEvent, false) } // Registers a hidden config option without get/set events. func RegisterHiddenOption(key string, t Type, defaultValue interface{}) { - registerOption(key, t, defaultValue, EmptyEvent, EmptyEvent, true) + registerOption(key, t, defaultValue, nil, EmptyEvent, EmptyEvent, true) } // Registers a config option with get/set events. func RegisterOptionWithEvents(key string, t Type, defaultValue interface{}, get, set Event) { - registerOption(key, t, defaultValue, get, set, false) + registerOption(key, t, defaultValue, nil, get, set, false) +} + +func registerOption(key string, t Type, defaultValue interface{}, envAliases []string, get, set Event, hidden bool) { + registry[key] = Option{key, t, defaultValue, envAliases, get, set, true, hidden} +} + +// EnvVarNames returns every environment variable that can override this option: its canonical +// ACTIVESTATE_CONFIG_* variable first, followed by any legacy aliases. +func EnvVarNames(opt Option) []string { + names := make([]string, 0, len(opt.EnvAliases)+1) + names = append(names, CanonicalEnvVarName(opt.Name)) + names = append(names, opt.EnvAliases...) + return names } -func registerOption(key string, t Type, defaultValue interface{}, get, set Event, hidden bool) { - registry[key] = Option{key, t, defaultValue, get, set, true, hidden} +// EnvOverride returns the effective override value for the option when one of its environment +// variables is currently set to a non-empty, valid value, coerced to the option's type. The second +// return value is the name of the variable in effect; the bool reports whether an override applies. +// +// A variable whose value cannot be strictly coerced to the option's type (an unparseable bool/int +// or an enum value outside the allowed set) is ignored, so a mis-typed variable falls back to the +// stored/default value rather than silently changing behavior to a zero value. +func EnvOverride(opt Option) (interface{}, string, bool) { + for _, name := range EnvVarNames(opt) { + raw, ok := os.LookupEnv(name) + if !ok || raw == "" { + continue + } + if value, valid := coerceToType(opt, raw); valid { + return value, name, true + } + } + return nil, "", false +} + +// coerceToType converts a raw environment-variable string to the option's configured type so that +// callers receive the same Go type they would get from a stored value. The bool result reports +// whether the raw value is valid for the option's type; invalid values must not be applied. +func coerceToType(opt Option, raw string) (interface{}, bool) { + switch opt.Type { + case Bool: + v, err := cast.ToBoolE(raw) + if err != nil { + return nil, false + } + return v, true + case Int: + v, err := cast.ToIntE(raw) + if err != nil { + return nil, false + } + return v, true + case Enum: + if enums, ok := opt.Default.(*Enums); ok { + for _, option := range enums.Options { + if option == raw { + return raw, true + } + } + return nil, false + } + return raw, true + default: // String + return raw, true + } } func KnownOption(rule Option) bool { diff --git a/internal/mediators/config/registry_test.go b/internal/mediators/config/registry_test.go new file mode 100644 index 0000000000..b66829e1c4 --- /dev/null +++ b/internal/mediators/config/registry_test.go @@ -0,0 +1,98 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCanonicalEnvVarName(t *testing.T) { + assert.Equal(t, "ACTIVESTATE_CONFIG_API_HOST", CanonicalEnvVarName("api.host")) + assert.Equal(t, "ACTIVESTATE_CONFIG_AUTOUPDATE", CanonicalEnvVarName("autoupdate")) + assert.Equal(t, "ACTIVESTATE_CONFIG_SECURITY_PROMPT_LEVEL", CanonicalEnvVarName("security.prompt.level")) + assert.Equal(t, "ACTIVESTATE_CONFIG_PRIVATEINGREDIENT_MTLS_CERT", CanonicalEnvVarName("privateingredient.mtls_cert")) +} + +func TestEnvOverrideCanonical(t *testing.T) { + RegisterOption("test.canonical.key", String, "default") + opt := GetOption("test.canonical.key") + + // Not set -> no override. + _, _, ok := EnvOverride(opt) + assert.False(t, ok) + + // Canonical variable applies. + t.Setenv("ACTIVESTATE_CONFIG_TEST_CANONICAL_KEY", "from-canonical") + value, envVar, ok := EnvOverride(opt) + assert.True(t, ok) + assert.Equal(t, "from-canonical", value) + assert.Equal(t, "ACTIVESTATE_CONFIG_TEST_CANONICAL_KEY", envVar) +} + +func TestEnvOverrideAliasAndPrecedence(t *testing.T) { + RegisterOption("test.alias.key", String, "default", "LEGACY_ALIAS_VAR") + opt := GetOption("test.alias.key") + + // A legacy alias applies when the canonical var is unset. + t.Setenv("LEGACY_ALIAS_VAR", "from-alias") + value, envVar, ok := EnvOverride(opt) + assert.True(t, ok) + assert.Equal(t, "from-alias", value) + assert.Equal(t, "LEGACY_ALIAS_VAR", envVar) + + // The canonical variable takes precedence over the alias. + t.Setenv("ACTIVESTATE_CONFIG_TEST_ALIAS_KEY", "from-canonical") + value, envVar, ok = EnvOverride(opt) + assert.True(t, ok) + assert.Equal(t, "from-canonical", value) + assert.Equal(t, "ACTIVESTATE_CONFIG_TEST_ALIAS_KEY", envVar) +} + +func TestEnvOverrideTypeCoercion(t *testing.T) { + RegisterOption("test.coerce.bool", Bool, false) + RegisterOption("test.coerce.int", Int, 0) + + t.Setenv("ACTIVESTATE_CONFIG_TEST_COERCE_BOOL", "true") + v, _, ok := EnvOverride(GetOption("test.coerce.bool")) + assert.True(t, ok) + assert.Equal(t, true, v, "bool env value should be coerced to a real bool") + + t.Setenv("ACTIVESTATE_CONFIG_TEST_COERCE_INT", "42") + v, _, ok = EnvOverride(GetOption("test.coerce.int")) + assert.True(t, ok) + assert.Equal(t, 42, v, "int env value should be coerced to a real int") +} + +func TestEnvOverrideEmptyIgnored(t *testing.T) { + RegisterOption("test.empty.key", String, "default") + t.Setenv("ACTIVESTATE_CONFIG_TEST_EMPTY_KEY", "") + _, _, ok := EnvOverride(GetOption("test.empty.key")) + assert.False(t, ok, "an empty env var should be treated as unset") +} + +func TestEnvOverrideInvalidIgnored(t *testing.T) { + RegisterOption("test.invalid.bool", Bool, false) + RegisterOption("test.invalid.int", Int, 0) + RegisterOption("test.invalid.enum", Enum, NewEnum([]string{"low", "high"}, "low")) + + // An unparseable bool must be ignored rather than coerced to false. + t.Setenv("ACTIVESTATE_CONFIG_TEST_INVALID_BOOL", "notabool") + _, _, ok := EnvOverride(GetOption("test.invalid.bool")) + assert.False(t, ok, "an unparseable bool env var must not apply") + + // An unparseable int must be ignored rather than coerced to 0. + t.Setenv("ACTIVESTATE_CONFIG_TEST_INVALID_INT", "notanint") + _, _, ok = EnvOverride(GetOption("test.invalid.int")) + assert.False(t, ok, "an unparseable int env var must not apply") + + // An enum value outside the allowed set must be ignored. + t.Setenv("ACTIVESTATE_CONFIG_TEST_INVALID_ENUM", "banana") + _, _, ok = EnvOverride(GetOption("test.invalid.enum")) + assert.False(t, ok, "an out-of-set enum env var must not apply") + + // A valid enum value still applies. + t.Setenv("ACTIVESTATE_CONFIG_TEST_INVALID_ENUM", "high") + value, _, ok := EnvOverride(GetOption("test.invalid.enum")) + assert.True(t, ok) + assert.Equal(t, "high", value) +} diff --git a/internal/runners/config/config.go b/internal/runners/config/config.go index 689b40e7ef..b890b56e7f 100644 --- a/internal/runners/config/config.go +++ b/internal/runners/config/config.go @@ -29,11 +29,46 @@ func NewList(prime primeable) (*List, error) { }, nil } +// Where an option's effective value comes from. +const ( + sourceEnvironment = "environment" // overridden by an environment variable + sourceLocal = "local" // set locally by the user (stored in config.db) + sourceDefault = "default" // neither set nor overridden; using the built-in default +) + type structuredConfigData struct { Key string `json:"key"` Value interface{} `json:"value"` Default interface{} `json:"default"` - opt mediator.Option + // Source is where the effective value comes from: "environment", "local", or "default". + Source string `json:"source"` + // EnvVar is the canonical environment variable that can override this key (always present). + EnvVar string `json:"envVar"` + // Env is the name of the environment variable currently overriding this value, if any. + Env string `json:"env,omitempty"` + opt mediator.Option +} + +// effectiveValue returns the value the State Tool will actually use for the option, along with the +// name of the environment variable overriding it (empty when the value comes from stored config or +// the built-in default). +func effectiveValue(cfg *config.Instance, opt mediator.Option) (interface{}, string) { + if envValue, envVar, ok := mediator.EnvOverride(opt); ok { + return envValue, envVar + } + return cfg.Get(opt.Name), "" +} + +// configSource reports where an option's effective value comes from, and the name of the +// environment variable in effect when the source is the environment. +func configSource(cfg *config.Instance, opt mediator.Option) (source string, envVar string) { + if _, name, ok := mediator.EnvOverride(opt); ok { + return sourceEnvironment, name + } + if cfg.IsSet(opt.Name) { + return sourceLocal, "" + } + return sourceDefault, "" } func (c *List) Run() error { @@ -44,11 +79,15 @@ func (c *List) Run() error { var data []structuredConfigData for _, opt := range registered { - configuredValue := cfg.Get(opt.Name) + configuredValue, envVar := effectiveValue(cfg, opt) + source, _ := configSource(cfg, opt) data = append(data, structuredConfigData{ Key: opt.Name, Value: configuredValue, Default: mediator.GetDefault(opt), + Source: source, + EnvVar: mediator.CanonicalEnvVarName(opt.Name), + Env: envVar, opt: opt, }) } @@ -68,12 +107,13 @@ func (c *List) renderUserFacing(configData []structuredConfigData) error { cfg := c.prime.Config() out := c.prime.Output() - tbl := table.New(locale.Ts("key", "value", "default")) + tbl := table.New(locale.Ts("key", "value", "source", "default")) tbl.HideDash = true for _, config := range configData { tbl.AddRow([]string{ fmt.Sprintf("[CYAN]%s[/RESET]", config.Key), renderConfigValue(cfg, config.opt), + renderConfigSource(cfg, config.opt), fmt.Sprintf("[DISABLED]%s[/RESET]", formatValue(config.opt, config.Default)), }) } @@ -87,7 +127,7 @@ func (c *List) renderUserFacing(configData []structuredConfigData) error { } func renderConfigValue(cfg *config.Instance, opt mediator.Option) string { - configured := cfg.Get(opt.Name) + configured, _ := effectiveValue(cfg, opt) var tags []string if opt.Type == mediator.Bool { if configured == true { @@ -98,11 +138,6 @@ func renderConfigValue(cfg *config.Instance, opt mediator.Option) string { } value := formatValue(opt, configured) - if cfg.IsSet(opt.Name) { - tags = append(tags, "[BOLD]") - value = value + "*" - } - if len(tags) > 0 { return fmt.Sprintf("%s%s[/RESET]", strings.Join(tags, ""), value) } @@ -110,6 +145,20 @@ func renderConfigValue(cfg *config.Instance, opt mediator.Option) string { return value } +// renderConfigSource renders the Source column: the environment variable in effect (when overridden +// by the environment), "local" (set by the user), or a de-emphasized "default". +func renderConfigSource(cfg *config.Instance, opt mediator.Option) string { + source, envVar := configSource(cfg, opt) + switch source { + case sourceEnvironment: + return fmt.Sprintf("[BOLD]%s[/RESET]", envVar) + case sourceLocal: + return fmt.Sprintf("[BOLD]%s[/RESET]", locale.Tl("config_source_local", "local")) + default: + return fmt.Sprintf("[DISABLED]%s[/RESET]", locale.Tl("config_source_default", "default")) + } +} + func formatValue(opt mediator.Option, value interface{}) string { switch opt.Type { case mediator.Enum, mediator.String: diff --git a/internal/runners/config/env_test.go b/internal/runners/config/env_test.go new file mode 100644 index 0000000000..6f7dd10722 --- /dev/null +++ b/internal/runners/config/env_test.go @@ -0,0 +1,69 @@ +package config + +import ( + "testing" + + "github.com/ActiveState/cli/internal/config" + configMediator "github.com/ActiveState/cli/internal/mediators/config" + "github.com/ActiveState/cli/internal/testhelpers/outputhelper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestEffectiveValueEnvOverride verifies precedence used by `state config` (list): an environment +// variable override wins over a stored value, which wins over the default. +func TestEffectiveValueEnvOverride(t *testing.T) { + cfg, err := config.New() + require.NoError(t, err) + defer func() { require.NoError(t, cfg.Close()) }() + + configMediator.RegisterOption("test.env.host", configMediator.String, "default-host", "TEST_ENV_HOST_OVERRIDE") + opt := configMediator.GetOption("test.env.host") + + // No env, no stored value -> default, not overridden. + v, envVar := effectiveValue(cfg, opt) + assert.Equal(t, "default-host", v) + assert.Equal(t, "", envVar) + + // Stored value wins when env is not set. + require.NoError(t, cfg.Set("test.env.host", "user-host")) + v, envVar = effectiveValue(cfg, opt) + assert.Equal(t, "user-host", v) + assert.Equal(t, "", envVar) + + // Env override wins over the stored value and reports the source var. + t.Setenv("TEST_ENV_HOST_OVERRIDE", "env-host") + v, envVar = effectiveValue(cfg, opt) + assert.Equal(t, "env-host", v) + assert.Equal(t, "TEST_ENV_HOST_OVERRIDE", envVar) + + // An empty env var is treated as not set, falling back to the stored value. + t.Setenv("TEST_ENV_HOST_OVERRIDE", "") + v, envVar = effectiveValue(cfg, opt) + assert.Equal(t, "user-host", v) + assert.Equal(t, "", envVar) +} + +// TestGetEnvOverride verifies `state config get ` reports the environment override value. +func TestGetEnvOverride(t *testing.T) { + cfg, err := config.New() + require.NoError(t, err) + defer func() { require.NoError(t, cfg.Close()) }() + + configMediator.RegisterOption("test.env.get", configMediator.String, "", "TEST_ENV_GET_OVERRIDE") + require.NoError(t, cfg.Set("test.env.get", "stored-value")) + + outputer := outputhelper.NewCatcher() + get := &Get{outputer, cfg} + + // Without the env var, the stored value is reported. + require.NoError(t, get.Run(GetParams{Key: Key("test.env.get")})) + assert.Contains(t, outputer.CombinedOutput(), "stored-value") + + // With the env var set, the override is reported instead. + t.Setenv("TEST_ENV_GET_OVERRIDE", "env-value") + outputer = outputhelper.NewCatcher() + get = &Get{outputer, cfg} + require.NoError(t, get.Run(GetParams{Key: Key("test.env.get")})) + assert.Contains(t, outputer.CombinedOutput(), "env-value") +} diff --git a/internal/updater/checker.go b/internal/updater/checker.go index b11c2789c0..3d520169c8 100644 --- a/internal/updater/checker.go +++ b/internal/updater/checker.go @@ -35,7 +35,7 @@ var ( ) func init() { - configMediator.RegisterOption(constants.UpdateInfoEndpointConfig, configMediator.String, "") + configMediator.RegisterOption(constants.UpdateInfoEndpointConfig, configMediator.String, "", constants.TestUpdateInfoURLEnvVarName) } type Checker struct { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index b3a7f4bed8..0b5fe7bd44 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -36,7 +36,7 @@ const ( ) func init() { - configMediator.RegisterOption(constants.UpdateEndpointConfig, configMediator.String, "") + configMediator.RegisterOption(constants.UpdateEndpointConfig, configMediator.String, "", constants.TestUpdateURLEnvVarName) } type ErrorInProgress struct{ *locale.LocalizedError } diff --git a/pkg/platform/api/settings.go b/pkg/platform/api/settings.go index 947f6ff367..25e936bf6d 100644 --- a/pkg/platform/api/settings.go +++ b/pkg/platform/api/settings.go @@ -15,7 +15,7 @@ import ( ) func init() { - configMediator.RegisterOption(constants.APIHostConfig, configMediator.String, "") + configMediator.RegisterOption(constants.APIHostConfig, configMediator.String, "", constants.APIHostEnvVarName) } // Service records available api services diff --git a/test/integration/config_int_test.go b/test/integration/config_int_test.go index ea5972d39d..b85c48789e 100644 --- a/test/integration/config_int_test.go +++ b/test/integration/config_int_test.go @@ -96,6 +96,7 @@ func (suite *ConfigIntegrationTestSuite) TestList() { cp := ts.Spawn("config") cp.Expect("Key") cp.Expect("Value") + cp.Expect("Source") cp.Expect("Default") cp.Expect("optin.buildscripts") cp.Expect("false") @@ -108,14 +109,52 @@ func (suite *ConfigIntegrationTestSuite) TestList() { cp = ts.Spawn("config") cp.Expect("Key") cp.Expect("Value") + cp.Expect("Source") cp.Expect("Default") cp.Expect("optin.buildscripts") - cp.Expect("true*") + // The value is set locally (via `state config set`), so the Source column reads "local". + cp.Expect("true") + cp.Expect("local") cp.ExpectExitCode(0) suite.Require().NotContains(cp.Snapshot(), constants.AsyncRuntimeConfig) } +func (suite *ConfigIntegrationTestSuite) TestListEnvSource() { + suite.OnlyRunForTags(tagsuite.Config) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + // optin.buildscripts defaults to false. Override it via its canonical environment variable + // (without ever running `state config set`), so any non-default value proves the environment + // override took effect. + envVar := "ACTIVESTATE_CONFIG_OPTIN_BUILDSCRIPTS" + + // Human-readable table: the value reflects the environment override. + cp := ts.SpawnWithOpts( + e2e.OptArgs("config"), + e2e.OptAppendEnv(envVar+"=true"), + ) + cp.Expect("Key") + cp.Expect("Value") + cp.Expect("Source") + cp.Expect("Default") + cp.Expect("optin.buildscripts") + cp.Expect("true") + cp.ExpectExitCode(0) + + // JSON output reliably exposes the source metadata (the table wraps long env var names). The + // "environment" source and the overriding variable name are both reported. + cp = ts.SpawnWithOpts( + e2e.OptArgs("config", "-o", "json"), + e2e.OptAppendEnv(envVar+"=true"), + ) + cp.Expect(`"source":"environment"`) + cp.Expect(`"env":"` + envVar + `"`) + cp.ExpectExitCode(0) + AssertValidJSON(suite.T(), cp) +} + func (suite *ConfigIntegrationTestSuite) TestAPIHostConfig() { suite.OnlyRunForTags(tagsuite.Config) ts := e2e.New(suite.T(), false)