diff --git a/api/consul.go b/api/consul.go index 64e085e618dd..9a11187c08cb 100644 --- a/api/consul.go +++ b/api/consul.go @@ -1,5 +1,7 @@ package api +import "time" + // Consul represents configuration related to consul. type Consul struct { // (Enterprise-only) Namespace represents a Consul namespace. @@ -33,3 +35,558 @@ func (c *Consul) MergeNamespace(namespace *string) { c.Namespace = *namespace } } + +// ConsulConnect represents a Consul Connect jobspec stanza. +type ConsulConnect struct { + Native bool `hcl:"native,optional"` + Gateway *ConsulGateway `hcl:"gateway,block"` + SidecarService *ConsulSidecarService `mapstructure:"sidecar_service" hcl:"sidecar_service,block"` + SidecarTask *SidecarTask `mapstructure:"sidecar_task" hcl:"sidecar_task,block"` +} + +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 `hcl:"tags,optional"` + Port string `hcl:"port,optional"` + Proxy *ConsulProxy `hcl:"proxy,block"` + DisableDefaultTCPCheck bool `mapstructure:"disable_default_tcp_check" hcl:"disable_default_tcp_check,optional"` +} + +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 `hcl:"name,optional"` + Driver string `hcl:"driver,optional"` + User string `hcl:"user,optional"` + Config map[string]interface{} `hcl:"config,block"` + Env map[string]string `hcl:"env,block"` + Resources *Resources `hcl:"resources,block"` + Meta map[string]string `hcl:"meta,block"` + KillTimeout *time.Duration `mapstructure:"kill_timeout" hcl:"kill_timeout,optional"` + LogConfig *LogConfig `mapstructure:"logs" hcl:"logs,block"` + ShutdownDelay *time.Duration `mapstructure:"shutdown_delay" hcl:"shutdown_delay,optional"` + KillSignal string `mapstructure:"kill_signal" hcl:"kill_signal,optional"` +} + +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" hcl:"local_service_address,optional"` + LocalServicePort int `mapstructure:"local_service_port" hcl:"local_service_port,optional"` + ExposeConfig *ConsulExposeConfig `mapstructure:"expose" hcl:"expose,block"` + Upstreams []*ConsulUpstream `hcl:"upstreams,block"` + Config map[string]interface{} `hcl:"config,block"` +} + +func (cp *ConsulProxy) Canonicalize() { + if cp == nil { + return + } + + cp.ExposeConfig.Canonicalize() + + if len(cp.Upstreams) == 0 { + cp.Upstreams = nil + } + + for _, upstream := range cp.Upstreams { + upstream.Canonicalize() + } + + if len(cp.Config) == 0 { + cp.Config = nil + } +} + +// ConsulMeshGateway is used to configure mesh gateway usage when connecting to +// a connect upstream in another datacenter. +type ConsulMeshGateway struct { + // Mode configures how an upstream should be accessed with regard to using + // mesh gateways. + // + // local - the connect proxy makes outbound connections through mesh gateway + // originating in the same datacenter. + // + // remote - the connect proxy makes outbound connections to a mesh gateway + // in the destination datacenter. + // + // none (default) - no mesh gateway is used, the proxy makes outbound connections + // directly to destination services. + // + // https://www.consul.io/docs/connect/gateways/mesh-gateway#modes-of-operation + Mode string `mapstructure:"mode" hcl:"mode,optional"` +} + +func (c *ConsulMeshGateway) Canonicalize() { + // Mode may be empty string, indicating behavior will defer to Consul + // service-defaults config entry. + return +} + +func (c *ConsulMeshGateway) Copy() *ConsulMeshGateway { + if c == nil { + return nil + } + + return &ConsulMeshGateway{ + Mode: c.Mode, + } +} + +// ConsulUpstream represents a Consul Connect upstream jobspec stanza. +type ConsulUpstream struct { + DestinationName string `mapstructure:"destination_name" hcl:"destination_name,optional"` + LocalBindPort int `mapstructure:"local_bind_port" hcl:"local_bind_port,optional"` + Datacenter string `mapstructure:"datacenter" hcl:"datacenter,optional"` + LocalBindAddress string `mapstructure:"local_bind_address" hcl:"local_bind_address,optional"` + MeshGateway *ConsulMeshGateway `mapstructure:"mesh_gateway" hcl:"mesh_gateway,block"` +} + +func (cu *ConsulUpstream) Copy() *ConsulUpstream { + if cu == nil { + return nil + } + return &ConsulUpstream{ + DestinationName: cu.DestinationName, + LocalBindPort: cu.LocalBindPort, + Datacenter: cu.Datacenter, + LocalBindAddress: cu.LocalBindAddress, + MeshGateway: cu.MeshGateway.Copy(), + } +} + +func (cu *ConsulUpstream) Canonicalize() { + if cu == nil { + return + } + cu.MeshGateway.Canonicalize() +} + +type ConsulExposeConfig struct { + Path []*ConsulExposePath `mapstructure:"path" hcl:"path,block"` +} + +func (cec *ConsulExposeConfig) Canonicalize() { + if cec == nil { + return + } + + if len(cec.Path) == 0 { + cec.Path = nil + } +} + +type ConsulExposePath struct { + Path string `hcl:"path,optional"` + Protocol string `hcl:"protocol,optional"` + LocalPathPort int `mapstructure:"local_path_port" hcl:"local_path_port,optional"` + ListenerPort string `mapstructure:"listener_port" hcl:"listener_port,optional"` +} + +// 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 `hcl:"proxy,block"` + + // Ingress represents the Consul Configuration Entry for an Ingress Gateway. + Ingress *ConsulIngressConfigEntry `hcl:"ingress,block"` + + // Terminating represents the Consul Configuration Entry for a Terminating Gateway. + Terminating *ConsulTerminatingConfigEntry `hcl:"terminating,block"` + + // Mesh indicates the Consul service should be a Mesh Gateway. + Mesh *ConsulMeshConfigEntry `hcl:"mesh,block"` +} + +func (g *ConsulGateway) Canonicalize() { + if g == nil { + return + } + g.Proxy.Canonicalize() + g.Ingress.Canonicalize() + g.Terminating.Canonicalize() +} + +func (g *ConsulGateway) Copy() *ConsulGateway { + if g == nil { + return nil + } + + return &ConsulGateway{ + Proxy: g.Proxy.Copy(), + Ingress: g.Ingress.Copy(), + Terminating: g.Terminating.Copy(), + } +} + +type ConsulGatewayBindAddress struct { + Name string `hcl:",label"` + Address string `mapstructure:"address" hcl:"address,optional"` + Port int `mapstructure:"port" hcl:"port,optional"` +} + +var ( + // defaultGatewayConnectTimeout is the default amount of time connections to + // upstreams are allowed before timing out. + 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" hcl:"connect_timeout,optional"` + EnvoyGatewayBindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses" hcl:"envoy_gateway_bind_tagged_addresses,optional"` + EnvoyGatewayBindAddresses map[string]*ConsulGatewayBindAddress `mapstructure:"envoy_gateway_bind_addresses" hcl:"envoy_gateway_bind_addresses,block"` + EnvoyGatewayNoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind" hcl:"envoy_gateway_no_default_bind,optional"` + EnvoyDNSDiscoveryType string `mapstructure:"envoy_dns_discovery_type" hcl:"envoy_dns_discovery_type,optional"` + Config map[string]interface{} `hcl:"config,block"` // 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 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 `hcl:"enabled,optional"` +} + +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 `hcl:"name,optional"` + + Hosts []string `hcl:"hosts,optional"` +} + +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 `hcl:"port,optional"` + Protocol string `hcl:"protocol,optional"` + Services []*ConsulIngressService `hcl:"service,block"` +} + +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 `hcl:"tls,block"` + Listeners []*ConsulIngressListener `hcl:"listener,block"` +} + +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, + } +} + +type ConsulLinkedService struct { + Name string `hcl:"name,optional"` + CAFile string `hcl:"ca_file,optional" mapstructure:"ca_file"` + CertFile string `hcl:"cert_file,optional" mapstructure:"cert_file"` + KeyFile string `hcl:"key_file,optional" mapstructure:"key_file"` + SNI string `hcl:"sni,optional"` +} + +func (s *ConsulLinkedService) Canonicalize() { + // nothing to do for now +} + +func (s *ConsulLinkedService) Copy() *ConsulLinkedService { + if s == nil { + return nil + } + + return &ConsulLinkedService{ + Name: s.Name, + CAFile: s.CAFile, + CertFile: s.CertFile, + KeyFile: s.KeyFile, + SNI: s.SNI, + } +} + +// ConsulTerminatingConfigEntry represents the Consul Configuration Entry type +// for a Terminating Gateway. +// +// https://www.consul.io/docs/agent/config-entries/terminating-gateway#available-fields +type ConsulTerminatingConfigEntry struct { + // Namespace is not yet supported. + // Namespace string + + Services []*ConsulLinkedService `hcl:"service,block"` +} + +func (e *ConsulTerminatingConfigEntry) Canonicalize() { + if e == nil { + return + } + + if len(e.Services) == 0 { + e.Services = nil + } + + for _, service := range e.Services { + service.Canonicalize() + } +} + +func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry { + if e == nil { + return nil + } + + var services []*ConsulLinkedService = nil + if n := len(e.Services); n > 0 { + services = make([]*ConsulLinkedService, n) + for i := 0; i < n; i++ { + services[i] = e.Services[i].Copy() + } + } + + return &ConsulTerminatingConfigEntry{ + Services: services, + } +} + +// ConsulMeshConfigEntry is a stub used to represent that the gateway service type +// should be for a Mesh Gateway. Unlike Ingress and Terminating, there is no +// actual Consul Config Entry type for mesh-gateway, at least for now. We still +// create a type for future proofing, instead just using a bool for example. +type ConsulMeshConfigEntry struct { + // nothing in here +} + +func (e *ConsulMeshConfigEntry) Canonicalize() { + return +} + +func (e *ConsulMeshConfigEntry) Copy() *ConsulMeshConfigEntry { + if e == nil { + return nil + } + return new(ConsulMeshConfigEntry) +} diff --git a/api/consul_test.go b/api/consul_test.go index 81ebaf48b8a0..0c32c4c8168d 100644 --- a/api/consul_test.go +++ b/api/consul_test.go @@ -2,6 +2,7 @@ package api import ( "testing" + "time" "github.com/hashicorp/nomad/api/internal/testutil" "github.com/stretchr/testify/require" @@ -60,3 +61,455 @@ func TestConsul_MergeNamespace(t *testing.T) { require.Nil(t, ns) }) } + +func TestConsulConnect_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil connect", func(t *testing.T) { + cc := (*ConsulConnect)(nil) + cc.Canonicalize() + require.Nil(t, cc) + }) + + t.Run("empty connect", func(t *testing.T) { + cc := new(ConsulConnect) + cc.Canonicalize() + require.Empty(t, cc.Native) + require.Nil(t, cc.SidecarService) + require.Nil(t, cc.SidecarTask) + }) +} + +func TestConsulSidecarService_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil sidecar_service", func(t *testing.T) { + css := (*ConsulSidecarService)(nil) + css.Canonicalize() + require.Nil(t, css) + }) + + t.Run("empty sidecar_service", func(t *testing.T) { + css := new(ConsulSidecarService) + css.Canonicalize() + require.Empty(t, css.Tags) + require.Nil(t, css.Proxy) + }) + + t.Run("non-empty sidecar_service", func(t *testing.T) { + css := &ConsulSidecarService{ + Tags: make([]string, 0), + Port: "port", + Proxy: &ConsulProxy{ + LocalServiceAddress: "lsa", + LocalServicePort: 80, + }, + } + css.Canonicalize() + require.Equal(t, &ConsulSidecarService{ + Tags: nil, + Port: "port", + Proxy: &ConsulProxy{ + LocalServiceAddress: "lsa", + LocalServicePort: 80}, + }, css) + }) +} + +func TestConsulProxy_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil proxy", func(t *testing.T) { + cp := (*ConsulProxy)(nil) + cp.Canonicalize() + require.Nil(t, cp) + }) + + t.Run("empty proxy", func(t *testing.T) { + cp := new(ConsulProxy) + cp.Canonicalize() + require.Empty(t, cp.LocalServiceAddress) + require.Zero(t, cp.LocalServicePort) + require.Nil(t, cp.ExposeConfig) + require.Nil(t, cp.Upstreams) + require.Empty(t, cp.Config) + }) + + t.Run("non empty proxy", func(t *testing.T) { + cp := &ConsulProxy{ + LocalServiceAddress: "127.0.0.1", + LocalServicePort: 80, + ExposeConfig: new(ConsulExposeConfig), + Upstreams: make([]*ConsulUpstream, 0), + Config: make(map[string]interface{}), + } + cp.Canonicalize() + require.Equal(t, "127.0.0.1", cp.LocalServiceAddress) + require.Equal(t, 80, cp.LocalServicePort) + require.Equal(t, &ConsulExposeConfig{}, cp.ExposeConfig) + require.Nil(t, cp.Upstreams) + require.Nil(t, cp.Config) + }) +} + +func TestConsulUpstream_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil upstream", func(t *testing.T) { + cu := (*ConsulUpstream)(nil) + result := cu.Copy() + require.Nil(t, result) + }) + + t.Run("complete upstream", func(t *testing.T) { + cu := &ConsulUpstream{ + DestinationName: "dest1", + Datacenter: "dc2", + LocalBindPort: 2000, + LocalBindAddress: "10.0.0.1", + MeshGateway: &ConsulMeshGateway{Mode: "remote"}, + } + result := cu.Copy() + require.Equal(t, cu, result) + }) +} + +func TestConsulUpstream_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil upstream", func(t *testing.T) { + cu := (*ConsulUpstream)(nil) + cu.Canonicalize() + require.Nil(t, cu) + }) + + t.Run("complete", func(t *testing.T) { + cu := &ConsulUpstream{ + DestinationName: "dest1", + Datacenter: "dc2", + LocalBindPort: 2000, + LocalBindAddress: "10.0.0.1", + MeshGateway: &ConsulMeshGateway{Mode: ""}, + } + cu.Canonicalize() + require.Equal(t, &ConsulUpstream{ + DestinationName: "dest1", + Datacenter: "dc2", + LocalBindPort: 2000, + LocalBindAddress: "10.0.0.1", + MeshGateway: &ConsulMeshGateway{Mode: ""}, + }, cu) + }) +} + +func TestSidecarTask_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil sidecar_task", func(t *testing.T) { + st := (*SidecarTask)(nil) + st.Canonicalize() + require.Nil(t, st) + }) + + t.Run("empty sidecar_task", func(t *testing.T) { + st := new(SidecarTask) + st.Canonicalize() + require.Nil(t, st.Config) + require.Nil(t, st.Env) + require.Equal(t, DefaultResources(), st.Resources) + require.Equal(t, DefaultLogConfig(), st.LogConfig) + require.Nil(t, st.Meta) + require.Equal(t, 5*time.Second, *st.KillTimeout) + require.Equal(t, 0*time.Second, *st.ShutdownDelay) + }) + + t.Run("non empty sidecar_task resources", func(t *testing.T) { + exp := DefaultResources() + exp.MemoryMB = intToPtr(333) + st := &SidecarTask{ + Resources: &Resources{MemoryMB: intToPtr(333)}, + } + st.Canonicalize() + require.Equal(t, exp, st.Resources) + }) +} + +func TestConsulGateway_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + cg := (*ConsulGateway)(nil) + cg.Canonicalize() + require.Nil(t, cg) + }) + + t.Run("set defaults", func(t *testing.T) { + cg := &ConsulGateway{ + Proxy: &ConsulGatewayProxy{ + ConnectTimeout: nil, + EnvoyGatewayBindTaggedAddresses: true, + EnvoyGatewayBindAddresses: make(map[string]*ConsulGatewayBindAddress, 0), + EnvoyGatewayNoDefaultBind: true, + Config: make(map[string]interface{}, 0), + }, + Ingress: &ConsulIngressConfigEntry{ + TLS: &ConsulGatewayTLSConfig{ + Enabled: false, + }, + Listeners: make([]*ConsulIngressListener, 0), + }, + } + cg.Canonicalize() + require.Equal(t, timeToPtr(5*time.Second), cg.Proxy.ConnectTimeout) + require.True(t, cg.Proxy.EnvoyGatewayBindTaggedAddresses) + require.Nil(t, cg.Proxy.EnvoyGatewayBindAddresses) + require.True(t, cg.Proxy.EnvoyGatewayNoDefaultBind) + require.Empty(t, cg.Proxy.EnvoyDNSDiscoveryType) + require.Nil(t, cg.Proxy.Config) + require.Nil(t, cg.Ingress.Listeners) + }) +} + +func TestConsulGateway_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + result := (*ConsulGateway)(nil).Copy() + require.Nil(t, result) + }) + + gateway := &ConsulGateway{ + Proxy: &ConsulGatewayProxy{ + ConnectTimeout: timeToPtr(3 * time.Second), + EnvoyGatewayBindTaggedAddresses: true, + EnvoyGatewayBindAddresses: map[string]*ConsulGatewayBindAddress{ + "listener1": {Address: "10.0.0.1", Port: 2000}, + "listener2": {Address: "10.0.0.1", Port: 2001}, + }, + EnvoyGatewayNoDefaultBind: true, + EnvoyDNSDiscoveryType: "STRICT_DNS", + Config: map[string]interface{}{ + "foo": "bar", + "baz": 3, + }, + }, + Ingress: &ConsulIngressConfigEntry{ + TLS: &ConsulGatewayTLSConfig{ + Enabled: true, + }, + Listeners: []*ConsulIngressListener{{ + Port: 3333, + Protocol: "tcp", + Services: []*ConsulIngressService{{ + Name: "service1", + Hosts: []string{ + "127.0.0.1", "127.0.0.1:3333", + }}, + }}, + }, + }, + Terminating: &ConsulTerminatingConfigEntry{ + Services: []*ConsulLinkedService{{ + Name: "linked-service1", + }}, + }, + } + + t.Run("complete", func(t *testing.T) { + result := gateway.Copy() + require.Equal(t, gateway, result) + }) +} + +func TestConsulIngressConfigEntry_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + c := (*ConsulIngressConfigEntry)(nil) + c.Canonicalize() + require.Nil(t, c) + }) + + t.Run("empty fields", func(t *testing.T) { + c := &ConsulIngressConfigEntry{ + TLS: nil, + Listeners: []*ConsulIngressListener{}, + } + c.Canonicalize() + require.Nil(t, c.TLS) + require.Nil(t, c.Listeners) + }) + + t.Run("complete", func(t *testing.T) { + c := &ConsulIngressConfigEntry{ + TLS: &ConsulGatewayTLSConfig{Enabled: true}, + Listeners: []*ConsulIngressListener{{ + Port: 9090, + Protocol: "http", + Services: []*ConsulIngressService{{ + Name: "service1", + Hosts: []string{"1.1.1.1"}, + }}, + }}, + } + c.Canonicalize() + require.Equal(t, &ConsulIngressConfigEntry{ + TLS: &ConsulGatewayTLSConfig{Enabled: true}, + Listeners: []*ConsulIngressListener{{ + Port: 9090, + Protocol: "http", + Services: []*ConsulIngressService{{ + Name: "service1", + Hosts: []string{"1.1.1.1"}, + }}, + }}, + }, c) + }) +} + +func TestConsulIngressConfigEntry_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + result := (*ConsulIngressConfigEntry)(nil).Copy() + require.Nil(t, result) + }) + + entry := &ConsulIngressConfigEntry{ + TLS: &ConsulGatewayTLSConfig{ + Enabled: true, + }, + Listeners: []*ConsulIngressListener{{ + Port: 1111, + Protocol: "http", + Services: []*ConsulIngressService{{ + Name: "service1", + Hosts: []string{"1.1.1.1", "1.1.1.1:9000"}, + }, { + Name: "service2", + Hosts: []string{"2.2.2.2"}, + }}, + }}, + } + + t.Run("complete", func(t *testing.T) { + result := entry.Copy() + require.Equal(t, entry, result) + }) +} + +func TestConsulTerminatingConfigEntry_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + c := (*ConsulTerminatingConfigEntry)(nil) + c.Canonicalize() + require.Nil(t, c) + }) + + t.Run("empty services", func(t *testing.T) { + c := &ConsulTerminatingConfigEntry{ + Services: []*ConsulLinkedService{}, + } + c.Canonicalize() + require.Nil(t, c.Services) + }) +} + +func TestConsulTerminatingConfigEntry_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + result := (*ConsulIngressConfigEntry)(nil).Copy() + require.Nil(t, result) + }) + + entry := &ConsulTerminatingConfigEntry{ + Services: []*ConsulLinkedService{{ + Name: "servic1", + }, { + Name: "service2", + CAFile: "ca_file.pem", + CertFile: "cert_file.pem", + KeyFile: "key_file.pem", + SNI: "sni.terminating.consul", + }}, + } + + t.Run("complete", func(t *testing.T) { + result := entry.Copy() + require.Equal(t, entry, result) + }) +} + +func TestConsulMeshConfigEntry_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + ce := (*ConsulMeshConfigEntry)(nil) + ce.Canonicalize() + require.Nil(t, ce) + }) + + t.Run("instantiated", func(t *testing.T) { + ce := new(ConsulMeshConfigEntry) + ce.Canonicalize() + require.NotNil(t, ce) + }) +} + +func TestConsulMeshConfigEntry_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + ce := (*ConsulMeshConfigEntry)(nil) + ce2 := ce.Copy() + require.Nil(t, ce2) + }) + + t.Run("instantiated", func(t *testing.T) { + ce := new(ConsulMeshConfigEntry) + ce2 := ce.Copy() + require.NotNil(t, ce2) + }) +} + +func TestConsulMeshGateway_Canonicalize(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + c := (*ConsulMeshGateway)(nil) + c.Canonicalize() + require.Nil(t, c) + }) + + t.Run("unset mode", func(t *testing.T) { + c := &ConsulMeshGateway{Mode: ""} + c.Canonicalize() + require.Equal(t, "", c.Mode) + }) + + t.Run("set mode", func(t *testing.T) { + c := &ConsulMeshGateway{Mode: "remote"} + c.Canonicalize() + require.Equal(t, "remote", c.Mode) + }) +} + +func TestConsulMeshGateway_Copy(t *testing.T) { + testutil.Parallel(t) + + t.Run("nil", func(t *testing.T) { + c := (*ConsulMeshGateway)(nil) + result := c.Copy() + require.Nil(t, result) + }) + + t.Run("instantiated", func(t *testing.T) { + c := &ConsulMeshGateway{ + Mode: "local", + } + result := c.Copy() + require.Equal(t, c, result) + }) +} diff --git a/api/service_registrations.go b/api/service_registrations.go deleted file mode 100644 index ebf1418b41fe..000000000000 --- a/api/service_registrations.go +++ /dev/null @@ -1,129 +0,0 @@ -package api - -import ( - "fmt" - "net/url" -) - -// ServiceRegistrations is used to query the service endpoints. -type ServiceRegistrations struct { - client *Client -} - -// ServiceRegistration is an instance of a single allocation advertising itself -// as a named service with a specific address. Each registration is constructed -// from the job specification Service block. Whether the service is registered -// within Nomad, and therefore generates a ServiceRegistration is controlled by -// the Service.Provider parameter. -type ServiceRegistration struct { - - // ID is the unique identifier for this registration. It currently follows - // the Consul service registration format to provide consistency between - // the two solutions. - ID string - - // ServiceName is the human friendly identifier for this service - // registration. - ServiceName string - - // Namespace represents the namespace within which this service is - // registered. - Namespace string - - // NodeID is Node.ID on which this service registration is currently - // running. - NodeID string - - // Datacenter is the DC identifier of the node as identified by - // Node.Datacenter. - Datacenter string - - // JobID is Job.ID and represents the job which contained the service block - // which resulted in this service registration. - JobID string - - // AllocID is Allocation.ID and represents the allocation within which this - // service is running. - AllocID string - - // Tags are determined from either Service.Tags or Service.CanaryTags and - // help identify this service. Tags can also be used to perform lookups of - // services depending on their state and role. - Tags []string - - // Address is the IP address of this service registration. This information - // comes from the client and is not guaranteed to be routable; this depends - // on cluster network topology. - Address string - - // Port is the port number on which this service registration is bound. It - // is determined by a combination of factors on the client. - Port int - - CreateIndex uint64 - ModifyIndex uint64 -} - -// ServiceRegistrationListStub represents all service registrations held within a -// single namespace. -type ServiceRegistrationListStub struct { - - // Namespace details the namespace in which these services have been - // registered. - Namespace string - - // Services is a list of services found within the namespace. - Services []*ServiceRegistrationStub -} - -// ServiceRegistrationStub is the stub object describing an individual -// namespaced service. The object is built in a manner which would allow us to -// add additional fields in the future, if we wanted. -type ServiceRegistrationStub struct { - - // ServiceName is the human friendly name for this service as specified - // within Service.Name. - ServiceName string - - // Tags is a list of unique tags found for this service. The list is - // de-duplicated automatically by Nomad. - Tags []string -} - -// ServiceRegistrations returns a new handle on the services endpoints. -func (c *Client) ServiceRegistrations() *ServiceRegistrations { - return &ServiceRegistrations{client: c} -} - -// List can be used to list all service registrations currently stored within -// the target namespace. It returns a stub response object. -func (s *ServiceRegistrations) List(q *QueryOptions) ([]*ServiceRegistrationListStub, *QueryMeta, error) { - var resp []*ServiceRegistrationListStub - qm, err := s.client.query("/v1/services", &resp, q) - if err != nil { - return nil, qm, err - } - return resp, qm, nil -} - -// Get is used to return a list of service registrations whose name matches the -// specified parameter. -func (s *ServiceRegistrations) Get(serviceName string, q *QueryOptions) ([]*ServiceRegistration, *QueryMeta, error) { - var resp []*ServiceRegistration - qm, err := s.client.query("/v1/service/"+url.PathEscape(serviceName), &resp, q) - if err != nil { - return nil, qm, err - } - return resp, qm, nil -} - -// Delete can be used to delete an individual service registration as defined -// by its service name and service ID. -func (s *ServiceRegistrations) Delete(serviceName, serviceID string, q *WriteOptions) (*WriteMeta, error) { - path := fmt.Sprintf("/v1/service/%s/%s", url.PathEscape(serviceName), url.PathEscape(serviceID)) - wm, err := s.client.delete(path, nil, q) - if err != nil { - return nil, err - } - return wm, nil -} diff --git a/api/service_registrations_test.go b/api/service_registrations_test.go deleted file mode 100644 index b957e194b432..000000000000 --- a/api/service_registrations_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package api - -import ( - "testing" -) - -func TestServiceRegistrations_List(t *testing.T) { - // TODO(jrasell) add tests once registration process is in place. -} - -func TestServiceRegistrations_Get(t *testing.T) { - // TODO(jrasell) add tests once registration process is in place. -} - -func TestServiceRegistrations_Delete(t *testing.T) { - // TODO(jrasell) add tests once registration process is in place. -} diff --git a/api/services.go b/api/services.go index d927c514ec2e..f531c2964416 100644 --- a/api/services.go +++ b/api/services.go @@ -2,9 +2,134 @@ package api import ( "fmt" + "net/url" "time" ) +// ServiceRegistration is an instance of a single allocation advertising itself +// as a named service with a specific address. Each registration is constructed +// from the job specification Service block. Whether the service is registered +// within Nomad, and therefore generates a ServiceRegistration is controlled by +// the Service.Provider parameter. +type ServiceRegistration struct { + + // ID is the unique identifier for this registration. It currently follows + // the Consul service registration format to provide consistency between + // the two solutions. + ID string + + // ServiceName is the human friendly identifier for this service + // registration. + ServiceName string + + // Namespace represents the namespace within which this service is + // registered. + Namespace string + + // NodeID is Node.ID on which this service registration is currently + // running. + NodeID string + + // Datacenter is the DC identifier of the node as identified by + // Node.Datacenter. + Datacenter string + + // JobID is Job.ID and represents the job which contained the service block + // which resulted in this service registration. + JobID string + + // AllocID is Allocation.ID and represents the allocation within which this + // service is running. + AllocID string + + // Tags are determined from either Service.Tags or Service.CanaryTags and + // help identify this service. Tags can also be used to perform lookups of + // services depending on their state and role. + Tags []string + + // Address is the IP address of this service registration. This information + // comes from the client and is not guaranteed to be routable; this depends + // on cluster network topology. + Address string + + // Port is the port number on which this service registration is bound. It + // is determined by a combination of factors on the client. + Port int + + CreateIndex uint64 + ModifyIndex uint64 +} + +// ServiceRegistrationListStub represents all service registrations held within a +// single namespace. +type ServiceRegistrationListStub struct { + + // Namespace details the namespace in which these services have been + // registered. + Namespace string + + // Services is a list of services found within the namespace. + Services []*ServiceRegistrationStub +} + +// ServiceRegistrationStub is the stub object describing an individual +// namespaced service. The object is built in a manner which would allow us to +// add additional fields in the future, if we wanted. +type ServiceRegistrationStub struct { + + // ServiceName is the human friendly name for this service as specified + // within Service.Name. + ServiceName string + + // Tags is a list of unique tags found for this service. The list is + // de-duplicated automatically by Nomad. + Tags []string +} + + +// Services is used to query the service endpoints. +type Services struct { + client *Client +} + +// Services returns a new handle on the services endpoints. +func (c *Client) Services() *Services { + return &Services{client: c} +} + +// List can be used to list all service registrations currently stored within +// the target namespace. It returns a stub response object. +func (s *Services) List(q *QueryOptions) ([]*ServiceRegistrationListStub, *QueryMeta, error) { + var resp []*ServiceRegistrationListStub + qm, err := s.client.query("/v1/services", &resp, q) + if err != nil { + return nil, qm, err + } + return resp, qm, nil +} + +// Get is used to return a list of service registrations whose name matches the +// specified parameter. +func (s *Services) Get(serviceName string, q *QueryOptions) ([]*ServiceRegistration, *QueryMeta, error) { + var resp []*ServiceRegistration + qm, err := s.client.query("/v1/service/"+url.PathEscape(serviceName), &resp, q) + if err != nil { + return nil, qm, err + } + return resp, qm, nil +} + +// Delete can be used to delete an individual service registration as defined +// by its service name and service ID. +func (s *Services) Delete(serviceName, serviceID string, q *WriteOptions) (*WriteMeta, error) { + path := fmt.Sprintf("/v1/service/%s/%s", url.PathEscape(serviceName), url.PathEscape(serviceID)) + wm, err := s.client.delete(path, nil, q) + if err != nil { + return nil, err + } + return wm, nil +} + // CheckRestart describes if and when a task should be restarted based on // failing health checks. type CheckRestart struct { @@ -181,557 +306,3 @@ func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) { } } } - -// ConsulConnect represents a Consul Connect jobspec stanza. -type ConsulConnect struct { - Native bool `hcl:"native,optional"` - Gateway *ConsulGateway `hcl:"gateway,block"` - SidecarService *ConsulSidecarService `mapstructure:"sidecar_service" hcl:"sidecar_service,block"` - SidecarTask *SidecarTask `mapstructure:"sidecar_task" hcl:"sidecar_task,block"` -} - -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 `hcl:"tags,optional"` - Port string `hcl:"port,optional"` - Proxy *ConsulProxy `hcl:"proxy,block"` - DisableDefaultTCPCheck bool `mapstructure:"disable_default_tcp_check" hcl:"disable_default_tcp_check,optional"` -} - -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 `hcl:"name,optional"` - Driver string `hcl:"driver,optional"` - User string `hcl:"user,optional"` - Config map[string]interface{} `hcl:"config,block"` - Env map[string]string `hcl:"env,block"` - Resources *Resources `hcl:"resources,block"` - Meta map[string]string `hcl:"meta,block"` - KillTimeout *time.Duration `mapstructure:"kill_timeout" hcl:"kill_timeout,optional"` - LogConfig *LogConfig `mapstructure:"logs" hcl:"logs,block"` - ShutdownDelay *time.Duration `mapstructure:"shutdown_delay" hcl:"shutdown_delay,optional"` - KillSignal string `mapstructure:"kill_signal" hcl:"kill_signal,optional"` -} - -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" hcl:"local_service_address,optional"` - LocalServicePort int `mapstructure:"local_service_port" hcl:"local_service_port,optional"` - ExposeConfig *ConsulExposeConfig `mapstructure:"expose" hcl:"expose,block"` - Upstreams []*ConsulUpstream `hcl:"upstreams,block"` - Config map[string]interface{} `hcl:"config,block"` -} - -func (cp *ConsulProxy) Canonicalize() { - if cp == nil { - return - } - - cp.ExposeConfig.Canonicalize() - - if len(cp.Upstreams) == 0 { - cp.Upstreams = nil - } - - for _, upstream := range cp.Upstreams { - upstream.Canonicalize() - } - - if len(cp.Config) == 0 { - cp.Config = nil - } -} - -// ConsulMeshGateway is used to configure mesh gateway usage when connecting to -// a connect upstream in another datacenter. -type ConsulMeshGateway struct { - // Mode configures how an upstream should be accessed with regard to using - // mesh gateways. - // - // local - the connect proxy makes outbound connections through mesh gateway - // originating in the same datacenter. - // - // remote - the connect proxy makes outbound connections to a mesh gateway - // in the destination datacenter. - // - // none (default) - no mesh gateway is used, the proxy makes outbound connections - // directly to destination services. - // - // https://www.consul.io/docs/connect/gateways/mesh-gateway#modes-of-operation - Mode string `mapstructure:"mode" hcl:"mode,optional"` -} - -func (c *ConsulMeshGateway) Canonicalize() { - // Mode may be empty string, indicating behavior will defer to Consul - // service-defaults config entry. - return -} - -func (c *ConsulMeshGateway) Copy() *ConsulMeshGateway { - if c == nil { - return nil - } - - return &ConsulMeshGateway{ - Mode: c.Mode, - } -} - -// ConsulUpstream represents a Consul Connect upstream jobspec stanza. -type ConsulUpstream struct { - DestinationName string `mapstructure:"destination_name" hcl:"destination_name,optional"` - LocalBindPort int `mapstructure:"local_bind_port" hcl:"local_bind_port,optional"` - Datacenter string `mapstructure:"datacenter" hcl:"datacenter,optional"` - LocalBindAddress string `mapstructure:"local_bind_address" hcl:"local_bind_address,optional"` - MeshGateway *ConsulMeshGateway `mapstructure:"mesh_gateway" hcl:"mesh_gateway,block"` -} - -func (cu *ConsulUpstream) Copy() *ConsulUpstream { - if cu == nil { - return nil - } - return &ConsulUpstream{ - DestinationName: cu.DestinationName, - LocalBindPort: cu.LocalBindPort, - Datacenter: cu.Datacenter, - LocalBindAddress: cu.LocalBindAddress, - MeshGateway: cu.MeshGateway.Copy(), - } -} - -func (cu *ConsulUpstream) Canonicalize() { - if cu == nil { - return - } - cu.MeshGateway.Canonicalize() -} - -type ConsulExposeConfig struct { - Path []*ConsulExposePath `mapstructure:"path" hcl:"path,block"` -} - -func (cec *ConsulExposeConfig) Canonicalize() { - if cec == nil { - return - } - - if len(cec.Path) == 0 { - cec.Path = nil - } -} - -type ConsulExposePath struct { - Path string `hcl:"path,optional"` - Protocol string `hcl:"protocol,optional"` - LocalPathPort int `mapstructure:"local_path_port" hcl:"local_path_port,optional"` - ListenerPort string `mapstructure:"listener_port" hcl:"listener_port,optional"` -} - -// 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 `hcl:"proxy,block"` - - // Ingress represents the Consul Configuration Entry for an Ingress Gateway. - Ingress *ConsulIngressConfigEntry `hcl:"ingress,block"` - - // Terminating represents the Consul Configuration Entry for a Terminating Gateway. - Terminating *ConsulTerminatingConfigEntry `hcl:"terminating,block"` - - // Mesh indicates the Consul service should be a Mesh Gateway. - Mesh *ConsulMeshConfigEntry `hcl:"mesh,block"` -} - -func (g *ConsulGateway) Canonicalize() { - if g == nil { - return - } - g.Proxy.Canonicalize() - g.Ingress.Canonicalize() - g.Terminating.Canonicalize() -} - -func (g *ConsulGateway) Copy() *ConsulGateway { - if g == nil { - return nil - } - - return &ConsulGateway{ - Proxy: g.Proxy.Copy(), - Ingress: g.Ingress.Copy(), - Terminating: g.Terminating.Copy(), - } -} - -type ConsulGatewayBindAddress struct { - Name string `hcl:",label"` - Address string `mapstructure:"address" hcl:"address,optional"` - Port int `mapstructure:"port" hcl:"port,optional"` -} - -var ( - // defaultGatewayConnectTimeout is the default amount of time connections to - // upstreams are allowed before timing out. - 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" hcl:"connect_timeout,optional"` - EnvoyGatewayBindTaggedAddresses bool `mapstructure:"envoy_gateway_bind_tagged_addresses" hcl:"envoy_gateway_bind_tagged_addresses,optional"` - EnvoyGatewayBindAddresses map[string]*ConsulGatewayBindAddress `mapstructure:"envoy_gateway_bind_addresses" hcl:"envoy_gateway_bind_addresses,block"` - EnvoyGatewayNoDefaultBind bool `mapstructure:"envoy_gateway_no_default_bind" hcl:"envoy_gateway_no_default_bind,optional"` - EnvoyDNSDiscoveryType string `mapstructure:"envoy_dns_discovery_type" hcl:"envoy_dns_discovery_type,optional"` - Config map[string]interface{} `hcl:"config,block"` // 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 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 `hcl:"enabled,optional"` -} - -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 `hcl:"name,optional"` - - Hosts []string `hcl:"hosts,optional"` -} - -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 `hcl:"port,optional"` - Protocol string `hcl:"protocol,optional"` - Services []*ConsulIngressService `hcl:"service,block"` -} - -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 `hcl:"tls,block"` - Listeners []*ConsulIngressListener `hcl:"listener,block"` -} - -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, - } -} - -type ConsulLinkedService struct { - Name string `hcl:"name,optional"` - CAFile string `hcl:"ca_file,optional" mapstructure:"ca_file"` - CertFile string `hcl:"cert_file,optional" mapstructure:"cert_file"` - KeyFile string `hcl:"key_file,optional" mapstructure:"key_file"` - SNI string `hcl:"sni,optional"` -} - -func (s *ConsulLinkedService) Canonicalize() { - // nothing to do for now -} - -func (s *ConsulLinkedService) Copy() *ConsulLinkedService { - if s == nil { - return nil - } - - return &ConsulLinkedService{ - Name: s.Name, - CAFile: s.CAFile, - CertFile: s.CertFile, - KeyFile: s.KeyFile, - SNI: s.SNI, - } -} - -// ConsulTerminatingConfigEntry represents the Consul Configuration Entry type -// for a Terminating Gateway. -// -// https://www.consul.io/docs/agent/config-entries/terminating-gateway#available-fields -type ConsulTerminatingConfigEntry struct { - // Namespace is not yet supported. - // Namespace string - - Services []*ConsulLinkedService `hcl:"service,block"` -} - -func (e *ConsulTerminatingConfigEntry) Canonicalize() { - if e == nil { - return - } - - if len(e.Services) == 0 { - e.Services = nil - } - - for _, service := range e.Services { - service.Canonicalize() - } -} - -func (e *ConsulTerminatingConfigEntry) Copy() *ConsulTerminatingConfigEntry { - if e == nil { - return nil - } - - var services []*ConsulLinkedService = nil - if n := len(e.Services); n > 0 { - services = make([]*ConsulLinkedService, n) - for i := 0; i < n; i++ { - services[i] = e.Services[i].Copy() - } - } - - return &ConsulTerminatingConfigEntry{ - Services: services, - } -} - -// ConsulMeshConfigEntry is a stub used to represent that the gateway service type -// should be for a Mesh Gateway. Unlike Ingress and Terminating, there is no -// actual Consul Config Entry type for mesh-gateway, at least for now. We still -// create a type for future proofing, instead just using a bool for example. -type ConsulMeshConfigEntry struct { - // nothing in here -} - -func (e *ConsulMeshConfigEntry) Canonicalize() { - return -} - -func (e *ConsulMeshConfigEntry) Copy() *ConsulMeshConfigEntry { - if e == nil { - return nil - } - return new(ConsulMeshConfigEntry) -} diff --git a/api/services_test.go b/api/services_test.go index 02b606eb02af..c9ef884e6974 100644 --- a/api/services_test.go +++ b/api/services_test.go @@ -9,6 +9,19 @@ import ( "github.com/stretchr/testify/require" ) +func TestServiceRegistrations_List(t *testing.T) { + // TODO(jrasell) add tests once registration process is in place. +} + +func TestServiceRegistrations_Get(t *testing.T) { + // TODO(jrasell) add tests once registration process is in place. +} + +func TestServiceRegistrations_Delete(t *testing.T) { + // TODO(jrasell) add tests once registration process is in place. +} + + func TestService_Canonicalize(t *testing.T) { testutil.Parallel(t) @@ -128,146 +141,6 @@ func TestService_CheckRestart(t *testing.T) { require.True(t, service.Checks[2].CheckRestart.IgnoreWarnings) } -func TestService_Connect_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil connect", func(t *testing.T) { - cc := (*ConsulConnect)(nil) - cc.Canonicalize() - require.Nil(t, cc) - }) - - t.Run("empty connect", func(t *testing.T) { - cc := new(ConsulConnect) - cc.Canonicalize() - require.Empty(t, cc.Native) - require.Nil(t, cc.SidecarService) - require.Nil(t, cc.SidecarTask) - }) -} - -func TestService_Connect_ConsulSidecarService_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil sidecar_service", func(t *testing.T) { - css := (*ConsulSidecarService)(nil) - css.Canonicalize() - require.Nil(t, css) - }) - - t.Run("empty sidecar_service", func(t *testing.T) { - css := new(ConsulSidecarService) - css.Canonicalize() - require.Empty(t, css.Tags) - require.Nil(t, css.Proxy) - }) - - t.Run("non-empty sidecar_service", func(t *testing.T) { - css := &ConsulSidecarService{ - Tags: make([]string, 0), - Port: "port", - Proxy: &ConsulProxy{ - LocalServiceAddress: "lsa", - LocalServicePort: 80, - }, - } - css.Canonicalize() - require.Equal(t, &ConsulSidecarService{ - Tags: nil, - Port: "port", - Proxy: &ConsulProxy{ - LocalServiceAddress: "lsa", - LocalServicePort: 80}, - }, css) - }) -} - -func TestService_Connect_ConsulProxy_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil proxy", func(t *testing.T) { - cp := (*ConsulProxy)(nil) - cp.Canonicalize() - require.Nil(t, cp) - }) - - t.Run("empty proxy", func(t *testing.T) { - cp := new(ConsulProxy) - cp.Canonicalize() - require.Empty(t, cp.LocalServiceAddress) - require.Zero(t, cp.LocalServicePort) - require.Nil(t, cp.ExposeConfig) - require.Nil(t, cp.Upstreams) - require.Empty(t, cp.Config) - }) - - t.Run("non empty proxy", func(t *testing.T) { - cp := &ConsulProxy{ - LocalServiceAddress: "127.0.0.1", - LocalServicePort: 80, - ExposeConfig: new(ConsulExposeConfig), - Upstreams: make([]*ConsulUpstream, 0), - Config: make(map[string]interface{}), - } - cp.Canonicalize() - require.Equal(t, "127.0.0.1", cp.LocalServiceAddress) - require.Equal(t, 80, cp.LocalServicePort) - require.Equal(t, &ConsulExposeConfig{}, cp.ExposeConfig) - require.Nil(t, cp.Upstreams) - require.Nil(t, cp.Config) - }) -} - -func TestService_Connect_ConsulUpstream_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil upstream", func(t *testing.T) { - cu := (*ConsulUpstream)(nil) - result := cu.Copy() - require.Nil(t, result) - }) - - t.Run("complete upstream", func(t *testing.T) { - cu := &ConsulUpstream{ - DestinationName: "dest1", - Datacenter: "dc2", - LocalBindPort: 2000, - LocalBindAddress: "10.0.0.1", - MeshGateway: &ConsulMeshGateway{Mode: "remote"}, - } - result := cu.Copy() - require.Equal(t, cu, result) - }) -} - -func TestService_Connect_ConsulUpstream_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil upstream", func(t *testing.T) { - cu := (*ConsulUpstream)(nil) - cu.Canonicalize() - require.Nil(t, cu) - }) - - t.Run("complete", func(t *testing.T) { - cu := &ConsulUpstream{ - DestinationName: "dest1", - Datacenter: "dc2", - LocalBindPort: 2000, - LocalBindAddress: "10.0.0.1", - MeshGateway: &ConsulMeshGateway{Mode: ""}, - } - cu.Canonicalize() - require.Equal(t, &ConsulUpstream{ - DestinationName: "dest1", - Datacenter: "dc2", - LocalBindPort: 2000, - LocalBindAddress: "10.0.0.1", - MeshGateway: &ConsulMeshGateway{Mode: ""}, - }, cu) - }) -} - func TestService_Connect_proxy_settings(t *testing.T) { testutil.Parallel(t) @@ -319,316 +192,4 @@ func TestService_Tags(t *testing.T) { r.True(service.EnableTagOverride) r.Equal([]string{"a", "b"}, service.Tags) r.Equal([]string{"c", "d"}, service.CanaryTags) -} - -func TestService_Connect_SidecarTask_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil sidecar_task", func(t *testing.T) { - st := (*SidecarTask)(nil) - st.Canonicalize() - require.Nil(t, st) - }) - - t.Run("empty sidecar_task", func(t *testing.T) { - st := new(SidecarTask) - st.Canonicalize() - require.Nil(t, st.Config) - require.Nil(t, st.Env) - require.Equal(t, DefaultResources(), st.Resources) - require.Equal(t, DefaultLogConfig(), st.LogConfig) - require.Nil(t, st.Meta) - require.Equal(t, 5*time.Second, *st.KillTimeout) - require.Equal(t, 0*time.Second, *st.ShutdownDelay) - }) - - t.Run("non empty sidecar_task resources", func(t *testing.T) { - exp := DefaultResources() - exp.MemoryMB = intToPtr(333) - st := &SidecarTask{ - Resources: &Resources{MemoryMB: intToPtr(333)}, - } - st.Canonicalize() - require.Equal(t, exp, st.Resources) - }) -} - -func TestService_ConsulGateway_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - cg := (*ConsulGateway)(nil) - cg.Canonicalize() - require.Nil(t, cg) - }) - - t.Run("set defaults", func(t *testing.T) { - cg := &ConsulGateway{ - Proxy: &ConsulGatewayProxy{ - ConnectTimeout: nil, - EnvoyGatewayBindTaggedAddresses: true, - EnvoyGatewayBindAddresses: make(map[string]*ConsulGatewayBindAddress, 0), - EnvoyGatewayNoDefaultBind: true, - Config: make(map[string]interface{}, 0), - }, - Ingress: &ConsulIngressConfigEntry{ - TLS: &ConsulGatewayTLSConfig{ - Enabled: false, - }, - Listeners: make([]*ConsulIngressListener, 0), - }, - } - cg.Canonicalize() - require.Equal(t, timeToPtr(5*time.Second), cg.Proxy.ConnectTimeout) - require.True(t, cg.Proxy.EnvoyGatewayBindTaggedAddresses) - require.Nil(t, cg.Proxy.EnvoyGatewayBindAddresses) - require.True(t, cg.Proxy.EnvoyGatewayNoDefaultBind) - require.Empty(t, cg.Proxy.EnvoyDNSDiscoveryType) - require.Nil(t, cg.Proxy.Config) - require.Nil(t, cg.Ingress.Listeners) - }) -} - -func TestService_ConsulGateway_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - result := (*ConsulGateway)(nil).Copy() - require.Nil(t, result) - }) - - gateway := &ConsulGateway{ - Proxy: &ConsulGatewayProxy{ - ConnectTimeout: timeToPtr(3 * time.Second), - EnvoyGatewayBindTaggedAddresses: true, - EnvoyGatewayBindAddresses: map[string]*ConsulGatewayBindAddress{ - "listener1": {Address: "10.0.0.1", Port: 2000}, - "listener2": {Address: "10.0.0.1", Port: 2001}, - }, - EnvoyGatewayNoDefaultBind: true, - EnvoyDNSDiscoveryType: "STRICT_DNS", - Config: map[string]interface{}{ - "foo": "bar", - "baz": 3, - }, - }, - Ingress: &ConsulIngressConfigEntry{ - TLS: &ConsulGatewayTLSConfig{ - Enabled: true, - }, - Listeners: []*ConsulIngressListener{{ - Port: 3333, - Protocol: "tcp", - Services: []*ConsulIngressService{{ - Name: "service1", - Hosts: []string{ - "127.0.0.1", "127.0.0.1:3333", - }}, - }}, - }, - }, - Terminating: &ConsulTerminatingConfigEntry{ - Services: []*ConsulLinkedService{{ - Name: "linked-service1", - }}, - }, - } - - t.Run("complete", func(t *testing.T) { - result := gateway.Copy() - require.Equal(t, gateway, result) - }) -} - -func TestService_ConsulIngressConfigEntry_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - c := (*ConsulIngressConfigEntry)(nil) - c.Canonicalize() - require.Nil(t, c) - }) - - t.Run("empty fields", func(t *testing.T) { - c := &ConsulIngressConfigEntry{ - TLS: nil, - Listeners: []*ConsulIngressListener{}, - } - c.Canonicalize() - require.Nil(t, c.TLS) - require.Nil(t, c.Listeners) - }) - - t.Run("complete", func(t *testing.T) { - c := &ConsulIngressConfigEntry{ - TLS: &ConsulGatewayTLSConfig{Enabled: true}, - Listeners: []*ConsulIngressListener{{ - Port: 9090, - Protocol: "http", - Services: []*ConsulIngressService{{ - Name: "service1", - Hosts: []string{"1.1.1.1"}, - }}, - }}, - } - c.Canonicalize() - require.Equal(t, &ConsulIngressConfigEntry{ - TLS: &ConsulGatewayTLSConfig{Enabled: true}, - Listeners: []*ConsulIngressListener{{ - Port: 9090, - Protocol: "http", - Services: []*ConsulIngressService{{ - Name: "service1", - Hosts: []string{"1.1.1.1"}, - }}, - }}, - }, c) - }) -} - -func TestService_ConsulIngressConfigEntry_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - result := (*ConsulIngressConfigEntry)(nil).Copy() - require.Nil(t, result) - }) - - entry := &ConsulIngressConfigEntry{ - TLS: &ConsulGatewayTLSConfig{ - Enabled: true, - }, - Listeners: []*ConsulIngressListener{{ - Port: 1111, - Protocol: "http", - Services: []*ConsulIngressService{{ - Name: "service1", - Hosts: []string{"1.1.1.1", "1.1.1.1:9000"}, - }, { - Name: "service2", - Hosts: []string{"2.2.2.2"}, - }}, - }}, - } - - t.Run("complete", func(t *testing.T) { - result := entry.Copy() - require.Equal(t, entry, result) - }) -} - -func TestService_ConsulTerminatingConfigEntry_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - c := (*ConsulTerminatingConfigEntry)(nil) - c.Canonicalize() - require.Nil(t, c) - }) - - t.Run("empty services", func(t *testing.T) { - c := &ConsulTerminatingConfigEntry{ - Services: []*ConsulLinkedService{}, - } - c.Canonicalize() - require.Nil(t, c.Services) - }) -} - -func TestService_ConsulTerminatingConfigEntry_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - result := (*ConsulIngressConfigEntry)(nil).Copy() - require.Nil(t, result) - }) - - entry := &ConsulTerminatingConfigEntry{ - Services: []*ConsulLinkedService{{ - Name: "servic1", - }, { - Name: "service2", - CAFile: "ca_file.pem", - CertFile: "cert_file.pem", - KeyFile: "key_file.pem", - SNI: "sni.terminating.consul", - }}, - } - - t.Run("complete", func(t *testing.T) { - result := entry.Copy() - require.Equal(t, entry, result) - }) -} - -func TestService_ConsulMeshConfigEntry_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - ce := (*ConsulMeshConfigEntry)(nil) - ce.Canonicalize() - require.Nil(t, ce) - }) - - t.Run("instantiated", func(t *testing.T) { - ce := new(ConsulMeshConfigEntry) - ce.Canonicalize() - require.NotNil(t, ce) - }) -} - -func TestService_ConsulMeshConfigEntry_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - ce := (*ConsulMeshConfigEntry)(nil) - ce2 := ce.Copy() - require.Nil(t, ce2) - }) - - t.Run("instantiated", func(t *testing.T) { - ce := new(ConsulMeshConfigEntry) - ce2 := ce.Copy() - require.NotNil(t, ce2) - }) -} - -func TestService_ConsulMeshGateway_Canonicalize(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - c := (*ConsulMeshGateway)(nil) - c.Canonicalize() - require.Nil(t, c) - }) - - t.Run("unset mode", func(t *testing.T) { - c := &ConsulMeshGateway{Mode: ""} - c.Canonicalize() - require.Equal(t, "", c.Mode) - }) - - t.Run("set mode", func(t *testing.T) { - c := &ConsulMeshGateway{Mode: "remote"} - c.Canonicalize() - require.Equal(t, "remote", c.Mode) - }) -} - -func TestService_ConsulMeshGateway_Copy(t *testing.T) { - testutil.Parallel(t) - - t.Run("nil", func(t *testing.T) { - c := (*ConsulMeshGateway)(nil) - result := c.Copy() - require.Nil(t, result) - }) - - t.Run("instantiated", func(t *testing.T) { - c := &ConsulMeshGateway{ - Mode: "local", - } - result := c.Copy() - require.Equal(t, c, result) - }) -} +} \ No newline at end of file diff --git a/command/service_delete.go b/command/service_delete.go index 8971a05b02c9..0c1570a0adc1 100644 --- a/command/service_delete.go +++ b/command/service_delete.go @@ -58,7 +58,7 @@ func (s *ServiceDeleteCommand) Run(args []string) int { return 1 } - if _, err := client.ServiceRegistrations().Delete(args[0], args[1], nil); err != nil { + if _, err := client.Services().Delete(args[0], args[1], nil); err != nil { s.Ui.Error(fmt.Sprintf("Error deleting service registration: %s", err)) return 1 } diff --git a/command/service_delete_test.go b/command/service_delete_test.go index 6e17d5f14924..b1862fb6a6e0 100644 --- a/command/service_delete_test.go +++ b/command/service_delete_test.go @@ -61,7 +61,7 @@ func TestServiceDeleteCommand_Run(t *testing.T) { require.Equal(t, 0, registerCode) // Detail the service as we need the ID. - serviceList, _, err := client.ServiceRegistrations().Get("service-discovery-nomad-delete", nil) + serviceList, _, err := client.Services().Get("service-discovery-nomad-delete", nil) require.NoError(t, err) require.Len(t, serviceList, 1) diff --git a/command/service_info.go b/command/service_info.go index 2f54e5edab44..95f413c362fe 100644 --- a/command/service_info.go +++ b/command/service_info.go @@ -92,7 +92,7 @@ func (s *ServiceInfoCommand) Run(args []string) int { return 1 } - serviceInfo, _, err := client.ServiceRegistrations().Get(args[0], nil) + serviceInfo, _, err := client.Services().Get(args[0], nil) if err != nil { s.Ui.Error(fmt.Sprintf("Error listing service registrations: %s", err)) return 1 diff --git a/command/service_list.go b/command/service_list.go index 8e132494f6db..b64d56e30424 100644 --- a/command/service_list.go +++ b/command/service_list.go @@ -89,7 +89,7 @@ func (s *ServiceListCommand) Run(args []string) int { return 1 } - list, _, err := client.ServiceRegistrations().List(nil) + list, _, err := client.Services().List(nil) if err != nil { s.Ui.Error(fmt.Sprintf("Error listing service registrations: %s", err)) return 1