1package consul
2
3import (
4	"fmt"
5	"net"
6	"strconv"
7
8	"github.com/hashicorp/consul/api"
9	"github.com/hashicorp/nomad/helper"
10	"github.com/hashicorp/nomad/nomad/structs"
11)
12
13// newConnect creates a new Consul AgentServiceConnect struct based on a Nomad
14// Connect struct. If the nomad Connect struct is nil, nil will be returned to
15// disable Connect for this service.
16func newConnect(serviceId string, serviceName string, nc *structs.ConsulConnect, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceConnect, error) {
17	switch {
18	case nc == nil:
19		// no connect stanza means there is no connect service to register
20		return nil, nil
21
22	case nc.IsGateway():
23		// gateway settings are configured on the service block on the consul side
24		return nil, nil
25
26	case nc.IsNative():
27		// the service is connect native
28		return &api.AgentServiceConnect{Native: true}, nil
29
30	case nc.HasSidecar():
31		// must register the sidecar for this service
32		if nc.SidecarService.Port == "" {
33			nc.SidecarService.Port = fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, serviceName)
34		}
35		sidecarReg, err := connectSidecarRegistration(serviceId, nc.SidecarService, networks, ports)
36		if err != nil {
37			return nil, err
38		}
39		return &api.AgentServiceConnect{SidecarService: sidecarReg}, nil
40
41	default:
42		// a non-nil but empty connect block makes no sense
43		return nil, fmt.Errorf("Connect configuration empty for service %s", serviceName)
44	}
45}
46
47// newConnectGateway creates a new Consul AgentServiceConnectProxyConfig struct based on
48// a Nomad Connect struct. If the Nomad Connect struct does not contain a gateway, nil
49// will be returned as this service is not a gateway.
50func newConnectGateway(serviceName string, connect *structs.ConsulConnect) *api.AgentServiceConnectProxyConfig {
51	if !connect.IsGateway() {
52		return nil
53	}
54
55	var envoyConfig map[string]interface{}
56
57	// Populate the envoy configuration from the gateway.proxy stanza, if
58	// such configuration is provided.
59	if proxy := connect.Gateway.Proxy; proxy != nil {
60		envoyConfig = make(map[string]interface{})
61
62		if len(proxy.EnvoyGatewayBindAddresses) > 0 {
63			envoyConfig["envoy_gateway_bind_addresses"] = proxy.EnvoyGatewayBindAddresses
64		}
65
66		if proxy.EnvoyGatewayNoDefaultBind {
67			envoyConfig["envoy_gateway_no_default_bind"] = true
68		}
69
70		if proxy.EnvoyGatewayBindTaggedAddresses {
71			envoyConfig["envoy_gateway_bind_tagged_addresses"] = true
72		}
73
74		if proxy.EnvoyDNSDiscoveryType != "" {
75			envoyConfig["envoy_dns_discovery_type"] = proxy.EnvoyDNSDiscoveryType
76		}
77
78		if proxy.ConnectTimeout != nil {
79			envoyConfig["connect_timeout_ms"] = proxy.ConnectTimeout.Milliseconds()
80		}
81
82		if len(proxy.Config) > 0 {
83			for k, v := range proxy.Config {
84				envoyConfig[k] = v
85			}
86		}
87	}
88
89	return &api.AgentServiceConnectProxyConfig{Config: envoyConfig}
90}
91
92func connectSidecarRegistration(serviceId string, css *structs.ConsulSidecarService, networks structs.Networks, ports structs.AllocatedPorts) (*api.AgentServiceRegistration, error) {
93	if css == nil {
94		// no sidecar stanza means there is no sidecar service to register
95		return nil, nil
96	}
97
98	cMapping, err := connectPort(css.Port, networks, ports)
99	if err != nil {
100		return nil, err
101	}
102
103	proxy, err := connectSidecarProxy(css.Proxy, cMapping.To, networks)
104	if err != nil {
105		return nil, err
106	}
107
108	// if the service has a TCP check that's failing, we need an alias to
109	// ensure service discovery excludes this sidecar from queries
110	// (ex. in the case of Connect upstreams)
111	checks := api.AgentServiceChecks{{
112		Name:         "Connect Sidecar Aliasing " + serviceId,
113		AliasService: serviceId,
114	}}
115	if !css.DisableDefaultTCPCheck {
116		checks = append(checks, &api.AgentServiceCheck{
117			Name:     "Connect Sidecar Listening",
118			TCP:      net.JoinHostPort(cMapping.HostIP, strconv.Itoa(cMapping.Value)),
119			Interval: "10s",
120		})
121	}
122
123	return &api.AgentServiceRegistration{
124		Tags:    helper.CopySliceString(css.Tags),
125		Port:    cMapping.Value,
126		Address: cMapping.HostIP,
127		Proxy:   proxy,
128		Checks:  checks,
129	}, nil
130}
131
132func connectSidecarProxy(proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) {
133	if proxy == nil {
134		proxy = new(structs.ConsulProxy)
135	}
136
137	expose, err := connectProxyExpose(proxy.Expose, networks)
138	if err != nil {
139		return nil, err
140	}
141
142	return &api.AgentServiceConnectProxyConfig{
143		LocalServiceAddress: proxy.LocalServiceAddress,
144		LocalServicePort:    proxy.LocalServicePort,
145		Config:              connectProxyConfig(proxy.Config, cPort),
146		Upstreams:           connectUpstreams(proxy.Upstreams),
147		Expose:              expose,
148	}, nil
149}
150
151func connectProxyExpose(expose *structs.ConsulExposeConfig, networks structs.Networks) (api.ExposeConfig, error) {
152	if expose == nil {
153		return api.ExposeConfig{}, nil
154	}
155
156	paths, err := connectProxyExposePaths(expose.Paths, networks)
157	if err != nil {
158		return api.ExposeConfig{}, err
159	}
160
161	return api.ExposeConfig{
162		Checks: false,
163		Paths:  paths,
164	}, nil
165}
166
167func connectProxyExposePaths(in []structs.ConsulExposePath, networks structs.Networks) ([]api.ExposePath, error) {
168	if len(in) == 0 {
169		return nil, nil
170	}
171
172	paths := make([]api.ExposePath, len(in))
173	for i, path := range in {
174		if _, exposedPort, err := connectExposePathPort(path.ListenerPort, networks); err != nil {
175			return nil, err
176		} else {
177			paths[i] = api.ExposePath{
178				ListenerPort:    exposedPort,
179				Path:            path.Path,
180				LocalPathPort:   path.LocalPathPort,
181				Protocol:        path.Protocol,
182				ParsedFromCheck: false,
183			}
184		}
185	}
186	return paths, nil
187}
188
189func connectUpstreams(in []structs.ConsulUpstream) []api.Upstream {
190	if len(in) == 0 {
191		return nil
192	}
193
194	upstreams := make([]api.Upstream, len(in))
195	for i, upstream := range in {
196		upstreams[i] = api.Upstream{
197			DestinationName:  upstream.DestinationName,
198			LocalBindPort:    upstream.LocalBindPort,
199			Datacenter:       upstream.Datacenter,
200			LocalBindAddress: upstream.LocalBindAddress,
201		}
202	}
203	return upstreams
204}
205
206func connectProxyConfig(cfg map[string]interface{}, port int) map[string]interface{} {
207	if cfg == nil {
208		cfg = make(map[string]interface{})
209	}
210	cfg["bind_address"] = "0.0.0.0"
211	cfg["bind_port"] = port
212	return cfg
213}
214
215func connectNetworkInvariants(networks structs.Networks) error {
216	if n := len(networks); n != 1 {
217		return fmt.Errorf("Connect only supported with exactly 1 network (found %d)", n)
218	}
219	return nil
220}
221
222// connectPort returns the network and port for the Connect proxy sidecar
223// defined for this service. An error is returned if the network and port
224// cannot be determined.
225func connectPort(portLabel string, networks structs.Networks, ports structs.AllocatedPorts) (structs.AllocatedPortMapping, error) {
226	if err := connectNetworkInvariants(networks); err != nil {
227		return structs.AllocatedPortMapping{}, err
228	}
229	mapping, ok := ports.Get(portLabel)
230	if !ok {
231		mapping = networks.Port(portLabel)
232		if mapping.Value > 0 {
233			return mapping, nil
234		}
235		return structs.AllocatedPortMapping{}, fmt.Errorf("No port of label %q defined", portLabel)
236	}
237	return mapping, nil
238}
239
240// connectExposePathPort returns the port for the exposed path for the exposed
241// proxy path.
242func connectExposePathPort(portLabel string, networks structs.Networks) (string, int, error) {
243	if err := connectNetworkInvariants(networks); err != nil {
244		return "", 0, err
245	}
246
247	mapping := networks.Port(portLabel)
248	if mapping.Value == 0 {
249		return "", 0, fmt.Errorf("No port of label %q defined", portLabel)
250	}
251
252	return mapping.HostIP, mapping.Value, nil
253}
254