Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
186 lines
4.0 KiB
Go
186 lines
4.0 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package configx
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/tidwall/sjson"
|
|
|
|
"github.com/ory/jsonschema/v3"
|
|
|
|
"github.com/spf13/cast"
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/x/castx"
|
|
"github.com/ory/x/jsonschemax"
|
|
)
|
|
|
|
var isNumRegex = regexp.MustCompile("^[0-9]+$")
|
|
|
|
func NewKoanfEnv(prefix string, rawSchema []byte, schema *jsonschema.Schema) (*Env, error) {
|
|
paths, err := getSchemaPaths(rawSchema, schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Env{
|
|
paths: paths,
|
|
prefix: prefix,
|
|
}, nil
|
|
}
|
|
|
|
// Env implements an environment variables provider.
|
|
type Env struct {
|
|
prefix string
|
|
paths []jsonschemax.Path
|
|
}
|
|
|
|
// ReadBytes is not supported by the env provider.
|
|
func (e *Env) ReadBytes() ([]byte, error) {
|
|
return nil, errors.New("env provider does not support this method")
|
|
}
|
|
|
|
// Read reads all available environment variables into a key:value map
|
|
// and returns it.
|
|
func (e *Env) Read() (map[string]interface{}, error) {
|
|
// Collect the environment variable keys.
|
|
var keys []string
|
|
for _, k := range os.Environ() {
|
|
if e.prefix != "" {
|
|
if strings.HasPrefix(k, e.prefix) {
|
|
keys = append(keys, k)
|
|
}
|
|
} else {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
|
|
raw := "{}"
|
|
var err error
|
|
for _, k := range keys {
|
|
parts := strings.SplitN(k, "=", 2)
|
|
|
|
key, value := e.extract(parts[0], parts[1])
|
|
// If the callback blanked the key, it should be omitted
|
|
if key == "" {
|
|
continue
|
|
}
|
|
|
|
raw, err = sjson.Set(raw, key, value)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Watch is not supported.
|
|
func (e *Env) Watch(cb func(event interface{}, err error)) error {
|
|
return errors.New("env provider does not support this method")
|
|
}
|
|
|
|
func (e *Env) extract(key string, value string) (string, interface{}) {
|
|
key = strings.Replace(strings.ToLower(strings.TrimPrefix(key, e.prefix)), "_", ".", -1)
|
|
|
|
for _, path := range e.paths {
|
|
normalized := strings.Replace(path.Name, "_", ".", -1)
|
|
name := path.Name
|
|
|
|
// Crazy hack to get arrays working.
|
|
var indices []string
|
|
searchParts := strings.Split(normalized, ".")
|
|
keyParts := strings.Split(key, ".")
|
|
if len(searchParts) == len(keyParts) {
|
|
for k, search := range searchParts {
|
|
if search != keyParts[k] {
|
|
indices = nil
|
|
}
|
|
|
|
if search != "#" {
|
|
continue
|
|
}
|
|
|
|
if !isNumRegex.MatchString(keyParts[k]) {
|
|
continue
|
|
}
|
|
|
|
searchParts[k] = keyParts[k]
|
|
indices = append(indices, keyParts[k])
|
|
}
|
|
}
|
|
|
|
if len(indices) > 0 {
|
|
normalized = strings.Join(searchParts, ".")
|
|
for _, index := range indices {
|
|
name = strings.Replace(name, "#", index, 1)
|
|
}
|
|
}
|
|
|
|
if normalized == key {
|
|
switch path.TypeHint {
|
|
case jsonschemax.String:
|
|
return name, cast.ToString(value)
|
|
case jsonschemax.Float:
|
|
return name, cast.ToFloat64(value)
|
|
case jsonschemax.Int:
|
|
return name, cast.ToInt64(value)
|
|
case jsonschemax.Bool:
|
|
return name, cast.ToBool(value)
|
|
case jsonschemax.Nil:
|
|
return name, nil
|
|
case jsonschemax.BoolSlice:
|
|
if !gjson.Valid(value) {
|
|
return name, cast.ToBoolSlice(value)
|
|
}
|
|
fallthrough
|
|
case jsonschemax.StringSlice:
|
|
if !gjson.Valid(value) {
|
|
return name, castx.ToStringSlice(value)
|
|
}
|
|
fallthrough
|
|
case jsonschemax.IntSlice:
|
|
if !gjson.Valid(value) {
|
|
return name, cast.ToIntSlice(value)
|
|
}
|
|
fallthrough
|
|
case jsonschemax.FloatSlice:
|
|
if !gjson.Valid(value) {
|
|
return name, castx.ToFloatSlice(value)
|
|
}
|
|
fallthrough
|
|
case jsonschemax.JSON:
|
|
return name, decode(value)
|
|
default:
|
|
return name, value
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
func decode(value string) (v interface{}) {
|
|
b := []byte(value)
|
|
var arr []interface{}
|
|
if err := json.Unmarshal(b, &arr); err == nil {
|
|
return &arr
|
|
}
|
|
h := map[string]interface{}{}
|
|
if err := json.Unmarshal(b, &h); err == nil {
|
|
return &h
|
|
}
|
|
return nil
|
|
}
|