Merge pull request #12189 from hashicorp/f-gh-264

jobspec: add service block provider parameter
This commit is contained in:
James Rasell 2022-03-14 13:18:51 +01:00 committed by GitHub
commit c00976acc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 617 additions and 65 deletions

View File

@ -742,6 +742,7 @@ func TestJobs_Canonicalize(t *testing.T) {
PortLabel: "db",
AddressMode: "auto",
OnUpdate: "require_healthy",
Provider: "consul",
Checks: []ServiceCheck{
{
Name: "alive",

View File

@ -116,12 +116,21 @@ type Service struct {
CanaryMeta map[string]string `hcl:"canary_meta,block"`
TaskName string `mapstructure:"task" hcl:"task,optional"`
OnUpdate string `mapstructure:"on_update" hcl:"on_update,optional"`
// Provider defines which backend system provides the service registration
// mechanism for this service. This supports either structs.ProviderConsul
// or structs.ProviderNomad and defaults for the former.
Provider string `hcl:"provider,optional"`
}
const (
OnUpdateRequireHealthy = "require_healthy"
OnUpdateIgnoreWarn = "ignore_warnings"
OnUpdateIgnore = "ignore"
// ServiceProviderConsul is the default provider for services when no
// parameter is set.
ServiceProviderConsul = "consul"
)
// Canonicalize the Service by ensuring its name and address mode are set. Task
@ -145,6 +154,11 @@ func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) {
s.OnUpdate = OnUpdateRequireHealthy
}
// Default the service provider.
if s.Provider == "" {
s.Provider = ServiceProviderConsul
}
s.Connect.Canonicalize()
// Canonicalize CheckRestart on Checks and merge Service.CheckRestart

View File

@ -21,6 +21,7 @@ func TestService_Canonicalize(t *testing.T) {
require.Equal(t, fmt.Sprintf("%s-%s-%s", *j.Name, *tg.Name, task.Name), s.Name)
require.Equal(t, "auto", s.AddressMode)
require.Equal(t, OnUpdateRequireHealthy, s.OnUpdate)
require.Equal(t, ServiceProviderConsul, s.Provider)
}
func TestServiceCheck_Canonicalize(t *testing.T) {

View File

@ -1372,6 +1372,7 @@ func ApiServicesToStructs(in []*api.Service, group bool) []*structs.Service {
Meta: helper.CopyMapStringString(s.Meta),
CanaryMeta: helper.CopyMapStringString(s.CanaryMeta),
OnUpdate: s.OnUpdate,
Provider: s.Provider,
}
if l := len(s.Checks); l != 0 {

View File

@ -2902,6 +2902,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Services: []*structs.Service{
{
Name: "groupserviceA",
Provider: "consul",
Tags: []string{"a", "b"},
CanaryTags: []string{"d", "e"},
EnableTagOverride: true,
@ -2993,6 +2994,7 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) {
Services: []*structs.Service{
{
Name: "serviceA",
Provider: "consul",
Tags: []string{"1", "2"},
CanaryTags: []string{"3", "4"},
EnableTagOverride: true,

View File

@ -51,6 +51,7 @@ func parseService(o *ast.ObjectItem) (*api.Service, error) {
"meta",
"canary_meta",
"on_update",
"provider",
}
if err := checkHCLKeys(o.Val, valid); err != nil {
return nil, err

View File

@ -1763,6 +1763,32 @@ func TestParse(t *testing.T) {
},
false,
},
{
"service-provider.hcl",
&api.Job{
ID: stringToPtr("service-provider"),
Name: stringToPtr("service-provider"),
TaskGroups: []*api.TaskGroup{
{
Count: intToPtr(5),
Name: stringToPtr("group"),
Tasks: []*api.Task{
{
Name: "task",
Driver: "docker",
Services: []*api.Service{
{
Name: "service-provider",
Provider: "nomad",
},
},
},
},
},
},
},
false,
},
}
for _, tc := range cases {

View File

@ -0,0 +1,14 @@
job "service-provider" {
group "group" {
count = 5
task "task" {
driver = "docker"
service {
name = "service-provider"
provider = "nomad"
}
}
}
}

View File

@ -70,13 +70,17 @@ func (j *Job) ConsulUsages() map[string]*ConsulUsage {
// Gather group services
for _, service := range tg.Services {
m[namespace].Services = append(m[namespace].Services, service.Name)
if service.Provider == ServiceProviderConsul {
m[namespace].Services = append(m[namespace].Services, service.Name)
}
}
// Gather task services and KV usage
for _, task := range tg.Tasks {
for _, service := range task.Services {
m[namespace].Services = append(m[namespace].Services, service.Name)
if service.Provider == ServiceProviderConsul {
m[namespace].Services = append(m[namespace].Services, service.Name)
}
}
if len(task.Templates) > 0 {
m[namespace].KV = true

View File

@ -2872,6 +2872,12 @@ func TestTaskGroupDiff(t *testing.T) {
Old: "",
New: "",
},
{
Type: DiffTypeNone,
Name: "Provider",
Old: "",
New: "",
},
{
Type: DiffTypeEdited,
Name: "TaskName",
@ -5601,6 +5607,10 @@ func TestTaskDiff(t *testing.T) {
Old: "foo",
New: "bar",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeAdded,
Name: "TaskName",
@ -5745,6 +5755,10 @@ func TestTaskDiff(t *testing.T) {
Type: DiffTypeNone,
Name: "PortLabel",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -6264,6 +6278,10 @@ func TestTaskDiff(t *testing.T) {
Old: "",
New: "",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -7347,6 +7365,10 @@ func TestServicesDiff(t *testing.T) {
Old: "http",
New: "https",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -7432,6 +7454,10 @@ func TestServicesDiff(t *testing.T) {
Name: "PortLabel",
New: "http",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -7491,6 +7517,10 @@ func TestServicesDiff(t *testing.T) {
Name: "PortLabel",
New: "https",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -7556,6 +7586,9 @@ func TestServicesDiff(t *testing.T) {
Name: "PortLabel",
Old: "http",
New: "https-redirect",
}, {
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
@ -7627,6 +7660,10 @@ func TestServicesDiff(t *testing.T) {
Old: "http",
New: "http",
},
{
Type: DiffTypeNone,
Name: "Provider",
},
{
Type: DiffTypeNone,
Name: "TaskName",
@ -7654,6 +7691,72 @@ func TestServicesDiff(t *testing.T) {
},
},
},
{
Name: "Service with different provider",
Contextual: true,
Old: []*Service{
{
Name: "webapp",
Provider: "nomad",
PortLabel: "http",
},
},
New: []*Service{
{
Name: "webapp",
Provider: "consul",
PortLabel: "http",
},
},
Expected: []*ObjectDiff{
{
Type: DiffTypeEdited,
Name: "Service",
Fields: []*FieldDiff{
{
Type: DiffTypeNone,
Name: "AddressMode",
},
{
Type: DiffTypeNone,
Name: "EnableTagOverride",
Old: "false",
New: "false",
},
{
Type: DiffTypeNone,
Name: "Name",
Old: "webapp",
New: "webapp",
},
{
Type: DiffTypeNone,
Name: "Namespace",
},
{
Type: DiffTypeNone,
Name: "OnUpdate",
},
{
Type: DiffTypeNone,
Name: "PortLabel",
Old: "http",
New: "http",
},
{
Type: DiffTypeEdited,
Name: "Provider",
Old: "nomad",
New: "consul",
},
{
Type: DiffTypeNone,
Name: "TaskName",
},
},
},
},
},
}
for _, c := range cases {

View File

@ -420,6 +420,15 @@ const (
AddressModeHost = "host"
AddressModeDriver = "driver"
AddressModeAlloc = "alloc"
// ServiceProviderConsul is the default service provider and the way Nomad
// worked before native service discovery.
ServiceProviderConsul = "consul"
// ServiceProviderNomad is the native service discovery provider. At the
// time of writing, there are a number of restrictions around its
// functionality and use.
ServiceProviderNomad = "nomad"
)
// Service represents a Consul service definition
@ -468,6 +477,11 @@ type Service struct {
// OnUpdate Specifies how the service and its checks should be evaluated
// during an update
OnUpdate string
// Provider dictates which service discovery provider to use. This can be
// either ServiceProviderConsul or ServiceProviderNomad and defaults to the former when
// left empty by the operator.
Provider string
}
const (
@ -504,7 +518,7 @@ func (s *Service) Copy() *Service {
// Canonicalize interpolates values of Job, Task Group and Task in the Service
// Name. This also generates check names, service id and check ids.
func (s *Service) Canonicalize(job string, taskGroup string, task string) {
func (s *Service) Canonicalize(job, taskGroup, task, jobNamespace string) {
// Ensure empty lists are treated as null to avoid scheduler issues when
// using DeepEquals
if len(s.Tags) == 0 {
@ -528,10 +542,23 @@ func (s *Service) Canonicalize(job string, taskGroup string, task string) {
check.Canonicalize(s.Name)
}
// Set the provider to its default value. The value of consul ensures this
// new feature and parameter behaves in the same manner a previous versions
// which did not include this.
if s.Provider == "" {
s.Provider = ServiceProviderConsul
}
// Consul API returns "default" whether the namespace is empty or set as
// such, so we coerce our copy of the service to be the same.
if s.Namespace == "" {
// such, so we coerce our copy of the service to be the same if using the
// consul provider.
//
// When using ServiceProviderNomad, set the namespace to that of the job. This
// makes modifications and diffs on the service correct.
if s.Namespace == "" && s.Provider == ServiceProviderConsul {
s.Namespace = "default"
} else if s.Provider == ServiceProviderNomad {
s.Namespace = jobNamespace
}
}
@ -563,6 +590,26 @@ func (s *Service) Validate() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service on_update must be %q, %q, or %q; not %q", OnUpdateRequireHealthy, OnUpdateIgnoreWarn, OnUpdateIgnore, s.OnUpdate))
}
// Up until this point, all service validation has been independent of the
// provider. From this point on, we have different validation paths. We can
// also catch an incorrect provider parameter.
switch s.Provider {
case ServiceProviderConsul:
s.validateConsulService(&mErr)
case ServiceProviderNomad:
s.validateNomadService(&mErr)
default:
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service provider must be %q, or %q; not %q",
ServiceProviderConsul, ServiceProviderNomad, s.Provider))
}
return mErr.ErrorOrNil()
}
// validateConsulService performs validation on a service which is using the
// consul provider.
func (s *Service) validateConsulService(mErr *multierror.Error) {
// check checks
for _, c := range s.Checks {
if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() {
@ -595,8 +642,23 @@ func (s *Service) Validate() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Service %s is Connect Native and requires setting the task", s.Name))
}
}
}
return mErr.ErrorOrNil()
// validateNomadService performs validation on a service which is using the
// nomad provider.
func (s *Service) validateNomadService(mErr *multierror.Error) {
// Service blocks for the Nomad provider do not support checks. We perform
// a nil check, as an empty check list is nilled within the service
// canonicalize function.
if s.Checks != nil {
mErr.Errors = append(mErr.Errors, errors.New("Service with provider nomad cannot include Check blocks"))
}
// Services using the Nomad provider do not support Consul connect.
if s.Connect != nil {
mErr.Errors = append(mErr.Errors, errors.New("Service with provider nomad cannot include Connect blocks"))
}
}
// ValidateName checks if the service Name is valid and should be called after
@ -614,7 +676,8 @@ func (s *Service) ValidateName(name string) error {
}
// Hash returns a base32 encoded hash of a Service's contents excluding checks
// as they're hashed independently.
// as they're hashed independently and the provider in order to not cause churn
// during cluster upgrades.
func (s *Service) Hash(allocID, taskName string, canary bool) string {
h := sha1.New()
hashString(h, allocID)
@ -632,6 +695,11 @@ func (s *Service) Hash(allocID, taskName string, canary bool) string {
hashString(h, s.OnUpdate)
hashString(h, s.Namespace)
// Don't hash the provider parameter, so we don't cause churn of all
// registered services when upgrading Nomad versions. The provider is not
// used at the level the hash is and therefore is not needed to tell
// whether the service has changed.
// Base32 is used for encoding the hash as sha1 hashes can always be
// encoded without padding, only 4 bytes larger than base64, and saves
// 8 bytes vs hex. Since these hashes are used in Consul URLs it's nice
@ -687,6 +755,10 @@ func (s *Service) Equals(o *Service) bool {
return s == o
}
if s.Provider != o.Provider {
return false
}
if s.Namespace != o.Namespace {
return false
}

View File

@ -1,6 +1,8 @@
package structs
import (
"errors"
"github.com/hashicorp/go-multierror"
"testing"
"time"
@ -1472,3 +1474,81 @@ func TestConsulMeshGateway_Validate(t *testing.T) {
require.NoError(t, err)
})
}
func TestService_validateNomadService(t *testing.T) {
t.Parallel()
testCases := []struct {
inputService *Service
inputErr *multierror.Error
expectedOutputErrors []error
name string
}{
{
inputService: &Service{
Name: "webapp",
PortLabel: "http",
Namespace: "default",
Provider: "nomad",
},
inputErr: &multierror.Error{},
expectedOutputErrors: []error{},
name: "valid service",
},
{
inputService: &Service{
Name: "webapp",
PortLabel: "http",
Namespace: "default",
Provider: "nomad",
Checks: []*ServiceCheck{
{Name: "some-check"},
},
},
inputErr: &multierror.Error{},
expectedOutputErrors: []error{errors.New("Service with provider nomad cannot include Check blocks")},
name: "invalid service due to checks",
},
{
inputService: &Service{
Name: "webapp",
PortLabel: "http",
Namespace: "default",
Provider: "nomad",
Connect: &ConsulConnect{
Native: true,
},
},
inputErr: &multierror.Error{},
expectedOutputErrors: []error{errors.New("Service with provider nomad cannot include Connect blocks")},
name: "invalid service due to connect",
},
{
inputService: &Service{
Name: "webapp",
PortLabel: "http",
Namespace: "default",
Provider: "nomad",
Connect: &ConsulConnect{
Native: true,
},
Checks: []*ServiceCheck{
{Name: "some-check"},
},
},
inputErr: &multierror.Error{},
expectedOutputErrors: []error{
errors.New("Service with provider nomad cannot include Check blocks"),
errors.New("Service with provider nomad cannot include Connect blocks"),
},
name: "invalid service due to checks and connect",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.inputService.validateNomadService(tc.inputErr)
require.ElementsMatch(t, tc.expectedOutputErrors, tc.inputErr.Errors)
})
}
}

View File

@ -6195,7 +6195,7 @@ func (tg *TaskGroup) Canonicalize(job *Job) {
}
for _, service := range tg.Services {
service.Canonicalize(job.Name, tg.Name, "group")
service.Canonicalize(job.Name, tg.Name, "group", job.Namespace)
}
for _, network := range tg.Networks {
@ -6491,6 +6491,10 @@ func (tg *TaskGroup) validateServices() error {
var mErr multierror.Error
knownTasks := make(map[string]struct{})
// Track the providers used for this task group. Currently, Nomad only
// allows the use of a single service provider within a task group.
configuredProviders := make(map[string]struct{})
// Create a map of known tasks and their services so we can compare
// vs the group-level services and checks
for _, task := range tg.Tasks {
@ -6504,9 +6508,22 @@ func (tg *TaskGroup) validateServices() error {
mErr.Errors = append(mErr.Errors, fmt.Errorf("Check %s is invalid: only task group service checks can be assigned tasks", check.Name))
}
}
// Add the service provider to the tracking, if it has not already
// been seen.
if _, ok := configuredProviders[service.Provider]; !ok {
configuredProviders[service.Provider] = struct{}{}
}
}
}
for i, service := range tg.Services {
// Add the service provider to the tracking, if it has not already been
// seen.
if _, ok := configuredProviders[service.Provider]; !ok {
configuredProviders[service.Provider] = struct{}{}
}
if err := service.Validate(); err != nil {
outer := fmt.Errorf("Service[%d] %s validation failed: %s", i, service.Name, err)
mErr.Errors = append(mErr.Errors, outer)
@ -6535,6 +6552,15 @@ func (tg *TaskGroup) validateServices() error {
}
}
}
// The initial feature release of native service discovery only allows for
// a single service provider to be used across all services in a task
// group.
if len(configuredProviders) > 1 {
mErr.Errors = append(mErr.Errors,
errors.New("Multiple service providers used: task group services must use the same provider"))
}
return mErr.ErrorOrNil()
}
@ -6946,7 +6972,7 @@ func (t *Task) Canonicalize(job *Job, tg *TaskGroup) {
}
for _, service := range t.Services {
service.Canonicalize(job.Name, tg.Name, t.Name)
service.Canonicalize(job.Name, tg.Name, t.Name, job.Namespace)
}
// If Resources are nil initialize them to defaults, otherwise canonicalize

View File

@ -371,6 +371,7 @@ func testJob() *Job {
{
Name: "${TASK}-frontend",
PortLabel: "http",
Provider: "consul",
},
},
Tasks: []*Task{
@ -1203,7 +1204,8 @@ func TestTaskGroup_Validate(t *testing.T) {
Name: "group-a",
Services: []*Service{
{
Name: "service-a",
Name: "service-a",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "check-a",
@ -1225,6 +1227,47 @@ func TestTaskGroup_Validate(t *testing.T) {
expected = `Check check-a invalid: only script and gRPC checks should have tasks`
require.Contains(t, err.Error(), expected)
tg = &TaskGroup{
Name: "group-a",
Services: []*Service{
{
Name: "service-a",
Provider: "nomad",
},
{
Name: "service-b",
Provider: "consul",
},
},
Tasks: []*Task{{Name: "task-a"}},
}
err = tg.Validate(&Job{})
expected = "Multiple service providers used: task group services must use the same provider"
require.Contains(t, err.Error(), expected)
tg = &TaskGroup{
Name: "group-a",
Services: []*Service{
{
Name: "service-a",
Provider: "nomad",
},
},
Tasks: []*Task{
{
Name: "task-a",
Services: []*Service{
{
Name: "service-b",
Provider: "consul",
},
},
},
},
}
err = tg.Validate(&Job{})
expected = "Multiple service providers used: task group services must use the same provider"
require.Contains(t, err.Error(), expected)
}
func TestTaskGroupNetwork_Validate(t *testing.T) {
@ -1689,6 +1732,7 @@ func TestNetworkResource_Copy(t *testing.T) {
func TestTask_Validate_Services(t *testing.T) {
s1 := &Service{
Name: "service-name",
Provider: "consul",
PortLabel: "bar",
Checks: []*ServiceCheck{
{
@ -1711,15 +1755,18 @@ func TestTask_Validate_Services(t *testing.T) {
s2 := &Service{
Name: "service-name",
Provider: "consul",
PortLabel: "bar",
}
s3 := &Service{
Name: "service-A",
Provider: "consul",
PortLabel: "a",
}
s4 := &Service{
Name: "service-A",
Provider: "consul",
PortLabel: "b",
}
@ -1812,25 +1859,30 @@ func TestTask_Validate_Service_AddressMode_Ok(t *testing.T) {
{
// https://github.com/hashicorp/nomad/issues/3681#issuecomment-357274177
Name: "DriverModeWithLabel",
Provider: "consul",
PortLabel: "http",
AddressMode: AddressModeDriver,
},
{
Name: "DriverModeWithPort",
Provider: "consul",
PortLabel: "80",
AddressMode: AddressModeDriver,
},
{
Name: "HostModeWithLabel",
Provider: "consul",
PortLabel: "http",
AddressMode: AddressModeHost,
},
{
Name: "HostModeWithoutLabel",
Provider: "consul",
AddressMode: AddressModeHost,
},
{
Name: "DriverModeWithoutLabel",
Provider: "consul",
AddressMode: AddressModeDriver,
},
}
@ -2030,6 +2082,7 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
{
Service: &Service{
Name: "invalid-driver",
Provider: "consul",
PortLabel: "80",
AddressMode: "host",
},
@ -2054,6 +2107,7 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
{
Service: &Service{
Name: "http-driver-fail-2",
Provider: "consul",
PortLabel: "80",
AddressMode: "driver",
Checks: []*ServiceCheck{
@ -2071,6 +2125,7 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
{
Service: &Service{
Name: "http-driver-fail-3",
Provider: "consul",
PortLabel: "80",
AddressMode: "driver",
Checks: []*ServiceCheck{
@ -2088,6 +2143,7 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
{
Service: &Service{
Name: "http-driver-passes",
Provider: "consul",
PortLabel: "80",
AddressMode: "driver",
Checks: []*ServiceCheck{
@ -2117,7 +2173,8 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
},
{
Service: &Service{
Name: "empty-address-3673-passes-1",
Name: "empty-address-3673-passes-1",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "valid-port-label",
@ -2143,7 +2200,8 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) {
},
{
Service: &Service{
Name: "empty-address-3673-fails",
Name: "empty-address-3673-fails",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "empty-is-not-ok",
@ -2192,8 +2250,9 @@ func TestTask_Validate_Service_Check_GRPC(t *testing.T) {
Timeout: time.Second,
}
service := &Service{
Name: "test",
Checks: []*ServiceCheck{invalidGRPC},
Name: "test",
Provider: "consul",
Checks: []*ServiceCheck{invalidGRPC},
}
assert.Error(t, service.Validate())
@ -3119,6 +3178,7 @@ func BenchmarkEncodeDecode(b *testing.B) {
func TestInvalidServiceCheck(t *testing.T) {
s := Service{
Name: "service-name",
Provider: "consul",
PortLabel: "bar",
Checks: []*ServiceCheck{
{
@ -3133,6 +3193,7 @@ func TestInvalidServiceCheck(t *testing.T) {
s = Service{
Name: "service.name",
Provider: "consul",
PortLabel: "bar",
}
if err := s.ValidateName(s.Name); err == nil {
@ -3141,6 +3202,7 @@ func TestInvalidServiceCheck(t *testing.T) {
s = Service{
Name: "-my-service",
Provider: "consul",
PortLabel: "bar",
}
if err := s.Validate(); err == nil {
@ -3149,6 +3211,7 @@ func TestInvalidServiceCheck(t *testing.T) {
s = Service{
Name: "my-service-${NOMAD_META_FOO}",
Provider: "consul",
PortLabel: "bar",
}
if err := s.Validate(); err != nil {
@ -3157,6 +3220,7 @@ func TestInvalidServiceCheck(t *testing.T) {
s = Service{
Name: "my_service-${NOMAD_META_FOO}",
Provider: "consul",
PortLabel: "bar",
}
if err := s.Validate(); err == nil {
@ -3165,6 +3229,7 @@ func TestInvalidServiceCheck(t *testing.T) {
s = Service{
Name: "abcdef0123456789-abcdef0123456789-abcdef0123456789-abcdef0123456",
Provider: "consul",
PortLabel: "bar",
}
if err := s.ValidateName(s.Name); err == nil {
@ -3172,7 +3237,8 @@ func TestInvalidServiceCheck(t *testing.T) {
}
s = Service{
Name: "service-name",
Name: "service-name",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "check-tcp",
@ -3194,7 +3260,8 @@ func TestInvalidServiceCheck(t *testing.T) {
}
s = Service{
Name: "service-name",
Name: "service-name",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "check-script",
@ -3210,7 +3277,8 @@ func TestInvalidServiceCheck(t *testing.T) {
}
s = Service{
Name: "service-name",
Name: "service-name",
Provider: "consul",
Checks: []*ServiceCheck{
{
Name: "tcp-check",
@ -3261,62 +3329,198 @@ func TestDistinctCheckID(t *testing.T) {
}
func TestService_Canonicalize(t *testing.T) {
job := "example"
taskGroup := "cache"
task := "redis"
s := Service{
Name: "${TASK}-db",
testCases := []struct {
inputService *Service
inputJob string
inputTaskGroup string
inputTask string
inputJobNamespace string
expectedOutputService *Service
name string
}{
{
inputService: &Service{
Name: "${TASK}-db",
},
inputJob: "example",
inputTaskGroup: "cache",
inputTask: "redis",
inputJobNamespace: "platform",
expectedOutputService: &Service{
Name: "redis-db",
Provider: "consul",
Namespace: "default",
},
name: "interpolate task in name",
},
{
inputService: &Service{
Name: "db",
},
inputJob: "example",
inputTaskGroup: "cache",
inputTask: "redis",
inputJobNamespace: "platform",
expectedOutputService: &Service{
Name: "db",
Provider: "consul",
Namespace: "default",
},
name: "no interpolation in name",
},
{
inputService: &Service{
Name: "${JOB}-${TASKGROUP}-${TASK}-db",
},
inputJob: "example",
inputTaskGroup: "cache",
inputTask: "redis",
inputJobNamespace: "platform",
expectedOutputService: &Service{
Name: "example-cache-redis-db",
Provider: "consul",
Namespace: "default",
},
name: "interpolate job, taskgroup and task in name",
},
{
inputService: &Service{
Name: "${BASE}-db",
},
inputJob: "example",
inputTaskGroup: "cache",
inputTask: "redis",
inputJobNamespace: "platform",
expectedOutputService: &Service{
Name: "example-cache-redis-db",
Provider: "consul",
Namespace: "default",
},
name: "interpolate base in name",
},
{
inputService: &Service{
Name: "db",
Provider: "nomad",
},
inputJob: "example",
inputTaskGroup: "cache",
inputTask: "redis",
inputJobNamespace: "platform",
expectedOutputService: &Service{
Name: "db",
Provider: "nomad",
Namespace: "platform",
},
name: "nomad provider",
},
}
s.Canonicalize(job, taskGroup, task)
if s.Name != "redis-db" {
t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.inputService.Canonicalize(tc.inputJob, tc.inputTaskGroup, tc.inputTask, tc.inputJobNamespace)
assert.Equal(t, tc.expectedOutputService, tc.inputService)
})
}
s.Name = "db"
s.Canonicalize(job, taskGroup, task)
if s.Name != "db" {
t.Fatalf("Expected name: %v, Actual: %v", "redis-db", s.Name)
}
s.Name = "${JOB}-${TASKGROUP}-${TASK}-db"
s.Canonicalize(job, taskGroup, task)
if s.Name != "example-cache-redis-db" {
t.Fatalf("Expected name: %v, Actual: %v", "example-cache-redis-db", s.Name)
}
s.Name = "${BASE}-db"
s.Canonicalize(job, taskGroup, task)
if s.Name != "example-cache-redis-db" {
t.Fatalf("Expected name: %v, Actual: %v", "example-cache-redis-db", s.Name)
}
}
func TestService_Validate(t *testing.T) {
s := Service{
Name: "testservice",
testCases := []struct {
inputService *Service
expectedError bool
expectedErrorContains string
name string
}{
{
inputService: &Service{
Name: "testservice",
},
expectedError: false,
name: "base service",
},
{
inputService: &Service{
Name: "testservice",
Connect: &ConsulConnect{
Native: true,
},
},
expectedError: true,
expectedErrorContains: "Connect Native and requires setting the task",
name: "Native Connect without task name",
},
{
inputService: &Service{
Name: "testservice",
TaskName: "testtask",
Connect: &ConsulConnect{
Native: true,
},
},
expectedError: false,
name: "Native Connect with task name",
},
{
inputService: &Service{
Name: "testservice",
TaskName: "testtask",
Connect: &ConsulConnect{
Native: true,
SidecarService: &ConsulSidecarService{},
},
},
expectedError: true,
expectedErrorContains: "Consul Connect must be exclusively native",
name: "Native Connect with Sidecar",
},
{
inputService: &Service{
Name: "testservice",
Provider: "nomad",
Checks: []*ServiceCheck{
{
Name: "servicecheck",
},
},
},
expectedError: true,
expectedErrorContains: "Service with provider nomad cannot include Check blocks",
name: "provider nomad with checks",
},
{
inputService: &Service{
Name: "testservice",
Provider: "nomad",
Connect: &ConsulConnect{
Native: true,
},
},
expectedError: true,
expectedErrorContains: "Service with provider nomad cannot include Connect blocks",
name: "provider nomad with connect",
},
{
inputService: &Service{
Name: "testservice",
Provider: "nomad",
},
expectedError: false,
name: "provider nomad valid",
},
}
s.Canonicalize("testjob", "testgroup", "testtask")
// Base service should be valid
require.NoError(t, s.Validate())
// Native Connect requires task name on service
s.Connect = &ConsulConnect{
Native: true,
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.inputService.Canonicalize("testjob", "testgroup", "testtask", "testnamespace")
err := tc.inputService.Validate()
if tc.expectedError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrorContains)
} else {
assert.NoError(t, err)
}
})
}
require.Error(t, s.Validate())
// Native Connect should work with task name on service set
s.TaskName = "testtask"
require.NoError(t, s.Validate())
// Native Connect + Sidecar should be invalid
s.Connect.SidecarService = &ConsulSidecarService{}
require.Error(t, s.Validate())
}
func TestService_Equals(t *testing.T) {
@ -3324,7 +3528,7 @@ func TestService_Equals(t *testing.T) {
Name: "testservice",
}
s.Canonicalize("testjob", "testgroup", "testtask")
s.Canonicalize("testjob", "testgroup", "testtask", "default")
o := s.Copy()
@ -3362,6 +3566,9 @@ func TestService_Equals(t *testing.T) {
o.EnableTagOverride = true
assertDiff()
o.Provider = "nomad"
assertDiff()
}
func TestJob_ExpandServiceNames(t *testing.T) {