pomerium/config/custom.go
2021-01-21 15:23:06 -05:00

194 lines
4.2 KiB
Go

package config
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// A StringSlice is a slice of strings.
type StringSlice []string
// NewStringSlice creatse a new StringSlice.
func NewStringSlice(values ...string) StringSlice {
return StringSlice(values)
}
const (
array = iota
arrayValue
object
objectKey
objectValue
)
// UnmarshalJSON unmarshals a JSON document into the string slice.
func (slc *StringSlice) UnmarshalJSON(data []byte) error {
typeStack := []int{array}
stateStack := []int{arrayValue}
var vals []string
dec := json.NewDecoder(bytes.NewReader(data))
for {
token, err := dec.Token()
if err != nil {
break
}
if delim, ok := token.(json.Delim); ok {
switch delim {
case '[':
typeStack = append(typeStack, array)
stateStack = append(stateStack, arrayValue)
case '{':
typeStack = append(typeStack, object)
stateStack = append(stateStack, objectKey)
case ']', '}':
typeStack = typeStack[:len(typeStack)-1]
stateStack = stateStack[:len(stateStack)-1]
}
continue
}
switch stateStack[len(stateStack)-1] {
case objectKey:
stateStack[len(stateStack)-1] = objectValue
case objectValue:
stateStack[len(stateStack)-1] = objectKey
fallthrough
default:
switch t := token.(type) {
case bool:
vals = append(vals, fmt.Sprint(t))
case float64:
vals = append(vals, fmt.Sprint(t))
case json.Number:
vals = append(vals, fmt.Sprint(t))
case string:
vals = append(vals, t)
default:
}
}
}
*slc = StringSlice(vals)
return nil
}
// UnmarshalYAML unmarshals a YAML document into the string slice. UnmarshalJSON is
// reused as the actual implementation.
func (slc *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error {
var i interface{}
err := unmarshal(&i)
if err != nil {
return err
}
bs, err := json.Marshal(i)
if err != nil {
return err
}
return slc.UnmarshalJSON(bs)
}
// DecodeOptionsHookFunc returns a decode hook that will attempt to convert any type to a StringSlice.
func DecodeOptionsHookFunc() mapstructure.DecodeHookFunc {
return func(f, t reflect.Type, data interface{}) (interface{}, error) {
if t != reflect.TypeOf(Options{}) {
return data, nil
}
m, ok := data.(map[string]interface{})
if !ok {
return data, nil
}
ps, ok := m[policyKey].([]interface{})
if !ok {
return data, nil
}
for _, p := range ps {
pm, ok := p.(map[interface{}]interface{})
if !ok {
continue
}
raw, ok := pm[healthCheckKey]
if ok {
hc := new(envoy_config_core_v3.HealthCheck)
if err := parseJSONPB(raw, hc); err != nil {
return nil, fmt.Errorf("%s: %w", healthCheckKey, err)
}
pm[healthCheckKey] = hc
}
rawTo, ok := pm[toKey]
if !ok {
continue
}
rawBS, err := json.Marshal(rawTo)
if err != nil {
return nil, err
}
var slc StringSlice
err = json.Unmarshal(rawBS, &slc)
if err != nil {
return nil, err
}
pm[toKey] = slc
}
return data, nil
}
}
// parseJSONPB takes an intermediate representation and parses it using protobuf parser
// that correctly handles oneof and other data types
func parseJSONPB(raw interface{}, dst proto.Message) error {
ms, err := serializable(raw)
if err != nil {
return err
}
data, err := json.Marshal(ms)
if err != nil {
return err
}
return protojson.Unmarshal(data, dst)
}
// serializable converts mapstructure nested map into map[string]interface{} that is serializable to JSON
func serializable(in interface{}) (interface{}, error) {
switch typed := in.(type) {
case map[interface{}]interface{}:
m := make(map[string]interface{})
for k, v := range typed {
kstr, ok := k.(string)
if !ok {
return nil, errKeysMustBeStrings
}
val, err := serializable(v)
if err != nil {
return nil, err
}
m[kstr] = val
}
return m, nil
case []interface{}:
out := make([]interface{}, 0, len(typed))
for _, elem := range typed {
val, err := serializable(elem)
if err != nil {
return nil, err
}
out = append(out, val)
}
return out, nil
default:
return in, nil
}
}