package nomad import ( "testing" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/nomad/structs" "github.com/stretchr/testify/require" ) func Test_jobImpliedConstraints_Mutate(t *testing.T) { ci.Parallel(t) 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: "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", 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", }, { 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 { 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) }) } }