Skip to content

Commit 04bf502

Browse files
committed
consul/connect: Add support for Connect terminating gateways
This PR implements Nomad built-in support for running Consul Connect terminating gateways. Such a gateway can be used by services running inside the service mesh to access "legacy" services running outside the service mesh while still making use of Consul's service identity based networking and ACL policies. https://www.consul.io/docs/connect/gateways/terminating-gateway These gateways are declared as part of a task group level service definition within the connect stanza. service { connect { gateway { proxy { // envoy proxy configuration } terminating { // terminating-gateway configuration entry } } } } Currently Envoy is the only supported gateway implementation in Consul. The gateay task can be customized by configuring the connect.sidecar_task block. When the gateway.terminating field is set, Nomad will write/update the Configuration Entry into Consul on job submission. Because CEs are global in scope and there may be more than one Nomad cluster communicating with Consul, there is an assumption that any terminating gateway defined in Nomad for a particular service will be the same among Nomad clusters. Gateways require Consul 1.8.0+, checked by a node constraint. Closes #9445
1 parent 42aa0c3 commit 04bf502

28 files changed

+1986
-380
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## 1.0.3 (Unreleased)
22

3+
FEATURES:
4+
5+
* **Terminating Gateways**: Adds built-in support for running Consul Connect terminating gateways [[GH-9829](https://github.com/hashicorp/nomad/pull/9829)]
6+
37
BUG FIXES:
48

59
* consul/connect: Fixed a bug where gateway proxy connection default timeout not set [[GH-9851](https://github.com/hashicorp/nomad/pull/9851)]

api/services.go

+78-9
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ type ConsulGateway struct {
302302
// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
303303
Ingress *ConsulIngressConfigEntry `hcl:"ingress,block"`
304304

305-
// Terminating is not yet supported.
306-
// Terminating *ConsulTerminatingConfigEntry
305+
// Terminating represents the Consul Configuration Entry for a Terminating Gateway.
306+
Terminating *ConsulTerminatingConfigEntry `hcl:"terminating,block"`
307307

308308
// Mesh is not yet supported.
309309
// Mesh *ConsulMeshConfigEntry
@@ -315,6 +315,7 @@ func (g *ConsulGateway) Canonicalize() {
315315
}
316316
g.Proxy.Canonicalize()
317317
g.Ingress.Canonicalize()
318+
g.Terminating.Canonicalize()
318319
}
319320

320321
func (g *ConsulGateway) Copy() *ConsulGateway {
@@ -323,8 +324,9 @@ func (g *ConsulGateway) Copy() *ConsulGateway {
323324
}
324325

325326
return &ConsulGateway{
326-
Proxy: g.Proxy.Copy(),
327-
Ingress: g.Ingress.Copy(),
327+
Proxy: g.Proxy.Copy(),
328+
Ingress: g.Ingress.Copy(),
329+
Terminating: g.Terminating.Copy(),
328330
}
329331
}
330332

@@ -335,8 +337,8 @@ type ConsulGatewayBindAddress struct {
335337
}
336338

337339
var (
338-
// defaultConnectTimeout is the default amount of time a connect gateway will
339-
// wait for a response from an upstream service (same as consul)
340+
// defaultGatewayConnectTimeout is the default amount of time connections to
341+
// upstreams are allowed before timing out.
340342
defaultGatewayConnectTimeout = 5 * time.Second
341343
)
342344

@@ -349,6 +351,7 @@ type ConsulGatewayProxy struct {
349351
EnvoyGatewayBindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses" hcl:"envoy_gateway_bind_tagged_addresses,optional"`
350352
EnvoyGatewayBindAddresses map[string]*ConsulGatewayBindAddress `mapstructure:"envoy_gateway_bind_addresses" hcl:"envoy_gateway_bind_addresses,block"`
351353
EnvoyGatewayNoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind" hcl:"envoy_gateway_no_default_bind,optional"`
354+
EnvoyDNSDiscoveryType string `mapstructure:"envoy_dns_discovery_type" hcl:"envoy_dns_discovery_type,optional"`
352355
Config map[string]interface{} `hcl:"config,block"` // escape hatch envoy config
353356
}
354357

@@ -397,6 +400,7 @@ func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
397400
EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
398401
EnvoyGatewayBindAddresses: binds,
399402
EnvoyGatewayNoDefaultBind: p.EnvoyGatewayNoDefaultBind,
403+
EnvoyDNSDiscoveryType: p.EnvoyDNSDiscoveryType,
400404
Config: config,
401405
}
402406
}
@@ -549,9 +553,74 @@ func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
549553
}
550554
}
551555

552-
// ConsulTerminatingConfigEntry is not yet supported.
553-
// type ConsulTerminatingConfigEntry struct {
554-
// }
556+
type ConsulLinkedService struct {
557+
Name string `hcl:"name,optional"`
558+
CAFile string `hcl:"ca_file,optional"`
559+
CertFile string `hcl:"cert_file,optional"`
560+
KeyFile string `hcl:"key_file,optional"`
561+
SNI string `hcl:"sni,optional"`
562+
}
563+
564+
func (s *ConsulLinkedService) Canonicalize() {
565+
// nothing to do for now
566+
}
567+
568+
func (s *ConsulLinkedService) Copy() *ConsulLinkedService {
569+
if s == nil {
570+
return nil
571+
}
572+
573+
return &ConsulLinkedService{
574+
Name: s.Name,
575+
CAFile: s.CAFile,
576+
CertFile: s.CertFile,
577+
KeyFile: s.KeyFile,
578+
SNI: s.SNI,
579+
}
580+
}
581+
582+
// ConsulTerminatingConfigEntry represents the Consul Configuration Entry type
583+
// for a Terminating Gateway.
584+
//
585+
// https://www.consul.io/docs/agent/config-entries/terminating-gateway#available-fields
586+
type ConsulTerminatingConfigEntry struct {
587+
// Namespace is not yet supported.
588+
// Namespace string
589+
590+
Services []*ConsulLinkedService `hcl:"service,block"`
591+
}
592+
593+
func (e *ConsulTerminatingConfigEntry) Canonicalize() {
594+
if e == nil {
595+
return
596+
}
597+
598+
if len(e.Services) == 0 {
599+
e.Services = nil
600+
}
601+
602+
for _, service := range e.Services {
603+
service.Canonicalize()
604+
}
605+
}
606+
607+
func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry {
608+
if e == nil {
609+
return nil
610+
}
611+
612+
var services []*ConsulLinkedService = nil
613+
if n := len(e.Services); n > 0 {
614+
services = make([]*ConsulLinkedService, n)
615+
for i := 0; i < n; i++ {
616+
services[i] = e.Services[i].Copy()
617+
}
618+
}
619+
620+
return &ConsulTerminatingConfigEntry{
621+
Services: services,
622+
}
623+
}
555624

556625
// ConsulMeshConfigEntry is not yet supported.
557626
// type ConsulMeshConfigEntry struct {

api/services_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ func TestService_ConsulGateway_Canonicalize(t *testing.T) {
291291
}
292292
cg.Canonicalize()
293293
require.Equal(t, timeToPtr(5*time.Second), cg.Proxy.ConnectTimeout)
294+
require.True(t, cg.Proxy.EnvoyGatewayBindTaggedAddresses)
294295
require.Nil(t, cg.Proxy.EnvoyGatewayBindAddresses)
296+
require.True(t, cg.Proxy.EnvoyGatewayNoDefaultBind)
297+
require.Empty(t, cg.Proxy.EnvoyDNSDiscoveryType)
295298
require.Nil(t, cg.Proxy.Config)
296299
require.Nil(t, cg.Ingress.Listeners)
297300
})
@@ -314,6 +317,7 @@ func TestService_ConsulGateway_Copy(t *testing.T) {
314317
"listener2": {Address: "10.0.0.1", Port: 2001},
315318
},
316319
EnvoyGatewayNoDefaultBind: true,
320+
EnvoyDNSDiscoveryType: "STRICT_DNS",
317321
Config: map[string]interface{}{
318322
"foo": "bar",
319323
"baz": 3,
@@ -334,6 +338,11 @@ func TestService_ConsulGateway_Copy(t *testing.T) {
334338
}},
335339
},
336340
},
341+
Terminating: &ConsulTerminatingConfigEntry{
342+
Services: []*ConsulLinkedService{{
343+
Name: "linked-service1",
344+
}},
345+
},
337346
}
338347

339348
t.Run("complete", func(t *testing.T) {
@@ -418,3 +427,47 @@ func TestService_ConsulIngressConfigEntry_Copy(t *testing.T) {
418427
require.Equal(t, entry, result)
419428
})
420429
}
430+
431+
func TestService_ConsulTerminatingConfigEntry_Canonicalize(t *testing.T) {
432+
t.Parallel()
433+
434+
t.Run("nil", func(t *testing.T) {
435+
c := (*ConsulTerminatingConfigEntry)(nil)
436+
c.Canonicalize()
437+
require.Nil(t, c)
438+
})
439+
440+
t.Run("empty services", func(t *testing.T) {
441+
c := &ConsulTerminatingConfigEntry{
442+
Services: []*ConsulLinkedService{},
443+
}
444+
c.Canonicalize()
445+
require.Nil(t, c.Services)
446+
})
447+
}
448+
449+
func TestService_ConsulTerminatingConfigEntry_Copy(t *testing.T) {
450+
t.Parallel()
451+
452+
t.Run("nil", func(t *testing.T) {
453+
result := (*ConsulIngressConfigEntry)(nil).Copy()
454+
require.Nil(t, result)
455+
})
456+
457+
entry := &ConsulTerminatingConfigEntry{
458+
Services: []*ConsulLinkedService{{
459+
Name: "servic1",
460+
}, {
461+
Name: "service2",
462+
CAFile: "ca_file.pem",
463+
CertFile: "cert_file.pem",
464+
KeyFile: "key_file.pem",
465+
SNI: "sni.terminating.consul",
466+
}},
467+
}
468+
469+
t.Run("complete", func(t *testing.T) {
470+
result := entry.Copy()
471+
require.Equal(t, entry, result)
472+
})
473+
}

client/allocrunner/taskrunner/envoy_bootstrap_hook.go

+14-9
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,16 @@ func (envoyBootstrapHook) Name() string {
110110
return envoyBootstrapHookName
111111
}
112112

113+
func isConnectKind(kind string) bool {
114+
kinds := []string{structs.ConnectProxyPrefix, structs.ConnectIngressPrefix, structs.ConnectTerminatingPrefix}
115+
return helper.SliceStringContains(kinds, kind)
116+
}
117+
113118
func (_ *envoyBootstrapHook) extractNameAndKind(kind structs.TaskKind) (string, string, error) {
114-
serviceKind := kind.Name()
115119
serviceName := kind.Value()
120+
serviceKind := kind.Name()
116121

117-
switch serviceKind {
118-
case structs.ConnectProxyPrefix, structs.ConnectIngressPrefix:
119-
default:
122+
if !isConnectKind(serviceKind) {
120123
return "", "", errors.New("envoy must be used as connect sidecar or gateway")
121124
}
122125

@@ -350,13 +353,15 @@ func (h *envoyBootstrapHook) newEnvoyBootstrapArgs(
350353
proxyID string // gateway only
351354
)
352355

353-
if service.Connect.HasSidecar() {
356+
switch {
357+
case service.Connect.HasSidecar():
354358
sidecarForID = h.proxyServiceID(group, service)
355-
}
356-
357-
if service.Connect.IsGateway() {
358-
gateway = "ingress" // more types in the future
359+
case service.Connect.IsIngress():
360+
proxyID = h.proxyServiceID(group, service)
361+
gateway = "ingress"
362+
case service.Connect.IsTerminating():
359363
proxyID = h.proxyServiceID(group, service)
364+
gateway = "terminating"
360365
}
361366

362367
h.logger.Debug("bootstrapping envoy",

command/agent/consul/connect.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ func newConnect(serviceName string, nc *structs.ConsulConnect, networks structs.
2626
return &api.AgentServiceConnect{Native: true}, nil
2727

2828
case nc.HasSidecar():
29+
// must register the sidecar for this service
2930
sidecarReg, err := connectSidecarRegistration(serviceName, nc.SidecarService, networks)
3031
if err != nil {
3132
return nil, err
3233
}
3334
return &api.AgentServiceConnect{SidecarService: sidecarReg}, nil
3435

3536
default:
37+
// a non-nil but empty connect block makes no sense
3638
return nil, fmt.Errorf("Connect configuration empty for service %s", serviceName)
3739
}
3840
}
@@ -64,6 +66,10 @@ func newConnectGateway(serviceName string, connect *structs.ConsulConnect) *api.
6466
envoyConfig["envoy_gateway_bind_tagged_addresses"] = true
6567
}
6668

69+
if proxy.EnvoyDNSDiscoveryType != "" {
70+
envoyConfig["envoy_dns_discovery_type"] = proxy.EnvoyDNSDiscoveryType
71+
}
72+
6773
if proxy.ConnectTimeout != nil {
6874
envoyConfig["connect_timeout_ms"] = proxy.ConnectTimeout.Milliseconds()
6975
}
@@ -89,7 +95,7 @@ func connectSidecarRegistration(serviceName string, css *structs.ConsulSidecarSe
8995
return nil, err
9096
}
9197

92-
proxy, err := connectProxy(css.Proxy, cPort.To, networks)
98+
proxy, err := connectSidecarProxy(css.Proxy, cPort.To, networks)
9399
if err != nil {
94100
return nil, err
95101
}
@@ -102,7 +108,7 @@ func connectSidecarRegistration(serviceName string, css *structs.ConsulSidecarSe
102108
}, nil
103109
}
104110

105-
func connectProxy(proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) {
111+
func connectSidecarProxy(proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) {
106112
if proxy == nil {
107113
proxy = new(structs.ConsulProxy)
108114
}

command/agent/consul/connect_test.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func TestConnect_connectProxy(t *testing.T) {
119119
// If the input proxy is nil, we expect the output to be a proxy with its
120120
// config set to default values.
121121
t.Run("nil proxy", func(t *testing.T) {
122-
proxy, err := connectProxy(nil, 2000, testConnectNetwork)
122+
proxy, err := connectSidecarProxy(nil, 2000, testConnectNetwork)
123123
require.NoError(t, err)
124124
require.Equal(t, &api.AgentServiceConnectProxyConfig{
125125
LocalServiceAddress: "",
@@ -134,7 +134,7 @@ func TestConnect_connectProxy(t *testing.T) {
134134
})
135135

136136
t.Run("bad proxy", func(t *testing.T) {
137-
_, err := connectProxy(&structs.ConsulProxy{
137+
_, err := connectSidecarProxy(&structs.ConsulProxy{
138138
LocalServiceAddress: "0.0.0.0",
139139
LocalServicePort: 2000,
140140
Upstreams: nil,
@@ -149,7 +149,7 @@ func TestConnect_connectProxy(t *testing.T) {
149149
})
150150

151151
t.Run("normal", func(t *testing.T) {
152-
proxy, err := connectProxy(&structs.ConsulProxy{
152+
proxy, err := connectSidecarProxy(&structs.ConsulProxy{
153153
LocalServiceAddress: "0.0.0.0",
154154
LocalServicePort: 2000,
155155
Upstreams: nil,
@@ -453,6 +453,7 @@ func TestConnect_newConnectGateway(t *testing.T) {
453453
},
454454
},
455455
EnvoyGatewayNoDefaultBind: true,
456+
EnvoyDNSDiscoveryType: "STRICT_DNS",
456457
Config: map[string]interface{}{
457458
"foo": 1,
458459
},
@@ -470,6 +471,7 @@ func TestConnect_newConnectGateway(t *testing.T) {
470471
},
471472
},
472473
"envoy_gateway_no_default_bind": true,
474+
"envoy_dns_discovery_type": "STRICT_DNS",
473475
"foo": 1,
474476
},
475477
}, result)

command/agent/consul/service_client.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -891,10 +891,21 @@ func (c *ServiceClient) serviceRegs(ops *operations, service *structs.Service, w
891891
// This enables the consul UI to show that Nomad registered this service
892892
meta["external-source"] = "nomad"
893893

894-
// Explicitly set the service kind in case this service represents a Connect gateway.
894+
// Explicitly set the Consul service Kind in case this service represents
895+
// one of the Connect gateway types.
895896
kind := api.ServiceKindTypical
896-
if service.Connect.IsGateway() {
897+
switch {
898+
case service.Connect.IsIngress():
897899
kind = api.ServiceKindIngressGateway
900+
case service.Connect.IsTerminating():
901+
kind = api.ServiceKindTerminatingGateway
902+
// set the default port if bridge / default listener set
903+
if defaultBind, exists := service.Connect.Gateway.Proxy.EnvoyGatewayBindAddresses["default"]; exists {
904+
portLabel := fmt.Sprintf("%s-%s", structs.ConnectTerminatingPrefix, service.Name)
905+
if dynPort, ok := workload.Ports.Get(portLabel); ok {
906+
defaultBind.Port = dynPort.Value
907+
}
908+
}
898909
}
899910

900911
// Build the Consul Service registration request

0 commit comments

Comments
 (0)