package config import ( "bytes" "encoding/json" "os" "reflect" "github.com/mitchellh/mapstructure" ) // SecretValue defines what types of secrets this secret container supports type SecretValue interface { ~string | ~[]string | ~map[string]string } // SecretFilePath is the source to the file containing the secret. type SecretFilePath string const SecretPlaceholder = "[SECRET]" // Secret encapsulates a secret value, which is loaded from a file, or directly from configuration / environment variable type Secret[T SecretValue] struct { value T source SecretFilePath } // MarshalJSON implements the json.Marshaler interface, used to hide the actual secret value in logs etc. func (s Secret[T]) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]string{"value": SecretPlaceholder, "source": string(s.source)}) } // MarshalText implements the encoding.TextMarshaler interface, used to hide the actual secret value in logs etc. func (s *Secret[T]) MarshalText() ([]byte, error) { return []byte(SecretPlaceholder), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface, used to load the secret from config files and env. values func (s *Secret[T]) UnmarshalText(text []byte) error { var val T switch any(val).(type) { case string: s.value = any(string(text)).(T) return nil default: return json.Unmarshal(text, &s.value) } } func (s Secret[T]) Value() T { return s.value } func SecretFromSecretPath[T SecretValue](path SecretFilePath) (Secret[T], error) { contentBytes, err := os.ReadFile(string(path)) if err != nil { return Secret[T]{}, err } secret := Secret[T]{source: path} err = secret.UnmarshalText(bytes.TrimSpace(contentBytes)) return secret, err } func SecretFromValue[T SecretValue](value T) Secret[T] { return Secret[T]{value: value} } // SecretFilePathUnmarshalHookFunc is a mapstructure.DecodeHookFunc that will convert a SecretFilePath to a Secret func SecretFilePathUnmarshalHookFunc() mapstructure.DecodeHookFuncType { return func(from, to reflect.Type, data interface{}) (interface{}, error) { if from != reflect.TypeOf(SecretFilePath("")) { return data, nil } // Reflection does not work with generics as of 1.20, so we have to do this manually if to == reflect.TypeOf(Secret[map[string]string]{}) { return SecretFromSecretPath[map[string]string](data.(SecretFilePath)) } else if to == reflect.TypeOf(Secret[[]string]{}) { return SecretFromSecretPath[[]string](data.(SecretFilePath)) } else if to == reflect.TypeOf(Secret[string]{}) { return SecretFromSecretPath[string](data.(SecretFilePath)) } else { return data, nil } } }