package config import ( "fmt" "io" "os" "reflect" "regexp" "strings" ) var environmentPattern = regexp.MustCompile(`\$\{([A-Z_][A-Z0-9_]*)\}`) type Resolver interface { LookupEnv(string) (string, bool) ReadFile(string) ([]byte, error) } type OSResolver struct{} func (OSResolver) LookupEnv(name string) (string, bool) { return os.LookupEnv(name) } func (OSResolver) ReadFile(path string) ([]byte, error) { return os.ReadFile(path) } func LoadResolved(reader io.Reader, resolver Resolver) (*Config, error) { if resolver == nil { return nil, fmt.Errorf("resolve configuration: resolver is required") } cfg, err := decode(reader) if err != nil { return nil, err } if err := expandValue(reflect.ValueOf(cfg).Elem(), resolver.LookupEnv); err != nil { return nil, err } if err := resolveSecretFiles(cfg, resolver); err != nil { return nil, err } if err := Validate(cfg); err != nil { return nil, err } return cfg, nil } func resolveSecretFiles(cfg *Config, resolver Resolver) error { for _, auth := range []*Auth{&cfg.Gateway.Auth, &cfg.Distribution.Auth, &cfg.Admin.Auth} { if err := resolveSecret(&auth.Password, &auth.PasswordFile, resolver); err != nil { return err } if err := resolveSecret(&auth.Token, &auth.TokenFile, resolver); err != nil { return err } for index := range auth.Methods { method := &auth.Methods[index] if err := resolveSecret(&method.Password, &method.PasswordFile, resolver); err != nil { return err } if err := resolveSecret(&method.Value, &method.ValueFile, resolver); err != nil { return err } } } for name, upstream := range cfg.Upstreams { if err := resolveSecret(&upstream.API.Auth.Password, &upstream.API.Auth.PasswordFile, resolver); err != nil { return fmt.Errorf("resolve upstream %q api password: %w", name, err) } if err := resolveSecret(&upstream.API.Auth.Token, &upstream.API.Auth.TokenFile, resolver); err != nil { return fmt.Errorf("resolve upstream %q api token: %w", name, err) } if err := resolveSecret(&upstream.API.Auth.Value, &upstream.API.Auth.ValueFile, resolver); err != nil { return fmt.Errorf("resolve upstream %q api key: %w", name, err) } if err := resolveSecret(&upstream.ProxyAuth.Password, &upstream.ProxyAuth.PasswordFile, resolver); err != nil { return fmt.Errorf("resolve upstream %q proxy password: %w", name, err) } cfg.Upstreams[name] = upstream } return nil } func resolveSecret(value, fileRef *string, resolver Resolver) error { if *value != "" { *fileRef = "" return nil } if *fileRef == "" { return nil } content, err := resolver.ReadFile(*fileRef) if err != nil { return err } resolved := strings.TrimRight(string(content), "\r\n") if resolved == "" { return fmt.Errorf("secret file %q is empty", *fileRef) } *value = resolved *fileRef = "" return nil } func expandValue(value reflect.Value, lookup func(string) (string, bool)) error { switch value.Kind() { case reflect.String: expanded, err := expandEnvironment(value.String(), lookup) if err != nil { return err } value.SetString(expanded) case reflect.Struct: for index := 0; index < value.NumField(); index++ { if err := expandValue(value.Field(index), lookup); err != nil { return err } } case reflect.Slice: for index := 0; index < value.Len(); index++ { if err := expandValue(value.Index(index), lookup); err != nil { return err } } case reflect.Map: iterator := value.MapRange() for iterator.Next() { item := reflect.New(value.Type().Elem()).Elem() item.Set(iterator.Value()) if err := expandValue(item, lookup); err != nil { return err } value.SetMapIndex(iterator.Key(), item) } } return nil } func expandEnvironment(value string, lookup func(string) (string, bool)) (string, error) { var resolveErr error expanded := environmentPattern.ReplaceAllStringFunc(value, func(match string) string { name := environmentPattern.FindStringSubmatch(match)[1] resolved, ok := lookup(name) if !ok { resolveErr = fmt.Errorf("resolve configuration: environment variable %s is not set", name) return match } return resolved }) return expanded, resolveErr }