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