diff --git a/.changelog/12602.txt b/.changelog/12602.txt new file mode 100644 index 000000000..3d32d0fa2 --- /dev/null +++ b/.changelog/12602.txt @@ -0,0 +1,3 @@ +```release-note:improvement +consul: Added implicit Consul constraint for task groups utilising Consul service and check registrations +``` diff --git a/client/alloc_watcher_e2e_test.go b/client/alloc_watcher_e2e_test.go index c36afd7a8..18cac24a7 100644 --- a/client/alloc_watcher_e2e_test.go +++ b/client/alloc_watcher_e2e_test.go @@ -72,6 +72,8 @@ func TestPrevAlloc_StreamAllocDir_TLS(t *testing.T) { Operand: "=", }, } + job.TaskGroups[0].Constraints = nil + job.TaskGroups[0].Tasks[0].Services = nil job.TaskGroups[0].Count = 1 job.TaskGroups[0].EphemeralDisk.Sticky = true job.TaskGroups[0].EphemeralDisk.Migrate = true diff --git a/client/client_test.go b/client/client_test.go index e6f2e4579..8750b9531 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -441,6 +441,7 @@ func TestClient_UpdateAllocStatus(t *testing.T) { job := mock.Job() // allow running job on any node including self client, that may not be a Linux box job.Constraints = nil + job.TaskGroups[0].Constraints = nil job.TaskGroups[0].Count = 1 task := job.TaskGroups[0].Tasks[0] task.Driver = "mock_driver" diff --git a/command/integration_test.go b/command/integration_test.go index 1cf207010..1f1945e9d 100644 --- a/command/integration_test.go +++ b/command/integration_test.go @@ -53,7 +53,7 @@ func TestIntegration_Command_RoundTripJob(t *testing.T) { defer srv.Shutdown() { - cmd := exec.Command("nomad", "job", "init") + cmd := exec.Command("nomad", "job", "init", "-short") cmd.Dir = tmpDir assert.Nil(cmd.Run()) } diff --git a/nomad/job_endpoint_hooks.go b/nomad/job_endpoint_hooks.go index 4d1989dfd..5ad972777 100644 --- a/nomad/job_endpoint_hooks.go +++ b/nomad/job_endpoint_hooks.go @@ -24,6 +24,16 @@ var ( Operand: structs.ConstraintSemver, } + // consulServiceDiscoveryConstraint is the implicit constraint added to + // task groups which include services utilising the Consul provider. The + // Consul version is pinned to a minimum of that which introduced the + // namespace feature. + consulServiceDiscoveryConstraint = &structs.Constraint{ + LTarget: "${attr.consul.version}", + RTarget: ">= 1.7.0", + Operand: structs.ConstraintSemver, + } + // nativeServiceDiscoveryConstraint is the constraint injected into task // groups that utilise Nomad's native service discovery feature. This is // needed, as operators can disable the client functionality, and therefore @@ -134,79 +144,96 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro // Identify which task groups are utilising Nomad native service discovery. nativeServiceDisco := j.RequiredNativeServiceDiscovery() + // Identify which task groups are utilising Consul service discovery. + consulServiceDisco := j.RequiredConsulServiceDiscovery() + // Hot path - if len(signals) == 0 && len(vaultBlocks) == 0 && len(nativeServiceDisco) == 0 { + if len(signals) == 0 && len(vaultBlocks) == 0 && + len(nativeServiceDisco) == 0 && len(consulServiceDisco) == 0 { return j, nil, nil } - // Add Vault constraints if no Vault constraint exists + // Iterate through all the task groups within the job and add any required + // constraints. When adding new implicit constraints, they should go inside + // this single loop, with a new constraintMatcher if needed. for _, tg := range j.TaskGroups { - _, ok := vaultBlocks[tg.Name] - if !ok { - // Not requesting Vault - continue + + // If the task group utilises Vault, run the mutator. + if _, ok := vaultBlocks[tg.Name]; ok { + mutateConstraint(constraintMatcherLeft, tg, vaultConstraint) } - found := false - for _, c := range tg.Constraints { - if c.LTarget == vaultConstraintLTarget { - found = true - break - } + // Check whether the task group is using signals. In the case that it + // is, we flatten the signals and build a constraint, then run the + // mutator. + if tgSignals, ok := signals[tg.Name]; ok { + required := helper.MapStringStringSliceValueSet(tgSignals) + sigConstraint := getSignalConstraint(required) + mutateConstraint(constraintMatcherFull, tg, sigConstraint) } - if !found { - tg.Constraints = append(tg.Constraints, vaultConstraint) - } - } - - // Add signal constraints - for _, tg := range j.TaskGroups { - tgSignals, ok := signals[tg.Name] - if !ok { - // Not requesting signal - continue + // If the task group utilises Nomad service discovery, run the mutator. + if ok := nativeServiceDisco[tg.Name]; ok { + mutateConstraint(constraintMatcherFull, tg, nativeServiceDiscoveryConstraint) } - // Flatten the signals - required := helper.MapStringStringSliceValueSet(tgSignals) - sigConstraint := getSignalConstraint(required) - - found := false - for _, c := range tg.Constraints { - if c.Equals(sigConstraint) { - found = true - break - } - } - - if !found { - tg.Constraints = append(tg.Constraints, sigConstraint) - } - } - - // Add the Nomad service discovery constraints. - for _, tg := range j.TaskGroups { - if ok := nativeServiceDisco[tg.Name]; !ok { - continue - } - - found := false - for _, c := range tg.Constraints { - if c.Equals(nativeServiceDiscoveryConstraint) { - found = true - break - } - } - - if !found { - tg.Constraints = append(tg.Constraints, nativeServiceDiscoveryConstraint) + // If the task group utilises Consul service discovery, run the mutator. + if ok := consulServiceDisco[tg.Name]; ok { + mutateConstraint(constraintMatcherLeft, tg, consulServiceDiscoveryConstraint) } } return j, nil, nil } +// constraintMatcher is a custom type which helps control how constraints are +// identified as being present within a task group. +type constraintMatcher uint + +const ( + // constraintMatcherFull ensures that a constraint is only considered found + // when they match totally. This check is performed using the + // structs.Constraint Equals function. + constraintMatcherFull constraintMatcher = iota + + // constraintMatcherLeft ensure that a constraint is considered found if + // the constraints LTarget is matched only. This allows an existing + // constraint to override the proposed implicit one. + constraintMatcherLeft +) + +// mutateConstraint is a generic mutator used to set implicit constraints +// within the task group if they are needed. +func mutateConstraint(matcher constraintMatcher, taskGroup *structs.TaskGroup, constraint *structs.Constraint) { + + var found bool + + // It's possible to switch on the matcher within the constraint loop to + // reduce repetition. This, however, means switching per constraint, + // therefore we do it here. + switch matcher { + case constraintMatcherFull: + for _, c := range taskGroup.Constraints { + if c.Equals(constraint) { + found = true + break + } + } + case constraintMatcherLeft: + for _, c := range taskGroup.Constraints { + if c.LTarget == constraint.LTarget { + found = true + break + } + } + } + + // If we didn't find a suitable constraint match, add one. + if !found { + taskGroup.Constraints = append(taskGroup.Constraints, constraint) + } +} + // jobValidate validates a Job and task drivers and returns an error if there is // a validation problem or if the Job is of a type a user is not allowed to // submit. diff --git a/nomad/job_endpoint_hooks_test.go b/nomad/job_endpoint_hooks_test.go index f93637287..9d84fcad9 100644 --- a/nomad/job_endpoint_hooks_test.go +++ b/nomad/job_endpoint_hooks_test.go @@ -39,6 +39,406 @@ func Test_jobImpliedConstraints_Mutate(t *testing.T) { expectedOutputError: nil, name: "no needed constraints", }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{}, + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Vault: &structs.Vault{}, + Name: "group1-task1", + }, + }, + Constraints: []*structs.Constraint{vaultConstraint}, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task with vault", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Vault: &structs.Vault{}, + Name: "group1-task1", + }, + { + Vault: &structs.Vault{}, + Name: "group1-task2", + }, + { + Vault: &structs.Vault{}, + Name: "group1-task3", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Vault: &structs.Vault{}, + Name: "group1-task1", + }, + { + Vault: &structs.Vault{}, + Name: "group1-task2", + }, + { + Vault: &structs.Vault{}, + Name: "group1-task3", + }, + }, + Constraints: []*structs.Constraint{vaultConstraint}, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "group with multiple tasks with vault", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + }, + }, + }, + { + Name: "group2", + Tasks: []*structs.Task{ + { + Name: "group2-task1", + Vault: &structs.Vault{}, + }, + }, + }, + { + Name: "group3", + Tasks: []*structs.Task{ + { + Name: "group3-task1", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + }, + }, + }, + { + Name: "group2", + Tasks: []*structs.Task{ + { + Name: "group2-task1", + Vault: &structs.Vault{}, + }, + }, + Constraints: []*structs.Constraint{vaultConstraint}, + }, + { + Name: "group3", + Tasks: []*structs.Task{ + { + Name: "group3-task1", + }, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "multiple groups only one with vault", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{}, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: vaultConstraintLTarget, + RTarget: ">= 1.0.0", + Operand: structs.ConstraintSemver, + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{}, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: vaultConstraintLTarget, + RTarget: ">= 1.0.0", + Operand: structs.ConstraintSemver, + }, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "existing vault version constraint", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{}, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${node.class}", + RTarget: "high-memory", + Operand: "=", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "group1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{}, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${node.class}", + RTarget: "high-memory", + Operand: "=", + }, + vaultConstraint, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "vault with other constraints", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{ + ChangeSignal: "SIGINT", + ChangeMode: structs.VaultChangeModeSignal, + }, + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Vault: &structs.Vault{ + ChangeSignal: "SIGINT", + ChangeMode: structs.VaultChangeModeSignal, + }, + }, + }, + Constraints: []*structs.Constraint{ + vaultConstraint, + { + LTarget: "${attr.os.signals}", + RTarget: "SIGINT", + Operand: "set_contains", + }, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task with vault signal change", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + KillSignal: "SIGINT", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + KillSignal: "SIGINT", + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${attr.os.signals}", + RTarget: "SIGINT", + Operand: "set_contains", + }, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task with kill signal", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Templates: []*structs.Template{ + { + ChangeMode: "signal", + ChangeSignal: "SIGINT", + }, + }, + }, + { + Name: "group1-task2", + Templates: []*structs.Template{ + { + ChangeMode: "signal", + ChangeSignal: "SIGHUP", + }, + }, + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Tasks: []*structs.Task{ + { + Name: "group1-task1", + Templates: []*structs.Template{ + { + ChangeMode: "signal", + ChangeSignal: "SIGINT", + }, + }, + }, + { + Name: "group1-task2", + Templates: []*structs.Template{ + { + ChangeMode: "signal", + ChangeSignal: "SIGHUP", + }, + }, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${attr.os.signals}", + RTarget: "SIGHUP,SIGINT", + Operand: "set_contains", + }, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "multiple tasks with template signal change", + }, { inputJob: &structs.Job{ Name: "example", @@ -156,6 +556,155 @@ func Test_jobImpliedConstraints_Mutate(t *testing.T) { expectedOutputError: nil, name: "task group nomad discovery other constraints", }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + Constraints: []*structs.Constraint{consulServiceDiscoveryConstraint}, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task group Consul discovery", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + Constraints: []*structs.Constraint{consulServiceDiscoveryConstraint}, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + Constraints: []*structs.Constraint{consulServiceDiscoveryConstraint}, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task group Consul discovery constraint found", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${node.class}", + RTarget: "high-memory", + Operand: "=", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + Provider: structs.ServiceProviderConsul, + }, + }, + Constraints: []*structs.Constraint{ + { + LTarget: "${node.class}", + RTarget: "high-memory", + Operand: "=", + }, + consulServiceDiscoveryConstraint, + }, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task group Consul discovery other constraints", + }, + { + inputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + }, + }, + }, + }, + }, + expectedOutputJob: &structs.Job{ + Name: "example", + TaskGroups: []*structs.TaskGroup{ + { + Name: "example-group-1", + Services: []*structs.Service{ + { + Name: "example-group-service-1", + }, + }, + Constraints: []*structs.Constraint{consulServiceDiscoveryConstraint}, + }, + }, + }, + expectedOutputWarnings: nil, + expectedOutputError: nil, + name: "task group with empty provider", + }, } for _, tc := range testCases { diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 05a8a0f33..3a3cc1cd6 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -1646,15 +1646,13 @@ func TestJobEndpoint_Register_Vault_Policies(t *testing.T) { t.Fatalf("vault token not cleared") } - // Check that an implicit constraint was created + // Check that an implicit constraints were created for Vault and Consul. constraints := out.TaskGroups[0].Constraints - if l := len(constraints); l != 1 { + if l := len(constraints); l != 2 { t.Fatalf("Unexpected number of tests: %v", l) } - if !constraints[0].Equal(vaultConstraint) { - t.Fatalf("bad constraint; got %#v; want %#v", constraints[0], vaultConstraint) - } + require.ElementsMatch(t, constraints, []*structs.Constraint{consulServiceDiscoveryConstraint, vaultConstraint}) // Create the register request with another job asking for a vault policy but // send the root Vault token @@ -6482,15 +6480,11 @@ func TestJobEndpoint_ImplicitConstraints_Vault(t *testing.T) { t.Fatalf("index mis-match") } - // Check that there is an implicit vault constraint - constraints := out.TaskGroups[0].Constraints - if len(constraints) != 1 { - t.Fatalf("Expected an implicit constraint") - } - - if !constraints[0].Equal(vaultConstraint) { - t.Fatalf("Expected implicit vault constraint") - } + // Check that there is an implicit Vault and Consul constraint. + require.Len(t, out.TaskGroups[0].Constraints, 2) + require.ElementsMatch(t, out.TaskGroups[0].Constraints, []*structs.Constraint{ + consulServiceDiscoveryConstraint, vaultConstraint, + }) } func TestJobEndpoint_ValidateJob_ConsulConnect(t *testing.T) { @@ -6640,20 +6634,11 @@ func TestJobEndpoint_ImplicitConstraints_Signals(t *testing.T) { t.Fatalf("index mis-match") } - // Check that there is an implicit signal constraint - constraints := out.TaskGroups[0].Constraints - if len(constraints) != 1 { - t.Fatalf("Expected an implicit constraint") - } - - sigConstraint := getSignalConstraint([]string{signal1, signal2}) - if !strings.HasPrefix(sigConstraint.RTarget, "SIGHUP") { - t.Fatalf("signals not sorted: %v", sigConstraint.RTarget) - } - - if !constraints[0].Equal(sigConstraint) { - t.Fatalf("Expected implicit vault constraint") - } + // Check that there is an implicit signal and Consul constraint. + require.Len(t, out.TaskGroups[0].Constraints, 2) + require.ElementsMatch(t, out.TaskGroups[0].Constraints, []*structs.Constraint{ + getSignalConstraint([]string{signal1, signal2}), consulServiceDiscoveryConstraint}, + ) } func TestJobEndpoint_ValidateJobUpdate(t *testing.T) { diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index 9dee62843..e2202d4ef 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -34,6 +34,7 @@ func Node() *structs.Node { "nomad.version": "0.5.0", "driver.exec": "1", "driver.mock_driver": "1", + "consul.version": "1.11.4", }, // TODO Remove once clientv2 gets merged @@ -251,6 +252,13 @@ func Job() *structs.Job { { Name: "web", Count: 10, + Constraints: []*structs.Constraint{ + { + LTarget: "${attr.consul.version}", + RTarget: ">= 1.7.0", + Operand: structs.ConstraintSemver, + }, + }, EphemeralDisk: &structs.EphemeralDisk{ SizeMB: 150, }, diff --git a/nomad/structs/job.go b/nomad/structs/job.go index 7d638f51d..1ab637353 100644 --- a/nomad/structs/job.go +++ b/nomad/structs/job.go @@ -60,3 +60,42 @@ func requiresNativeServiceDiscovery(services []*Service) bool { } return false } + +// RequiredConsulServiceDiscovery identifies which task groups, if any, within +// the job are utilising Consul service discovery. +func (j *Job) RequiredConsulServiceDiscovery() map[string]bool { + groups := make(map[string]bool) + + for _, tg := range j.TaskGroups { + + // It is possible for services using the Consul provider to be + // configured at the task group level, so check here first. This is + // a requirement for Consul Connect services. + if requiresConsulServiceDiscovery(tg.Services) { + groups[tg.Name] = true + continue + } + + // Iterate the tasks within the task group to check the services + // configured at this more traditional level. + for _, task := range tg.Tasks { + if requiresConsulServiceDiscovery(task.Services) { + groups[tg.Name] = true + continue + } + } + } + + return groups +} + +// requiresConsulServiceDiscovery identifies whether any of the services passed +// to the function are utilising Consul service discovery. +func requiresConsulServiceDiscovery(services []*Service) bool { + for _, tgService := range services { + if tgService.Provider == ServiceProviderConsul || tgService.Provider == "" { + return true + } + } + return false +} diff --git a/nomad/structs/job_test.go b/nomad/structs/job_test.go index 254c315b1..fc32584fd 100644 --- a/nomad/structs/job_test.go +++ b/nomad/structs/job_test.go @@ -154,3 +154,147 @@ func TestJob_RequiresNativeServiceDiscovery(t *testing.T) { }) } } + +func TestJob_RequiredConsulServiceDiscovery(t *testing.T) { + testCases := []struct { + inputJob *Job + expectedOutput map[string]bool + name string + }{ + { + inputJob: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "group1", + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + { + Name: "group2", + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + }, + }, + expectedOutput: map[string]bool{"group1": true, "group2": true}, + name: "multiple group services with Consul provider", + }, + { + inputJob: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "group1", + Tasks: []*Task{ + { + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + { + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + }, + }, + { + Name: "group2", + Tasks: []*Task{ + { + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + { + Services: []*Service{ + {Provider: "consul"}, + {Provider: "consul"}, + }, + }, + }, + }, + }, + }, + expectedOutput: map[string]bool{"group1": true, "group2": true}, + name: "multiple task services with Consul provider", + }, + { + inputJob: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "group1", + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + { + Name: "group2", + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + }, + }, + expectedOutput: map[string]bool{}, + name: "multiple group services with Nomad provider", + }, + { + inputJob: &Job{ + TaskGroups: []*TaskGroup{ + { + Name: "group1", + Tasks: []*Task{ + { + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + { + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + }, + }, + { + Name: "group2", + Tasks: []*Task{ + { + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + { + Services: []*Service{ + {Provider: "nomad"}, + {Provider: "nomad"}, + }, + }, + }, + }, + }, + }, + expectedOutput: map[string]bool{}, + name: "multiple task services with Nomad provider", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actualOutput := tc.inputJob.RequiredConsulServiceDiscovery() + require.Equal(t, tc.expectedOutput, actualOutput) + }) + } +}