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