package config import ( "os" "regexp" "strconv" "time" "github.com/drone/envsubst" "github.com/pkg/errors" "gopkg.in/yaml.v3" ) // var reVar = regexp.MustCompile(`^\${(\w+)}$`) var reVar = regexp.MustCompile(`\${(.*?)}`) type InterpolatedString string func (is *InterpolatedString) UnmarshalYAML(value *yaml.Node) error { var str string if err := value.Decode(&str); err != nil { return errors.WithStack(err) } if match := reVar.FindStringSubmatch(str); len(match) > 0 { *is = InterpolatedString(os.Getenv(match[1])) } else { *is = InterpolatedString(str) } return nil } type InterpolatedInt int func (ii *InterpolatedInt) UnmarshalYAML(value *yaml.Node) error { var str string if err := value.Decode(&str); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) } if match := reVar.FindStringSubmatch(str); len(match) > 0 { str = os.Getenv(match[1]) } intVal, err := strconv.ParseInt(str, 10, 32) if err != nil { return errors.Wrapf(err, "could not parse int '%v', line '%d'", str, value.Line) } *ii = InterpolatedInt(int(intVal)) return nil } type InterpolatedFloat float64 func (ifl *InterpolatedFloat) UnmarshalYAML(value *yaml.Node) error { var str string if err := value.Decode(&str); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) } if match := reVar.FindStringSubmatch(str); len(match) > 0 { str = os.Getenv(match[1]) } floatVal, err := strconv.ParseFloat(str, 10) if err != nil { return errors.Wrapf(err, "could not parse float '%v', line '%d'", str, value.Line) } *ifl = InterpolatedFloat(floatVal) return nil } type InterpolatedBool bool func (ib *InterpolatedBool) UnmarshalYAML(value *yaml.Node) error { var str string if err := value.Decode(&str); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) } if match := reVar.FindStringSubmatch(str); len(match) > 0 { str = os.Getenv(match[1]) } boolVal, err := strconv.ParseBool(str) if err != nil { return errors.Wrapf(err, "could not parse bool '%v', line '%d'", str, value.Line) } *ib = InterpolatedBool(boolVal) return nil } type InterpolatedMap struct { Data map[string]any getEnv func(string) string } func (im *InterpolatedMap) UnmarshalYAML(value *yaml.Node) error { var data map[string]any if err := value.Decode(&data); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) } if im.getEnv == nil { im.getEnv = os.Getenv } interpolated, err := im.interpolateRecursive(data) if err != nil { return errors.WithStack(err) } im.Data = interpolated.(map[string]any) return nil } func (im *InterpolatedMap) interpolateRecursive(data any) (any, error) { switch typ := data.(type) { case map[string]any: for key, value := range typ { value, err := im.interpolateRecursive(value) if err != nil { return nil, errors.WithStack(err) } typ[key] = value } case string: value, err := envsubst.Eval(typ, im.getEnv) if err != nil { return nil, errors.WithStack(err) } data = value case []any: for idx := range typ { value, err := im.interpolateRecursive(typ[idx]) if err != nil { return nil, errors.WithStack(err) } typ[idx] = value } } return data, nil } type InterpolatedStringSlice []string func (iss *InterpolatedStringSlice) UnmarshalYAML(value *yaml.Node) error { var data []string var evErr error if err := value.Decode(&data); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into map", value.Value, value.Line) } for index, value := range data { //match := reVar.FindStringSubmatch(value) re := regexp.MustCompile(`\${(.*?)}`) res := re.FindAllStringSubmatch(value, 10) if len(res) > 0 { value, evErr = envsubst.EvalEnv(value) if evErr != nil { return evErr } } data[index] = value } *iss = data return nil } type InterpolatedDuration time.Duration func (id *InterpolatedDuration) UnmarshalYAML(value *yaml.Node) error { var str string if err := value.Decode(&str); err != nil { return errors.Wrapf(err, "could not decode value '%v' (line '%d') into string", value.Value, value.Line) } if match := reVar.FindStringSubmatch(str); len(match) > 0 { str = os.Getenv(match[1]) } duration, err := time.ParseDuration(str) if err != nil { nanoseconds, err := strconv.ParseInt(str, 10, 64) if err != nil { return errors.Wrapf(err, "could not parse duration '%v', line '%d'", str, value.Line) } duration = time.Duration(nanoseconds) } *id = InterpolatedDuration(duration) return nil } func (id *InterpolatedDuration) MarshalYAML() (interface{}, error) { duration := time.Duration(*id) return duration.String(), nil } func NewInterpolatedDuration(d time.Duration) *InterpolatedDuration { id := InterpolatedDuration(d) return &id }