Skip to content

Commit

Permalink
job: add native service discovery job constraint mutator.
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasell committed Mar 14, 2022
1 parent 8e2c71e commit f5be448
Show file tree
Hide file tree
Showing 4 changed files with 385 additions and 1 deletion.
35 changes: 34 additions & 1 deletion nomad/job_endpoint_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ var (
RTarget: ">= 0.6.1",
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
// we need to ensure task groups are placed where they can run
// successfully.
nativeServiceDiscoveryConstraint = &structs.Constraint{
LTarget: "${attr.nomad.service_discovery}",
RTarget: "true",
Operand: "=",
}
)

type admissionController interface {
Expand Down Expand Up @@ -120,8 +131,11 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro
// Get the required signals
signals := j.RequiredSignals()

// Identify which task groups are utilising Nomad native service discovery.
nativeServiceDisco := j.RequiredNativeServiceDiscovery()

// Hot path
if len(signals) == 0 && len(policies) == 0 {
if len(signals) == 0 && len(policies) == 0 && len(nativeServiceDisco) == 0 {
return j, nil, nil
}

Expand Down Expand Up @@ -171,6 +185,25 @@ func (jobImpliedConstraints) Mutate(j *structs.Job) (*structs.Job, []error, erro
}
}

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

return j, nil, nil
}

Expand Down
169 changes: 169 additions & 0 deletions nomad/job_endpoint_hooks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package nomad

import (
"testing"

"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/require"
)

func Test_jobImpliedConstraints_Mutate(t *testing.T) {
t.Parallel()

testCases := []struct {
inputJob *structs.Job
expectedOutputJob *structs.Job
expectedOutputWarnings []error
expectedOutputError error
name string
}{
{
inputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
},
},
},
expectedOutputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
},
},
},
expectedOutputWarnings: nil,
expectedOutputError: nil,
name: "no needed constraints",
},
{
inputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
Services: []*structs.Service{
{
Name: "example-group-service-1",
Provider: structs.ServiceProviderNomad,
},
},
},
},
},
expectedOutputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
Services: []*structs.Service{
{
Name: "example-group-service-1",
Provider: structs.ServiceProviderNomad,
},
},
Constraints: []*structs.Constraint{nativeServiceDiscoveryConstraint},
},
},
},
expectedOutputWarnings: nil,
expectedOutputError: nil,
name: "task group nomad discovery",
},
{
inputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
Services: []*structs.Service{
{
Name: "example-group-service-1",
Provider: structs.ServiceProviderNomad,
},
},
Constraints: []*structs.Constraint{nativeServiceDiscoveryConstraint},
},
},
},
expectedOutputJob: &structs.Job{
Name: "example",
TaskGroups: []*structs.TaskGroup{
{
Name: "example-group-1",
Services: []*structs.Service{
{
Name: "example-group-service-1",
Provider: structs.ServiceProviderNomad,
},
},
Constraints: []*structs.Constraint{nativeServiceDiscoveryConstraint},
},
},
},
expectedOutputWarnings: nil,
expectedOutputError: nil,
name: "task group nomad 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.ServiceProviderNomad,
},
},
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.ServiceProviderNomad,
},
},
Constraints: []*structs.Constraint{
{
LTarget: "${node.class}",
RTarget: "high-memory",
Operand: "=",
},
nativeServiceDiscoveryConstraint,
},
},
},
},
expectedOutputWarnings: nil,
expectedOutputError: nil,
name: "task group nomad discovery other constraints",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
impl := jobImpliedConstraints{}
actualJob, actualWarnings, actualError := impl.Mutate(tc.inputJob)
require.Equal(t, tc.expectedOutputJob, actualJob)
require.ElementsMatch(t, tc.expectedOutputWarnings, actualWarnings)
require.Equal(t, tc.expectedOutputError, actualError)
})
}
}
38 changes: 38 additions & 0 deletions nomad/structs/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,41 @@ type JobServiceRegistrationsResponse struct {
Services []*ServiceRegistration
QueryMeta
}

// RequiredNativeServiceDiscovery identifies which task groups, if any, within
// the job are utilising Nomad native service discovery.
func (j *Job) RequiredNativeServiceDiscovery() map[string]bool {
groups := make(map[string]bool)

for _, tg := range j.TaskGroups {

// It is possible for services using the Nomad provider to be
// configured at the task group level, so check here first.
if requiresNativeServiceDiscovery(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 requiresNativeServiceDiscovery(task.Services) {
groups[tg.Name] = true
continue
}
}
}

return groups
}

// requiresNativeServiceDiscovery identifies whether any of the services passed
// to the function are utilising Nomad native service discovery.
func requiresNativeServiceDiscovery(services []*Service) bool {
for _, tgService := range services {
if tgService.Provider == ServiceProviderNomad {
return true
}
}
return false
}
Loading

0 comments on commit f5be448

Please sign in to comment.