| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- //go:build unit
- package config
- import (
- "reflect"
- "sort"
- "strings"
- "testing"
- "github.com/spf13/viper"
- )
- // collectMapstructureKeys walks a config struct and returns every dotted key
- // viper would need in order to populate it.
- func collectMapstructureKeys(t reflect.Type, prefix string, out map[string]string) {
- for i := 0; i < t.NumField(); i++ {
- field := t.Field(i)
- if field.PkgPath != "" {
- continue // unexported
- }
- tag := field.Tag.Get("mapstructure")
- name, _, _ := strings.Cut(tag, ",")
- if name == "-" {
- continue
- }
- if name == "" {
- name = strings.ToLower(field.Name)
- }
- key := name
- if prefix != "" {
- key = prefix + "." + name
- }
- ft := field.Type
- for ft.Kind() == reflect.Ptr {
- ft = ft.Elem()
- }
- if ft.Kind() == reflect.Struct {
- collectMapstructureKeys(ft, key, out)
- continue
- }
- if ft.Kind() == reflect.Map {
- // A map cannot be expressed in a single environment variable, so it
- // is out of scope here — such settings need a config file either way.
- continue
- }
- if ft.Kind() == reflect.Slice {
- elem := ft.Elem()
- for elem.Kind() == reflect.Ptr {
- elem = elem.Elem()
- }
- if elem.Kind() == reflect.Struct {
- // AutomaticEnv exposes one string value. Viper's string-to-slice
- // hook can populate scalar slices, but it cannot decode a string
- // into []struct. Registering a default would turn silent ignore
- // into a startup unmarshal error, so structured slices remain
- // config-file-only just like maps.
- continue
- }
- }
- out[strings.ToLower(key)] = ft.String()
- }
- }
- // TestConfigKeysAreEnvReachable is the systemic guard behind the image_storage
- // bug: viper.Unmarshal only decodes keys returned by AllKeys(), which unions
- // SetDefault keys, config-file keys and explicit BindEnv keys. AutomaticEnv can
- // override a key already in that union but never introduces one, and the
- // viper_bind_struct escape hatch is compiled out (we build with -tags embed).
- //
- // So a Config field with no registered default is unreachable by environment
- // variable whenever the deployment has no config.yaml containing it — the
- // operator sets the variable, the loader discards it, and the feature behaves
- // as if it were never configured. That is exactly how image_storage credentials
- // were lost, silently disabling async image tasks for env-driven deployments.
- //
- // When this fails, register a zero-valued default in setEnvReachableDefaults
- // for each reported scalar key. Maps and slices of structs are config-file-only.
- func TestConfigKeysAreEnvReachable(t *testing.T) {
- bound := map[string]string{}
- collectMapstructureKeys(reflect.TypeOf(Config{}), "", bound)
- viper.Reset()
- t.Cleanup(viper.Reset)
- setDefaults()
- registered := map[string]struct{}{}
- for _, key := range viper.AllKeys() {
- registered[key] = struct{}{}
- }
- var unreachable []string
- for key, kind := range bound {
- if _, ok := registered[key]; !ok {
- unreachable = append(unreachable, key+" ("+kind+")")
- }
- }
- sort.Strings(unreachable)
- if len(unreachable) > 0 {
- t.Fatalf("%d config keys have no default registered, so their environment variables are silently ignored:\n %s",
- len(unreachable), strings.Join(unreachable, "\n "))
- }
- }
|