open-nomad/api/tasks_test.go

809 lines
20 KiB
Go

package api
import (
"path/filepath"
"reflect"
"testing"
"time"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTaskGroup_NewTaskGroup(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 2)
expect := &TaskGroup{
Name: stringToPtr("grp1"),
Count: intToPtr(2),
}
if !reflect.DeepEqual(grp, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp)
}
}
func TestTaskGroup_Constrain(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 1)
// Add a constraint to the group
out := grp.Constrain(NewConstraint("kernel.name", "=", "darwin"))
if n := len(grp.Constraints); n != 1 {
t.Fatalf("expected 1 constraint, got: %d", n)
}
// Check that the group was returned
if out != grp {
t.Fatalf("expected: %#v, got: %#v", grp, out)
}
// Add a second constraint
grp.Constrain(NewConstraint("memory.totalbytes", ">=", "128000000"))
expect := []*Constraint{
{
LTarget: "kernel.name",
RTarget: "darwin",
Operand: "=",
},
{
LTarget: "memory.totalbytes",
RTarget: "128000000",
Operand: ">=",
},
}
if !reflect.DeepEqual(grp.Constraints, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp.Constraints)
}
}
func TestTaskGroup_AddAffinity(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 1)
// Add an affinity to the group
out := grp.AddAffinity(NewAffinity("kernel.version", "=", "4.6", 100))
if n := len(grp.Affinities); n != 1 {
t.Fatalf("expected 1 affinity, got: %d", n)
}
// Check that the group was returned
if out != grp {
t.Fatalf("expected: %#v, got: %#v", grp, out)
}
// Add a second affinity
grp.AddAffinity(NewAffinity("${node.affinity}", "=", "dc2", 50))
expect := []*Affinity{
{
LTarget: "kernel.version",
RTarget: "4.6",
Operand: "=",
Weight: 100,
},
{
LTarget: "${node.affinity}",
RTarget: "dc2",
Operand: "=",
Weight: 50,
},
}
if !reflect.DeepEqual(grp.Affinities, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp.Constraints)
}
}
func TestTaskGroup_SetMeta(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 1)
// Initializes an empty map
out := grp.SetMeta("foo", "bar")
if grp.Meta == nil {
t.Fatalf("should be initialized")
}
// Check that we returned the group
if out != grp {
t.Fatalf("expect: %#v, got: %#v", grp, out)
}
// Add a second meta k/v
grp.SetMeta("baz", "zip")
expect := map[string]string{"foo": "bar", "baz": "zip"}
if !reflect.DeepEqual(grp.Meta, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp.Meta)
}
}
func TestTaskGroup_AddSpread(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 1)
// Create and add spread
spreadTarget := NewSpreadTarget("r1", 50)
spread := NewSpread("${meta.rack}", 100, []*SpreadTarget{spreadTarget})
out := grp.AddSpread(spread)
if n := len(grp.Spreads); n != 1 {
t.Fatalf("expected 1 spread, got: %d", n)
}
// Check that the group was returned
if out != grp {
t.Fatalf("expected: %#v, got: %#v", grp, out)
}
// Add a second spread
spreadTarget2 := NewSpreadTarget("dc1", 100)
spread2 := NewSpread("${node.datacenter}", 100, []*SpreadTarget{spreadTarget2})
grp.AddSpread(spread2)
expect := []*Spread{
{
Attribute: "${meta.rack}",
Weight: intToPtr(100),
SpreadTarget: []*SpreadTarget{
{
Value: "r1",
Percent: 50,
},
},
},
{
Attribute: "${node.datacenter}",
Weight: intToPtr(100),
SpreadTarget: []*SpreadTarget{
{
Value: "dc1",
Percent: 100,
},
},
},
}
if !reflect.DeepEqual(grp.Spreads, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp.Spreads)
}
}
func TestTaskGroup_AddTask(t *testing.T) {
t.Parallel()
grp := NewTaskGroup("grp1", 1)
// Add the task to the task group
out := grp.AddTask(NewTask("task1", "java"))
if n := len(grp.Tasks); n != 1 {
t.Fatalf("expected 1 task, got: %d", n)
}
// Check that we returned the group
if out != grp {
t.Fatalf("expect: %#v, got: %#v", grp, out)
}
// Add a second task
grp.AddTask(NewTask("task2", "exec"))
expect := []*Task{
{
Name: "task1",
Driver: "java",
},
{
Name: "task2",
Driver: "exec",
},
}
if !reflect.DeepEqual(grp.Tasks, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, grp.Tasks)
}
}
func TestTask_NewTask(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
expect := &Task{
Name: "task1",
Driver: "exec",
}
if !reflect.DeepEqual(task, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task)
}
}
func TestTask_SetConfig(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
// Initializes an empty map
out := task.SetConfig("foo", "bar")
if task.Config == nil {
t.Fatalf("should be initialized")
}
// Check that we returned the task
if out != task {
t.Fatalf("expect: %#v, got: %#v", task, out)
}
// Set another config value
task.SetConfig("baz", "zip")
expect := map[string]interface{}{"foo": "bar", "baz": "zip"}
if !reflect.DeepEqual(task.Config, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task.Config)
}
}
func TestTask_SetMeta(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
// Initializes an empty map
out := task.SetMeta("foo", "bar")
if task.Meta == nil {
t.Fatalf("should be initialized")
}
// Check that we returned the task
if out != task {
t.Fatalf("expect: %#v, got: %#v", task, out)
}
// Set another meta k/v
task.SetMeta("baz", "zip")
expect := map[string]string{"foo": "bar", "baz": "zip"}
if !reflect.DeepEqual(task.Meta, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task.Meta)
}
}
func TestTask_Require(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
// Create some require resources
resources := &Resources{
CPU: intToPtr(1250),
MemoryMB: intToPtr(128),
DiskMB: intToPtr(2048),
Networks: []*NetworkResource{
{
CIDR: "0.0.0.0/0",
MBits: intToPtr(100),
ReservedPorts: []Port{{"", 80}, {"", 443}},
},
},
}
out := task.Require(resources)
if !reflect.DeepEqual(task.Resources, resources) {
t.Fatalf("expect: %#v, got: %#v", resources, task.Resources)
}
// Check that we returned the task
if out != task {
t.Fatalf("expect: %#v, got: %#v", task, out)
}
}
func TestTask_Constrain(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
// Add a constraint to the task
out := task.Constrain(NewConstraint("kernel.name", "=", "darwin"))
if n := len(task.Constraints); n != 1 {
t.Fatalf("expected 1 constraint, got: %d", n)
}
// Check that the task was returned
if out != task {
t.Fatalf("expected: %#v, got: %#v", task, out)
}
// Add a second constraint
task.Constrain(NewConstraint("memory.totalbytes", ">=", "128000000"))
expect := []*Constraint{
{
LTarget: "kernel.name",
RTarget: "darwin",
Operand: "=",
},
{
LTarget: "memory.totalbytes",
RTarget: "128000000",
Operand: ">=",
},
}
if !reflect.DeepEqual(task.Constraints, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task.Constraints)
}
}
func TestTask_AddAffinity(t *testing.T) {
t.Parallel()
task := NewTask("task1", "exec")
// Add an affinity to the task
out := task.AddAffinity(NewAffinity("kernel.version", "=", "4.6", 100))
require := require.New(t)
require.Len(out.Affinities, 1)
// Check that the task was returned
if out != task {
t.Fatalf("expected: %#v, got: %#v", task, out)
}
// Add a second affinity
task.AddAffinity(NewAffinity("${node.datacenter}", "=", "dc2", 50))
expect := []*Affinity{
{
LTarget: "kernel.version",
RTarget: "4.6",
Operand: "=",
Weight: 100,
},
{
LTarget: "${node.datacenter}",
RTarget: "dc2",
Operand: "=",
Weight: 50,
},
}
if !reflect.DeepEqual(task.Affinities, expect) {
t.Fatalf("expect: %#v, got: %#v", expect, task.Affinities)
}
}
func TestTask_Artifact(t *testing.T) {
t.Parallel()
a := TaskArtifact{
GetterSource: stringToPtr("http://localhost/foo.txt"),
GetterMode: stringToPtr("file"),
}
a.Canonicalize()
if *a.GetterMode != "file" {
t.Errorf("expected file but found %q", *a.GetterMode)
}
if filepath.ToSlash(*a.RelativeDest) != "local/foo.txt" {
t.Errorf("expected local/foo.txt but found %q", *a.RelativeDest)
}
}
// Ensures no regression on https://github.com/hashicorp/nomad/issues/3132
func TestTaskGroup_Canonicalize_Update(t *testing.T) {
job := &Job{
ID: stringToPtr("test"),
Update: &UpdateStrategy{
AutoRevert: boolToPtr(false),
Canary: intToPtr(0),
HealthCheck: stringToPtr(""),
HealthyDeadline: timeToPtr(0),
ProgressDeadline: timeToPtr(0),
MaxParallel: intToPtr(0),
MinHealthyTime: timeToPtr(0),
Stagger: timeToPtr(0),
},
}
job.Canonicalize()
tg := &TaskGroup{
Name: stringToPtr("foo"),
}
tg.Canonicalize(job)
assert.Nil(t, tg.Update)
}
// Verifies that reschedule policy is merged correctly
func TestTaskGroup_Canonicalize_ReschedulePolicy(t *testing.T) {
type testCase struct {
desc string
jobReschedulePolicy *ReschedulePolicy
taskReschedulePolicy *ReschedulePolicy
expected *ReschedulePolicy
}
testCases := []testCase{
{
desc: "Default",
jobReschedulePolicy: nil,
taskReschedulePolicy: nil,
expected: &ReschedulePolicy{
Attempts: intToPtr(structs.DefaultBatchJobReschedulePolicy.Attempts),
Interval: timeToPtr(structs.DefaultBatchJobReschedulePolicy.Interval),
Delay: timeToPtr(structs.DefaultBatchJobReschedulePolicy.Delay),
DelayFunction: stringToPtr(structs.DefaultBatchJobReschedulePolicy.DelayFunction),
MaxDelay: timeToPtr(structs.DefaultBatchJobReschedulePolicy.MaxDelay),
Unlimited: boolToPtr(structs.DefaultBatchJobReschedulePolicy.Unlimited),
},
},
{
desc: "Empty job reschedule policy",
jobReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(0),
Interval: timeToPtr(0),
Delay: timeToPtr(0),
MaxDelay: timeToPtr(0),
DelayFunction: stringToPtr(""),
Unlimited: boolToPtr(false),
},
taskReschedulePolicy: nil,
expected: &ReschedulePolicy{
Attempts: intToPtr(0),
Interval: timeToPtr(0),
Delay: timeToPtr(0),
MaxDelay: timeToPtr(0),
DelayFunction: stringToPtr(""),
Unlimited: boolToPtr(false),
},
},
{
desc: "Inherit from job",
jobReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(1),
Interval: timeToPtr(20 * time.Second),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
taskReschedulePolicy: nil,
expected: &ReschedulePolicy{
Attempts: intToPtr(1),
Interval: timeToPtr(20 * time.Second),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
},
{
desc: "Set in task",
jobReschedulePolicy: nil,
taskReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(5),
Interval: timeToPtr(2 * time.Minute),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
expected: &ReschedulePolicy{
Attempts: intToPtr(5),
Interval: timeToPtr(2 * time.Minute),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
},
{
desc: "Merge from job",
jobReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(1),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
},
taskReschedulePolicy: &ReschedulePolicy{
Interval: timeToPtr(5 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
expected: &ReschedulePolicy{
Attempts: intToPtr(1),
Interval: timeToPtr(5 * time.Minute),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(10 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
},
{
desc: "Override from group",
jobReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(1),
MaxDelay: timeToPtr(10 * time.Second),
},
taskReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(5),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(20 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
expected: &ReschedulePolicy{
Attempts: intToPtr(5),
Interval: timeToPtr(structs.DefaultBatchJobReschedulePolicy.Interval),
Delay: timeToPtr(20 * time.Second),
MaxDelay: timeToPtr(20 * time.Minute),
DelayFunction: stringToPtr("constant"),
Unlimited: boolToPtr(false),
},
},
{
desc: "Attempts from job, default interval",
jobReschedulePolicy: &ReschedulePolicy{
Attempts: intToPtr(1),
},
taskReschedulePolicy: nil,
expected: &ReschedulePolicy{
Attempts: intToPtr(1),
Interval: timeToPtr(structs.DefaultBatchJobReschedulePolicy.Interval),
Delay: timeToPtr(structs.DefaultBatchJobReschedulePolicy.Delay),
DelayFunction: stringToPtr(structs.DefaultBatchJobReschedulePolicy.DelayFunction),
MaxDelay: timeToPtr(structs.DefaultBatchJobReschedulePolicy.MaxDelay),
Unlimited: boolToPtr(structs.DefaultBatchJobReschedulePolicy.Unlimited),
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
job := &Job{
ID: stringToPtr("test"),
Reschedule: tc.jobReschedulePolicy,
Type: stringToPtr(JobTypeBatch),
}
job.Canonicalize()
tg := &TaskGroup{
Name: stringToPtr("foo"),
ReschedulePolicy: tc.taskReschedulePolicy,
}
tg.Canonicalize(job)
assert.Equal(t, tc.expected, tg.ReschedulePolicy)
})
}
}
// Verifies that migrate strategy is merged correctly
func TestTaskGroup_Canonicalize_MigrateStrategy(t *testing.T) {
type testCase struct {
desc string
jobType string
jobMigrate *MigrateStrategy
taskMigrate *MigrateStrategy
expected *MigrateStrategy
}
testCases := []testCase{
{
desc: "Default batch",
jobType: "batch",
jobMigrate: nil,
taskMigrate: nil,
expected: nil,
},
{
desc: "Default service",
jobType: "service",
jobMigrate: nil,
taskMigrate: nil,
expected: &MigrateStrategy{
MaxParallel: intToPtr(1),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(10 * time.Second),
HealthyDeadline: timeToPtr(5 * time.Minute),
},
},
{
desc: "Empty job migrate strategy",
jobType: "service",
jobMigrate: &MigrateStrategy{
MaxParallel: intToPtr(0),
HealthCheck: stringToPtr(""),
MinHealthyTime: timeToPtr(0),
HealthyDeadline: timeToPtr(0),
},
taskMigrate: nil,
expected: &MigrateStrategy{
MaxParallel: intToPtr(0),
HealthCheck: stringToPtr(""),
MinHealthyTime: timeToPtr(0),
HealthyDeadline: timeToPtr(0),
},
},
{
desc: "Inherit from job",
jobType: "service",
jobMigrate: &MigrateStrategy{
MaxParallel: intToPtr(3),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
taskMigrate: nil,
expected: &MigrateStrategy{
MaxParallel: intToPtr(3),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
},
{
desc: "Set in task",
jobType: "service",
jobMigrate: nil,
taskMigrate: &MigrateStrategy{
MaxParallel: intToPtr(3),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
expected: &MigrateStrategy{
MaxParallel: intToPtr(3),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
},
{
desc: "Merge from job",
jobType: "service",
jobMigrate: &MigrateStrategy{
MaxParallel: intToPtr(11),
},
taskMigrate: &MigrateStrategy{
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
expected: &MigrateStrategy{
MaxParallel: intToPtr(11),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
},
{
desc: "Override from group",
jobType: "service",
jobMigrate: &MigrateStrategy{
MaxParallel: intToPtr(11),
},
taskMigrate: &MigrateStrategy{
MaxParallel: intToPtr(5),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
expected: &MigrateStrategy{
MaxParallel: intToPtr(5),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(2),
HealthyDeadline: timeToPtr(2),
},
},
{
desc: "Parallel from job, defaulting",
jobType: "service",
jobMigrate: &MigrateStrategy{
MaxParallel: intToPtr(5),
},
taskMigrate: nil,
expected: &MigrateStrategy{
MaxParallel: intToPtr(5),
HealthCheck: stringToPtr("checks"),
MinHealthyTime: timeToPtr(10 * time.Second),
HealthyDeadline: timeToPtr(5 * time.Minute),
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
job := &Job{
ID: stringToPtr("test"),
Migrate: tc.jobMigrate,
Type: stringToPtr(tc.jobType),
}
job.Canonicalize()
tg := &TaskGroup{
Name: stringToPtr("foo"),
Migrate: tc.taskMigrate,
}
tg.Canonicalize(job)
assert.Equal(t, tc.expected, tg.Migrate)
})
}
}
// TestService_CheckRestart asserts Service.CheckRestart settings are properly
// inherited by Checks.
func TestService_CheckRestart(t *testing.T) {
job := &Job{Name: stringToPtr("job")}
tg := &TaskGroup{Name: stringToPtr("group")}
task := &Task{Name: "task"}
service := &Service{
CheckRestart: &CheckRestart{
Limit: 11,
Grace: timeToPtr(11 * time.Second),
IgnoreWarnings: true,
},
Checks: []ServiceCheck{
{
Name: "all-set",
CheckRestart: &CheckRestart{
Limit: 22,
Grace: timeToPtr(22 * time.Second),
IgnoreWarnings: true,
},
},
{
Name: "some-set",
CheckRestart: &CheckRestart{
Limit: 33,
Grace: timeToPtr(33 * time.Second),
},
},
{
Name: "unset",
},
},
}
service.Canonicalize(task, tg, job)
assert.Equal(t, service.Checks[0].CheckRestart.Limit, 22)
assert.Equal(t, *service.Checks[0].CheckRestart.Grace, 22*time.Second)
assert.True(t, service.Checks[0].CheckRestart.IgnoreWarnings)
assert.Equal(t, service.Checks[1].CheckRestart.Limit, 33)
assert.Equal(t, *service.Checks[1].CheckRestart.Grace, 33*time.Second)
assert.True(t, service.Checks[1].CheckRestart.IgnoreWarnings)
assert.Equal(t, service.Checks[2].CheckRestart.Limit, 11)
assert.Equal(t, *service.Checks[2].CheckRestart.Grace, 11*time.Second)
assert.True(t, service.Checks[2].CheckRestart.IgnoreWarnings)
}
// TestSpread_Canonicalize asserts that the spread stanza is canonicalized correctly
func TestSpread_Canonicalize(t *testing.T) {
job := &Job{
ID: stringToPtr("test"),
Type: stringToPtr("batch"),
}
job.Canonicalize()
tg := &TaskGroup{
Name: stringToPtr("foo"),
}
type testCase struct {
desc string
spread *Spread
expectedWeight int
}
cases := []testCase{
{
"Nil spread",
&Spread{
Attribute: "test",
Weight: nil,
},
50,
},
{
"Zero spread",
&Spread{
Attribute: "test",
Weight: intToPtr(0),
},
0,
},
{
"Non Zero spread",
&Spread{
Attribute: "test",
Weight: intToPtr(100),
},
100,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
require := require.New(t)
tg.Spreads = []*Spread{tc.spread}
tg.Canonicalize(job)
for _, spr := range tg.Spreads {
require.Equal(tc.expectedWeight, *spr.Weight)
}
})
}
}