github.com/outbrain/consul@v1.4.5/agent/structs/service_definition.go (about)

     1  package structs
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"reflect"
     7  
     8  	"github.com/hashicorp/go-multierror"
     9  	"github.com/mitchellh/copystructure"
    10  	"github.com/mitchellh/mapstructure"
    11  	"github.com/mitchellh/reflectwalk"
    12  )
    13  
    14  // ServiceDefinition is used to JSON decode the Service definitions. For
    15  // documentation on specific fields see NodeService which is better documented.
    16  type ServiceDefinition struct {
    17  	Kind              ServiceKind `json:",omitempty"`
    18  	ID                string
    19  	Name              string
    20  	Tags              []string
    21  	Address           string
    22  	Meta              map[string]string
    23  	Port              int
    24  	Check             CheckType
    25  	Checks            CheckTypes
    26  	Weights           *Weights
    27  	Token             string
    28  	EnableTagOverride bool
    29  	// DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination
    30  	// ProxyDestination is deprecated in favor of Proxy.DestinationServiceName
    31  	ProxyDestination string `json:",omitempty"`
    32  
    33  	// Proxy is the configuration set for Kind = connect-proxy. It is mandatory in
    34  	// that case and an error to be set for any other kind. This config is part of
    35  	// a proxy service definition and is distinct from but shares some fields with
    36  	// the Connect.Proxy which configures a managed proxy as part of the actual
    37  	// service's definition. This duplication is ugly but seemed better than the
    38  	// alternative which was to re-use the same struct fields for both cases even
    39  	// though the semantics are different and the non-shared fields make no sense
    40  	// in the other case. ProxyConfig may be a more natural name here, but it's
    41  	// confusing for the UX because one of the fields in ConnectProxyConfig is
    42  	// also called just "Config"
    43  	Proxy *ConnectProxyConfig
    44  
    45  	Connect *ServiceConnect
    46  }
    47  
    48  func (s *ServiceDefinition) NodeService() *NodeService {
    49  	ns := &NodeService{
    50  		Kind:              s.Kind,
    51  		ID:                s.ID,
    52  		Service:           s.Name,
    53  		Tags:              s.Tags,
    54  		Address:           s.Address,
    55  		Meta:              s.Meta,
    56  		Port:              s.Port,
    57  		Weights:           s.Weights,
    58  		EnableTagOverride: s.EnableTagOverride,
    59  	}
    60  	if s.Connect != nil {
    61  		ns.Connect = *s.Connect
    62  	}
    63  	if s.Proxy != nil {
    64  		ns.Proxy = *s.Proxy
    65  		// Ensure the Upstream type is defaulted
    66  		for i := range ns.Proxy.Upstreams {
    67  			if ns.Proxy.Upstreams[i].DestinationType == "" {
    68  				ns.Proxy.Upstreams[i].DestinationType = UpstreamDestTypeService
    69  			}
    70  		}
    71  	} else {
    72  		// DEPRECATED (ProxyDestination) - remove this when removing ProxyDestination
    73  		// Legacy convert ProxyDestination into a Proxy config
    74  		ns.Proxy.DestinationServiceName = s.ProxyDestination
    75  	}
    76  	if ns.ID == "" && ns.Service != "" {
    77  		ns.ID = ns.Service
    78  	}
    79  	return ns
    80  }
    81  
    82  // ConnectManagedProxy returns a ConnectManagedProxy from the ServiceDefinition
    83  // if one is configured validly. Note that is may return nil if no proxy is
    84  // configured and will also return nil error in this case too as it's an
    85  // expected case. The error returned indicates that there was an attempt to
    86  // configure a proxy made but that it was invalid input, e.g. invalid
    87  // "exec_mode".
    88  func (s *ServiceDefinition) ConnectManagedProxy() (*ConnectManagedProxy, error) {
    89  	if s.Connect == nil || s.Connect.Proxy == nil {
    90  		return nil, nil
    91  	}
    92  
    93  	// NodeService performs some simple normalization like copying ID from Name
    94  	// which we shouldn't hard code ourselves here...
    95  	ns := s.NodeService()
    96  
    97  	execMode, err := NewProxyExecMode(s.Connect.Proxy.ExecMode)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  
   102  	// If upstreams were set in the config and NOT in the actual Upstreams field,
   103  	// extract them out to the new explicit Upstreams and unset in config to make
   104  	// transition smooth.
   105  	if deprecatedUpstreams, ok := s.Connect.Proxy.Config["upstreams"]; ok {
   106  		if len(s.Connect.Proxy.Upstreams) == 0 {
   107  			if slice, ok := deprecatedUpstreams.([]interface{}); ok {
   108  				for _, raw := range slice {
   109  					var oldU deprecatedBuiltInProxyUpstreamConfig
   110  					var decMeta mapstructure.Metadata
   111  					decCfg := &mapstructure.DecoderConfig{
   112  						Metadata: &decMeta,
   113  						Result:   &oldU,
   114  					}
   115  					dec, err := mapstructure.NewDecoder(decCfg)
   116  					if err != nil {
   117  						// Just skip it - we never used to parse this so never failed
   118  						// invalid stuff till it hit the proxy. This is a best-effort
   119  						// attempt to not break existing service definitions so it's not the
   120  						// end of the world if we don't have exactly the same failure mode
   121  						// for invalid input.
   122  						continue
   123  					}
   124  					err = dec.Decode(raw)
   125  					if err != nil {
   126  						// same logic as above
   127  						continue
   128  					}
   129  
   130  					newT := UpstreamDestTypeService
   131  					if oldU.DestinationType == "prepared_query" {
   132  						newT = UpstreamDestTypePreparedQuery
   133  					}
   134  					u := Upstream{
   135  						DestinationType:      newT,
   136  						DestinationName:      oldU.DestinationName,
   137  						DestinationNamespace: oldU.DestinationNamespace,
   138  						Datacenter:           oldU.DestinationDatacenter,
   139  						LocalBindAddress:     oldU.LocalBindAddress,
   140  						LocalBindPort:        oldU.LocalBindPort,
   141  					}
   142  					// Any unrecognized keys should be copied into the config map
   143  					if len(decMeta.Unused) > 0 {
   144  						u.Config = make(map[string]interface{})
   145  						// Paranoid type assertion - mapstructure would have errored if this
   146  						// wasn't safe but panics are bad...
   147  						if rawMap, ok := raw.(map[string]interface{}); ok {
   148  							for _, k := range decMeta.Unused {
   149  								u.Config[k] = rawMap[k]
   150  							}
   151  						}
   152  					}
   153  					s.Connect.Proxy.Upstreams = append(s.Connect.Proxy.Upstreams, u)
   154  				}
   155  			}
   156  		}
   157  		// Remove upstreams even if we didn't add them for consistency.
   158  		delete(s.Connect.Proxy.Config, "upstreams")
   159  	}
   160  
   161  	p := &ConnectManagedProxy{
   162  		ExecMode:  execMode,
   163  		Command:   s.Connect.Proxy.Command,
   164  		Config:    s.Connect.Proxy.Config,
   165  		Upstreams: s.Connect.Proxy.Upstreams,
   166  		// ProxyService will be setup when the agent registers the configured
   167  		// proxies and starts them etc.
   168  		TargetServiceID: ns.ID,
   169  	}
   170  
   171  	// Ensure the Upstream type is defaulted
   172  	for i := range p.Upstreams {
   173  		if p.Upstreams[i].DestinationType == "" {
   174  			p.Upstreams[i].DestinationType = UpstreamDestTypeService
   175  		}
   176  	}
   177  
   178  	return p, nil
   179  }
   180  
   181  // deprecatedBuiltInProxyUpstreamConfig is a struct for extracting old
   182  // connect/proxy.UpstreamConfiguration syntax upstreams from existing managed
   183  // proxy configs to convert them to new first-class Upstreams.
   184  type deprecatedBuiltInProxyUpstreamConfig struct {
   185  	LocalBindAddress      string `json:"local_bind_address" hcl:"local_bind_address,attr" mapstructure:"local_bind_address"`
   186  	LocalBindPort         int    `json:"local_bind_port" hcl:"local_bind_port,attr" mapstructure:"local_bind_port"`
   187  	DestinationName       string `json:"destination_name" hcl:"destination_name,attr" mapstructure:"destination_name"`
   188  	DestinationNamespace  string `json:"destination_namespace" hcl:"destination_namespace,attr" mapstructure:"destination_namespace"`
   189  	DestinationType       string `json:"destination_type" hcl:"destination_type,attr" mapstructure:"destination_type"`
   190  	DestinationDatacenter string `json:"destination_datacenter" hcl:"destination_datacenter,attr" mapstructure:"destination_datacenter"`
   191  	// ConnectTimeoutMs is removed explicitly because any additional config we
   192  	// find including this field should be put into the opaque Config map in
   193  	// Upstream.
   194  }
   195  
   196  // Validate validates the service definition. This also calls the underlying
   197  // Validate method on the NodeService.
   198  //
   199  // NOTE(mitchellh): This currently only validates fields related to Connect
   200  // and is incomplete with regards to other fields.
   201  func (s *ServiceDefinition) Validate() error {
   202  	var result error
   203  
   204  	if s.Kind == ServiceKindTypical {
   205  		if s.Connect != nil {
   206  			if s.Connect.Proxy != nil {
   207  				if s.Connect.Native {
   208  					result = multierror.Append(result, fmt.Errorf(
   209  						"Services that are Connect native may not have a proxy configuration"))
   210  				}
   211  
   212  				if s.Port == 0 {
   213  					result = multierror.Append(result, fmt.Errorf(
   214  						"Services with a Connect managed proxy must have a port set"))
   215  				}
   216  			}
   217  		}
   218  	}
   219  
   220  	// Validate the NodeService which covers a lot
   221  	if err := s.NodeService().Validate(); err != nil {
   222  		result = multierror.Append(result, err)
   223  	}
   224  
   225  	return result
   226  }
   227  
   228  func (s *ServiceDefinition) CheckTypes() (checks CheckTypes, err error) {
   229  	if !s.Check.Empty() {
   230  		err := s.Check.Validate()
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  		checks = append(checks, &s.Check)
   235  	}
   236  	for _, check := range s.Checks {
   237  		if err := check.Validate(); err != nil {
   238  			return nil, err
   239  		}
   240  		checks = append(checks, check)
   241  	}
   242  	return checks, nil
   243  }
   244  
   245  // ServiceDefinitionConnectProxy is the connect proxy config  within a service
   246  // registration. Note this is duplicated in config.ServiceConnectProxy and needs
   247  // to be kept in sync.
   248  type ServiceDefinitionConnectProxy struct {
   249  	Command   []string               `json:",omitempty"`
   250  	ExecMode  string                 `json:",omitempty"`
   251  	Config    map[string]interface{} `json:",omitempty"`
   252  	Upstreams []Upstream             `json:",omitempty"`
   253  }
   254  
   255  func (p *ServiceDefinitionConnectProxy) MarshalJSON() ([]byte, error) {
   256  	type typeCopy ServiceDefinitionConnectProxy
   257  	copy := typeCopy(*p)
   258  
   259  	// If we have config, then we want to run it through our proxyConfigWalker
   260  	// which is a reflectwalk implementation that attempts to turn arbitrary
   261  	// interface{} values into JSON-safe equivalents (more or less). This
   262  	// should always work because the config input is either HCL or JSON and
   263  	// both are JSON compatible.
   264  	if copy.Config != nil {
   265  		configCopyRaw, err := copystructure.Copy(copy.Config)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  		configCopy, ok := configCopyRaw.(map[string]interface{})
   270  		if !ok {
   271  			// This should never fail because we KNOW the input type,
   272  			// but we don't ever want to risk the panic.
   273  			return nil, fmt.Errorf("internal error: config copy is not right type")
   274  		}
   275  		if err := reflectwalk.Walk(configCopy, &proxyConfigWalker{}); err != nil {
   276  			return nil, err
   277  		}
   278  
   279  		copy.Config = configCopy
   280  	}
   281  
   282  	return json.Marshal(&copy)
   283  }
   284  
   285  var typMapIfaceIface = reflect.TypeOf(map[interface{}]interface{}{})
   286  
   287  // proxyConfigWalker implements interfaces for the reflectwalk package
   288  // (github.com/mitchellh/reflectwalk) that can be used to automatically
   289  // make the proxy configuration safe for JSON usage.
   290  //
   291  // Most of the implementation here is just keeping track of where we are
   292  // in the reflectwalk process, so that we can replace values. The key logic
   293  // is in Slice() and SliceElem().
   294  //
   295  // In particular we're looking to replace two cases the msgpack codec causes:
   296  //
   297  //   1.) String values get turned into byte slices. JSON will base64-encode
   298  //       this and we don't want that, so we convert them back to strings.
   299  //
   300  //   2.) Nested maps turn into map[interface{}]interface{}. JSON cannot
   301  //       encode this, so we need to turn it back into map[string]interface{}.
   302  //
   303  // This is tested via the TestServiceDefinitionConnectProxy_json test.
   304  type proxyConfigWalker struct {
   305  	lastValue    reflect.Value        // lastValue of map, required for replacement
   306  	loc, lastLoc reflectwalk.Location // locations
   307  	cs           []reflect.Value      // container stack
   308  	csKey        []reflect.Value      // container keys (maps) stack
   309  	csData       interface{}          // current container data
   310  	sliceIndex   []int                // slice index stack (one for each slice in cs)
   311  }
   312  
   313  func (w *proxyConfigWalker) Enter(loc reflectwalk.Location) error {
   314  	w.lastLoc = w.loc
   315  	w.loc = loc
   316  	return nil
   317  }
   318  
   319  func (w *proxyConfigWalker) Exit(loc reflectwalk.Location) error {
   320  	w.loc = reflectwalk.None
   321  	w.lastLoc = reflectwalk.None
   322  
   323  	switch loc {
   324  	case reflectwalk.Map:
   325  		w.cs = w.cs[:len(w.cs)-1]
   326  	case reflectwalk.MapValue:
   327  		w.csKey = w.csKey[:len(w.csKey)-1]
   328  	case reflectwalk.Slice:
   329  		// Split any values that need to be split
   330  		w.cs = w.cs[:len(w.cs)-1]
   331  	case reflectwalk.SliceElem:
   332  		w.csKey = w.csKey[:len(w.csKey)-1]
   333  		w.sliceIndex = w.sliceIndex[:len(w.sliceIndex)-1]
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func (w *proxyConfigWalker) Map(m reflect.Value) error {
   340  	w.cs = append(w.cs, m)
   341  	return nil
   342  }
   343  
   344  func (w *proxyConfigWalker) MapElem(m, k, v reflect.Value) error {
   345  	w.csData = k
   346  	w.csKey = append(w.csKey, k)
   347  
   348  	w.lastValue = v
   349  	return nil
   350  }
   351  
   352  func (w *proxyConfigWalker) Slice(v reflect.Value) error {
   353  	// If we find a []byte slice, it is an HCL-string converted to []byte.
   354  	// Convert it back to a Go string and replace the value so that JSON
   355  	// doesn't base64-encode it.
   356  	if v.Type() == reflect.TypeOf([]byte{}) {
   357  		resultVal := reflect.ValueOf(string(v.Interface().([]byte)))
   358  		switch w.lastLoc {
   359  		case reflectwalk.MapKey:
   360  			m := w.cs[len(w.cs)-1]
   361  
   362  			// Delete the old value
   363  			var zero reflect.Value
   364  			m.SetMapIndex(w.csData.(reflect.Value), zero)
   365  
   366  			// Set the new key with the existing value
   367  			m.SetMapIndex(resultVal, w.lastValue)
   368  
   369  			// Set the key to be the new key
   370  			w.csData = resultVal
   371  		case reflectwalk.MapValue:
   372  			// If we're in a map, then the only way to set a map value is
   373  			// to set it directly.
   374  			m := w.cs[len(w.cs)-1]
   375  			mk := w.csData.(reflect.Value)
   376  			m.SetMapIndex(mk, resultVal)
   377  		case reflectwalk.Slice:
   378  			s := w.cs[len(w.cs)-1]
   379  			s.Index(w.sliceIndex[len(w.sliceIndex)-1]).Set(resultVal)
   380  		default:
   381  			return fmt.Errorf("cannot convert []byte")
   382  		}
   383  	}
   384  
   385  	w.cs = append(w.cs, v)
   386  	return nil
   387  }
   388  
   389  func (w *proxyConfigWalker) SliceElem(i int, elem reflect.Value) error {
   390  	w.csKey = append(w.csKey, reflect.ValueOf(i))
   391  	w.sliceIndex = append(w.sliceIndex, i)
   392  
   393  	// We're looking specifically for map[interface{}]interface{}, but the
   394  	// values in a slice are wrapped up in interface{} so we need to unwrap
   395  	// that first. Therefore, we do three checks: 1.) is it valid? so we
   396  	// don't panic, 2.) is it an interface{}? so we can unwrap it and 3.)
   397  	// after unwrapping the interface do we have the map we expect?
   398  	if !elem.IsValid() {
   399  		return nil
   400  	}
   401  
   402  	if elem.Kind() != reflect.Interface {
   403  		return nil
   404  	}
   405  
   406  	if inner := elem.Elem(); inner.Type() == typMapIfaceIface {
   407  		// map[interface{}]interface{}, attempt to weakly decode into string keys
   408  		var target map[string]interface{}
   409  		if err := mapstructure.WeakDecode(inner.Interface(), &target); err != nil {
   410  			return err
   411  		}
   412  
   413  		elem.Set(reflect.ValueOf(target))
   414  	}
   415  
   416  	return nil
   417  }