Files
nomad/api/allocations_test.go
Luiz Aoqui b24dddce2a api: set last index and request time on alloc stop (#16319)
Some of the methods in `Allocations()` incorrectly use the `putQuery`
in API calls where `put` is more appropriate since they are not reading
information back. These methods are also not returning request metadata
such as `LastIndex` back to callers, which can be useful to have in some
scenarios.

They also provide poor developer experience as they take an
`*api.Allocation` struct when only the allocation ID is necessary. This
can lead consumers to make unnecessary API calls to fetch the full
allocation.

Fixing these problems require updating the methods' signatures so they
take `*WriteOptions` instead of `*QueryOptions` and return `*WriteMeta`,
but this is a breaking change that requires advanced notice to consumers.

This commit adds a future breaking change notice and also fixes the
`Stop` method so it properly returns request metadata in a backwards
compatible way.
2023-03-03 15:52:41 -05:00

444 lines
10 KiB
Go

package api
import (
"context"
"fmt"
"os"
"sort"
"testing"
"time"
"github.com/hashicorp/nomad/api/internal/testutil"
"github.com/shoenig/test"
"github.com/shoenig/test/must"
)
func TestAllocations_List(t *testing.T) {
testutil.RequireRoot(t)
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.DevMode = true
})
defer s.Stop()
a := c.Allocations()
// wait for node
_ = oneNodeFromNodeList(t, c.Nodes())
// Querying when no allocs exist returns nothing
allocs, qm, err := a.List(nil)
must.NoError(t, err)
must.Zero(t, qm.LastIndex)
must.Len(t, 0, allocs)
// Create a job and attempt to register it
job := testJob()
resp, wm, err := c.Jobs().Register(job, nil)
must.NoError(t, err)
must.NotNil(t, resp)
must.UUIDv4(t, resp.EvalID)
assertWriteMeta(t, wm)
// List the allocations again
qo := &QueryOptions{
WaitIndex: wm.LastIndex,
}
allocs, qm, err = a.List(qo)
must.NoError(t, err)
must.NonZero(t, qm.LastIndex)
// Check that we got the allocation back
must.Len(t, 1, allocs)
must.Eq(t, resp.EvalID, allocs[0].EvalID)
// Resources should be unset by default
must.Nil(t, allocs[0].AllocatedResources)
}
func TestAllocations_PrefixList(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
a := c.Allocations()
// Querying when no allocs exist returns nothing
allocs, qm, err := a.PrefixList("")
must.NoError(t, err)
must.Zero(t, qm.LastIndex)
must.Len(t, 0, allocs)
// TODO: do something that causes an allocation to actually happen
// so we can query for them.
return
//job := &Job{
//ID: stringToPtr("job1"),
//Name: stringToPtr("Job #1"),
//Type: stringToPtr(JobTypeService),
//}
//eval, _, err := c.Jobs().Register(job, nil)
//if err != nil {
//t.Fatalf("err: %s", err)
//}
//// List the allocations by prefix
//allocs, qm, err = a.PrefixList("foobar")
//if err != nil {
//t.Fatalf("err: %s", err)
//}
//if qm.LastIndex == 0 {
//t.Fatalf("bad index: %d", qm.LastIndex)
//}
//// Check that we got the allocation back
//if len(allocs) == 0 || allocs[0].EvalID != eval {
//t.Fatalf("bad: %#v", allocs)
//}
}
func TestAllocations_List_Resources(t *testing.T) {
testutil.RequireRoot(t)
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.DevMode = true
})
defer s.Stop()
a := c.Allocations()
// wait for node
_ = oneNodeFromNodeList(t, c.Nodes())
// Create a job and register it
job := testJob()
resp, wm, err := c.Jobs().Register(job, nil)
must.NoError(t, err)
must.NotNil(t, resp)
must.UUIDv4(t, resp.EvalID)
assertWriteMeta(t, wm)
qo := &QueryOptions{
Params: map[string]string{"resources": "true"},
WaitIndex: wm.LastIndex,
}
var allocationStubs []*AllocationListStub
var qm *QueryMeta
allocationStubs, qm, err = a.List(qo)
must.NoError(t, err)
// Check that we got the allocation back with resources
must.Positive(t, qm.LastIndex)
must.Len(t, 1, allocationStubs)
alloc := allocationStubs[0]
must.Eq(t, resp.EvalID, alloc.EvalID,
must.Sprintf("registration: %#v", resp),
must.Sprintf("allocation: %#v", alloc),
)
must.NotNil(t, alloc.AllocatedResources)
}
func TestAllocations_CreateIndexSort(t *testing.T) {
testutil.Parallel(t)
allocs := []*AllocationListStub{
{CreateIndex: 2},
{CreateIndex: 1},
{CreateIndex: 5},
}
sort.Sort(AllocIndexSort(allocs))
expect := []*AllocationListStub{
{CreateIndex: 5},
{CreateIndex: 2},
{CreateIndex: 1},
}
must.Eq(t, allocs, expect)
}
func TestAllocations_RescheduleInfo(t *testing.T) {
testutil.Parallel(t)
// Create a job, task group and alloc
job := &Job{
Name: pointerOf("foo"),
Namespace: pointerOf(DefaultNamespace),
ID: pointerOf("bar"),
ParentID: pointerOf("lol"),
TaskGroups: []*TaskGroup{
{
Name: pointerOf("bar"),
Tasks: []*Task{
{
Name: "task1",
},
},
},
},
}
job.Canonicalize()
alloc := &Allocation{
ID: generateUUID(),
Namespace: DefaultNamespace,
EvalID: generateUUID(),
Name: "foo-bar[1]",
NodeID: generateUUID(),
TaskGroup: *job.TaskGroups[0].Name,
JobID: *job.ID,
Job: job,
}
type testCase struct {
desc string
reschedulePolicy *ReschedulePolicy
rescheduleTracker *RescheduleTracker
time time.Time
expAttempted int
expTotal int
}
testCases := []testCase{
{
desc: "no reschedule policy",
expAttempted: 0,
expTotal: 0,
},
{
desc: "no reschedule events",
reschedulePolicy: &ReschedulePolicy{
Attempts: pointerOf(3),
Interval: pointerOf(15 * time.Minute),
},
expAttempted: 0,
expTotal: 3,
},
{
desc: "all reschedule events within interval",
reschedulePolicy: &ReschedulePolicy{
Attempts: pointerOf(3),
Interval: pointerOf(15 * time.Minute),
},
time: time.Now(),
rescheduleTracker: &RescheduleTracker{
Events: []*RescheduleEvent{
{
RescheduleTime: time.Now().Add(-5 * time.Minute).UTC().UnixNano(),
},
},
},
expAttempted: 1,
expTotal: 3,
},
{
desc: "some reschedule events outside interval",
reschedulePolicy: &ReschedulePolicy{
Attempts: pointerOf(3),
Interval: pointerOf(15 * time.Minute),
},
time: time.Now(),
rescheduleTracker: &RescheduleTracker{
Events: []*RescheduleEvent{
{
RescheduleTime: time.Now().Add(-45 * time.Minute).UTC().UnixNano(),
},
{
RescheduleTime: time.Now().Add(-30 * time.Minute).UTC().UnixNano(),
},
{
RescheduleTime: time.Now().Add(-10 * time.Minute).UTC().UnixNano(),
},
{
RescheduleTime: time.Now().Add(-5 * time.Minute).UTC().UnixNano(),
},
},
},
expAttempted: 2,
expTotal: 3,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
alloc.RescheduleTracker = tc.rescheduleTracker
job.TaskGroups[0].ReschedulePolicy = tc.reschedulePolicy
attempted, total := alloc.RescheduleInfo(tc.time)
must.Eq(t, tc.expAttempted, attempted)
must.Eq(t, tc.expTotal, total)
})
}
}
func TestAllocations_Stop(t *testing.T) {
testutil.RequireRoot(t)
testutil.Parallel(t)
c, s := makeClient(t, nil, func(c *testutil.TestServerConfig) {
c.DevMode = true
})
defer s.Stop()
a := c.Allocations()
// wait for node
_ = oneNodeFromNodeList(t, c.Nodes())
// Create a job and register it
job := testJob()
_, wm, err := c.Jobs().Register(job, nil)
must.NoError(t, err)
// List allocations.
stubs, qm, err := a.List(&QueryOptions{WaitIndex: wm.LastIndex})
must.NoError(t, err)
must.SliceLen(t, 1, stubs)
// Stop the first allocation.
resp, err := a.Stop(&Allocation{ID: stubs[0].ID}, &QueryOptions{WaitIndex: qm.LastIndex})
must.NoError(t, err)
test.UUIDv4(t, resp.EvalID)
test.NonZero(t, resp.LastIndex)
// Stop allocation that doesn't exist.
resp, err = a.Stop(&Allocation{ID: "invalid"}, &QueryOptions{WaitIndex: qm.LastIndex})
must.Error(t, err)
}
// TestAllocations_ExecErrors ensures errors are properly formatted
func TestAllocations_ExecErrors(t *testing.T) {
testutil.Parallel(t)
c, s := makeClient(t, nil, nil)
defer s.Stop()
a := c.Allocations()
job := &Job{
Name: pointerOf("foo"),
Namespace: pointerOf(DefaultNamespace),
ID: pointerOf("bar"),
ParentID: pointerOf("lol"),
TaskGroups: []*TaskGroup{
{
Name: pointerOf("bar"),
Tasks: []*Task{
{
Name: "task1",
},
},
},
},
}
job.Canonicalize()
allocID := generateUUID()
alloc := &Allocation{
ID: allocID,
Namespace: DefaultNamespace,
EvalID: generateUUID(),
Name: "foo-bar[1]",
NodeID: generateUUID(),
TaskGroup: *job.TaskGroups[0].Name,
JobID: *job.ID,
Job: job,
}
// Querying when no allocs exist returns nothing
sizeCh := make(chan TerminalSize, 1)
// make a request that will result in an error
// ensure the error is what we expect
exitCode, err := a.Exec(context.Background(), alloc, "bar", false, []string{"command"}, os.Stdin, os.Stdout, os.Stderr, sizeCh, nil)
must.Eq(t, -2, exitCode)
must.EqError(t, err, fmt.Sprintf("Unknown allocation \"%s\"", allocID))
}
func TestAllocation_ServerTerminalStatus(t *testing.T) {
testutil.Parallel(t)
testCases := []struct {
inputAllocation *Allocation
expectedOutput bool
name string
}{
{
inputAllocation: &Allocation{DesiredStatus: AllocDesiredStatusEvict},
expectedOutput: true,
name: "alloc desired status evict",
},
{
inputAllocation: &Allocation{DesiredStatus: AllocDesiredStatusStop},
expectedOutput: true,
name: "alloc desired status stop",
},
{
inputAllocation: &Allocation{DesiredStatus: AllocDesiredStatusRun},
expectedOutput: false,
name: "alloc desired status run",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
must.Eq(t, tc.expectedOutput, tc.inputAllocation.ServerTerminalStatus())
})
}
}
func TestAllocation_ClientTerminalStatus(t *testing.T) {
testutil.Parallel(t)
testCases := []struct {
inputAllocation *Allocation
expectedOutput bool
name string
}{
{
inputAllocation: &Allocation{ClientStatus: AllocClientStatusLost},
expectedOutput: true,
name: "alloc client status lost",
},
{
inputAllocation: &Allocation{ClientStatus: AllocClientStatusFailed},
expectedOutput: true,
name: "alloc client status failed",
},
{
inputAllocation: &Allocation{ClientStatus: AllocClientStatusComplete},
expectedOutput: true,
name: "alloc client status complete",
},
{
inputAllocation: &Allocation{ClientStatus: AllocClientStatusRunning},
expectedOutput: false,
name: "alloc client status complete",
},
{
inputAllocation: &Allocation{ClientStatus: AllocClientStatusPending},
expectedOutput: false,
name: "alloc client status running",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
must.Eq(t, tc.expectedOutput, tc.inputAllocation.ClientTerminalStatus())
})
}
}
func TestAllocations_ShouldMigrate(t *testing.T) {
testutil.Parallel(t)
must.True(t, DesiredTransition{Migrate: pointerOf(true)}.ShouldMigrate())
must.False(t, DesiredTransition{}.ShouldMigrate())
must.False(t, DesiredTransition{Migrate: pointerOf(false)}.ShouldMigrate())
}
func TestAllocations_Services(t *testing.T) {
t.Skip("needs to be implemented")
// TODO(jrasell) add tests once registration process is in place.
}