open-nomad/api/services.go
Seth Hoenig 5b072029f2 consul/connect: add initial support for ingress gateways
This PR adds initial support for running Consul Connect Ingress Gateways (CIGs) in Nomad. These gateways are declared as part of a task group level service definition within the connect stanza.

```hcl
service {
  connect {
    gateway {
      proxy {
        // envoy proxy configuration
      }
      ingress {
        // ingress-gateway configuration entry
      }
    }
  }
}
```

A gateway can be run in `bridge` or `host` networking mode, with the caveat that host networking necessitates manually specifying the Envoy admin listener (which cannot be disabled) via the service port value.

Currently Envoy is the only supported gateway implementation in Consul, and Nomad only supports running Envoy as a gateway using the docker driver.

Aims to address #8294 and tangentially #8647
2020-08-21 16:21:54 -05:00

566 lines
13 KiB
Go

package api
import (
"fmt"
"time"
)
// CheckRestart describes if and when a task should be restarted based on
// failing health checks.
type CheckRestart struct {
Limit int `mapstructure:"limit"`
Grace *time.Duration `mapstructure:"grace"`
IgnoreWarnings bool `mapstructure:"ignore_warnings"`
}
// Canonicalize CheckRestart fields if not nil.
func (c *CheckRestart) Canonicalize() {
if c == nil {
return
}
if c.Grace == nil {
c.Grace = timeToPtr(1 * time.Second)
}
}
// Copy returns a copy of CheckRestart or nil if unset.
func (c *CheckRestart) Copy() *CheckRestart {
if c == nil {
return nil
}
nc := new(CheckRestart)
nc.Limit = c.Limit
if c.Grace != nil {
g := *c.Grace
nc.Grace = &g
}
nc.IgnoreWarnings = c.IgnoreWarnings
return nc
}
// Merge values from other CheckRestart over default values on this
// CheckRestart and return merged copy.
func (c *CheckRestart) Merge(o *CheckRestart) *CheckRestart {
if c == nil {
// Just return other
return o
}
nc := c.Copy()
if o == nil {
// Nothing to merge
return nc
}
if o.Limit > 0 {
nc.Limit = o.Limit
}
if o.Grace != nil {
nc.Grace = o.Grace
}
if o.IgnoreWarnings {
nc.IgnoreWarnings = o.IgnoreWarnings
}
return nc
}
// ServiceCheck represents the consul health check that Nomad registers.
type ServiceCheck struct {
//FIXME Id is unused. Remove?
Id string
Name string
Type string
Command string
Args []string
Path string
Protocol string
PortLabel string `mapstructure:"port"`
Expose bool
AddressMode string `mapstructure:"address_mode"`
Interval time.Duration
Timeout time.Duration
InitialStatus string `mapstructure:"initial_status"`
TLSSkipVerify bool `mapstructure:"tls_skip_verify"`
Header map[string][]string
Method string
CheckRestart *CheckRestart `mapstructure:"check_restart"`
GRPCService string `mapstructure:"grpc_service"`
GRPCUseTLS bool `mapstructure:"grpc_use_tls"`
TaskName string `mapstructure:"task"`
SuccessBeforePassing int `mapstructure:"success_before_passing"`
FailuresBeforeCritical int `mapstructure:"failures_before_critical"`
}
// Service represents a Consul service definition.
type Service struct {
//FIXME Id is unused. Remove?
Id string
Name string
Tags []string
CanaryTags []string `mapstructure:"canary_tags"`
EnableTagOverride bool `mapstructure:"enable_tag_override"`
PortLabel string `mapstructure:"port"`
AddressMode string `mapstructure:"address_mode"`
Checks []ServiceCheck
CheckRestart *CheckRestart `mapstructure:"check_restart"`
Connect *ConsulConnect
Meta map[string]string
CanaryMeta map[string]string
TaskName string `mapstructure:"task"`
}
// Canonicalize the Service by ensuring its name and address mode are set. Task
// will be nil for group services.
func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) {
if s.Name == "" {
if t != nil {
s.Name = fmt.Sprintf("%s-%s-%s", *job.Name, *tg.Name, t.Name)
} else {
s.Name = fmt.Sprintf("%s-%s", *job.Name, *tg.Name)
}
}
// Default to AddressModeAuto
if s.AddressMode == "" {
s.AddressMode = "auto"
}
s.Connect.Canonicalize()
// Canonicalize CheckRestart on Checks and merge Service.CheckRestart
// into each check.
for i, check := range s.Checks {
s.Checks[i].CheckRestart = s.CheckRestart.Merge(check.CheckRestart)
s.Checks[i].CheckRestart.Canonicalize()
if s.Checks[i].SuccessBeforePassing < 0 {
s.Checks[i].SuccessBeforePassing = 0
}
if s.Checks[i].FailuresBeforeCritical < 0 {
s.Checks[i].FailuresBeforeCritical = 0
}
}
}
// ConsulConnect represents a Consul Connect jobspec stanza.
type ConsulConnect struct {
Native bool
Gateway *ConsulGateway
SidecarService *ConsulSidecarService `mapstructure:"sidecar_service"`
SidecarTask *SidecarTask `mapstructure:"sidecar_task"`
}
func (cc *ConsulConnect) Canonicalize() {
if cc == nil {
return
}
cc.SidecarService.Canonicalize()
cc.SidecarTask.Canonicalize()
cc.Gateway.Canonicalize()
}
// ConsulSidecarService represents a Consul Connect SidecarService jobspec
// stanza.
type ConsulSidecarService struct {
Tags []string
Port string
Proxy *ConsulProxy
}
func (css *ConsulSidecarService) Canonicalize() {
if css == nil {
return
}
if len(css.Tags) == 0 {
css.Tags = nil
}
css.Proxy.Canonicalize()
}
// SidecarTask represents a subset of Task fields that can be set to override
// the fields of the Task generated for the sidecar
type SidecarTask struct {
Name string
Driver string
User string
Config map[string]interface{}
Env map[string]string
Resources *Resources
Meta map[string]string
KillTimeout *time.Duration `mapstructure:"kill_timeout"`
LogConfig *LogConfig `mapstructure:"logs"`
ShutdownDelay *time.Duration `mapstructure:"shutdown_delay"`
KillSignal string `mapstructure:"kill_signal"`
}
func (st *SidecarTask) Canonicalize() {
if st == nil {
return
}
if len(st.Config) == 0 {
st.Config = nil
}
if len(st.Env) == 0 {
st.Env = nil
}
if st.Resources == nil {
st.Resources = DefaultResources()
} else {
st.Resources.Canonicalize()
}
if st.LogConfig == nil {
st.LogConfig = DefaultLogConfig()
} else {
st.LogConfig.Canonicalize()
}
if len(st.Meta) == 0 {
st.Meta = nil
}
if st.KillTimeout == nil {
st.KillTimeout = timeToPtr(5 * time.Second)
}
if st.ShutdownDelay == nil {
st.ShutdownDelay = timeToPtr(0)
}
}
// ConsulProxy represents a Consul Connect sidecar proxy jobspec stanza.
type ConsulProxy struct {
LocalServiceAddress string `mapstructure:"local_service_address"`
LocalServicePort int `mapstructure:"local_service_port"`
ExposeConfig *ConsulExposeConfig `mapstructure:"expose"`
Upstreams []*ConsulUpstream
Config map[string]interface{}
}
func (cp *ConsulProxy) Canonicalize() {
if cp == nil {
return
}
cp.ExposeConfig.Canonicalize()
if len(cp.Upstreams) == 0 {
cp.Upstreams = nil
}
if len(cp.Config) == 0 {
cp.Config = nil
}
}
// ConsulUpstream represents a Consul Connect upstream jobspec stanza.
type ConsulUpstream struct {
DestinationName string `mapstructure:"destination_name"`
LocalBindPort int `mapstructure:"local_bind_port"`
}
type ConsulExposeConfig struct {
Path []*ConsulExposePath `mapstructure:"path"`
}
func (cec *ConsulExposeConfig) Canonicalize() {
if cec == nil {
return
}
if len(cec.Path) == 0 {
cec.Path = nil
}
}
type ConsulExposePath struct {
Path string
Protocol string
LocalPathPort int `mapstructure:"local_path_port"`
ListenerPort string `mapstructure:"listener_port"`
}
// ConsulGateway is used to configure one of the Consul Connect Gateway types.
type ConsulGateway struct {
// Proxy is used to configure the Envoy instance acting as the gateway.
Proxy *ConsulGatewayProxy
// Ingress represents the Consul Configuration Entry for an Ingress Gateway.
Ingress *ConsulIngressConfigEntry
// Terminating is not yet supported.
// Terminating *ConsulTerminatingConfigEntry
// Mesh is not yet supported.
// Mesh *ConsulMeshConfigEntry
}
func (g *ConsulGateway) Canonicalize() {
if g == nil {
return
}
g.Proxy.Canonicalize()
g.Ingress.Canonicalize()
}
func (g *ConsulGateway) Copy() *ConsulGateway {
if g == nil {
return nil
}
return &ConsulGateway{
Proxy: g.Proxy.Copy(),
Ingress: g.Ingress.Copy(),
}
}
type ConsulGatewayBindAddress struct {
Address string `mapstructure:"address"`
Port int `mapstructure:"port"`
}
const (
defaultDNSDiscoveryType = "LOGICAL_DNS"
)
var (
defaultGatewayConnectTimeout = 5 * time.Second
)
// ConsulGatewayProxy is used to tune parameters of the proxy instance acting as
// one of the forms of Connect gateways that Consul supports.
//
// https://www.consul.io/docs/connect/proxies/envoy#gateway-options
type ConsulGatewayProxy struct {
ConnectTimeout *time.Duration `mapstructure:"connect_timeout"`
EnvoyGatewayBindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses"`
EnvoyGatewayBindAddresses map[string]*ConsulGatewayBindAddress `mapstructure:"envoy_gateway_bind_addresses"`
EnvoyGatewayNoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind"`
EnvoyDNSDiscoveryType string `mapstructure:"envoy_dns_discovery_type"`
Config map[string]interface{} // escape hatch envoy config
}
func (p *ConsulGatewayProxy) Canonicalize() {
if p == nil {
return
}
if p.ConnectTimeout == nil {
// same as the default from consul
p.ConnectTimeout = timeToPtr(defaultGatewayConnectTimeout)
}
if p.EnvoyDNSDiscoveryType == "" {
// same as default from consul
p.EnvoyDNSDiscoveryType = defaultDNSDiscoveryType
}
if len(p.EnvoyGatewayBindAddresses) == 0 {
p.EnvoyGatewayBindAddresses = nil
}
if len(p.Config) == 0 {
p.Config = nil
}
}
func (p *ConsulGatewayProxy) Copy() *ConsulGatewayProxy {
if p == nil {
return nil
}
var binds map[string]*ConsulGatewayBindAddress = nil
if p.EnvoyGatewayBindAddresses != nil {
binds = make(map[string]*ConsulGatewayBindAddress, len(p.EnvoyGatewayBindAddresses))
for k, v := range p.EnvoyGatewayBindAddresses {
binds[k] = v
}
}
var config map[string]interface{} = nil
if p.Config != nil {
config = make(map[string]interface{}, len(p.Config))
for k, v := range p.Config {
config[k] = v
}
}
return &ConsulGatewayProxy{
ConnectTimeout: timeToPtr(*p.ConnectTimeout),
EnvoyGatewayBindTaggedAddresses: p.EnvoyGatewayBindTaggedAddresses,
EnvoyGatewayBindAddresses: binds,
EnvoyGatewayNoDefaultBind: p.EnvoyGatewayNoDefaultBind,
EnvoyDNSDiscoveryType: p.EnvoyDNSDiscoveryType,
Config: config,
}
}
// ConsulGatewayTLSConfig is used to configure TLS for a gateway.
type ConsulGatewayTLSConfig struct {
Enabled bool
}
func (tc *ConsulGatewayTLSConfig) Canonicalize() {
}
func (tc *ConsulGatewayTLSConfig) Copy() *ConsulGatewayTLSConfig {
if tc == nil {
return nil
}
return &ConsulGatewayTLSConfig{
Enabled: tc.Enabled,
}
}
// ConsulIngressService is used to configure a service fronted by the ingress gateway.
type ConsulIngressService struct {
// Namespace is not yet supported.
// Namespace string
Name string
Hosts []string
}
func (s *ConsulIngressService) Canonicalize() {
if s == nil {
return
}
if len(s.Hosts) == 0 {
s.Hosts = nil
}
}
func (s *ConsulIngressService) Copy() *ConsulIngressService {
if s == nil {
return nil
}
var hosts []string = nil
if n := len(s.Hosts); n > 0 {
hosts = make([]string, n)
copy(hosts, s.Hosts)
}
return &ConsulIngressService{
Name: s.Name,
Hosts: hosts,
}
}
const (
defaultIngressListenerProtocol = "tcp"
)
// ConsulIngressListener is used to configure a listener on a Consul Ingress
// Gateway.
type ConsulIngressListener struct {
Port int
Protocol string
Services []*ConsulIngressService
}
func (l *ConsulIngressListener) Canonicalize() {
if l == nil {
return
}
if l.Protocol == "" {
// same as default from consul
l.Protocol = defaultIngressListenerProtocol
}
if len(l.Services) == 0 {
l.Services = nil
}
}
func (l *ConsulIngressListener) Copy() *ConsulIngressListener {
if l == nil {
return nil
}
var services []*ConsulIngressService = nil
if n := len(l.Services); n > 0 {
services = make([]*ConsulIngressService, n)
for i := 0; i < n; i++ {
services[i] = l.Services[i].Copy()
}
}
return &ConsulIngressListener{
Port: l.Port,
Protocol: l.Protocol,
Services: services,
}
}
// ConsulIngressConfigEntry represents the Consul Configuration Entry type for
// an Ingress Gateway.
//
// https://www.consul.io/docs/agent/config-entries/ingress-gateway#available-fields
type ConsulIngressConfigEntry struct {
// Namespace is not yet supported.
// Namespace string
TLS *ConsulGatewayTLSConfig
Listeners []*ConsulIngressListener
}
func (e *ConsulIngressConfigEntry) Canonicalize() {
if e == nil {
return
}
e.TLS.Canonicalize()
if len(e.Listeners) == 0 {
e.Listeners = nil
}
for _, listener := range e.Listeners {
listener.Canonicalize()
}
}
func (e *ConsulIngressConfigEntry) Copy() *ConsulIngressConfigEntry {
if e == nil {
return nil
}
var listeners []*ConsulIngressListener = nil
if n := len(e.Listeners); n > 0 {
listeners = make([]*ConsulIngressListener, n)
for i := 0; i < n; i++ {
listeners[i] = e.Listeners[i].Copy()
}
}
return &ConsulIngressConfigEntry{
TLS: e.TLS.Copy(),
Listeners: listeners,
}
}
// ConsulTerminatingConfigEntry is not yet supported.
// type ConsulTerminatingConfigEntry struct {
// }
// ConsulMeshConfigEntry is not yet supported.
// type ConsulMeshConfigEntry struct {
// }