open-nomad/nomad/structs/services_test.go
Seth Hoenig 20802da8fd connect: correctly deal with nil sidecar_service task stanza
Before, if the sidecar_service stanza of a connect enabled service
was missing, the job submission would cause a panic in the nomad
agent. Since the panic was happening in the API handler the agent
itself continued running, but this change will the condition more
gracefully.

By fixing the `Copy` method, the API handler now returns the proper
error.

$ nomad job run foo.nomad
Error submitting job: Unexpected response code: 500 (1 error occurred:
	* Task group api validation failed: 2 errors occurred:
	* Missing tasks for task group
	* Task group service validation failed: 1 error occurred:
	* Service[0] count-api validation failed: 1 error occurred:
	* Consul Connect must be native or use a sidecar service
2020-04-09 20:28:17 -06:00

357 lines
9.2 KiB
Go

package structs
import (
"testing"
"time"
"github.com/hashicorp/nomad/helper"
"github.com/stretchr/testify/require"
)
func TestService_Hash(t *testing.T) {
t.Parallel()
original := &Service{
Name: "myService",
PortLabel: "portLabel",
// AddressMode: "bridge", // not hashed (used internally by Nomad)
Tags: []string{"original", "tags"},
CanaryTags: []string{"canary", "tags"},
// Checks: nil, // not hashed (managed independently)
Connect: &ConsulConnect{
// Native: false, // not hashed
SidecarService: &ConsulSidecarService{
Tags: []string{"original", "sidecar", "tags"},
Port: "9000",
Proxy: &ConsulProxy{
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 24000,
Config: map[string]interface{}{"foo": "bar"},
Upstreams: []ConsulUpstream{{
DestinationName: "upstream1",
LocalBindPort: 29000,
}},
},
},
// SidecarTask: nil // not hashed
}}
type svc = Service
type tweaker = func(service *svc)
hash := func(s *svc, canary bool) string {
return s.Hash("AllocID", "TaskName", canary)
}
t.Run("matching and is canary", func(t *testing.T) {
require.Equal(t, hash(original, true), hash(original, true))
})
t.Run("matching and is not canary", func(t *testing.T) {
require.Equal(t, hash(original, false), hash(original, false))
})
t.Run("matching mod canary", func(t *testing.T) {
require.NotEqual(t, hash(original, true), hash(original, false))
})
try := func(t *testing.T, tweak tweaker) {
originalHash := hash(original, true)
modifiable := original.Copy()
tweak(modifiable)
tweakedHash := hash(modifiable, true)
require.NotEqual(t, originalHash, tweakedHash)
}
// these tests use tweaker to modify 1 field and make the false assertion
// on comparing the resulting hash output
t.Run("mod name", func(t *testing.T) {
try(t, func(s *svc) { s.Name = "newName" })
})
t.Run("mod port label", func(t *testing.T) {
try(t, func(s *svc) { s.PortLabel = "newPortLabel" })
})
t.Run("mod tags", func(t *testing.T) {
try(t, func(s *svc) { s.Tags = []string{"new", "tags"} })
})
t.Run("mod canary tags", func(t *testing.T) {
try(t, func(s *svc) { s.CanaryTags = []string{"new", "tags"} })
})
t.Run("mod enable tag override", func(t *testing.T) {
try(t, func(s *svc) { s.EnableTagOverride = true })
})
t.Run("mod connect sidecar tags", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Tags = []string{"new", "tags"} })
})
t.Run("mod connect sidecar port", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Port = "9090" })
})
t.Run("mod connect sidecar proxy local service address", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Proxy.LocalServiceAddress = "1.1.1.1" })
})
t.Run("mod connect sidecar proxy local service port", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Proxy.LocalServicePort = 9999 })
})
t.Run("mod connect sidecar proxy config", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Config = map[string]interface{}{"foo": "baz"} })
})
t.Run("mod connect sidecar proxy upstream dest name", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].DestinationName = "dest2" })
})
t.Run("mod connect sidecar proxy upstream dest local bind port", func(t *testing.T) {
try(t, func(s *svc) { s.Connect.SidecarService.Proxy.Upstreams[0].LocalBindPort = 29999 })
})
}
func TestConsulConnect_Validate(t *testing.T) {
t.Parallel()
c := &ConsulConnect{}
// An empty Connect stanza is invalid
require.Error(t, c.Validate())
// Native=true is valid
c.Native = true
require.NoError(t, c.Validate())
// Native=true + Sidecar!=nil is invalid
c.SidecarService = &ConsulSidecarService{}
require.Error(t, c.Validate())
// Native=false + Sidecar!=nil is valid
c.Native = false
require.NoError(t, c.Validate())
}
func TestConsulConnect_CopyEquals(t *testing.T) {
t.Parallel()
c := &ConsulConnect{
SidecarService: &ConsulSidecarService{
Tags: []string{"tag1", "tag2"},
Port: "9001",
Proxy: &ConsulProxy{
LocalServiceAddress: "127.0.0.1",
LocalServicePort: 8080,
Upstreams: []ConsulUpstream{
{
DestinationName: "up1",
LocalBindPort: 9002,
},
{
DestinationName: "up2",
LocalBindPort: 9003,
},
},
Config: map[string]interface{}{
"foo": 1,
},
},
},
}
require.NoError(t, c.Validate())
// Copies should be equivalent
o := c.Copy()
require.True(t, c.Equals(o))
o.SidecarService.Proxy.Upstreams = nil
require.False(t, c.Equals(o))
}
func TestSidecarTask_MergeIntoTask(t *testing.T) {
t.Parallel()
task := MockJob().TaskGroups[0].Tasks[0]
sTask := &SidecarTask{
Name: "sidecar",
Driver: "sidecar",
User: "test",
Config: map[string]interface{}{
"foo": "bar",
},
Resources: &Resources{
CPU: 10000,
MemoryMB: 10000,
},
Env: map[string]string{
"sidecar": "proxy",
},
Meta: map[string]string{
"abc": "123",
},
KillTimeout: helper.TimeToPtr(15 * time.Second),
LogConfig: &LogConfig{
MaxFiles: 3,
},
ShutdownDelay: helper.TimeToPtr(5 * time.Second),
KillSignal: "SIGABRT",
}
expected := task.Copy()
expected.Name = "sidecar"
expected.Driver = "sidecar"
expected.User = "test"
expected.Config = map[string]interface{}{
"foo": "bar",
}
expected.Resources.CPU = 10000
expected.Resources.MemoryMB = 10000
expected.Env["sidecar"] = "proxy"
expected.Meta["abc"] = "123"
expected.KillTimeout = 15 * time.Second
expected.LogConfig.MaxFiles = 3
expected.ShutdownDelay = 5 * time.Second
expected.KillSignal = "SIGABRT"
sTask.MergeIntoTask(task)
require.Exactly(t, expected, task)
// Check that changing just driver config doesn't replace map
sTask.Config["abc"] = 123
expected.Config["abc"] = 123
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) {
t.Parallel()
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) {
t.Parallel()
require.True(t, (*ConsulExposeConfig)(nil).Equals(nil))
require.True(t, (&ConsulExposeConfig{
Paths: []ConsulExposePath{{
Path: "/health",
}},
}).Equals(&ConsulExposeConfig{
Paths: []ConsulExposePath{{
Path: "/health",
}},
}))
}
func TestConsulSidecarService_Copy(t *testing.T) {
t.Parallel()
t.Run("nil", func(t *testing.T) {
s := (*ConsulSidecarService)(nil)
result := s.Copy()
require.Nil(t, result)
})
t.Run("not nil", func(t *testing.T) {
s := &ConsulSidecarService{
Tags: []string{"foo", "bar"},
Port: "port1",
Proxy: &ConsulProxy{LocalServiceAddress: "10.0.0.1"},
}
result := s.Copy()
require.Equal(t, &ConsulSidecarService{
Tags: []string{"foo", "bar"},
Port: "port1",
Proxy: &ConsulProxy{LocalServiceAddress: "10.0.0.1"},
}, result)
})
}