proxy-pool/internal/config/resolve.go

148 lines
4.1 KiB
Go

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
}