diff --git a/api/services.go b/api/services.go index d371148a926..9a48ea06c47 100644 --- a/api/services.go +++ b/api/services.go @@ -168,8 +168,9 @@ type SidecarTask struct { // ConsulProxy represents a Consul Connect sidecar proxy jobspec stanza. type ConsulProxy struct { - LocalServiceAddress string `mapstructure:"local_service_address"` - LocalServicePort int `mapstructure:"local_service_port"` + LocalServiceAddress string `mapstructure:"local_service_address"` + LocalServicePort int `mapstructure:"local_service_port"` + ExposeConfig *ConsulExposeConfig `mapstructure:"expose"` Upstreams []*ConsulUpstream Config map[string]interface{} } @@ -179,3 +180,15 @@ type ConsulUpstream struct { DestinationName string `mapstructure:"destination_name"` LocalBindPort int `mapstructure:"local_bind_port"` } + +type ConsulExposeConfig struct { + Paths []*ConsulExposePath `mapstructure:"paths"` + // todo(shoenig): add magic for 'checks' option +} + +type ConsulExposePath struct { + Path string + Protocol string + LocalPathPort int `mapstructure:"local_path_port"` + ListenerPort string `mapstructure:"listener_port"` +} diff --git a/client/allocrunner/networking_bridge_linux.go b/client/allocrunner/networking_bridge_linux.go index 4bb17cb63f5..75e9612d1fe 100644 --- a/client/allocrunner/networking_bridge_linux.go +++ b/client/allocrunner/networking_bridge_linux.go @@ -86,8 +86,6 @@ func newBridgeNetworkConfigurator(log hclog.Logger, bridgeName, ipRange, cniPath // ensureForwardingRules ensures that a forwarding rule is added to iptables // to allow traffic inbound to the bridge network -// // ensureForwardingRules ensures that a forwarding rule is added to iptables -// to allow traffic inbound to the bridge network func (b *bridgeNetworkConfigurator) ensureForwardingRules() error { ipt, err := iptables.New() if err != nil { @@ -154,9 +152,9 @@ func (b *bridgeNetworkConfigurator) Setup(ctx context.Context, alloc *structs.Al return err } - // Depending on the version of bridge cni plugin used, a known race could occure + // Depending on the version of bridge cni plugin (< 0.8.4) a known race could occur // where two alloc attempt to create the nomad bridge at the same time, resulting - // in one of them to fail. This rety attempts to overcome any + // in one of them to fail. This retry attempts to overcome those erroneous failures. const retry = 3 for attempt := 1; ; attempt++ { //TODO eventually returning the IP from the result would be nice to have in the alloc diff --git a/command/agent/agent.go b/command/agent/agent.go index 42f21322f2d..572d1914c4b 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -587,7 +587,7 @@ func convertClientConfig(agentConfig *Config) (*clientconfig.Config, error) { conf.ACLTokenTTL = agentConfig.ACL.TokenTTL conf.ACLPolicyTTL = agentConfig.ACL.PolicyTTL - // Setup networking configration + // Setup networking configuration conf.CNIPath = agentConfig.Client.CNIPath conf.BridgeNetworkName = agentConfig.Client.BridgeNetworkName conf.BridgeNetworkAllocSubnet = agentConfig.Client.BridgeNetworkSubnet diff --git a/command/agent/consul/client.go b/command/agent/consul/client.go index e98152a3fa9..abf58f26cdf 100644 --- a/command/agent/consul/client.go +++ b/command/agent/consul/client.go @@ -1280,7 +1280,7 @@ func MakeCheckID(serviceID string, check *structs.ServiceCheck) string { // createCheckReg creates a Check that can be registered with Consul. // // Script checks simply have a TTL set and the caller is responsible for -// running the script and heartbeating. +// running the script and heart-beating. func createCheckReg(serviceID, checkID string, check *structs.ServiceCheck, host string, port int) (*api.AgentCheckRegistration, error) { chkReg := api.AgentCheckRegistration{ ID: checkID, @@ -1313,8 +1313,8 @@ func createCheckReg(serviceID, checkID string, check *structs.ServiceCheck, host if err != nil { return nil, err } - url := base.ResolveReference(relative) - chkReg.HTTP = url.String() + checkURL := base.ResolveReference(relative) + chkReg.HTTP = checkURL.String() chkReg.Method = check.Method chkReg.Header = check.Header @@ -1471,90 +1471,3 @@ func getAddress(addrMode, portLabel string, networks structs.Networks, driverNet return "", 0, fmt.Errorf("invalid address mode %q", addrMode) } } - -// newConnect creates a new Consul AgentServiceConnect struct based on a Nomad -// Connect struct. If the nomad Connect struct is nil, nil will be returned to -// disable Connect for this service. -func newConnect(serviceName string, nc *structs.ConsulConnect, networks structs.Networks) (*api.AgentServiceConnect, error) { - if nc == nil { - // No Connect stanza, returning nil is fine - return nil, nil - } - - cc := &api.AgentServiceConnect{ - Native: nc.Native, - } - - if nc.SidecarService == nil { - return cc, nil - } - - net, port, err := getConnectPort(serviceName, networks) - if err != nil { - return nil, err - } - - // Bind to netns IP(s):port - proxyConfig := map[string]interface{}{} - localServiceAddress := "" - localServicePort := 0 - if nc.SidecarService.Proxy != nil { - localServiceAddress = nc.SidecarService.Proxy.LocalServiceAddress - localServicePort = nc.SidecarService.Proxy.LocalServicePort - if nc.SidecarService.Proxy.Config != nil { - proxyConfig = nc.SidecarService.Proxy.Config - } - } - proxyConfig["bind_address"] = "0.0.0.0" - proxyConfig["bind_port"] = port.To - - // Advertise host IP:port - cc.SidecarService = &api.AgentServiceRegistration{ - Tags: helper.CopySliceString(nc.SidecarService.Tags), - Address: net.IP, - Port: port.Value, - - // Automatically configure the proxy to bind to all addresses - // within the netns. - Proxy: &api.AgentServiceConnectProxyConfig{ - LocalServiceAddress: localServiceAddress, - LocalServicePort: localServicePort, - Config: proxyConfig, - }, - } - - // If no further proxy settings were explicitly configured, exit early - if nc.SidecarService.Proxy == nil { - return cc, nil - } - - numUpstreams := len(nc.SidecarService.Proxy.Upstreams) - if numUpstreams == 0 { - return cc, nil - } - - upstreams := make([]api.Upstream, numUpstreams) - for i, nu := range nc.SidecarService.Proxy.Upstreams { - upstreams[i].DestinationName = nu.DestinationName - upstreams[i].LocalBindPort = nu.LocalBindPort - } - cc.SidecarService.Proxy.Upstreams = upstreams - - return cc, nil -} - -// getConnectPort returns the network and port for the Connect proxy sidecar -// defined for this service. An error is returned if the network and port -// cannot be determined. -func getConnectPort(serviceName string, networks structs.Networks) (*structs.NetworkResource, structs.Port, error) { - if n := len(networks); n != 1 { - return nil, structs.Port{}, fmt.Errorf("Connect only supported with exactly 1 network (found %d)", n) - } - - port, ok := networks[0].PortForService(serviceName) - if !ok { - return nil, structs.Port{}, fmt.Errorf("No Connect port defined for service %q", serviceName) - } - - return networks[0], port, nil -} diff --git a/command/agent/consul/connect.go b/command/agent/consul/connect.go new file mode 100644 index 00000000000..5adba80fa49 --- /dev/null +++ b/command/agent/consul/connect.go @@ -0,0 +1,176 @@ +package consul + +import ( + "fmt" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/nomad/helper" + "github.com/hashicorp/nomad/nomad/structs" +) + +// newConnect creates a new Consul AgentServiceConnect struct based on a Nomad +// Connect struct. If the nomad Connect struct is nil, nil will be returned to +// disable Connect for this service. +func newConnect(serviceName string, nc *structs.ConsulConnect, networks structs.Networks) (*api.AgentServiceConnect, error) { + if nc == nil { + // no connect stanza means there is no connect service to register + return nil, nil + } + + if nc.Native { + return &api.AgentServiceConnect{Native: true}, nil + } + + sidecarReg, err := connectSidecarRegistration(serviceName, nc.SidecarService, networks) + if err != nil { + return nil, err + } + + return &api.AgentServiceConnect{ + Native: false, + SidecarService: sidecarReg, + }, nil +} + +func connectSidecarRegistration(serviceName string, css *structs.ConsulSidecarService, networks structs.Networks) (*api.AgentServiceRegistration, error) { + if css == nil { + // no sidecar stanza means there is no sidecar service to register + return nil, nil + } + + cNet, cPort, err := connectPort(serviceName, networks) + if err != nil { + return nil, err + } + + proxy, err := connectProxy(css.Proxy, cPort.To, networks) + if err != nil { + return nil, err + } + + return &api.AgentServiceRegistration{ + Tags: helper.CopySliceString(css.Tags), + Port: cPort.Value, + Address: cNet.IP, + Proxy: proxy, + }, nil +} + +func connectProxy(proxy *structs.ConsulProxy, cPort int, networks structs.Networks) (*api.AgentServiceConnectProxyConfig, error) { + if proxy == nil { + return nil, nil + } + + expose, err := connectProxyExpose(proxy.Expose, networks) + if err != nil { + return nil, err + } + + return &api.AgentServiceConnectProxyConfig{ + LocalServiceAddress: proxy.LocalServiceAddress, + LocalServicePort: proxy.LocalServicePort, + Config: connectProxyConfig(proxy.Config, cPort), + Upstreams: connectUpstreams(proxy.Upstreams), + Expose: expose, + }, nil +} + +func connectProxyExpose(expose *structs.ConsulExposeConfig, networks structs.Networks) (api.ExposeConfig, error) { + if expose == nil { + return api.ExposeConfig{}, nil + } + + paths, err := connectProxyExposePaths(expose.Paths, networks) + if err != nil { + return api.ExposeConfig{}, err + } + + return api.ExposeConfig{ + Checks: false, + Paths: paths, + }, nil +} + +func connectProxyExposePaths(in []structs.ConsulExposePath, networks structs.Networks) ([]api.ExposePath, error) { + if len(in) == 0 { + return nil, nil + } + + paths := make([]api.ExposePath, len(in)) + for i, path := range in { + if _, exposedPort, err := connectExposePathPort(path.ListenerPort, networks); err != nil { + return nil, err + } else { + paths[i] = api.ExposePath{ + ListenerPort: exposedPort, + Path: path.Path, + LocalPathPort: path.LocalPathPort, + Protocol: path.Protocol, + ParsedFromCheck: false, + } + } + } + return paths, nil +} + +func connectUpstreams(in []structs.ConsulUpstream) []api.Upstream { + if len(in) == 0 { + return nil + } + + upstreams := make([]api.Upstream, len(in)) + for i, upstream := range in { + upstreams[i] = api.Upstream{ + DestinationName: upstream.DestinationName, + LocalBindPort: upstream.LocalBindPort, + } + } + return upstreams +} + +func connectProxyConfig(cfg map[string]interface{}, port int) map[string]interface{} { + if cfg == nil { + cfg = make(map[string]interface{}) + } + cfg["bind_address"] = "0.0.0.0" + cfg["bind_port"] = port + return cfg +} + +func connectNetworkInvariants(networks structs.Networks) error { + if n := len(networks); n != 1 { + return fmt.Errorf("Connect only supported with exactly 1 network (found %d)", n) + } + return nil +} + +// connectPort returns the network and port for the Connect proxy sidecar +// defined for this service. An error is returned if the network and port +// cannot be determined. +func connectPort(serviceName string, networks structs.Networks) (*structs.NetworkResource, structs.Port, error) { + if err := connectNetworkInvariants(networks); err != nil { + return nil, structs.Port{}, err + } + + port, ok := networks[0].PortForService(serviceName) + if !ok { + return nil, structs.Port{}, fmt.Errorf("No Connect port defined for service %q", serviceName) + } + + return networks[0], port, nil +} + +// connectExposePathPort returns the port for the exposed path for the exposed +// proxy path. +func connectExposePathPort(portLabel string, networks structs.Networks) (string, int, error) { + if err := connectNetworkInvariants(networks); err != nil { + return "", 0, err + } + + ip, port := networks.Port(portLabel) + if port == 0 { + return "", 0, fmt.Errorf("No port of label %q defined", portLabel) + } + + return ip, port, nil +} diff --git a/command/agent/consul/connect_test.go b/command/agent/consul/connect_test.go new file mode 100644 index 00000000000..5f14988c63e --- /dev/null +++ b/command/agent/consul/connect_test.go @@ -0,0 +1,376 @@ +package consul + +import ( + "testing" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/stretchr/testify/require" +) + +var ( + testConnectNetwork = structs.Networks{{ + Mode: "bridge", + Device: "eth0", + IP: "192.168.30.1", + DynamicPorts: []structs.Port{ + {Label: "healthPort", Value: 23100, To: 23100}, + {Label: "metricsPort", Value: 23200, To: 23200}, + {Label: "connect-proxy-redis", Value: 3000, To: 3000}, + }, + }} +) + +func TestConnect_newConnect(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + asr, err := newConnect("", nil, nil) + require.NoError(t, err) + require.Nil(t, asr) + }) + + t.Run("native", func(t *testing.T) { + asr, err := newConnect("", &structs.ConsulConnect{ + Native: true, + }, nil) + require.NoError(t, err) + require.True(t, asr.Native) + require.Nil(t, asr.SidecarService) + }) + + t.Run("with sidecar", func(t *testing.T) { + asr, err := newConnect("redis", &structs.ConsulConnect{ + Native: false, + SidecarService: &structs.ConsulSidecarService{ + Tags: []string{"foo", "bar"}, + Port: "sidecarPort", + }, + }, testConnectNetwork) + require.NoError(t, err) + require.Equal(t, &api.AgentServiceRegistration{ + Tags: []string{"foo", "bar"}, + Port: 3000, + Address: "192.168.30.1", + }, asr.SidecarService) + }) +} + +func TestConnect_connectSidecarRegistration(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + sidecarReg, err := connectSidecarRegistration("", nil, testConnectNetwork) + require.NoError(t, err) + require.Nil(t, sidecarReg) + }) + + t.Run("no service port", func(t *testing.T) { + _, err := connectSidecarRegistration("unknown", &structs.ConsulSidecarService{ + // irrelevant + }, testConnectNetwork) + require.EqualError(t, err, `No Connect port defined for service "unknown"`) + }) + + t.Run("bad proxy", func(t *testing.T) { + _, err := connectSidecarRegistration("redis", &structs.ConsulSidecarService{ + Proxy: &structs.ConsulProxy{ + Expose: &structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{ + ListenerPort: "badPort", + }}, + }, + }, + }, testConnectNetwork) + require.EqualError(t, err, `No port of label "badPort" defined`) + }) + + t.Run("normal", func(t *testing.T) { + proxy, err := connectSidecarRegistration("redis", &structs.ConsulSidecarService{ + Tags: []string{"foo", "bar"}, + Port: "sidecarPort", + }, testConnectNetwork) + require.NoError(t, err) + require.Equal(t, &api.AgentServiceRegistration{ + Tags: []string{"foo", "bar"}, + Port: 3000, + Address: "192.168.30.1", + }, proxy) + }) +} + +func TestConnect_connectProxy(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + proxy, err := connectProxy(nil, 2000, nil) + require.NoError(t, err) + require.Nil(t, proxy) + }) + + t.Run("bad proxy", func(t *testing.T) { + _, err := connectProxy(&structs.ConsulProxy{ + LocalServiceAddress: "0.0.0.0", + LocalServicePort: 2000, + Upstreams: nil, + Expose: &structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{ + ListenerPort: "badPort", + }}, + }, + Config: nil, + }, 2000, testConnectNetwork) + require.EqualError(t, err, `No port of label "badPort" defined`) + }) + + t.Run("normal", func(t *testing.T) { + proxy, err := connectProxy(&structs.ConsulProxy{ + LocalServiceAddress: "0.0.0.0", + LocalServicePort: 2000, + Upstreams: nil, + Expose: &structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8000, + ListenerPort: "healthPort", + }}, + }, + Config: nil, + }, 2000, testConnectNetwork) + require.NoError(t, err) + require.Equal(t, &api.AgentServiceConnectProxyConfig{ + LocalServiceAddress: "0.0.0.0", + LocalServicePort: 2000, + Upstreams: nil, + Expose: api.ExposeConfig{ + Paths: []api.ExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8000, + ListenerPort: 23100, + }}, + }, + Config: map[string]interface{}{ + "bind_address": "0.0.0.0", + "bind_port": 2000, + }, + }, proxy) + }) +} + +func TestConnect_connectProxyExpose(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + exposeConfig, err := connectProxyExpose(nil, nil) + require.NoError(t, err) + require.Equal(t, api.ExposeConfig{}, exposeConfig) + }) + + t.Run("bad port", func(t *testing.T) { + _, err := connectProxyExpose(&structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{ + ListenerPort: "badPort", + }}, + }, testConnectNetwork) + require.EqualError(t, err, `No port of label "badPort" defined`) + }) + + t.Run("normal", func(t *testing.T) { + expose, err := connectProxyExpose(&structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8000, + ListenerPort: "healthPort", + }}, + }, testConnectNetwork) + require.NoError(t, err) + require.Equal(t, api.ExposeConfig{ + Checks: false, + Paths: []api.ExposePath{{ + Path: "/health", + ListenerPort: 23100, + LocalPathPort: 8000, + Protocol: "http", + ParsedFromCheck: false, + }}, + }, expose) + }) +} + +func TestConnect_connectProxyExposePaths(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + upstreams, err := connectProxyExposePaths(nil, nil) + require.NoError(t, err) + require.Empty(t, upstreams) + }) + + t.Run("no network", func(t *testing.T) { + original := []structs.ConsulExposePath{{Path: "/path"}} + _, err := connectProxyExposePaths(original, nil) + require.EqualError(t, err, `Connect only supported with exactly 1 network (found 0)`) + }) + + t.Run("normal", func(t *testing.T) { + original := []structs.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8000, + ListenerPort: "healthPort", + }, { + Path: "/metrics", + Protocol: "grpc", + LocalPathPort: 9500, + ListenerPort: "metricsPort", + }} + exposePaths, err := connectProxyExposePaths(original, testConnectNetwork) + require.NoError(t, err) + require.Equal(t, []api.ExposePath{ + { + Path: "/health", + Protocol: "http", + LocalPathPort: 8000, + ListenerPort: 23100, + ParsedFromCheck: false, + }, + { + Path: "/metrics", + Protocol: "grpc", + LocalPathPort: 9500, + ListenerPort: 23200, + ParsedFromCheck: false, + }, + }, exposePaths) + }) +} + +func TestConnect_connectUpstreams(t *testing.T) { + t.Parallel() + + t.Run("nil", func(t *testing.T) { + require.Nil(t, connectUpstreams(nil)) + }) + + t.Run("not empty", func(t *testing.T) { + require.Equal(t, + []api.Upstream{{ + DestinationName: "foo", + LocalBindPort: 8000, + }, { + DestinationName: "bar", + LocalBindPort: 9000, + }}, + connectUpstreams([]structs.ConsulUpstream{{ + DestinationName: "foo", + LocalBindPort: 8000, + }, { + DestinationName: "bar", + LocalBindPort: 9000, + }}), + ) + }) +} + +func TestConnect_connectProxyConfig(t *testing.T) { + t.Parallel() + + t.Run("nil map", func(t *testing.T) { + require.Equal(t, map[string]interface{}{ + "bind_address": "0.0.0.0", + "bind_port": 42, + }, connectProxyConfig(nil, 42)) + }) + + t.Run("pre-existing map", func(t *testing.T) { + require.Equal(t, map[string]interface{}{ + "bind_address": "0.0.0.0", + "bind_port": 42, + "foo": "bar", + }, connectProxyConfig(map[string]interface{}{ + "foo": "bar", + }, 42)) + }) +} + +func TestConnect_getConnectPort(t *testing.T) { + t.Parallel() + + networks := structs.Networks{{ + IP: "192.168.30.1", + DynamicPorts: []structs.Port{{ + Label: "connect-proxy-foo", + Value: 23456, + To: 23456, + }}}} + + t.Run("normal", func(t *testing.T) { + nr, port, err := connectPort("foo", networks) + require.NoError(t, err) + require.Equal(t, structs.Port{ + Label: "connect-proxy-foo", + Value: 23456, + To: 23456, + }, port) + require.Equal(t, "192.168.30.1", nr.IP) + }) + + t.Run("no such service", func(t *testing.T) { + _, _, err := connectPort("other", networks) + require.EqualError(t, err, `No Connect port defined for service "other"`) + }) + + t.Run("no network", func(t *testing.T) { + _, _, err := connectPort("foo", nil) + require.EqualError(t, err, "Connect only supported with exactly 1 network (found 0)") + }) + + t.Run("multi network", func(t *testing.T) { + _, _, err := connectPort("foo", append(networks, &structs.NetworkResource{ + Device: "eth1", + IP: "10.0.10.0", + })) + require.EqualError(t, err, "Connect only supported with exactly 1 network (found 2)") + }) +} + +func TestConnect_getExposePathPort(t *testing.T) { + t.Parallel() + + networks := structs.Networks{{ + Device: "eth0", + IP: "192.168.30.1", + DynamicPorts: []structs.Port{{ + Label: "myPort", + Value: 23456, + To: 23456, + }}}} + + t.Run("normal", func(t *testing.T) { + ip, port, err := connectExposePathPort("myPort", networks) + require.NoError(t, err) + require.Equal(t, ip, "192.168.30.1") + require.Equal(t, 23456, port) + }) + + t.Run("no such port label", func(t *testing.T) { + _, _, err := connectExposePathPort("otherPort", networks) + require.EqualError(t, err, `No port of label "otherPort" defined`) + }) + + t.Run("no network", func(t *testing.T) { + _, _, err := connectExposePathPort("myPort", nil) + require.EqualError(t, err, "Connect only supported with exactly 1 network (found 0)") + }) + + t.Run("multi network", func(t *testing.T) { + _, _, err := connectExposePathPort("myPort", append(networks, &structs.NetworkResource{ + Device: "eth1", + IP: "10.0.10.0", + })) + require.EqualError(t, err, "Connect only supported with exactly 1 network (found 2)") + }) +} diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 3c1c5039def..333255940bf 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -1185,67 +1185,110 @@ func ApiConsulConnectToStructs(in *api.ConsulConnect) *structs.ConsulConnect { if in == nil { return nil } + return &structs.ConsulConnect{ + Native: in.Native, + SidecarService: apiConnectSidecarServiceToStructs(in.SidecarService), + SidecarTask: apiConnectSidecarTaskToStructs(in.SidecarTask), + } +} - out := &structs.ConsulConnect{ - Native: in.Native, +func apiConnectSidecarServiceToStructs(in *api.ConsulSidecarService) *structs.ConsulSidecarService { + if in == nil { + return nil + } + return &structs.ConsulSidecarService{ + Port: in.Port, + Tags: helper.CopySliceString(in.Tags), + Proxy: apiConnectSidecarServiceProxyToStructs(in.Proxy), } +} - if in.SidecarService != nil { +func apiConnectSidecarServiceProxyToStructs(in *api.ConsulProxy) *structs.ConsulProxy { + if in == nil { + return nil + } + return &structs.ConsulProxy{ + LocalServiceAddress: in.LocalServiceAddress, + LocalServicePort: in.LocalServicePort, + Upstreams: apiUpstreamsToStructs(in.Upstreams), + Expose: apiConsulExposeConfigToStructs(in.ExposeConfig), + Config: in.Config, + } +} - out.SidecarService = &structs.ConsulSidecarService{ - Tags: helper.CopySliceString(in.SidecarService.Tags), - Port: in.SidecarService.Port, +func apiUpstreamsToStructs(in []*api.ConsulUpstream) []structs.ConsulUpstream { + if len(in) == 0 { + return nil + } + upstreams := make([]structs.ConsulUpstream, len(in)) + for i, upstream := range in { + upstreams[i] = structs.ConsulUpstream{ + DestinationName: upstream.DestinationName, + LocalBindPort: upstream.LocalBindPort, } + } + return upstreams +} - if in.SidecarService.Proxy != nil { - - out.SidecarService.Proxy = &structs.ConsulProxy{ - LocalServiceAddress: in.SidecarService.Proxy.LocalServiceAddress, - LocalServicePort: in.SidecarService.Proxy.LocalServicePort, - Config: in.SidecarService.Proxy.Config, - } - - upstreams := make([]structs.ConsulUpstream, len(in.SidecarService.Proxy.Upstreams)) - for i, p := range in.SidecarService.Proxy.Upstreams { - upstreams[i] = structs.ConsulUpstream{ - DestinationName: p.DestinationName, - LocalBindPort: p.LocalBindPort, - } - } +func apiConsulExposeConfigToStructs(in *api.ConsulExposeConfig) *structs.ConsulExposeConfig { + if in == nil { + return nil + } + return &structs.ConsulExposeConfig{ + Paths: apiConsulExposePathsToStructs(in.Paths), + } +} - out.SidecarService.Proxy.Upstreams = upstreams +func apiConsulExposePathsToStructs(in []*api.ConsulExposePath) []structs.ConsulExposePath { + if len(in) == 0 { + return nil + } + paths := make([]structs.ConsulExposePath, len(in)) + for i, path := range in { + paths[i] = structs.ConsulExposePath{ + Path: path.Path, + Protocol: path.Protocol, + LocalPathPort: path.LocalPathPort, + ListenerPort: path.ListenerPort, } } + return paths +} - if in.SidecarTask != nil { - out.SidecarTask = &structs.SidecarTask{ - Name: in.SidecarTask.Name, - Driver: in.SidecarTask.Driver, - Config: in.SidecarTask.Config, - User: in.SidecarTask.User, - Env: in.SidecarTask.Env, - Resources: ApiResourcesToStructs(in.SidecarTask.Resources), - Meta: in.SidecarTask.Meta, - LogConfig: &structs.LogConfig{}, - ShutdownDelay: in.SidecarTask.ShutdownDelay, - KillSignal: in.SidecarTask.KillSignal, - } +func apiConnectSidecarTaskToStructs(in *api.SidecarTask) *structs.SidecarTask { + if in == nil { + return nil + } + return &structs.SidecarTask{ + Name: in.Name, + Driver: in.Driver, + User: in.User, + Config: in.Config, + Env: in.Env, + Resources: ApiResourcesToStructs(in.Resources), + Meta: in.Meta, + ShutdownDelay: in.ShutdownDelay, + KillSignal: in.KillSignal, + KillTimeout: in.KillTimeout, + LogConfig: apiLogConfigToStructs(in.LogConfig), + } +} - if in.SidecarTask.KillTimeout != nil { - out.SidecarTask.KillTimeout = in.SidecarTask.KillTimeout - } - if in.SidecarTask.LogConfig != nil { - out.SidecarTask.LogConfig = &structs.LogConfig{} - if in.SidecarTask.LogConfig.MaxFiles != nil { - out.SidecarTask.LogConfig.MaxFiles = *in.SidecarTask.LogConfig.MaxFiles - } - if in.SidecarTask.LogConfig.MaxFileSizeMB != nil { - out.SidecarTask.LogConfig.MaxFileSizeMB = *in.SidecarTask.LogConfig.MaxFileSizeMB - } - } +func apiLogConfigToStructs(in *api.LogConfig) *structs.LogConfig { + if in == nil { + return nil } + return &structs.LogConfig{ + MaxFiles: dereferenceInt(in.MaxFiles), + MaxFileSizeMB: dereferenceInt(in.MaxFileSizeMB), + } +} - return out +func dereferenceInt(in *int) int { + if in == nil { + return 0 + } + return *in } func ApiConstraintsToStructs(in []*api.Constraint) []*structs.Constraint { diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index b01929b9ac9..f53db52c506 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -2542,3 +2542,169 @@ func TestHTTP_JobValidate_SystemMigrate(t *testing.T) { require.Contains(t, resp.Error, `Job type "system" does not allow migrate block`) }) } + +func TestConversion_dereferenceInt(t *testing.T) { + t.Parallel() + require.Equal(t, 0, dereferenceInt(nil)) + require.Equal(t, 42, dereferenceInt(helper.IntToPtr(42))) +} + +func TestConversion_apiLogConfigToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiLogConfigToStructs(nil)) + require.Equal(t, &structs.LogConfig{ + MaxFiles: 2, + MaxFileSizeMB: 8, + }, apiLogConfigToStructs(&api.LogConfig{ + MaxFiles: helper.IntToPtr(2), + MaxFileSizeMB: helper.IntToPtr(8), + })) +} + +func TestConversion_apiConnectSidecarTaskToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiConnectSidecarTaskToStructs(nil)) + delay := time.Duration(200) + timeout := time.Duration(1000) + config := make(map[string]interface{}) + env := make(map[string]string) + meta := make(map[string]string) + require.Equal(t, &structs.SidecarTask{ + Name: "name", + Driver: "driver", + User: "user", + Config: config, + Env: env, + Resources: &structs.Resources{ + CPU: 1, + MemoryMB: 128, + }, + Meta: meta, + KillTimeout: &timeout, + LogConfig: &structs.LogConfig{ + MaxFiles: 2, + MaxFileSizeMB: 8, + }, + ShutdownDelay: &delay, + KillSignal: "SIGTERM", + }, apiConnectSidecarTaskToStructs(&api.SidecarTask{ + Name: "name", + Driver: "driver", + User: "user", + Config: config, + Env: env, + Resources: &api.Resources{ + CPU: helper.IntToPtr(1), + MemoryMB: helper.IntToPtr(128), + }, + Meta: meta, + KillTimeout: &timeout, + LogConfig: &api.LogConfig{ + MaxFiles: helper.IntToPtr(2), + MaxFileSizeMB: helper.IntToPtr(8), + }, + ShutdownDelay: &delay, + KillSignal: "SIGTERM", + })) +} + +func TestConversion_apiConsulExposePathsToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiConsulExposePathsToStructs(nil)) + require.Nil(t, apiConsulExposePathsToStructs(make([]*api.ConsulExposePath, 0))) + require.Equal(t, []structs.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8080, + ListenerPort: "hcPort", + }}, apiConsulExposePathsToStructs([]*api.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 8080, + ListenerPort: "hcPort", + }})) +} + +func TestConversion_apiConsulExposeConfigToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiConsulExposeConfigToStructs(nil)) + require.Equal(t, &structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{Path: "/health"}}, + }, apiConsulExposeConfigToStructs(&api.ConsulExposeConfig{ + Paths: []*api.ConsulExposePath{{Path: "/health"}}, + })) +} + +func TestConversion_apiUpstreamsToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiUpstreamsToStructs(nil)) + require.Nil(t, apiUpstreamsToStructs(make([]*api.ConsulUpstream, 0))) + require.Equal(t, []structs.ConsulUpstream{{ + DestinationName: "upstream", + LocalBindPort: 8000, + }}, apiUpstreamsToStructs([]*api.ConsulUpstream{{ + DestinationName: "upstream", + LocalBindPort: 8000, + }})) +} + +func TestConversion_apiConnectSidecarServiceProxyToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiConnectSidecarServiceProxyToStructs(nil)) + config := make(map[string]interface{}) + require.Equal(t, &structs.ConsulProxy{ + LocalServiceAddress: "192.168.30.1", + LocalServicePort: 9000, + Config: config, + Upstreams: []structs.ConsulUpstream{{ + DestinationName: "upstream", + }}, + Expose: &structs.ConsulExposeConfig{ + Paths: []structs.ConsulExposePath{{Path: "/health"}}, + }, + }, apiConnectSidecarServiceProxyToStructs(&api.ConsulProxy{ + LocalServiceAddress: "192.168.30.1", + LocalServicePort: 9000, + Config: config, + Upstreams: []*api.ConsulUpstream{{ + DestinationName: "upstream", + }}, + ExposeConfig: &api.ConsulExposeConfig{ + Paths: []*api.ConsulExposePath{{ + Path: "/health", + }}, + }, + })) +} + +func TestConversion_apiConnectSidecarServiceToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, apiConnectSidecarTaskToStructs(nil)) + require.Equal(t, &structs.ConsulSidecarService{ + Tags: []string{"foo"}, + Port: "myPort", + Proxy: &structs.ConsulProxy{ + LocalServiceAddress: "192.168.30.1", + }, + }, apiConnectSidecarServiceToStructs(&api.ConsulSidecarService{ + Tags: []string{"foo"}, + Port: "myPort", + Proxy: &api.ConsulProxy{ + LocalServiceAddress: "192.168.30.1", + }, + })) +} + +func TestConversion_ApiConsulConnectToStructs(t *testing.T) { + t.Parallel() + require.Nil(t, ApiConsulConnectToStructs(nil)) + require.Equal(t, &structs.ConsulConnect{ + Native: false, + SidecarService: &structs.ConsulSidecarService{Port: "myPort"}, + SidecarTask: &structs.SidecarTask{Name: "task"}, + }, ApiConsulConnectToStructs(&api.ConsulConnect{ + Native: false, + SidecarService: &api.ConsulSidecarService{Port: "myPort"}, + SidecarTask: &api.SidecarTask{Name: "task"}, + })) +} diff --git a/jobspec/parse_service.go b/jobspec/parse_service.go index e44754a301a..ff591160b18 100644 --- a/jobspec/parse_service.go +++ b/jobspec/parse_service.go @@ -306,7 +306,7 @@ func parseSidecarTask(item *ast.ObjectItem) (*api.SidecarTask, error) { KillSignal: task.KillSignal, } - // Parse ShutdownDelay seperatly to get pointer + // Parse ShutdownDelay separately to get pointer var m map[string]interface{} if err := hcl.DecodeObject(&m, item.Val); err != nil { return nil, err @@ -336,6 +336,7 @@ func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) { "local_service_address", "local_service_port", "upstreams", + "expose", "config", } @@ -353,15 +354,27 @@ func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) { } // Parse the proxy + uo := listVal.Filter("upstreams") - proxy.Upstreams = make([]*api.ConsulUpstream, len(uo.Items)) - for i := range uo.Items { - u, err := parseUpstream(uo.Items[i]) - if err != nil { - return nil, err + if len(uo.Items) > 0 { + proxy.Upstreams = make([]*api.ConsulUpstream, len(uo.Items)) + for i := range uo.Items { + u, err := parseUpstream(uo.Items[i]) + if err != nil { + return nil, err + } + proxy.Upstreams[i] = u } + } - proxy.Upstreams[i] = u + if eo := listVal.Filter("expose"); len(eo.Items) > 1 { + return nil, fmt.Errorf("only 1 expose object supported") + } else if len(eo.Items) == 1 { + if e, err := parseExpose(eo.Items[0]); err != nil { + return nil, err + } else { + proxy.ExposeConfig = e + } } // If we have config, then parse that @@ -389,6 +402,42 @@ func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) { return &proxy, nil } +func parseExpose(eo *ast.ObjectItem) (*api.ConsulExposeConfig, error) { + var listVal *ast.ObjectList + if eoType, ok := eo.Val.(*ast.ObjectType); ok { + listVal = eoType.List + } else { + return nil, fmt.Errorf("expose: should be an object") + } + + valid := []string{ + "paths", + } + + if err := helper.CheckHCLKeys(listVal, valid); err != nil { + return nil, multierror.Prefix(err, "expose ->") + } + + var m map[string]interface{} + if err := hcl.DecodeObject(&m, eo.Val); err != nil { + return nil, err + } + + // Build the expose block + var expose api.ConsulExposeConfig + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &expose, + }) + if err != nil { + return nil, err + } + if err := dec.Decode(m); err != nil { + return nil, err + } + + return &expose, nil +} + func parseUpstream(uo *ast.ObjectItem) (*api.ConsulUpstream, error) { valid := []string{ "destination_name", @@ -420,6 +469,7 @@ func parseUpstream(uo *ast.ObjectItem) (*api.ConsulUpstream, error) { return &upstream, nil } + func parseChecks(service *api.Service, checkObjs *ast.ObjectList) error { service.Checks = make([]api.ServiceCheck, len(checkObjs.Items)) for idx, co := range checkObjs.Items { diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index a54163d970f..ddc6d54db82 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -1117,6 +1117,34 @@ func TestParse(t *testing.T) { }, false, }, + { + "tg-service-proxy-expose.hcl", + &api.Job{ + ID: helper.StringToPtr("group_service_proxy_expose"), + Name: helper.StringToPtr("group_service_proxy_expose"), + TaskGroups: []*api.TaskGroup{{ + Name: helper.StringToPtr("group"), + Services: []*api.Service{{ + Name: "example", + Connect: &api.ConsulConnect{ + SidecarService: &api.ConsulSidecarService{ + Proxy: &api.ConsulProxy{ + ExposeConfig: &api.ConsulExposeConfig{ + Paths: []*api.ConsulExposePath{{ + Path: "/health", + Protocol: "http", + LocalPathPort: 2222, + ListenerPort: "healthcheck", + }}, + }, + }, + }, + }, + }}, + }}, + }, + false, + }, { "tg-service-enable-tag-override.hcl", &api.Job{ diff --git a/jobspec/test-fixtures/tg-service-proxy-expose.hcl b/jobspec/test-fixtures/tg-service-proxy-expose.hcl new file mode 100644 index 00000000000..2195a716f7c --- /dev/null +++ b/jobspec/test-fixtures/tg-service-proxy-expose.hcl @@ -0,0 +1,21 @@ +job "group_service_proxy_expose" { + group "group" { + service { + name = "example" + connect { + sidecar_service { + proxy { + expose { + paths = [{ + path = "/health" + protocol = "http" + local_path_port = 2222 + listener_port = "healthcheck" + }] + } + } + } + } + } + } +} diff --git a/nomad/job_endpoint_hook_connect.go b/nomad/job_endpoint_hook_connect.go index b18b4c1ec0f..fca81ef1d5e 100644 --- a/nomad/job_endpoint_hook_connect.go +++ b/nomad/job_endpoint_hook_connect.go @@ -30,9 +30,9 @@ var ( } // connectVersionConstraint is used when building the sidecar task to ensure - // the proper Consul version is used that supports the nessicary Connect - // features. This includes bootstraping envoy with a unix socket for Consul's - // grpc xDS api. + // the proper Consul version is used that supports the necessary Connect + // features. This includes bootstrapping envoy with a unix socket for Consul's + // gRPC xDS API. connectVersionConstraint = func() *structs.Constraint { return &structs.Constraint{ LTarget: "${attr.consul.version}", @@ -97,6 +97,8 @@ func isSidecarForService(t *structs.Task, svc string) bool { return t.Kind == structs.TaskKind(fmt.Sprintf("%s:%s", structs.ConnectProxyPrefix, svc)) } +// probably need to hack this up to look for checks on the service, and if they +// qualify, configure a port for envoy to use to expose their paths. func groupConnectHook(job *structs.Job, g *structs.TaskGroup) error { for _, service := range g.Services { if service.Connect.HasSidecar() { @@ -125,29 +127,28 @@ func groupConnectHook(job *structs.Job, g *structs.TaskGroup) error { // Canonicalize task since this mutator runs after job canonicalization task.Canonicalize(job, g) - // port to be added for the sidecar task's proxy port - port := structs.Port{ - Label: fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, service.Name), - - // -1 is a sentinel value to instruct the - // scheduler to map the host's dynamic port to - // the same port in the netns. - To: -1, - } - - // check that port hasn't already been defined before adding it to tg - var found bool - for _, p := range g.Networks[0].DynamicPorts { - if p.Label == port.Label { - found = true - break + makePort := func(label string) { + // check that port hasn't already been defined before adding it to tg + for _, p := range g.Networks[0].DynamicPorts { + if p.Label == label { + return + } } + g.Networks[0].DynamicPorts = append(g.Networks[0].DynamicPorts, structs.Port{ + Label: label, + // -1 is a sentinel value to instruct the + // scheduler to map the host's dynamic port to + // the same port in the netns. + To: -1, + }) } - if !found { - g.Networks[0].DynamicPorts = append(g.Networks[0].DynamicPorts, port) - } + + // create a port for the sidecar task's proxy port + makePort(fmt.Sprintf("%s-%s", structs.ConnectProxyPrefix, service.Name)) + // todo(shoenig) magic port for 'expose.checks' } } + return nil } diff --git a/nomad/structs/services.go b/nomad/structs/services.go index dbafdfd1164..5b9f38f2fa6 100644 --- a/nomad/structs/services.go +++ b/nomad/structs/services.go @@ -852,6 +852,10 @@ type ConsulProxy struct { // connect to. Upstreams []ConsulUpstream + // Expose configures the consul proxy.expose stanza to "open up" endpoints + // used by task-group level service checks using HTTP or gRPC protocols. + Expose *ConsulExposeConfig + // Config is a proxy configuration. It is opaque to Nomad and passed // directly to Consul. Config map[string]interface{} @@ -863,9 +867,11 @@ func (p *ConsulProxy) Copy() *ConsulProxy { return nil } - newP := ConsulProxy{} - newP.LocalServiceAddress = p.LocalServiceAddress - newP.LocalServicePort = p.LocalServicePort + newP := &ConsulProxy{ + LocalServiceAddress: p.LocalServiceAddress, + LocalServicePort: p.LocalServicePort, + Expose: p.Expose, + } if n := len(p.Upstreams); n > 0 { newP.Upstreams = make([]ConsulUpstream, n) @@ -883,7 +889,7 @@ func (p *ConsulProxy) Copy() *ConsulProxy { } } - return &newP + return newP } // Equals returns true if the structs are recursively equal. @@ -895,24 +901,16 @@ func (p *ConsulProxy) Equals(o *ConsulProxy) bool { if p.LocalServiceAddress != o.LocalServiceAddress { return false } + if p.LocalServicePort != o.LocalServicePort { return false } - if len(p.Upstreams) != len(o.Upstreams) { + + if !p.Expose.Equals(o.Expose) { return false } - // Order doesn't matter -OUTER: - for _, up := range p.Upstreams { - for _, innerUp := range o.Upstreams { - if up.Equals(&innerUp) { - // Match; find next upstream - continue OUTER - } - } - - // No match + if !upstreamsEquals(p.Upstreams, o.Upstreams) { return false } @@ -936,7 +934,24 @@ type ConsulUpstream struct { LocalBindPort int } -// Copy the stanza recursively. Returns nil if nil. +func upstreamsEquals(a, b []ConsulUpstream) bool { + if len(a) != len(b) { + return false + } + +LOOP: // order does not matter + for _, upA := range a { + for _, upB := range b { + if upA.Equals(&upB) { + continue LOOP + } + } + return false + } + return true +} + +// Copy the stanza recursively. Returns nil if u is nil. func (u *ConsulUpstream) Copy() *ConsulUpstream { if u == nil { return nil @@ -956,3 +971,54 @@ func (u *ConsulUpstream) Equals(o *ConsulUpstream) bool { return (*u) == (*o) } + +// ExposeConfig represents a Consul Connect expose jobspec stanza. +type ConsulExposeConfig struct { + Paths []ConsulExposePath +} + +type ConsulExposePath struct { + Path string + Protocol string + LocalPathPort int + ListenerPort string +} + +func exposePathsEqual(pathsA, pathsB []ConsulExposePath) bool { + if len(pathsA) != len(pathsB) { + return false + } + +LOOP: // order does not matter + for _, pathA := range pathsA { + for _, pathB := range pathsB { + if pathA == pathB { + continue LOOP + } + } + return false + } + return true +} + +// Copy the stanza. Returns nil if e is nil. +func (e *ConsulExposeConfig) Copy() *ConsulExposeConfig { + if e == nil { + return nil + } + paths := make([]ConsulExposePath, len(e.Paths)) + for i := 0; i < len(e.Paths); i++ { + paths[i] = e.Paths[i] + } + return &ConsulExposeConfig{ + Paths: paths, + } +} + +// Equals returns true if the structs are recursively equal. +func (e *ConsulExposeConfig) Equals(o *ConsulExposeConfig) bool { + if e == nil || o == nil { + return e == o + } + return exposePathsEqual(e.Paths, o.Paths) +} diff --git a/nomad/structs/services_test.go b/nomad/structs/services_test.go index ac5c73ee551..b889a9ec7c3 100644 --- a/nomad/structs/services_test.go +++ b/nomad/structs/services_test.go @@ -174,7 +174,6 @@ func TestConsulConnect_CopyEquals(t *testing.T) { } func TestSidecarTask_MergeIntoTask(t *testing.T) { - task := MockJob().TaskGroups[0].Tasks[0] sTask := &SidecarTask{ Name: "sidecar", @@ -226,5 +225,102 @@ func TestSidecarTask_MergeIntoTask(t *testing.T) { sTask.MergeIntoTask(task) require.Exactly(t, expected, task) +} + +func TestConsulUpstream_upstreamEquals(t *testing.T) { + t.Parallel() + + up := func(name string, port int) ConsulUpstream { + return ConsulUpstream{ + DestinationName: name, + LocalBindPort: port, + } + } + + t.Run("size mismatch", func(t *testing.T) { + a := []ConsulUpstream{up("foo", 8000)} + b := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} + require.False(t, upstreamsEquals(a, b)) + }) + + t.Run("different", func(t *testing.T) { + a := []ConsulUpstream{up("bar", 9000)} + b := []ConsulUpstream{up("foo", 8000)} + require.False(t, upstreamsEquals(a, b)) + }) + + t.Run("identical", func(t *testing.T) { + a := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} + b := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} + require.True(t, upstreamsEquals(a, b)) + }) + + t.Run("unsorted", func(t *testing.T) { + a := []ConsulUpstream{up("foo", 8000), up("bar", 9000)} + b := []ConsulUpstream{up("bar", 9000), up("foo", 8000)} + require.True(t, upstreamsEquals(a, b)) + }) +} + +func TestConsulExposePath_exposePathsEqual(t *testing.T) { + t.Parallel() + + expose := func(path, protocol, listen string, local int) ConsulExposePath { + return ConsulExposePath{ + Path: path, + Protocol: protocol, + LocalPathPort: local, + ListenerPort: listen, + } + } + + t.Run("size mismatch", func(t *testing.T) { + a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} + b := []ConsulExposePath{expose("/1", "http", "myPort", 8000), expose("/2", "http", "myPort", 8000)} + require.False(t, exposePathsEqual(a, b)) + }) + + t.Run("different", func(t *testing.T) { + a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} + b := []ConsulExposePath{expose("/2", "http", "myPort", 8000)} + require.False(t, exposePathsEqual(a, b)) + }) + + t.Run("identical", func(t *testing.T) { + a := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} + b := []ConsulExposePath{expose("/1", "http", "myPort", 8000)} + require.True(t, exposePathsEqual(a, b)) + }) + + t.Run("unsorted", func(t *testing.T) { + a := []ConsulExposePath{expose("/2", "http", "myPort", 8000), expose("/1", "http", "myPort", 8000)} + b := []ConsulExposePath{expose("/1", "http", "myPort", 8000), expose("/2", "http", "myPort", 8000)} + require.True(t, exposePathsEqual(a, b)) + }) +} + +func TestConsulExposeConfig_Copy(t *testing.T) { + require.Nil(t, (*ConsulExposeConfig)(nil).Copy()) + require.Equal(t, &ConsulExposeConfig{ + Paths: []ConsulExposePath{{ + Path: "/health", + }}, + }, (&ConsulExposeConfig{ + Paths: []ConsulExposePath{{ + Path: "/health", + }}, + }).Copy()) +} +func TestConsulExposeConfig_Equals(t *testing.T) { + require.True(t, (*ConsulExposeConfig)(nil).Equals(nil)) + require.True(t, (&ConsulExposeConfig{ + Paths: []ConsulExposePath{{ + Path: "/health", + }}, + }).Equals(&ConsulExposeConfig{ + Paths: []ConsulExposePath{{ + Path: "/health", + }}, + })) } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index a1e606c2036..e11ad69ec2a 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -2419,7 +2419,7 @@ type RequestedDevice struct { // to use. Constraints Constraints - // Affinities are a set of affinites to apply when selecting the device + // Affinities are a set of affinities to apply when selecting the device // to use. Affinities Affinities } @@ -2612,18 +2612,18 @@ func (n *NodeResources) Equals(o *NodeResources) bool { } // Equals equates Networks as a set -func (n *Networks) Equals(o *Networks) bool { - if n == o { +func (ns *Networks) Equals(o *Networks) bool { + if ns == o { return true } - if n == nil || o == nil { + if ns == nil || o == nil { return false } - if len(*n) != len(*o) { + if len(*ns) != len(*o) { return false } SETEQUALS: - for _, ne := range *n { + for _, ne := range *ns { for _, oe := range *o { if ne.Equals(oe) { continue SETEQUALS