job_hooks: add implicit constraint when using Consul for services. (#12602)
This commit is contained in:
parent
42068f8823
commit
010acce59f
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
consul: Added implicit Consul constraint for task groups utilising Consul service and check registrations
|
||||||
|
```
|
|
@ -72,6 +72,8 @@ func TestPrevAlloc_StreamAllocDir_TLS(t *testing.T) {
|
||||||
Operand: "=",
|
Operand: "=",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
job.TaskGroups[0].Constraints = nil
|
||||||
|
job.TaskGroups[0].Tasks[0].Services = nil
|
||||||
job.TaskGroups[0].Count = 1
|
job.TaskGroups[0].Count = 1
|
||||||
job.TaskGroups[0].EphemeralDisk.Sticky = true
|
job.TaskGroups[0].EphemeralDisk.Sticky = true
|
||||||
job.TaskGroups[0].EphemeralDisk.Migrate = true
|
job.TaskGroups[0].EphemeralDisk.Migrate = true
|
||||||
|
|
|
@ -441,6 +441,7 @@ func TestClient_UpdateAllocStatus(t *testing.T) {
|
||||||
job := mock.Job()
|
job := mock.Job()
|
||||||
// allow running job on any node including self client, that may not be a Linux box
|
// allow running job on any node including self client, that may not be a Linux box
|
||||||
job.Constraints = nil
|
job.Constraints = nil
|
||||||
|
job.TaskGroups[0].Constraints = nil
|
||||||
job.TaskGroups[0].Count = 1
|
job.TaskGroups[0].Count = 1
|
||||||
task := job.TaskGroups[0].Tasks[0]
|
task := job.TaskGroups[0].Tasks[0]
|
||||||
task.Driver = "mock_driver"
|
task.Driver = "mock_driver"
|
||||||
|
|
|
@ -53,7 +53,7 @@ func TestIntegration_Command_RoundTripJob(t *testing.T) {
|
||||||
defer srv.Shutdown()
|
defer srv.Shutdown()
|
||||||
|
|
||||||
{
|
{
|
||||||
cmd := exec.Command("nomad", "job", "init")
|
cmd := exec.Command("nomad", "job", "init", "-short")
|
||||||
cmd.Dir = tmpDir
|
cmd.Dir = tmpDir
|
||||||
assert.Nil(cmd.Run())
|
assert.Nil(cmd.Run())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,16 @@ var (
|
||||||
Operand: structs.ConstraintSemver,
|
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
|
// nativeServiceDiscoveryConstraint is the constraint injected into task
|
||||||
// groups that utilise Nomad's native service discovery feature. This is
|
// groups that utilise Nomad's native service discovery feature. This is
|
||||||
// needed, as operators can disable the client functionality, and therefore
|
// 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.
|
// Identify which task groups are utilising Nomad native service discovery.
|
||||||
nativeServiceDisco := j.RequiredNativeServiceDiscovery()
|
nativeServiceDisco := j.RequiredNativeServiceDiscovery()
|
||||||
|
|
||||||
|
// Identify which task groups are utilising Consul service discovery.
|
||||||
|
consulServiceDisco := j.RequiredConsulServiceDiscovery()
|
||||||
|
|
||||||
// Hot path
|
// 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
|
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 {
|
for _, tg := range j.TaskGroups {
|
||||||
_, ok := vaultBlocks[tg.Name]
|
|
||||||
if !ok {
|
// If the task group utilises Vault, run the mutator.
|
||||||
// Not requesting Vault
|
if _, ok := vaultBlocks[tg.Name]; ok {
|
||||||
continue
|
mutateConstraint(constraintMatcherLeft, tg, vaultConstraint)
|
||||||
}
|
}
|
||||||
|
|
||||||
found := false
|
// Check whether the task group is using signals. In the case that it
|
||||||
for _, c := range tg.Constraints {
|
// is, we flatten the signals and build a constraint, then run the
|
||||||
if c.LTarget == vaultConstraintLTarget {
|
// mutator.
|
||||||
found = true
|
if tgSignals, ok := signals[tg.Name]; ok {
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flatten the signals
|
|
||||||
required := helper.MapStringStringSliceValueSet(tgSignals)
|
required := helper.MapStringStringSliceValueSet(tgSignals)
|
||||||
sigConstraint := getSignalConstraint(required)
|
sigConstraint := getSignalConstraint(required)
|
||||||
|
mutateConstraint(constraintMatcherFull, tg, sigConstraint)
|
||||||
found := false
|
|
||||||
for _, c := range tg.Constraints {
|
|
||||||
if c.Equals(sigConstraint) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
// If the task group utilises Nomad service discovery, run the mutator.
|
||||||
tg.Constraints = append(tg.Constraints, sigConstraint)
|
if ok := nativeServiceDisco[tg.Name]; ok {
|
||||||
}
|
mutateConstraint(constraintMatcherFull, tg, nativeServiceDiscoveryConstraint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the Nomad service discovery constraints.
|
// If the task group utilises Consul service discovery, run the mutator.
|
||||||
for _, tg := range j.TaskGroups {
|
if ok := consulServiceDisco[tg.Name]; ok {
|
||||||
if ok := nativeServiceDisco[tg.Name]; !ok {
|
mutateConstraint(constraintMatcherLeft, tg, consulServiceDiscoveryConstraint)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
found := false
|
|
||||||
for _, c := range tg.Constraints {
|
|
||||||
if c.Equals(nativeServiceDiscoveryConstraint) {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
tg.Constraints = append(tg.Constraints, nativeServiceDiscoveryConstraint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return j, nil, nil
|
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
|
// 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
|
// a validation problem or if the Job is of a type a user is not allowed to
|
||||||
// submit.
|
// submit.
|
||||||
|
|
|
@ -39,6 +39,406 @@ func Test_jobImpliedConstraints_Mutate(t *testing.T) {
|
||||||
expectedOutputError: nil,
|
expectedOutputError: nil,
|
||||||
name: "no needed constraints",
|
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{
|
inputJob: &structs.Job{
|
||||||
Name: "example",
|
Name: "example",
|
||||||
|
@ -156,6 +556,155 @@ func Test_jobImpliedConstraints_Mutate(t *testing.T) {
|
||||||
expectedOutputError: nil,
|
expectedOutputError: nil,
|
||||||
name: "task group nomad discovery other constraints",
|
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 {
|
for _, tc := range testCases {
|
||||||
|
|
|
@ -1646,15 +1646,13 @@ func TestJobEndpoint_Register_Vault_Policies(t *testing.T) {
|
||||||
t.Fatalf("vault token not cleared")
|
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
|
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)
|
t.Fatalf("Unexpected number of tests: %v", l)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !constraints[0].Equal(vaultConstraint) {
|
require.ElementsMatch(t, constraints, []*structs.Constraint{consulServiceDiscoveryConstraint, vaultConstraint})
|
||||||
t.Fatalf("bad constraint; got %#v; want %#v", constraints[0], vaultConstraint)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the register request with another job asking for a vault policy but
|
// Create the register request with another job asking for a vault policy but
|
||||||
// send the root Vault token
|
// send the root Vault token
|
||||||
|
@ -6482,15 +6480,11 @@ func TestJobEndpoint_ImplicitConstraints_Vault(t *testing.T) {
|
||||||
t.Fatalf("index mis-match")
|
t.Fatalf("index mis-match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that there is an implicit vault constraint
|
// Check that there is an implicit Vault and Consul constraint.
|
||||||
constraints := out.TaskGroups[0].Constraints
|
require.Len(t, out.TaskGroups[0].Constraints, 2)
|
||||||
if len(constraints) != 1 {
|
require.ElementsMatch(t, out.TaskGroups[0].Constraints, []*structs.Constraint{
|
||||||
t.Fatalf("Expected an implicit constraint")
|
consulServiceDiscoveryConstraint, vaultConstraint,
|
||||||
}
|
})
|
||||||
|
|
||||||
if !constraints[0].Equal(vaultConstraint) {
|
|
||||||
t.Fatalf("Expected implicit vault constraint")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJobEndpoint_ValidateJob_ConsulConnect(t *testing.T) {
|
func TestJobEndpoint_ValidateJob_ConsulConnect(t *testing.T) {
|
||||||
|
@ -6640,20 +6634,11 @@ func TestJobEndpoint_ImplicitConstraints_Signals(t *testing.T) {
|
||||||
t.Fatalf("index mis-match")
|
t.Fatalf("index mis-match")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that there is an implicit signal constraint
|
// Check that there is an implicit signal and Consul constraint.
|
||||||
constraints := out.TaskGroups[0].Constraints
|
require.Len(t, out.TaskGroups[0].Constraints, 2)
|
||||||
if len(constraints) != 1 {
|
require.ElementsMatch(t, out.TaskGroups[0].Constraints, []*structs.Constraint{
|
||||||
t.Fatalf("Expected an implicit constraint")
|
getSignalConstraint([]string{signal1, signal2}), consulServiceDiscoveryConstraint},
|
||||||
}
|
)
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJobEndpoint_ValidateJobUpdate(t *testing.T) {
|
func TestJobEndpoint_ValidateJobUpdate(t *testing.T) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ func Node() *structs.Node {
|
||||||
"nomad.version": "0.5.0",
|
"nomad.version": "0.5.0",
|
||||||
"driver.exec": "1",
|
"driver.exec": "1",
|
||||||
"driver.mock_driver": "1",
|
"driver.mock_driver": "1",
|
||||||
|
"consul.version": "1.11.4",
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO Remove once clientv2 gets merged
|
// TODO Remove once clientv2 gets merged
|
||||||
|
@ -251,6 +252,13 @@ func Job() *structs.Job {
|
||||||
{
|
{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
Count: 10,
|
Count: 10,
|
||||||
|
Constraints: []*structs.Constraint{
|
||||||
|
{
|
||||||
|
LTarget: "${attr.consul.version}",
|
||||||
|
RTarget: ">= 1.7.0",
|
||||||
|
Operand: structs.ConstraintSemver,
|
||||||
|
},
|
||||||
|
},
|
||||||
EphemeralDisk: &structs.EphemeralDisk{
|
EphemeralDisk: &structs.EphemeralDisk{
|
||||||
SizeMB: 150,
|
SizeMB: 150,
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,3 +60,42 @@ func requiresNativeServiceDiscovery(services []*Service) bool {
|
||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue