Merge pull request #7323 from hashicorp/f-connect-expose-checks

connect: enable proxy.expose configuration
This commit is contained in:
Seth Hoenig 2020-03-31 17:20:23 -06:00 committed by GitHub
commit 2d73d92510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1203 additions and 212 deletions

View File

@ -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 {
Path []*ConsulExposePath `mapstructure:"path"`
// 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"`
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)")
})
}

View File

@ -73,22 +73,22 @@ func BuildAllocServices(node *structs.Node, alloc *structs.Allocation, restarter
}
// Copy method for easing tests
func (t *WorkloadServices) Copy() *WorkloadServices {
func (ws *WorkloadServices) Copy() *WorkloadServices {
newTS := new(WorkloadServices)
*newTS = *t
*newTS = *ws
// Deep copy Services
newTS.Services = make([]*structs.Service, len(t.Services))
for i := range t.Services {
newTS.Services[i] = t.Services[i].Copy()
newTS.Services = make([]*structs.Service, len(ws.Services))
for i := range ws.Services {
newTS.Services[i] = ws.Services[i].Copy()
}
return newTS
}
func (w *WorkloadServices) Name() string {
if w.Task != "" {
return w.Task
func (ws *WorkloadServices) Name() string {
if ws.Task != "" {
return ws.Task
}
return "group-" + w.Group
return "group-" + ws.Group
}

View File

@ -1185,67 +1185,110 @@ func ApiConsulConnectToStructs(in *api.ConsulConnect) *structs.ConsulConnect {
if in == nil {
return nil
}
out := &structs.ConsulConnect{
Native: in.Native,
return &structs.ConsulConnect{
Native: in.Native,
SidecarService: apiConnectSidecarServiceToStructs(in.SidecarService),
SidecarTask: apiConnectSidecarTaskToStructs(in.SidecarTask),
}
}
if in.SidecarService != nil {
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),
}
}
out.SidecarService = &structs.ConsulSidecarService{
Tags: helper.CopySliceString(in.SidecarService.Tags),
Port: in.SidecarService.Port,
}
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,
}
}
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,
}
}
out.SidecarService.Proxy.Upstreams = upstreams
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.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 apiConsulExposeConfigToStructs(in *api.ConsulExposeConfig) *structs.ConsulExposeConfig {
if in == nil {
return nil
}
return &structs.ConsulExposeConfig{
Paths: apiConsulExposePathsToStructs(in.Path),
}
}
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 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
}
return out
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),
}
}
func apiLogConfigToStructs(in *api.LogConfig) *structs.LogConfig {
if in == nil {
return nil
}
return &structs.LogConfig{
MaxFiles: dereferenceInt(in.MaxFiles),
MaxFileSizeMB: dereferenceInt(in.MaxFileSizeMB),
}
}
func dereferenceInt(in *int) int {
if in == nil {
return 0
}
return *in
}
func ApiConstraintsToStructs(in []*api.Constraint) []*structs.Constraint {

View File

@ -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{
Path: []*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{
Path: []*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"},
}))
}

View File

@ -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
}
proxy.Upstreams[i] = u
uo := listVal.Filter("upstreams")
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
}
}
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,74 @@ func parseProxy(o *ast.ObjectItem) (*api.ConsulProxy, error) {
return &proxy, nil
}
func parseExpose(eo *ast.ObjectItem) (*api.ConsulExposeConfig, error) {
valid := []string{
"path", // an array of path blocks
// todo(shoenig) checks boolean
}
if err := helper.CheckHCLKeys(eo.Val, valid); err != nil {
return nil, multierror.Prefix(err, "expose ->")
}
var expose api.ConsulExposeConfig
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")
}
// Parse the expose block
po := listVal.Filter("path") // array
if len(po.Items) > 0 {
expose.Path = make([]*api.ConsulExposePath, len(po.Items))
for i := range po.Items {
p, err := parseExposePath(po.Items[i])
if err != nil {
return nil, err
}
expose.Path[i] = p
}
}
return &expose, nil
}
func parseExposePath(epo *ast.ObjectItem) (*api.ConsulExposePath, error) {
valid := []string{
"path",
"protocol",
"local_path_port",
"listener_port",
}
if err := helper.CheckHCLKeys(epo.Val, valid); err != nil {
return nil, multierror.Prefix(err, "path ->")
}
var path api.ConsulExposePath
var m map[string]interface{}
if err := hcl.DecodeObject(&m, epo.Val); err != nil {
return nil, err
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &path,
})
if err != nil {
return nil, err
}
if err := dec.Decode(m); err != nil {
return nil, err
}
return &path, nil
}
func parseUpstream(uo *ast.ObjectItem) (*api.ConsulUpstream, error) {
valid := []string{
"destination_name",
@ -420,6 +501,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 {

View File

@ -1117,6 +1117,39 @@ 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{
Path: []*api.ConsulExposePath{{
Path: "/health",
Protocol: "http",
LocalPathPort: 2222,
ListenerPort: "healthcheck",
}, {
Path: "/metrics",
Protocol: "grpc",
LocalPathPort: 3000,
ListenerPort: "metrics",
}},
},
},
},
},
}},
}},
},
false,
},
{
"tg-service-enable-tag-override.hcl",
&api.Job{

View File

@ -0,0 +1,28 @@
job "group_service_proxy_expose" {
group "group" {
service {
name = "example"
connect {
sidecar_service {
proxy {
expose {
path = {
path = "/health"
protocol = "http"
local_path_port = 2222
listener_port = "healthcheck"
}
path = {
path = "/metrics"
protocol = "grpc"
local_path_port = 3000
listener_port = "metrics"
}
}
}
}
}
}
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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",
}},
}))
}

View File

@ -2422,7 +2422,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
}
@ -2615,18 +2615,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