1package api 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/mitchellh/mapstructure" 13) 14 15const ( 16 ServiceDefaults string = "service-defaults" 17 ProxyDefaults string = "proxy-defaults" 18 ServiceRouter string = "service-router" 19 ServiceSplitter string = "service-splitter" 20 ServiceResolver string = "service-resolver" 21 IngressGateway string = "ingress-gateway" 22 TerminatingGateway string = "terminating-gateway" 23 ServiceIntentions string = "service-intentions" 24 MeshConfig string = "mesh" 25 26 ProxyConfigGlobal string = "global" 27 MeshConfigMesh string = "mesh" 28) 29 30type ConfigEntry interface { 31 GetKind() string 32 GetName() string 33 GetNamespace() string 34 GetMeta() map[string]string 35 GetCreateIndex() uint64 36 GetModifyIndex() uint64 37} 38 39type MeshGatewayMode string 40 41const ( 42 // MeshGatewayModeDefault represents no specific mode and should 43 // be used to indicate that a different layer of the configuration 44 // chain should take precedence 45 MeshGatewayModeDefault MeshGatewayMode = "" 46 47 // MeshGatewayModeNone represents that the Upstream Connect connections 48 // should be direct and not flow through a mesh gateway. 49 MeshGatewayModeNone MeshGatewayMode = "none" 50 51 // MeshGatewayModeLocal represents that the Upstream Connect connections 52 // should be made to a mesh gateway in the local datacenter. 53 MeshGatewayModeLocal MeshGatewayMode = "local" 54 55 // MeshGatewayModeRemote represents that the Upstream Connect connections 56 // should be made to a mesh gateway in a remote datacenter. 57 MeshGatewayModeRemote MeshGatewayMode = "remote" 58) 59 60// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect 61// services 62type MeshGatewayConfig struct { 63 // Mode is the mode that should be used for the upstream connection. 64 Mode MeshGatewayMode `json:",omitempty"` 65} 66 67type ProxyMode string 68 69const ( 70 // ProxyModeDefault represents no specific mode and should 71 // be used to indicate that a different layer of the configuration 72 // chain should take precedence 73 ProxyModeDefault ProxyMode = "" 74 75 // ProxyModeTransparent represents that inbound and outbound application 76 // traffic is being captured and redirected through the proxy. 77 ProxyModeTransparent ProxyMode = "transparent" 78 79 // ProxyModeDirect represents that the proxy's listeners must be dialed directly 80 // by the local application and other proxies. 81 ProxyModeDirect ProxyMode = "direct" 82) 83 84type TransparentProxyConfig struct { 85 // The port of the listener where outbound application traffic is being redirected to. 86 OutboundListenerPort int `json:",omitempty" alias:"outbound_listener_port"` 87 88 // DialedDirectly indicates whether transparent proxies can dial this proxy instance directly. 89 // The discovery chain is not considered when dialing a service instance directly. 90 // This setting is useful when addressing stateful services, such as a database cluster with a leader node. 91 DialedDirectly bool `json:",omitempty" alias:"dialed_directly"` 92} 93 94// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect. 95// Users can expose individual paths and/or all HTTP/GRPC paths for checks. 96type ExposeConfig struct { 97 // Checks defines whether paths associated with Consul checks will be exposed. 98 // This flag triggers exposing all HTTP and GRPC check paths registered for the service. 99 Checks bool `json:",omitempty"` 100 101 // Paths is the list of paths exposed through the proxy. 102 Paths []ExposePath `json:",omitempty"` 103} 104 105type ExposePath struct { 106 // ListenerPort defines the port of the proxy's listener for exposed paths. 107 ListenerPort int `json:",omitempty" alias:"listener_port"` 108 109 // Path is the path to expose through the proxy, ie. "/metrics." 110 Path string `json:",omitempty"` 111 112 // LocalPathPort is the port that the service is listening on for the given path. 113 LocalPathPort int `json:",omitempty" alias:"local_path_port"` 114 115 // Protocol describes the upstream's service protocol. 116 // Valid values are "http" and "http2", defaults to "http" 117 Protocol string `json:",omitempty"` 118 119 // ParsedFromCheck is set if this path was parsed from a registered check 120 ParsedFromCheck bool 121} 122 123type UpstreamConfiguration struct { 124 // Overrides is a slice of per-service configuration. The name field is 125 // required. 126 Overrides []*UpstreamConfig `json:",omitempty"` 127 128 // Defaults contains default configuration for all upstreams of a given 129 // service. The name field must be empty. 130 Defaults *UpstreamConfig `json:",omitempty"` 131} 132 133type UpstreamConfig struct { 134 // Name is only accepted within a service-defaults config entry. 135 Name string `json:",omitempty"` 136 // Namespace is only accepted within a service-defaults config entry. 137 Namespace string `json:",omitempty"` 138 139 // EnvoyListenerJSON is a complete override ("escape hatch") for the upstream's 140 // listener. 141 // 142 // Note: This escape hatch is NOT compatible with the discovery chain and 143 // will be ignored if a discovery chain is active. 144 EnvoyListenerJSON string `json:",omitempty" alias:"envoy_listener_json"` 145 146 // EnvoyClusterJSON is a complete override ("escape hatch") for the upstream's 147 // cluster. The Connect client TLS certificate and context will be injected 148 // overriding any TLS settings present. 149 // 150 // Note: This escape hatch is NOT compatible with the discovery chain and 151 // will be ignored if a discovery chain is active. 152 EnvoyClusterJSON string `json:",omitempty" alias:"envoy_cluster_json"` 153 154 // Protocol describes the upstream's service protocol. Valid values are "tcp", 155 // "http" and "grpc". Anything else is treated as tcp. The enables protocol 156 // aware features like per-request metrics and connection pooling, tracing, 157 // routing etc. 158 Protocol string `json:",omitempty"` 159 160 // ConnectTimeoutMs is the number of milliseconds to timeout making a new 161 // connection to this upstream. Defaults to 5000 (5 seconds) if not set. 162 ConnectTimeoutMs int `json:",omitempty" alias:"connect_timeout_ms"` 163 164 // Limits are the set of limits that are applied to the proxy for a specific upstream of a 165 // service instance. 166 Limits *UpstreamLimits `json:",omitempty"` 167 168 // PassiveHealthCheck configuration determines how upstream proxy instances will 169 // be monitored for removal from the load balancing pool. 170 PassiveHealthCheck *PassiveHealthCheck `json:",omitempty" alias:"passive_health_check"` 171 172 // MeshGatewayConfig controls how Mesh Gateways are configured and used 173 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway" ` 174} 175 176type PassiveHealthCheck struct { 177 // Interval between health check analysis sweeps. Each sweep may remove 178 // hosts or return hosts to the pool. 179 Interval time.Duration `json:",omitempty"` 180 181 // MaxFailures is the count of consecutive failures that results in a host 182 // being removed from the pool. 183 MaxFailures uint32 `alias:"max_failures"` 184} 185 186// UpstreamLimits describes the limits that are associated with a specific 187// upstream of a service instance. 188type UpstreamLimits struct { 189 // MaxConnections is the maximum number of connections the local proxy can 190 // make to the upstream service. 191 MaxConnections *int `alias:"max_connections"` 192 193 // MaxPendingRequests is the maximum number of requests that will be queued 194 // waiting for an available connection. This is mostly applicable to HTTP/1.1 195 // clusters since all HTTP/2 requests are streamed over a single 196 // connection. 197 MaxPendingRequests *int `alias:"max_pending_requests"` 198 199 // MaxConcurrentRequests is the maximum number of in-flight requests that will be allowed 200 // to the upstream cluster at a point in time. This is mostly applicable to HTTP/2 201 // clusters since all HTTP/1.1 requests are limited by MaxConnections. 202 MaxConcurrentRequests *int `alias:"max_concurrent_requests"` 203} 204 205type ServiceConfigEntry struct { 206 Kind string 207 Name string 208 Namespace string `json:",omitempty"` 209 Protocol string `json:",omitempty"` 210 Mode ProxyMode `json:",omitempty"` 211 TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` 212 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 213 Expose ExposeConfig `json:",omitempty"` 214 ExternalSNI string `json:",omitempty" alias:"external_sni"` 215 UpstreamConfig *UpstreamConfiguration `json:",omitempty" alias:"upstream_config"` 216 217 Meta map[string]string `json:",omitempty"` 218 CreateIndex uint64 219 ModifyIndex uint64 220} 221 222func (s *ServiceConfigEntry) GetKind() string { 223 return s.Kind 224} 225 226func (s *ServiceConfigEntry) GetName() string { 227 return s.Name 228} 229 230func (s *ServiceConfigEntry) GetNamespace() string { 231 return s.Namespace 232} 233 234func (s *ServiceConfigEntry) GetMeta() map[string]string { 235 return s.Meta 236} 237 238func (s *ServiceConfigEntry) GetCreateIndex() uint64 { 239 return s.CreateIndex 240} 241 242func (s *ServiceConfigEntry) GetModifyIndex() uint64 { 243 return s.ModifyIndex 244} 245 246type ProxyConfigEntry struct { 247 Kind string 248 Name string 249 Namespace string `json:",omitempty"` 250 Mode ProxyMode `json:",omitempty"` 251 TransparentProxy *TransparentProxyConfig `json:",omitempty" alias:"transparent_proxy"` 252 Config map[string]interface{} `json:",omitempty"` 253 MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` 254 Expose ExposeConfig `json:",omitempty"` 255 Meta map[string]string `json:",omitempty"` 256 CreateIndex uint64 257 ModifyIndex uint64 258} 259 260func (p *ProxyConfigEntry) GetKind() string { 261 return p.Kind 262} 263 264func (p *ProxyConfigEntry) GetName() string { 265 return p.Name 266} 267 268func (p *ProxyConfigEntry) GetNamespace() string { 269 return p.Namespace 270} 271 272func (p *ProxyConfigEntry) GetMeta() map[string]string { 273 return p.Meta 274} 275 276func (p *ProxyConfigEntry) GetCreateIndex() uint64 { 277 return p.CreateIndex 278} 279 280func (p *ProxyConfigEntry) GetModifyIndex() uint64 { 281 return p.ModifyIndex 282} 283 284func makeConfigEntry(kind, name string) (ConfigEntry, error) { 285 switch kind { 286 case ServiceDefaults: 287 return &ServiceConfigEntry{Kind: kind, Name: name}, nil 288 case ProxyDefaults: 289 return &ProxyConfigEntry{Kind: kind, Name: name}, nil 290 case ServiceRouter: 291 return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil 292 case ServiceSplitter: 293 return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil 294 case ServiceResolver: 295 return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil 296 case IngressGateway: 297 return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil 298 case TerminatingGateway: 299 return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil 300 case ServiceIntentions: 301 return &ServiceIntentionsConfigEntry{Kind: kind, Name: name}, nil 302 case MeshConfig: 303 return &MeshConfigEntry{}, nil 304 default: 305 return nil, fmt.Errorf("invalid config entry kind: %s", kind) 306 } 307} 308 309func MakeConfigEntry(kind, name string) (ConfigEntry, error) { 310 return makeConfigEntry(kind, name) 311} 312 313// DecodeConfigEntry will decode the result of using json.Unmarshal of a config 314// entry into a map[string]interface{}. 315// 316// Important caveats: 317// 318// - This will NOT work if the map[string]interface{} was produced using HCL 319// decoding as that requires more extensive parsing to work around the issues 320// with map[string][]interface{} that arise. 321// 322// - This will only decode fields using their camel case json field 323// representations. 324func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) { 325 var entry ConfigEntry 326 327 kindVal, ok := raw["Kind"] 328 if !ok { 329 kindVal, ok = raw["kind"] 330 } 331 if !ok { 332 return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level") 333 } 334 335 if kindStr, ok := kindVal.(string); ok { 336 newEntry, err := makeConfigEntry(kindStr, "") 337 if err != nil { 338 return nil, err 339 } 340 entry = newEntry 341 } else { 342 return nil, fmt.Errorf("Kind value in payload is not a string") 343 } 344 345 decodeConf := &mapstructure.DecoderConfig{ 346 DecodeHook: mapstructure.ComposeDecodeHookFunc( 347 mapstructure.StringToTimeDurationHookFunc(), 348 mapstructure.StringToTimeHookFunc(time.RFC3339), 349 ), 350 Result: &entry, 351 WeaklyTypedInput: true, 352 } 353 354 decoder, err := mapstructure.NewDecoder(decodeConf) 355 if err != nil { 356 return nil, err 357 } 358 359 return entry, decoder.Decode(raw) 360} 361 362func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) { 363 var raw map[string]interface{} 364 if err := json.Unmarshal(data, &raw); err != nil { 365 return nil, err 366 } 367 368 return DecodeConfigEntry(raw) 369} 370 371func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) { 372 var entries []ConfigEntry 373 for _, rawEntry := range raw { 374 entry, err := DecodeConfigEntry(rawEntry) 375 if err != nil { 376 return nil, err 377 } 378 entries = append(entries, entry) 379 } 380 return entries, nil 381} 382 383// ConfigEntries can be used to query the Config endpoints 384type ConfigEntries struct { 385 c *Client 386} 387 388// Config returns a handle to the Config endpoints 389func (c *Client) ConfigEntries() *ConfigEntries { 390 return &ConfigEntries{c} 391} 392 393func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) { 394 if kind == "" || name == "" { 395 return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty") 396 } 397 398 entry, err := makeConfigEntry(kind, name) 399 if err != nil { 400 return nil, nil, err 401 } 402 403 r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name)) 404 r.setQueryOptions(q) 405 rtt, resp, err := requireOK(conf.c.doRequest(r)) 406 if err != nil { 407 return nil, nil, err 408 } 409 410 defer closeResponseBody(resp) 411 412 qm := &QueryMeta{} 413 parseQueryMeta(resp, qm) 414 qm.RequestTime = rtt 415 416 if err := decodeBody(resp, entry); err != nil { 417 return nil, nil, err 418 } 419 420 return entry, qm, nil 421} 422 423func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) { 424 if kind == "" { 425 return nil, nil, fmt.Errorf("The kind parameter must not be empty") 426 } 427 428 r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind)) 429 r.setQueryOptions(q) 430 rtt, resp, err := requireOK(conf.c.doRequest(r)) 431 if err != nil { 432 return nil, nil, err 433 } 434 435 defer closeResponseBody(resp) 436 437 qm := &QueryMeta{} 438 parseQueryMeta(resp, qm) 439 qm.RequestTime = rtt 440 441 var raw []map[string]interface{} 442 if err := decodeBody(resp, &raw); err != nil { 443 return nil, nil, err 444 } 445 446 entries, err := decodeConfigEntrySlice(raw) 447 if err != nil { 448 return nil, nil, err 449 } 450 451 return entries, qm, nil 452} 453 454func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) { 455 return conf.set(entry, nil, w) 456} 457 458func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) { 459 return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w) 460} 461 462func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) { 463 r := conf.c.newRequest("PUT", "/v1/config") 464 r.setWriteOptions(w) 465 for param, value := range params { 466 r.params.Set(param, value) 467 } 468 r.obj = entry 469 rtt, resp, err := requireOK(conf.c.doRequest(r)) 470 if err != nil { 471 return false, nil, err 472 } 473 defer closeResponseBody(resp) 474 475 var buf bytes.Buffer 476 if _, err := io.Copy(&buf, resp.Body); err != nil { 477 return false, nil, fmt.Errorf("Failed to read response: %v", err) 478 } 479 res := strings.Contains(buf.String(), "true") 480 481 wm := &WriteMeta{RequestTime: rtt} 482 return res, wm, nil 483} 484 485func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) { 486 if kind == "" || name == "" { 487 return nil, fmt.Errorf("Both kind and name parameters must not be empty") 488 } 489 490 r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name)) 491 r.setWriteOptions(w) 492 rtt, resp, err := requireOK(conf.c.doRequest(r)) 493 if err != nil { 494 return nil, err 495 } 496 closeResponseBody(resp) 497 wm := &WriteMeta{RequestTime: rtt} 498 return wm, nil 499} 500