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(©) 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 }