Skip to content
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
2 changes: 1 addition & 1 deletion cmd/state-svc/internal/notifications/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion cmd/state/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/analytics/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion internal/config/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions internal/locale/locales/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,12 @@ value:
other: Value
default:
other: Default
source:
other: Source
Comment on lines +1548 to +1549

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 19678dc — added config_source_local and config_source_default to en-us.yaml so the Source column no longer relies on the Tl fallback.

config_source_local:
other: local
config_source_default:
other: default
vulnerabilities:
other: Vulnerabilities (CVEs)
dependency_row:
Expand Down
110 changes: 98 additions & 12 deletions internal/mediators/config/registry.go
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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 {
Expand Down
98 changes: 98 additions & 0 deletions internal/mediators/config/registry_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading