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