mirror of https://github.com/status-im/consul.git
agent/structs: JSON marshal the configuration for a managed proxy
This commit is contained in:
parent
e9e6514c9b
commit
3c17144fb5
|
@ -1,9 +1,14 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mitchellh/copystructure"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/reflectwalk"
|
||||
)
|
||||
|
||||
// ServiceDefinition is used to JSON decode the Service definitions. For
|
||||
|
@ -130,7 +135,171 @@ func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
|
|||
// registration. Note this is duplicated in config.ServiceConnectProxy and needs
|
||||
// to be kept in sync.
|
||||
type ServiceDefinitionConnectProxy struct {
|
||||
Command []string
|
||||
ExecMode string
|
||||
Config map[string]interface{}
|
||||
Command []string `json:",omitempty"`
|
||||
ExecMode string `json:",omitempty"`
|
||||
Config map[string]interface{} `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (p *ServiceDefinitionConnectProxy) MarshalJSON() ([]byte, error) {
|
||||
type typeCopy ServiceDefinitionConnectProxy
|
||||
copy := typeCopy(*p)
|
||||
|
||||
// If we have config, then we want to run it through our proxyConfigWalker
|
||||
// which is a reflectwalk implementation that attempts to turn arbitrary
|
||||
// interface{} values into JSON-safe equivalents (more or less). This
|
||||
// should always work because the config input is either HCL or JSON and
|
||||
// both are JSON compatible.
|
||||
if copy.Config != nil {
|
||||
configCopyRaw, err := copystructure.Copy(copy.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configCopy, ok := configCopyRaw.(map[string]interface{})
|
||||
if !ok {
|
||||
// This should never fail because we KNOW the input type,
|
||||
// but we don't ever want to risk the panic.
|
||||
return nil, fmt.Errorf("internal error: config copy is not right type")
|
||||
}
|
||||
if err := reflectwalk.Walk(configCopy, &proxyConfigWalker{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
copy.Config = configCopy
|
||||
}
|
||||
|
||||
return json.Marshal(©)
|
||||
}
|
||||
|
||||
var typMapIfaceIface = reflect.TypeOf(map[interface{}]interface{}{})
|
||||
|
||||
// proxyConfigWalker implements interfaces for the reflectwalk package
|
||||
// (github.com/mitchellh/reflectwalk) that can be used to automatically
|
||||
// make the proxy configuration safe for JSON usage.
|
||||
//
|
||||
// Most of the implementation here is just keeping track of where we are
|
||||
// in the reflectwalk process, so that we can replace values. The key logic
|
||||
// is in Slice() and SliceElem().
|
||||
//
|
||||
// In particular we're looking to replace two cases HCL causes:
|
||||
//
|
||||
// 1.) String values get turned into byte slices. JSON will base64-encode
|
||||
// this and we don't want that, so we convert them back to strings.
|
||||
//
|
||||
// 2.) Nested maps turn into map[interface{}]interface{}. JSON cannot
|
||||
// encode this, so we need to turn it back into map[string]interface{}.
|
||||
//
|
||||
// This is tested via the TestServiceDefinitionConnectProxy_json test.
|
||||
type proxyConfigWalker struct {
|
||||
lastValue reflect.Value // lastValue of map, required for replacement
|
||||
loc, lastLoc reflectwalk.Location // locations
|
||||
cs []reflect.Value // container stack
|
||||
csKey []reflect.Value // container keys (maps) stack
|
||||
csData interface{} // current container data
|
||||
sliceIndex []int // slice index stack (one for each slice in cs)
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) Enter(loc reflectwalk.Location) error {
|
||||
w.lastLoc = w.loc
|
||||
w.loc = loc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) Exit(loc reflectwalk.Location) error {
|
||||
w.loc = reflectwalk.None
|
||||
w.lastLoc = reflectwalk.None
|
||||
|
||||
switch loc {
|
||||
case reflectwalk.Map:
|
||||
w.cs = w.cs[:len(w.cs)-1]
|
||||
case reflectwalk.MapValue:
|
||||
w.csKey = w.csKey[:len(w.csKey)-1]
|
||||
case reflectwalk.Slice:
|
||||
// Split any values that need to be split
|
||||
w.cs = w.cs[:len(w.cs)-1]
|
||||
case reflectwalk.SliceElem:
|
||||
w.csKey = w.csKey[:len(w.csKey)-1]
|
||||
w.sliceIndex = w.sliceIndex[:len(w.sliceIndex)-1]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) Map(m reflect.Value) error {
|
||||
w.cs = append(w.cs, m)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) MapElem(m, k, v reflect.Value) error {
|
||||
w.csData = k
|
||||
w.csKey = append(w.csKey, k)
|
||||
|
||||
w.lastValue = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) Slice(v reflect.Value) error {
|
||||
// If we find a []byte slice, it is an HCL-string converted to []byte.
|
||||
// Convert it back to a Go string and replace the value so that JSON
|
||||
// doesn't base64-encode it.
|
||||
if v.Type() == reflect.TypeOf([]byte{}) {
|
||||
resultVal := reflect.ValueOf(string(v.Interface().([]byte)))
|
||||
switch w.lastLoc {
|
||||
case reflectwalk.MapKey:
|
||||
m := w.cs[len(w.cs)-1]
|
||||
|
||||
// Delete the old value
|
||||
var zero reflect.Value
|
||||
m.SetMapIndex(w.csData.(reflect.Value), zero)
|
||||
|
||||
// Set the new key with the existing value
|
||||
m.SetMapIndex(resultVal, w.lastValue)
|
||||
|
||||
// Set the key to be the new key
|
||||
w.csData = resultVal
|
||||
case reflectwalk.MapValue:
|
||||
// If we're in a map, then the only way to set a map value is
|
||||
// to set it directly.
|
||||
m := w.cs[len(w.cs)-1]
|
||||
mk := w.csData.(reflect.Value)
|
||||
m.SetMapIndex(mk, resultVal)
|
||||
case reflectwalk.Slice:
|
||||
s := w.cs[len(w.cs)-1]
|
||||
s.Index(w.sliceIndex[len(w.sliceIndex)-1]).Set(resultVal)
|
||||
default:
|
||||
return fmt.Errorf("cannot convert []byte")
|
||||
}
|
||||
}
|
||||
|
||||
w.cs = append(w.cs, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *proxyConfigWalker) SliceElem(i int, elem reflect.Value) error {
|
||||
w.csKey = append(w.csKey, reflect.ValueOf(i))
|
||||
w.sliceIndex = append(w.sliceIndex, i)
|
||||
|
||||
// We're looking specifically for map[interface{}]interface{}, but the
|
||||
// values in a slice are wrapped up in interface{} so we need to unwrap
|
||||
// that first. Therefore, we do three checks: 1.) is it valid? so we
|
||||
// don't panic, 2.) is it an interface{}? so we can unwrap it and 3.)
|
||||
// after unwrapping the interface do we have the map we expect?
|
||||
if !elem.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if elem.Kind() != reflect.Interface {
|
||||
return nil
|
||||
}
|
||||
|
||||
if inner := elem.Elem(); inner.Type() == typMapIfaceIface {
|
||||
// map[interface{}]interface{}, attempt to weakly decode into string keys
|
||||
var target map[string]interface{}
|
||||
if err := mapstructure.WeakDecode(inner.Interface(), &target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
elem.Set(reflect.ValueOf(target))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -120,3 +121,86 @@ func TestServiceDefinitionValidate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDefinitionConnectProxy_json(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input *ServiceDefinitionConnectProxy
|
||||
Expected string
|
||||
Err string
|
||||
}{
|
||||
{
|
||||
"no config",
|
||||
&ServiceDefinitionConnectProxy{
|
||||
Command: []string{"foo"},
|
||||
ExecMode: "bar",
|
||||
},
|
||||
`
|
||||
{
|
||||
"Command": [
|
||||
"foo"
|
||||
],
|
||||
"ExecMode": "bar"
|
||||
}
|
||||
`,
|
||||
"",
|
||||
},
|
||||
|
||||
{
|
||||
"basic config",
|
||||
&ServiceDefinitionConnectProxy{
|
||||
Config: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
`
|
||||
{
|
||||
"Config": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
`,
|
||||
"",
|
||||
},
|
||||
|
||||
{
|
||||
"config with upstreams",
|
||||
&ServiceDefinitionConnectProxy{
|
||||
Config: map[string]interface{}{
|
||||
"upstreams": []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`
|
||||
{
|
||||
"Config": {
|
||||
"upstreams": [
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`,
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
result, err := json.MarshalIndent(tc.Input, "", "\t")
|
||||
t.Logf("error: %s", err)
|
||||
require.Equal(err != nil, tc.Err != "")
|
||||
if err != nil {
|
||||
require.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.Err))
|
||||
return
|
||||
}
|
||||
|
||||
require.Equal(strings.TrimSpace(tc.Expected), strings.TrimSpace(string(result)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue