306 lines
9.0 KiB
Go
306 lines
9.0 KiB
Go
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
|
|
// documentation on specific fields see NodeService which is better documented.
|
|
type ServiceDefinition struct {
|
|
Kind ServiceKind
|
|
ID string
|
|
Name string
|
|
Tags []string
|
|
Address string
|
|
Meta map[string]string
|
|
Port int
|
|
Check CheckType
|
|
Checks CheckTypes
|
|
Token string
|
|
EnableTagOverride bool
|
|
ProxyDestination string
|
|
Connect *ServiceConnect
|
|
}
|
|
|
|
func (s *ServiceDefinition) NodeService() *NodeService {
|
|
ns := &NodeService{
|
|
Kind: s.Kind,
|
|
ID: s.ID,
|
|
Service: s.Name,
|
|
Tags: s.Tags,
|
|
Address: s.Address,
|
|
Meta: s.Meta,
|
|
Port: s.Port,
|
|
EnableTagOverride: s.EnableTagOverride,
|
|
ProxyDestination: s.ProxyDestination,
|
|
}
|
|
if s.Connect != nil {
|
|
ns.Connect = *s.Connect
|
|
}
|
|
if ns.ID == "" && ns.Service != "" {
|
|
ns.ID = ns.Service
|
|
}
|
|
return ns
|
|
}
|
|
|
|
// ConnectManagedProxy returns a ConnectManagedProxy from the ServiceDefinition
|
|
// if one is configured validly. Note that is may return nil if no proxy is
|
|
// configured and will also return nil error in this case too as it's an
|
|
// expected case. The error returned indicates that there was an attempt to
|
|
// configure a proxy made but that it was invalid input, e.g. invalid
|
|
// "exec_mode".
|
|
func (s *ServiceDefinition) ConnectManagedProxy() (*ConnectManagedProxy, error) {
|
|
if s.Connect == nil || s.Connect.Proxy == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// NodeService performs some simple normalization like copying ID from Name
|
|
// which we shouldn't hard code ourselves here...
|
|
ns := s.NodeService()
|
|
|
|
execMode, err := NewProxyExecMode(s.Connect.Proxy.ExecMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p := &ConnectManagedProxy{
|
|
ExecMode: execMode,
|
|
Command: s.Connect.Proxy.Command,
|
|
Config: s.Connect.Proxy.Config,
|
|
// ProxyService will be setup when the agent registers the configured
|
|
// proxies and starts them etc.
|
|
TargetServiceID: ns.ID,
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Validate validates the service definition. This also calls the underlying
|
|
// Validate method on the NodeService.
|
|
//
|
|
// NOTE(mitchellh): This currently only validates fields related to Connect
|
|
// and is incomplete with regards to other fields.
|
|
func (s *ServiceDefinition) Validate() error {
|
|
var result error
|
|
|
|
if s.Kind == ServiceKindTypical {
|
|
if s.Connect != nil {
|
|
if s.Connect.Proxy != nil {
|
|
if s.Connect.Native {
|
|
result = multierror.Append(result, fmt.Errorf(
|
|
"Services that are Connect native may not have a proxy configuration"))
|
|
}
|
|
|
|
if s.Port == 0 {
|
|
result = multierror.Append(result, fmt.Errorf(
|
|
"Services with a Connect managed proxy must have a port set"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate the NodeService which covers a lot
|
|
if err := s.NodeService().Validate(); err != nil {
|
|
result = multierror.Append(result, err)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
|
|
if !s.Check.Empty() {
|
|
err := s.Check.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
checks = append(checks, &s.Check)
|
|
}
|
|
for _, check := range s.Checks {
|
|
if err := check.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
checks = append(checks, check)
|
|
}
|
|
return checks, nil
|
|
}
|
|
|
|
// ServiceDefinitionConnectProxy is the connect proxy config within a service
|
|
// registration. Note this is duplicated in config.ServiceConnectProxy and needs
|
|
// to be kept in sync.
|
|
type ServiceDefinitionConnectProxy struct {
|
|
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 the msgpack codec 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
|
|
}
|