open-nomad/api/tasks_test.go
Mahmood Ali 7bdd43f3e0 api: avoid codegen for syncing
Given that the values will rarely change, specially considering that any
changes would be backward incompatible change.  As such, it's simpler to
keep syncing manually in the rare occasion and avoid the syncing code
overhead.
2019-01-18 18:52:31 -05:00

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