agent/structs: JSON marshal the configuration for a managed proxy

This commit is contained in:
Mitchell Hashimoto 2018-06-13 11:03:28 +01:00 committed by Jack Pearkes
parent e9e6514c9b
commit 3c17144fb5
2 changed files with 256 additions and 3 deletions

View File

@ -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(&copy)
}
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
}

View File

@ -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)))
})
}
}