1package api
2
3import (
4	"bytes"
5	"encoding/json"
6	"fmt"
7	"io"
8	"strconv"
9	"strings"
10
11	"github.com/mitchellh/mapstructure"
12)
13
14const (
15	ServiceDefaults    string = "service-defaults"
16	ProxyDefaults      string = "proxy-defaults"
17	ServiceRouter      string = "service-router"
18	ServiceSplitter    string = "service-splitter"
19	ServiceResolver    string = "service-resolver"
20	IngressGateway     string = "ingress-gateway"
21	TerminatingGateway string = "terminating-gateway"
22
23	ProxyConfigGlobal string = "global"
24)
25
26type ConfigEntry interface {
27	GetKind() string
28	GetName() string
29	GetCreateIndex() uint64
30	GetModifyIndex() uint64
31}
32
33type MeshGatewayMode string
34
35const (
36	// MeshGatewayModeDefault represents no specific mode and should
37	// be used to indicate that a different layer of the configuration
38	// chain should take precedence
39	MeshGatewayModeDefault MeshGatewayMode = ""
40
41	// MeshGatewayModeNone represents that the Upstream Connect connections
42	// should be direct and not flow through a mesh gateway.
43	MeshGatewayModeNone MeshGatewayMode = "none"
44
45	// MeshGatewayModeLocal represents that the Upstrea Connect connections
46	// should be made to a mesh gateway in the local datacenter. This is
47	MeshGatewayModeLocal MeshGatewayMode = "local"
48
49	// MeshGatewayModeRemote represents that the Upstream Connect connections
50	// should be made to a mesh gateway in a remote datacenter.
51	MeshGatewayModeRemote MeshGatewayMode = "remote"
52)
53
54// MeshGatewayConfig controls how Mesh Gateways are used for upstream Connect
55// services
56type MeshGatewayConfig struct {
57	// Mode is the mode that should be used for the upstream connection.
58	Mode MeshGatewayMode `json:",omitempty"`
59}
60
61// ExposeConfig describes HTTP paths to expose through Envoy outside of Connect.
62// Users can expose individual paths and/or all HTTP/GRPC paths for checks.
63type ExposeConfig struct {
64	// Checks defines whether paths associated with Consul checks will be exposed.
65	// This flag triggers exposing all HTTP and GRPC check paths registered for the service.
66	Checks bool `json:",omitempty"`
67
68	// Paths is the list of paths exposed through the proxy.
69	Paths []ExposePath `json:",omitempty"`
70}
71
72type ExposePath struct {
73	// ListenerPort defines the port of the proxy's listener for exposed paths.
74	ListenerPort int `json:",omitempty" alias:"listener_port"`
75
76	// Path is the path to expose through the proxy, ie. "/metrics."
77	Path string `json:",omitempty"`
78
79	// LocalPathPort is the port that the service is listening on for the given path.
80	LocalPathPort int `json:",omitempty" alias:"local_path_port"`
81
82	// Protocol describes the upstream's service protocol.
83	// Valid values are "http" and "http2", defaults to "http"
84	Protocol string `json:",omitempty"`
85
86	// ParsedFromCheck is set if this path was parsed from a registered check
87	ParsedFromCheck bool
88}
89
90type ServiceConfigEntry struct {
91	Kind        string
92	Name        string
93	Namespace   string            `json:",omitempty"`
94	Protocol    string            `json:",omitempty"`
95	MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
96	Expose      ExposeConfig      `json:",omitempty"`
97	ExternalSNI string            `json:",omitempty" alias:"external_sni"`
98	CreateIndex uint64
99	ModifyIndex uint64
100}
101
102func (s *ServiceConfigEntry) GetKind() string {
103	return s.Kind
104}
105
106func (s *ServiceConfigEntry) GetName() string {
107	return s.Name
108}
109
110func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
111	return s.CreateIndex
112}
113
114func (s *ServiceConfigEntry) GetModifyIndex() uint64 {
115	return s.ModifyIndex
116}
117
118type ProxyConfigEntry struct {
119	Kind        string
120	Name        string
121	Namespace   string                 `json:",omitempty"`
122	Config      map[string]interface{} `json:",omitempty"`
123	MeshGateway MeshGatewayConfig      `json:",omitempty" alias:"mesh_gateway"`
124	Expose      ExposeConfig           `json:",omitempty"`
125	CreateIndex uint64
126	ModifyIndex uint64
127}
128
129func (p *ProxyConfigEntry) GetKind() string {
130	return p.Kind
131}
132
133func (p *ProxyConfigEntry) GetName() string {
134	return p.Name
135}
136
137func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
138	return p.CreateIndex
139}
140
141func (p *ProxyConfigEntry) GetModifyIndex() uint64 {
142	return p.ModifyIndex
143}
144
145func makeConfigEntry(kind, name string) (ConfigEntry, error) {
146	switch kind {
147	case ServiceDefaults:
148		return &ServiceConfigEntry{Kind: kind, Name: name}, nil
149	case ProxyDefaults:
150		return &ProxyConfigEntry{Kind: kind, Name: name}, nil
151	case ServiceRouter:
152		return &ServiceRouterConfigEntry{Kind: kind, Name: name}, nil
153	case ServiceSplitter:
154		return &ServiceSplitterConfigEntry{Kind: kind, Name: name}, nil
155	case ServiceResolver:
156		return &ServiceResolverConfigEntry{Kind: kind, Name: name}, nil
157	case IngressGateway:
158		return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
159	case TerminatingGateway:
160		return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
161	default:
162		return nil, fmt.Errorf("invalid config entry kind: %s", kind)
163	}
164}
165
166func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
167	return makeConfigEntry(kind, name)
168}
169
170// DecodeConfigEntry will decode the result of using json.Unmarshal of a config
171// entry into a map[string]interface{}.
172//
173// Important caveats:
174//
175// - This will NOT work if the map[string]interface{} was produced using HCL
176// decoding as that requires more extensive parsing to work around the issues
177// with map[string][]interface{} that arise.
178//
179// - This will only decode fields using their camel case json field
180// representations.
181func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
182	var entry ConfigEntry
183
184	kindVal, ok := raw["Kind"]
185	if !ok {
186		kindVal, ok = raw["kind"]
187	}
188	if !ok {
189		return nil, fmt.Errorf("Payload does not contain a kind/Kind key at the top level")
190	}
191
192	if kindStr, ok := kindVal.(string); ok {
193		newEntry, err := makeConfigEntry(kindStr, "")
194		if err != nil {
195			return nil, err
196		}
197		entry = newEntry
198	} else {
199		return nil, fmt.Errorf("Kind value in payload is not a string")
200	}
201
202	decodeConf := &mapstructure.DecoderConfig{
203		DecodeHook:       mapstructure.StringToTimeDurationHookFunc(),
204		Result:           &entry,
205		WeaklyTypedInput: true,
206	}
207
208	decoder, err := mapstructure.NewDecoder(decodeConf)
209	if err != nil {
210		return nil, err
211	}
212
213	return entry, decoder.Decode(raw)
214}
215
216func DecodeConfigEntryFromJSON(data []byte) (ConfigEntry, error) {
217	var raw map[string]interface{}
218	if err := json.Unmarshal(data, &raw); err != nil {
219		return nil, err
220	}
221
222	return DecodeConfigEntry(raw)
223}
224
225func decodeConfigEntrySlice(raw []map[string]interface{}) ([]ConfigEntry, error) {
226	var entries []ConfigEntry
227	for _, rawEntry := range raw {
228		entry, err := DecodeConfigEntry(rawEntry)
229		if err != nil {
230			return nil, err
231		}
232		entries = append(entries, entry)
233	}
234	return entries, nil
235}
236
237// ConfigEntries can be used to query the Config endpoints
238type ConfigEntries struct {
239	c *Client
240}
241
242// Config returns a handle to the Config endpoints
243func (c *Client) ConfigEntries() *ConfigEntries {
244	return &ConfigEntries{c}
245}
246
247func (conf *ConfigEntries) Get(kind string, name string, q *QueryOptions) (ConfigEntry, *QueryMeta, error) {
248	if kind == "" || name == "" {
249		return nil, nil, fmt.Errorf("Both kind and name parameters must not be empty")
250	}
251
252	entry, err := makeConfigEntry(kind, name)
253	if err != nil {
254		return nil, nil, err
255	}
256
257	r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s/%s", kind, name))
258	r.setQueryOptions(q)
259	rtt, resp, err := requireOK(conf.c.doRequest(r))
260	if err != nil {
261		return nil, nil, err
262	}
263
264	defer resp.Body.Close()
265
266	qm := &QueryMeta{}
267	parseQueryMeta(resp, qm)
268	qm.RequestTime = rtt
269
270	if err := decodeBody(resp, entry); err != nil {
271		return nil, nil, err
272	}
273
274	return entry, qm, nil
275}
276
277func (conf *ConfigEntries) List(kind string, q *QueryOptions) ([]ConfigEntry, *QueryMeta, error) {
278	if kind == "" {
279		return nil, nil, fmt.Errorf("The kind parameter must not be empty")
280	}
281
282	r := conf.c.newRequest("GET", fmt.Sprintf("/v1/config/%s", kind))
283	r.setQueryOptions(q)
284	rtt, resp, err := requireOK(conf.c.doRequest(r))
285	if err != nil {
286		return nil, nil, err
287	}
288
289	defer resp.Body.Close()
290
291	qm := &QueryMeta{}
292	parseQueryMeta(resp, qm)
293	qm.RequestTime = rtt
294
295	var raw []map[string]interface{}
296	if err := decodeBody(resp, &raw); err != nil {
297		return nil, nil, err
298	}
299
300	entries, err := decodeConfigEntrySlice(raw)
301	if err != nil {
302		return nil, nil, err
303	}
304
305	return entries, qm, nil
306}
307
308func (conf *ConfigEntries) Set(entry ConfigEntry, w *WriteOptions) (bool, *WriteMeta, error) {
309	return conf.set(entry, nil, w)
310}
311
312func (conf *ConfigEntries) CAS(entry ConfigEntry, index uint64, w *WriteOptions) (bool, *WriteMeta, error) {
313	return conf.set(entry, map[string]string{"cas": strconv.FormatUint(index, 10)}, w)
314}
315
316func (conf *ConfigEntries) set(entry ConfigEntry, params map[string]string, w *WriteOptions) (bool, *WriteMeta, error) {
317	r := conf.c.newRequest("PUT", "/v1/config")
318	r.setWriteOptions(w)
319	for param, value := range params {
320		r.params.Set(param, value)
321	}
322	r.obj = entry
323	rtt, resp, err := requireOK(conf.c.doRequest(r))
324	if err != nil {
325		return false, nil, err
326	}
327	defer resp.Body.Close()
328
329	var buf bytes.Buffer
330	if _, err := io.Copy(&buf, resp.Body); err != nil {
331		return false, nil, fmt.Errorf("Failed to read response: %v", err)
332	}
333	res := strings.Contains(buf.String(), "true")
334
335	wm := &WriteMeta{RequestTime: rtt}
336	return res, wm, nil
337}
338
339func (conf *ConfigEntries) Delete(kind string, name string, w *WriteOptions) (*WriteMeta, error) {
340	if kind == "" || name == "" {
341		return nil, fmt.Errorf("Both kind and name parameters must not be empty")
342	}
343
344	r := conf.c.newRequest("DELETE", fmt.Sprintf("/v1/config/%s/%s", kind, name))
345	r.setWriteOptions(w)
346	rtt, resp, err := requireOK(conf.c.doRequest(r))
347	if err != nil {
348		return nil, err
349	}
350	resp.Body.Close()
351	wm := &WriteMeta{RequestTime: rtt}
352	return wm, nil
353}
354