open-nomad/nomad/state/state_store_test.go

11019 lines
278 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package state
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/kr/pretty"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testStateStore(t *testing.T) *StateStore {
return TestStateStore(t)
}
func TestStateStore_InvalidConfig(t *testing.T) {
config := &StateStoreConfig{
// default zero value, but explicit because it causes validation failure
JobTrackedVersions: 0,
}
store, err := NewStateStore(config)
must.Nil(t, store)
must.Error(t, err)
must.ErrorContains(t, err, "JobTrackedVersions must be positive")
}
func TestStateStore_Blocking_Error(t *testing.T) {
ci.Parallel(t)
expected := fmt.Errorf("test error")
errFn := func(memdb.WatchSet, *StateStore) (interface{}, uint64, error) {
return nil, 0, expected
}
state := testStateStore(t)
_, idx, err := state.BlockingQuery(errFn, 10, context.Background())
assert.EqualError(t, err, expected.Error())
assert.Zero(t, idx)
}
func TestStateStore_Blocking_Timeout(t *testing.T) {
ci.Parallel(t)
noopFn := func(memdb.WatchSet, *StateStore) (interface{}, uint64, error) {
return nil, 5, nil
}
state := testStateStore(t)
timeout := time.Now().Add(250 * time.Millisecond)
deadlineCtx, cancel := context.WithDeadline(context.Background(), timeout)
defer cancel()
_, idx, err := state.BlockingQuery(noopFn, 10, deadlineCtx)
assert.EqualError(t, err, context.DeadlineExceeded.Error())
assert.EqualValues(t, 5, idx)
assert.WithinDuration(t, timeout, time.Now(), 100*time.Millisecond)
}
func TestStateStore_Blocking_MinQuery(t *testing.T) {
ci.Parallel(t)
node := mock.Node()
count := 0
queryFn := func(ws memdb.WatchSet, s *StateStore) (interface{}, uint64, error) {
_, err := s.NodeByID(ws, node.ID)
if err != nil {
return nil, 0, err
}
count++
if count == 1 {
return false, 5, nil
} else if count > 2 {
return false, 20, fmt.Errorf("called too many times")
}
return true, 11, nil
}
state := testStateStore(t)
timeout := time.Now().Add(100 * time.Millisecond)
deadlineCtx, cancel := context.WithDeadline(context.Background(), timeout)
defer cancel()
time.AfterFunc(5*time.Millisecond, func() {
state.UpsertNode(structs.MsgTypeTestSetup, 11, node)
})
resp, idx, err := state.BlockingQuery(queryFn, 10, deadlineCtx)
if assert.Nil(t, err) {
assert.Equal(t, 2, count)
assert.EqualValues(t, 11, idx)
assert.True(t, resp.(bool))
}
}
// COMPAT 0.11: Uses AllocUpdateRequest.Alloc
// This test checks that:
// 1) The job is denormalized
// 2) Allocations are created
func TestStateStore_UpsertPlanResults_AllocationsCreated_Denormalized(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
job := alloc.Job
alloc.Job = nil
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
eval := mock.Eval()
eval.JobID = job.ID
// Create an eval
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
// Create a plan result
res := structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{alloc},
Job: job,
},
EvalID: eval.ID,
}
assert := assert.New(t)
err := state.UpsertPlanResults(structs.MsgTypeTestSetup, 1000, &res)
assert.Nil(err)
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
assert.Nil(err)
assert.Equal(alloc, out)
index, err := state.Index("allocs")
assert.Nil(err)
assert.EqualValues(1000, index)
if watchFired(ws) {
t.Fatalf("bad")
}
evalOut, err := state.EvalByID(ws, eval.ID)
assert.Nil(err)
assert.NotNil(evalOut)
assert.EqualValues(1000, evalOut.ModifyIndex)
}
// This test checks that:
// 1) The job is denormalized
// 2) Allocations are denormalized and updated with the diff
// That stopped allocs Job is unmodified
func TestStateStore_UpsertPlanResults_AllocationsDenormalized(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
job := alloc.Job
alloc.Job = nil
stoppedAlloc := mock.Alloc()
stoppedAlloc.Job = job
stoppedAllocDiff := &structs.AllocationDiff{
ID: stoppedAlloc.ID,
DesiredDescription: "desired desc",
ClientStatus: structs.AllocClientStatusLost,
}
preemptedAlloc := mock.Alloc()
preemptedAlloc.Job = job
preemptedAllocDiff := &structs.AllocationDiff{
ID: preemptedAlloc.ID,
PreemptedByAllocation: alloc.ID,
}
require := require.New(t)
require.NoError(state.UpsertAllocs(structs.MsgTypeTestSetup, 900, []*structs.Allocation{stoppedAlloc, preemptedAlloc}))
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, job))
// modify job and ensure that stopped and preempted alloc point to original Job
mJob := job.Copy()
mJob.TaskGroups[0].Name = "other"
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, mJob))
eval := mock.Eval()
eval.JobID = job.ID
// Create an eval
require.NoError(state.UpsertEvals(structs.MsgTypeTestSetup, 1, []*structs.Evaluation{eval}))
// Create a plan result
res := structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
AllocsUpdated: []*structs.Allocation{alloc},
AllocsStopped: []*structs.AllocationDiff{stoppedAllocDiff},
Job: mJob,
},
EvalID: eval.ID,
AllocsPreempted: []*structs.AllocationDiff{preemptedAllocDiff},
}
assert := assert.New(t)
planModifyIndex := uint64(1000)
err := state.UpsertPlanResults(structs.MsgTypeTestSetup, planModifyIndex, &res)
require.NoError(err)
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
require.NoError(err)
assert.Equal(alloc, out)
outJob, err := state.JobByID(ws, job.Namespace, job.ID)
require.NoError(err)
require.Equal(mJob.TaskGroups, outJob.TaskGroups)
require.NotEmpty(job.TaskGroups, outJob.TaskGroups)
updatedStoppedAlloc, err := state.AllocByID(ws, stoppedAlloc.ID)
require.NoError(err)
assert.Equal(stoppedAllocDiff.DesiredDescription, updatedStoppedAlloc.DesiredDescription)
assert.Equal(structs.AllocDesiredStatusStop, updatedStoppedAlloc.DesiredStatus)
assert.Equal(stoppedAllocDiff.ClientStatus, updatedStoppedAlloc.ClientStatus)
assert.Equal(planModifyIndex, updatedStoppedAlloc.AllocModifyIndex)
assert.Equal(planModifyIndex, updatedStoppedAlloc.AllocModifyIndex)
assert.Equal(job.TaskGroups, updatedStoppedAlloc.Job.TaskGroups)
updatedPreemptedAlloc, err := state.AllocByID(ws, preemptedAlloc.ID)
require.NoError(err)
assert.Equal(structs.AllocDesiredStatusEvict, updatedPreemptedAlloc.DesiredStatus)
assert.Equal(preemptedAllocDiff.PreemptedByAllocation, updatedPreemptedAlloc.PreemptedByAllocation)
assert.Equal(planModifyIndex, updatedPreemptedAlloc.AllocModifyIndex)
assert.Equal(planModifyIndex, updatedPreemptedAlloc.AllocModifyIndex)
assert.Equal(job.TaskGroups, updatedPreemptedAlloc.Job.TaskGroups)
index, err := state.Index("allocs")
require.NoError(err)
assert.EqualValues(planModifyIndex, index)
require.False(watchFired(ws))
evalOut, err := state.EvalByID(ws, eval.ID)
require.NoError(err)
require.NotNil(evalOut)
assert.EqualValues(planModifyIndex, evalOut.ModifyIndex)
}
// This test checks that the deployment is created and allocations count towards
// the deployment
func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
alloc2 := mock.Alloc()
job := alloc.Job
alloc.Job = nil
alloc2.Job = nil
d := mock.Deployment()
alloc.DeploymentID = d.ID
alloc2.DeploymentID = d.ID
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
eval := mock.Eval()
eval.JobID = job.ID
// Create an eval
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
// Create a plan result
res := structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{alloc, alloc2},
Job: job,
},
Deployment: d,
EvalID: eval.ID,
}
err := state.UpsertPlanResults(structs.MsgTypeTestSetup, 1000, &res)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
assert := assert.New(t)
out, err := state.AllocByID(ws, alloc.ID)
assert.Nil(err)
assert.Equal(alloc, out)
dout, err := state.DeploymentByID(ws, d.ID)
assert.Nil(err)
assert.NotNil(dout)
tg, ok := dout.TaskGroups[alloc.TaskGroup]
assert.True(ok)
assert.NotNil(tg)
assert.Equal(2, tg.PlacedAllocs)
evalOut, err := state.EvalByID(ws, eval.ID)
assert.Nil(err)
assert.NotNil(evalOut)
assert.EqualValues(1000, evalOut.ModifyIndex)
if watchFired(ws) {
t.Fatalf("bad")
}
// Update the allocs to be part of a new deployment
d2 := d.Copy()
d2.ID = uuid.Generate()
allocNew := alloc.Copy()
allocNew.DeploymentID = d2.ID
allocNew2 := alloc2.Copy()
allocNew2.DeploymentID = d2.ID
// Create another plan
res = structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{allocNew, allocNew2},
Job: job,
},
Deployment: d2,
EvalID: eval.ID,
}
err = state.UpsertPlanResults(structs.MsgTypeTestSetup, 1001, &res)
if err != nil {
t.Fatalf("err: %v", err)
}
dout, err = state.DeploymentByID(ws, d2.ID)
assert.Nil(err)
assert.NotNil(dout)
tg, ok = dout.TaskGroups[alloc.TaskGroup]
assert.True(ok)
assert.NotNil(tg)
assert.Equal(2, tg.PlacedAllocs)
evalOut, err = state.EvalByID(ws, eval.ID)
assert.Nil(err)
assert.NotNil(evalOut)
assert.EqualValues(1001, evalOut.ModifyIndex)
}
// This test checks that:
// 1) Preempted allocations in plan results are updated
// 2) Evals are inserted for preempted jobs
func TestStateStore_UpsertPlanResults_PreemptedAllocs(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
alloc := mock.Alloc()
job := alloc.Job
alloc.Job = nil
// Insert job
err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, job)
require.NoError(err)
// Create an eval
eval := mock.Eval()
eval.JobID = job.ID
err = state.UpsertEvals(structs.MsgTypeTestSetup, 1, []*structs.Evaluation{eval})
require.NoError(err)
// Insert alloc that will be preempted in the plan
preemptedAlloc := mock.Alloc()
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 2, []*structs.Allocation{preemptedAlloc})
require.NoError(err)
minimalPreemptedAlloc := &structs.Allocation{
ID: preemptedAlloc.ID,
PreemptedByAllocation: alloc.ID,
ModifyTime: time.Now().Unix(),
}
// Create eval for preempted job
eval2 := mock.Eval()
eval2.JobID = preemptedAlloc.JobID
// Create a plan result
res := structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{alloc},
Job: job,
},
EvalID: eval.ID,
NodePreemptions: []*structs.Allocation{minimalPreemptedAlloc},
PreemptionEvals: []*structs.Evaluation{eval2},
}
err = state.UpsertPlanResults(structs.MsgTypeTestSetup, 1000, &res)
require.NoError(err)
ws := memdb.NewWatchSet()
// Verify alloc and eval created by plan
out, err := state.AllocByID(ws, alloc.ID)
require.NoError(err)
require.Equal(alloc, out)
index, err := state.Index("allocs")
require.NoError(err)
require.EqualValues(1000, index)
evalOut, err := state.EvalByID(ws, eval.ID)
require.NoError(err)
require.NotNil(evalOut)
require.EqualValues(1000, evalOut.ModifyIndex)
// Verify preempted alloc
preempted, err := state.AllocByID(ws, preemptedAlloc.ID)
require.NoError(err)
require.Equal(preempted.DesiredStatus, structs.AllocDesiredStatusEvict)
require.Equal(preempted.DesiredDescription, fmt.Sprintf("Preempted by alloc ID %v", alloc.ID))
require.Equal(preempted.Job.ID, preemptedAlloc.Job.ID)
require.Equal(preempted.Job, preemptedAlloc.Job)
// Verify eval for preempted job
preemptedJobEval, err := state.EvalByID(ws, eval2.ID)
require.NoError(err)
require.NotNil(preemptedJobEval)
require.EqualValues(1000, preemptedJobEval.ModifyIndex)
}
// This test checks that deployment updates are applied correctly
func TestStateStore_UpsertPlanResults_DeploymentUpdates(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a job that applies to all
job := mock.Job()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
// Create a deployment that we will update its status
doutstanding := mock.Deployment()
doutstanding.JobID = job.ID
if err := state.UpsertDeployment(1000, doutstanding); err != nil {
t.Fatalf("err: %v", err)
}
eval := mock.Eval()
eval.JobID = job.ID
// Create an eval
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
alloc := mock.Alloc()
alloc.Job = nil
dnew := mock.Deployment()
dnew.JobID = job.ID
alloc.DeploymentID = dnew.ID
// Update the old deployment
update := &structs.DeploymentStatusUpdate{
DeploymentID: doutstanding.ID,
Status: "foo",
StatusDescription: "bar",
}
// Create a plan result
res := structs.ApplyPlanResultsRequest{
AllocUpdateRequest: structs.AllocUpdateRequest{
Alloc: []*structs.Allocation{alloc},
Job: job,
},
Deployment: dnew,
DeploymentUpdates: []*structs.DeploymentStatusUpdate{update},
EvalID: eval.ID,
}
err := state.UpsertPlanResults(structs.MsgTypeTestSetup, 1000, &res)
if err != nil {
t.Fatalf("err: %v", err)
}
assert := assert.New(t)
ws := memdb.NewWatchSet()
// Check the deployments are correctly updated.
dout, err := state.DeploymentByID(ws, dnew.ID)
assert.Nil(err)
assert.NotNil(dout)
tg, ok := dout.TaskGroups[alloc.TaskGroup]
assert.True(ok)
assert.NotNil(tg)
assert.Equal(1, tg.PlacedAllocs)
doutstandingout, err := state.DeploymentByID(ws, doutstanding.ID)
assert.Nil(err)
assert.NotNil(doutstandingout)
assert.Equal(update.Status, doutstandingout.Status)
assert.Equal(update.StatusDescription, doutstandingout.StatusDescription)
assert.EqualValues(1000, doutstandingout.ModifyIndex)
evalOut, err := state.EvalByID(ws, eval.ID)
assert.Nil(err)
assert.NotNil(evalOut)
assert.EqualValues(1000, evalOut.ModifyIndex)
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertDeployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
deployment := mock.Deployment()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.DeploymentsByJobID(ws, deployment.Namespace, deployment.ID, true)
if err != nil {
t.Fatalf("bad: %v", err)
}
err = state.UpsertDeployment(1000, deployment)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.DeploymentByID(ws, deployment.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(deployment, out) {
t.Fatalf("bad: %#v %#v", deployment, out)
}
index, err := state.Index("deployment")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
// Tests that deployments of older create index and same job id are not returned
func TestStateStore_OldDeployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
job.ID = "job1"
state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
deploy1 := mock.Deployment()
deploy1.JobID = job.ID
deploy1.JobCreateIndex = job.CreateIndex
deploy2 := mock.Deployment()
deploy2.JobID = job.ID
deploy2.JobCreateIndex = 11
require := require.New(t)
// Insert both deployments
err := state.UpsertDeployment(1001, deploy1)
require.Nil(err)
err = state.UpsertDeployment(1002, deploy2)
require.Nil(err)
ws := memdb.NewWatchSet()
// Should return both deployments
deploys, err := state.DeploymentsByJobID(ws, deploy1.Namespace, job.ID, true)
require.Nil(err)
require.Len(deploys, 2)
// Should only return deploy1
deploys, err = state.DeploymentsByJobID(ws, deploy1.Namespace, job.ID, false)
require.Nil(err)
require.Len(deploys, 1)
require.Equal(deploy1.ID, deploys[0].ID)
}
func TestStateStore_DeleteDeployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
d1 := mock.Deployment()
d2 := mock.Deployment()
err := state.UpsertDeployment(1000, d1)
if err != nil {
t.Fatalf("err: %v", err)
}
if err := state.UpsertDeployment(1001, d2); err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
if _, err := state.DeploymentByID(ws, d1.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err = state.DeleteDeployment(1002, []string{d1.ID, d2.ID})
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.DeploymentByID(ws, d1.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", d1, out)
}
index, err := state.Index("deployment")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1002 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_Deployments(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var deployments []*structs.Deployment
for i := 0; i < 10; i++ {
deployment := mock.Deployment()
deployments = append(deployments, deployment)
err := state.UpsertDeployment(1000+uint64(i), deployment)
require.NoError(t, err)
}
ws := memdb.NewWatchSet()
it, err := state.Deployments(ws, SortDefault)
require.NoError(t, err)
var out []*structs.Deployment
for {
raw := it.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Deployment))
}
require.Equal(t, deployments, out)
require.False(t, watchFired(ws))
}
func TestStateStore_Deployments_Namespace(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns1.Name = "namespaced"
deploy1 := mock.Deployment()
deploy2 := mock.Deployment()
deploy1.Namespace = ns1.Name
deploy2.Namespace = ns1.Name
ns2 := mock.Namespace()
ns2.Name = "new-namespace"
deploy3 := mock.Deployment()
deploy4 := mock.Deployment()
deploy3.Namespace = ns2.Name
deploy4.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
// Create watchsets so we can test that update fires the watch
watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()}
_, err := state.DeploymentsByNamespace(watches[0], ns1.Name)
require.NoError(t, err)
_, err = state.DeploymentsByNamespace(watches[1], ns2.Name)
require.NoError(t, err)
require.NoError(t, state.UpsertDeployment(1001, deploy1))
require.NoError(t, state.UpsertDeployment(1002, deploy2))
require.NoError(t, state.UpsertDeployment(1003, deploy3))
require.NoError(t, state.UpsertDeployment(1004, deploy4))
require.True(t, watchFired(watches[0]))
require.True(t, watchFired(watches[1]))
ws := memdb.NewWatchSet()
iter1, err := state.DeploymentsByNamespace(ws, ns1.Name)
require.NoError(t, err)
iter2, err := state.DeploymentsByNamespace(ws, ns2.Name)
require.NoError(t, err)
var out1 []*structs.Deployment
for {
raw := iter1.Next()
if raw == nil {
break
}
out1 = append(out1, raw.(*structs.Deployment))
}
var out2 []*structs.Deployment
for {
raw := iter2.Next()
if raw == nil {
break
}
out2 = append(out2, raw.(*structs.Deployment))
}
require.Len(t, out1, 2)
require.Len(t, out2, 2)
for _, deploy := range out1 {
require.Equal(t, ns1.Name, deploy.Namespace)
}
for _, deploy := range out2 {
require.Equal(t, ns2.Name, deploy.Namespace)
}
index, err := state.Index("deployment")
require.NoError(t, err)
require.EqualValues(t, 1004, index)
require.False(t, watchFired(ws))
}
func TestStateStore_DeploymentsByIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
deploy := mock.Deployment()
deploy.ID = "11111111-662e-d0ab-d1c9-3e434af7bdb4"
err := state.UpsertDeployment(1000, deploy)
require.NoError(t, err)
gatherDeploys := func(iter memdb.ResultIterator) []*structs.Deployment {
var deploys []*structs.Deployment
for {
raw := iter.Next()
if raw == nil {
break
}
deploy := raw.(*structs.Deployment)
deploys = append(deploys, deploy)
}
return deploys
}
t.Run("first deployment", func(t *testing.T) {
// Create a watchset so we can test that getters don't cause it to fire
ws := memdb.NewWatchSet()
iter, err := state.DeploymentsByIDPrefix(ws, deploy.Namespace, deploy.ID, SortDefault)
require.NoError(t, err)
deploys := gatherDeploys(iter)
require.Len(t, deploys, 1)
require.False(t, watchFired(ws))
})
t.Run("using prefix", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.DeploymentsByIDPrefix(ws, deploy.Namespace, "11", SortDefault)
require.NoError(t, err)
deploys := gatherDeploys(iter)
require.Len(t, deploys, 1)
require.False(t, watchFired(ws))
})
deploy = mock.Deployment()
deploy.ID = "11222222-662e-d0ab-d1c9-3e434af7bdb4"
err = state.UpsertDeployment(1001, deploy)
require.NoError(t, err)
t.Run("more than one", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.DeploymentsByIDPrefix(ws, deploy.Namespace, "11", SortDefault)
require.NoError(t, err)
deploys := gatherDeploys(iter)
require.Len(t, deploys, 2)
})
t.Run("filter to one", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.DeploymentsByIDPrefix(ws, deploy.Namespace, "1111", SortDefault)
require.NoError(t, err)
deploys := gatherDeploys(iter)
require.Len(t, deploys, 1)
require.False(t, watchFired(ws))
})
t.Run("reverse order", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.DeploymentsByIDPrefix(ws, deploy.Namespace, "11", SortReverse)
require.NoError(t, err)
got := []string{}
for _, d := range gatherDeploys(iter) {
got = append(got, d.ID)
}
expected := []string{
"11222222-662e-d0ab-d1c9-3e434af7bdb4",
"11111111-662e-d0ab-d1c9-3e434af7bdb4",
}
require.Equal(t, expected, got)
require.False(t, watchFired(ws))
})
}
func TestStateStore_DeploymentsByIDPrefix_Namespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
deploy1 := mock.Deployment()
deploy1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2"
deploy2 := mock.Deployment()
deploy2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2"
sharedPrefix := "aabb"
ns1 := mock.Namespace()
ns1.Name = "namespace1"
ns2 := mock.Namespace()
ns2.Name = "namespace2"
deploy1.Namespace = ns1.Name
deploy2.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
require.NoError(t, state.UpsertDeployment(1000, deploy1))
require.NoError(t, state.UpsertDeployment(1001, deploy2))
gatherDeploys := func(iter memdb.ResultIterator) []*structs.Deployment {
var deploys []*structs.Deployment
for {
raw := iter.Next()
if raw == nil {
break
}
deploy := raw.(*structs.Deployment)
deploys = append(deploys, deploy)
}
return deploys
}
ws := memdb.NewWatchSet()
iter1, err := state.DeploymentsByIDPrefix(ws, ns1.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
iter2, err := state.DeploymentsByIDPrefix(ws, ns2.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
deploysNs1 := gatherDeploys(iter1)
deploysNs2 := gatherDeploys(iter2)
require.Len(t, deploysNs1, 1)
require.Len(t, deploysNs2, 1)
iter1, err = state.DeploymentsByIDPrefix(ws, ns1.Name, deploy1.ID[:8], SortDefault)
require.NoError(t, err)
deploysNs1 = gatherDeploys(iter1)
require.Len(t, deploysNs1, 1)
require.False(t, watchFired(ws))
}
func TestStateStore_UpsertNamespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns2 := mock.Namespace()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.NamespaceByName(ws, ns1.Name)
require.NoError(t, err)
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns1, ns2}))
require.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns1.Name)
require.NoError(t, err)
require.Equal(t, ns1, out)
out, err = state.NamespaceByName(ws, ns2.Name)
require.NoError(t, err)
require.Equal(t, ns2, out)
index, err := state.Index(TableNamespaces)
require.NoError(t, err)
require.EqualValues(t, 1000, index)
require.False(t, watchFired(ws))
}
func TestStateStore_DeleteNamespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns2 := mock.Namespace()
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns1, ns2}))
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
_, err := state.NamespaceByName(ws, ns1.Name)
require.NoError(t, err)
require.NoError(t, state.DeleteNamespaces(1001, []string{ns1.Name, ns2.Name}))
require.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns1.Name)
require.NoError(t, err)
require.Nil(t, out)
out, err = state.NamespaceByName(ws, ns2.Name)
require.NoError(t, err)
require.Nil(t, out)
index, err := state.Index(TableNamespaces)
require.NoError(t, err)
require.EqualValues(t, 1001, index)
require.False(t, watchFired(ws))
}
func TestStateStore_DeleteNamespaces_Default(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
ns.Name = structs.DefaultNamespace
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns}))
err := state.DeleteNamespaces(1002, []string{ns.Name})
require.Error(t, err)
require.Contains(t, err.Error(), "can not be deleted")
}
func TestStateStore_DeleteNamespaces_NonTerminalJobs(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns}))
job := mock.Job()
job.Namespace = ns.Name
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job))
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
_, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
err = state.DeleteNamespaces(1002, []string{ns.Name})
require.Error(t, err)
require.Contains(t, err.Error(), "one non-terminal")
require.False(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
require.NotNil(t, out)
index, err := state.Index(TableNamespaces)
require.NoError(t, err)
require.EqualValues(t, 1000, index)
require.False(t, watchFired(ws))
}
func TestStateStore_DeleteNamespaces_CSIVolumes(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns}))
plugin := mock.CSIPlugin()
vol := mock.CSIVolume(plugin)
vol.Namespace = ns.Name
require.NoError(t, state.UpsertCSIVolume(1001, []*structs.CSIVolume{vol}))
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
_, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
err = state.DeleteNamespaces(1002, []string{ns.Name})
require.Error(t, err)
require.Contains(t, err.Error(), "one CSI volume")
require.False(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
require.NotNil(t, out)
index, err := state.Index(TableNamespaces)
require.NoError(t, err)
require.EqualValues(t, 1000, index)
require.False(t, watchFired(ws))
}
func TestStateStore_DeleteNamespaces_Variables(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns}))
sv := mock.VariableEncrypted()
sv.Namespace = ns.Name
resp := state.VarSet(1001, &structs.VarApplyStateRequest{
Op: structs.VarOpSet,
Var: sv,
})
require.NoError(t, resp.Error)
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
_, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
err = state.DeleteNamespaces(1002, []string{ns.Name})
require.Error(t, err)
require.Contains(t, err.Error(), "one variable")
require.False(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
require.NotNil(t, out)
index, err := state.Index(TableNamespaces)
require.NoError(t, err)
require.EqualValues(t, 1000, index)
require.False(t, watchFired(ws))
}
func TestStateStore_Namespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var namespaces []*structs.Namespace
for i := 0; i < 10; i++ {
ns := mock.Namespace()
namespaces = append(namespaces, ns)
}
require.NoError(t, state.UpsertNamespaces(1000, namespaces))
// Create a watchset so we can test that getters don't cause it to fire
ws := memdb.NewWatchSet()
iter, err := state.Namespaces(ws)
require.NoError(t, err)
var out []*structs.Namespace
for {
raw := iter.Next()
if raw == nil {
break
}
ns := raw.(*structs.Namespace)
if ns.Name == structs.DefaultNamespace {
continue
}
out = append(out, ns)
}
namespaceSort(namespaces)
namespaceSort(out)
require.Equal(t, namespaces, out)
require.False(t, watchFired(ws))
}
func TestStateStore_NamespaceNames(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var namespaces []*structs.Namespace
expectedNames := []string{structs.DefaultNamespace}
for i := 0; i < 10; i++ {
ns := mock.Namespace()
namespaces = append(namespaces, ns)
expectedNames = append(expectedNames, ns.Name)
}
err := state.UpsertNamespaces(1000, namespaces)
require.NoError(t, err)
found, err := state.NamespaceNames()
require.NoError(t, err)
sort.Strings(expectedNames)
sort.Strings(found)
require.Equal(t, expectedNames, found)
}
func TestStateStore_NamespaceByNamePrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
ns.Name = "foobar"
require.NoError(t, state.UpsertNamespaces(1000, []*structs.Namespace{ns}))
// Create a watchset so we can test that getters don't cause it to fire
ws := memdb.NewWatchSet()
iter, err := state.NamespacesByNamePrefix(ws, ns.Name)
require.NoError(t, err)
gatherNamespaces := func(iter memdb.ResultIterator) []*structs.Namespace {
var namespaces []*structs.Namespace
for {
raw := iter.Next()
if raw == nil {
break
}
ns := raw.(*structs.Namespace)
namespaces = append(namespaces, ns)
}
return namespaces
}
namespaces := gatherNamespaces(iter)
require.Len(t, namespaces, 1)
require.False(t, watchFired(ws))
iter, err = state.NamespacesByNamePrefix(ws, "foo")
require.NoError(t, err)
namespaces = gatherNamespaces(iter)
require.Len(t, namespaces, 1)
ns = mock.Namespace()
ns.Name = "foozip"
err = state.UpsertNamespaces(1001, []*structs.Namespace{ns})
require.NoError(t, err)
require.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
iter, err = state.NamespacesByNamePrefix(ws, "foo")
require.NoError(t, err)
namespaces = gatherNamespaces(iter)
require.Len(t, namespaces, 2)
iter, err = state.NamespacesByNamePrefix(ws, "foob")
require.NoError(t, err)
namespaces = gatherNamespaces(iter)
require.Len(t, namespaces, 1)
require.False(t, watchFired(ws))
}
func TestStateStore_RestoreNamespace(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns := mock.Namespace()
restore, err := state.Restore()
require.NoError(t, err)
require.NoError(t, restore.NamespaceRestore(ns))
restore.Commit()
ws := memdb.NewWatchSet()
out, err := state.NamespaceByName(ws, ns.Name)
require.NoError(t, err)
require.Equal(t, out, ns)
}
// namespaceSort is used to sort namespaces by name
func namespaceSort(namespaces []*structs.Namespace) {
sort.Slice(namespaces, func(i, j int) bool {
return namespaces[i].Name < namespaces[j].Name
})
}
func TestStateStore_UpsertNode_Node(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.NodeByID(ws, node.ID)
require.NoError(err)
require.NoError(state.UpsertNode(structs.MsgTypeTestSetup, 1000, node))
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.NoError(err)
out2, err := state.NodeBySecretID(ws, node.SecretID)
require.NoError(err)
require.EqualValues(node, out)
require.EqualValues(node, out2)
require.Len(out.Events, 1)
require.Equal(NodeRegisterEventRegistered, out.Events[0].Message)
index, err := state.Index("nodes")
require.NoError(err)
require.EqualValues(1000, index)
require.False(watchFired(ws))
// Transition the node to down and then up and ensure we get a re-register
// event
down := out.Copy()
down.Status = structs.NodeStatusDown
require.NoError(state.UpsertNode(structs.MsgTypeTestSetup, 1001, down))
require.NoError(state.UpsertNode(structs.MsgTypeTestSetup, 1002, out))
out, err = state.NodeByID(ws, node.ID)
require.NoError(err)
require.Len(out.Events, 2)
require.Equal(NodeRegisterEventReregistered, out.Events[1].Message)
}
func TestStateStore_UpsertNode_NodePool(t *testing.T) {
ci.Parallel(t)
devPoolName := "dev"
nodeWithPoolID := uuid.Generate()
nodeWithoutPoolID := uuid.Generate()
testCases := []struct {
name string
nodeID string
pool string
createPool bool
expectedPool string
expectedPoolExists bool
validateFn func(*testing.T, *structs.Node, *structs.NodePool)
}{
{
name: "register new node in new node pool",
nodeID: "",
pool: "new",
createPool: true,
expectedPool: "new",
expectedPoolExists: true,
validateFn: func(t *testing.T, node *structs.Node, pool *structs.NodePool) {
// Verify node pool was created in the same transaction as the
// node registration.
must.Eq(t, pool.CreateIndex, node.ModifyIndex)
},
},
{
name: "register new node in existing node pool",
nodeID: "",
pool: devPoolName,
expectedPool: devPoolName,
expectedPoolExists: true,
validateFn: func(t *testing.T, node *structs.Node, pool *structs.NodePool) {
// Verify node pool was not modified.
must.NotEq(t, pool.CreateIndex, node.ModifyIndex)
},
},
{
name: "register new node in built-in node pool",
nodeID: "",
pool: structs.NodePoolDefault,
expectedPool: structs.NodePoolDefault,
expectedPoolExists: true,
validateFn: func(t *testing.T, node *structs.Node, pool *structs.NodePool) {
// Verify node pool was not modified.
must.Eq(t, 1, pool.ModifyIndex)
},
},
{
name: "move existing node to new node pool",
nodeID: nodeWithPoolID,
pool: "new",
createPool: true,
expectedPool: "new",
expectedPoolExists: true,
validateFn: func(t *testing.T, node *structs.Node, pool *structs.NodePool) {
// Verify node pool was created in the same transaction as the
// node was updated.
must.Eq(t, pool.CreateIndex, node.ModifyIndex)
},
},
{
name: "move existing node to existing node pool",
nodeID: nodeWithPoolID,
pool: devPoolName,
expectedPool: devPoolName,
expectedPoolExists: true,
},
{
name: "move existing node to built-in node pool",
nodeID: nodeWithPoolID,
pool: structs.NodePoolDefault,
expectedPool: structs.NodePoolDefault,
expectedPoolExists: true,
},
{
name: "update node without pool to new node pool",
nodeID: nodeWithoutPoolID,
pool: "new",
createPool: true,
expectedPool: "new",
expectedPoolExists: true,
},
{
name: "update node without pool to existing node pool",
nodeID: nodeWithoutPoolID,
pool: devPoolName,
expectedPool: devPoolName,
expectedPoolExists: true,
},
{
name: "update node without pool with empty string to default",
nodeID: nodeWithoutPoolID,
pool: "",
expectedPool: structs.NodePoolDefault,
expectedPoolExists: true,
},
{
name: "register new node in new node pool without creating it",
nodeID: "",
pool: "new",
createPool: false,
expectedPool: "new",
expectedPoolExists: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
state := testStateStore(t)
// Populate state with pre-existing node pool.
devPool := mock.NodePool()
devPool.Name = devPoolName
err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{devPool})
must.NoError(t, err)
// Populate state with pre-existing node assigned to the
// pre-existing node pool.
nodeWithPool := mock.Node()
nodeWithPool.ID = nodeWithPoolID
nodeWithPool.NodePool = devPool.Name
err = state.UpsertNode(structs.MsgTypeTestSetup, 1001, nodeWithPool)
must.NoError(t, err)
// Populate state with pre-existing node with nil node pool to
// simulate an upgrade path.
nodeWithoutPool := mock.Node()
nodeWithoutPool.ID = nodeWithoutPoolID
err = state.UpsertNode(structs.MsgTypeTestSetup, 1002, nodeWithoutPool)
must.NoError(t, err)
// Upsert test node.
var node *structs.Node
switch tc.nodeID {
case nodeWithPoolID:
node = nodeWithPool.Copy()
case nodeWithoutPoolID:
node = nodeWithoutPool.Copy()
default:
node = mock.Node()
}
node.NodePool = tc.pool
opts := []NodeUpsertOption{}
if tc.createPool {
opts = append(opts, NodeUpsertWithNodePool)
}
err = state.UpsertNode(structs.MsgTypeTestSetup, 1003, node, opts...)
must.NoError(t, err)
// Verify that node is part of the expected pool.
got, err := state.NodeByID(nil, node.ID)
must.NoError(t, err)
must.NotNil(t, got)
// Verify node pool exists if requests.
pool, err := state.NodePoolByName(nil, tc.expectedPool)
must.NoError(t, err)
if tc.expectedPoolExists {
must.NotNil(t, pool)
} else {
must.Nil(t, pool)
}
// Verify node was assigned to node pool.
must.Eq(t, tc.expectedPool, got.NodePool)
if tc.validateFn != nil {
tc.validateFn(t, got, pool)
}
})
}
}
func TestStateStore_DeleteNode_Node(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create and insert two nodes, which we'll delete
node0 := mock.Node()
node1 := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node0)
require.NoError(t, err)
err = state.UpsertNode(structs.MsgTypeTestSetup, 1001, node1)
require.NoError(t, err)
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
// Check that both nodes are not nil
out, err := state.NodeByID(ws, node0.ID)
require.NoError(t, err)
require.NotNil(t, out)
out, err = state.NodeByID(ws, node1.ID)
require.NoError(t, err)
require.NotNil(t, out)
// Delete both nodes in a batch, fires the watch
err = state.DeleteNode(structs.MsgTypeTestSetup, 1002, []string{node0.ID, node1.ID})
require.NoError(t, err)
require.True(t, watchFired(ws))
// Check that both nodes are nil
ws = memdb.NewWatchSet()
out, err = state.NodeByID(ws, node0.ID)
require.NoError(t, err)
require.Nil(t, out)
out, err = state.NodeByID(ws, node1.ID)
require.NoError(t, err)
require.Nil(t, out)
// Ensure that the index is still at 1002, from DeleteNode
index, err := state.Index("nodes")
require.NoError(t, err)
require.Equal(t, uint64(1002), index)
require.False(t, watchFired(ws))
}
func TestStateStore_UpdateNodeStatus_Node(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
require.NoError(state.UpsertNode(structs.MsgTypeTestSetup, 800, node))
// Create a watchset so we can test that update node status fires the watch
ws := memdb.NewWatchSet()
_, err := state.NodeByID(ws, node.ID)
require.NoError(err)
event := &structs.NodeEvent{
Message: "Node ready foo",
Subsystem: structs.NodeEventSubsystemCluster,
Timestamp: time.Now(),
}
require.NoError(state.UpdateNodeStatus(structs.MsgTypeTestSetup, 801, node.ID, structs.NodeStatusReady, 70, event))
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.NoError(err)
require.Equal(structs.NodeStatusReady, out.Status)
require.EqualValues(801, out.ModifyIndex)
require.EqualValues(70, out.StatusUpdatedAt)
require.Len(out.Events, 2)
require.Equal(event.Message, out.Events[1].Message)
index, err := state.Index("nodes")
require.NoError(err)
require.EqualValues(801, index)
require.False(watchFired(ws))
}
func TestStatStore_UpdateNodeStatus_LastMissedHeartbeatIndex(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
transitions []string
expectedIndexes []uint64
}{
{
name: "disconnect",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
},
expectedIndexes: []uint64{0, 1001},
},
{
name: "reconnect",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
structs.NodeStatusInit,
structs.NodeStatusReady,
},
expectedIndexes: []uint64{0, 1001, 1001, 0},
},
{
name: "down",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDown,
},
expectedIndexes: []uint64{0, 1001},
},
{
name: "multiple reconnects",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
structs.NodeStatusInit,
structs.NodeStatusReady,
structs.NodeStatusDown,
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
structs.NodeStatusInit,
structs.NodeStatusReady,
},
expectedIndexes: []uint64{0, 1001, 1001, 0, 1004, 0, 1006, 1006, 0},
},
{
name: "multiple heartbeats",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
structs.NodeStatusInit,
structs.NodeStatusReady,
structs.NodeStatusReady,
structs.NodeStatusReady,
},
expectedIndexes: []uint64{0, 1001, 1001, 0, 0, 0},
},
{
name: "delayed alloc update",
transitions: []string{
structs.NodeStatusReady,
structs.NodeStatusDisconnected,
structs.NodeStatusInit,
structs.NodeStatusInit,
structs.NodeStatusInit,
structs.NodeStatusReady,
},
expectedIndexes: []uint64{0, 1001, 1001, 1001, 1001, 0},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
state := testStateStore(t)
node := mock.Node()
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 999, node))
for i, status := range tc.transitions {
now := time.Now().UnixNano()
err := state.UpdateNodeStatus(structs.MsgTypeTestSetup, uint64(1000+i), node.ID, status, now, nil)
must.NoError(t, err)
ws := memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
must.NoError(t, err)
must.Eq(t, tc.expectedIndexes[i], out.LastMissedHeartbeatIndex)
must.Eq(t, status, out.Status)
}
})
}
}
func TestStateStore_BatchUpdateNodeDrain(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
n1, n2 := mock.Node(), mock.Node()
require.Nil(state.UpsertNode(structs.MsgTypeTestSetup, 1000, n1))
require.Nil(state.UpsertNode(structs.MsgTypeTestSetup, 1001, n2))
// Create a watchset so we can test that update node drain fires the watch
ws := memdb.NewWatchSet()
_, err := state.NodeByID(ws, n1.ID)
require.Nil(err)
expectedDrain := &structs.DrainStrategy{
DrainSpec: structs.DrainSpec{
Deadline: -1 * time.Second,
},
}
update := map[string]*structs.DrainUpdate{
n1.ID: {
DrainStrategy: expectedDrain,
},
n2.ID: {
DrainStrategy: expectedDrain,
},
}
event := &structs.NodeEvent{
Message: "Drain strategy enabled",
Subsystem: structs.NodeEventSubsystemDrain,
Timestamp: time.Now(),
}
events := map[string]*structs.NodeEvent{
n1.ID: event,
n2.ID: event,
}
require.Nil(state.BatchUpdateNodeDrain(structs.MsgTypeTestSetup, 1002, 7, update, events))
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
for _, id := range []string{n1.ID, n2.ID} {
out, err := state.NodeByID(ws, id)
require.Nil(err)
require.NotNil(out.DrainStrategy)
require.Equal(out.DrainStrategy, expectedDrain)
require.NotNil(out.LastDrain)
require.Equal(structs.DrainStatusDraining, out.LastDrain.Status)
require.Len(out.Events, 2)
require.EqualValues(1002, out.ModifyIndex)
require.EqualValues(7, out.StatusUpdatedAt)
}
index, err := state.Index("nodes")
require.Nil(err)
require.EqualValues(1002, index)
require.False(watchFired(ws))
}
func TestStateStore_UpdateNodeDrain_Node(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
require.Nil(state.UpsertNode(structs.MsgTypeTestSetup, 1000, node))
// Create a watchset so we can test that update node drain fires the watch
ws := memdb.NewWatchSet()
_, err := state.NodeByID(ws, node.ID)
require.Nil(err)
expectedDrain := &structs.DrainStrategy{
DrainSpec: structs.DrainSpec{
Deadline: -1 * time.Second,
},
}
event := &structs.NodeEvent{
Message: "Drain strategy enabled",
Subsystem: structs.NodeEventSubsystemDrain,
Timestamp: time.Now(),
}
require.Nil(state.UpdateNodeDrain(structs.MsgTypeTestSetup, 1001, node.ID, expectedDrain, false, 7, event, nil, ""))
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.Nil(err)
require.NotNil(out.DrainStrategy)
require.NotNil(out.LastDrain)
require.Equal(structs.DrainStatusDraining, out.LastDrain.Status)
require.Equal(out.DrainStrategy, expectedDrain)
require.Len(out.Events, 2)
require.EqualValues(1001, out.ModifyIndex)
require.EqualValues(7, out.StatusUpdatedAt)
index, err := state.Index("nodes")
require.Nil(err)
require.EqualValues(1001, index)
require.False(watchFired(ws))
}
func TestStateStore_AddSingleNodeEvent(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
// We create a new node event every time we register a node
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node)
require.Nil(err)
require.Equal(1, len(node.Events))
require.Equal(structs.NodeEventSubsystemCluster, node.Events[0].Subsystem)
require.Equal(NodeRegisterEventRegistered, node.Events[0].Message)
// Create a watchset so we can test that AddNodeEvent fires the watch
ws := memdb.NewWatchSet()
_, err = state.NodeByID(ws, node.ID)
require.Nil(err)
nodeEvent := &structs.NodeEvent{
Message: "failed",
Subsystem: "Driver",
Timestamp: time.Now(),
}
nodeEvents := map[string][]*structs.NodeEvent{
node.ID: {nodeEvent},
}
err = state.UpsertNodeEvents(structs.MsgTypeTestSetup, uint64(1001), nodeEvents)
require.Nil(err)
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.Nil(err)
require.Equal(2, len(out.Events))
require.Equal(nodeEvent, out.Events[1])
}
// To prevent stale node events from accumulating, we limit the number of
// stored node events to 10.
func TestStateStore_NodeEvents_RetentionWindow(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node)
if err != nil {
t.Fatalf("err: %v", err)
}
require.Equal(1, len(node.Events))
require.Equal(structs.NodeEventSubsystemCluster, node.Events[0].Subsystem)
require.Equal(NodeRegisterEventRegistered, node.Events[0].Message)
var out *structs.Node
for i := 1; i <= 20; i++ {
ws := memdb.NewWatchSet()
out, err = state.NodeByID(ws, node.ID)
require.Nil(err)
nodeEvent := &structs.NodeEvent{
Message: fmt.Sprintf("%dith failed", i),
Subsystem: "Driver",
Timestamp: time.Now(),
}
nodeEvents := map[string][]*structs.NodeEvent{
out.ID: {nodeEvent},
}
err := state.UpsertNodeEvents(structs.MsgTypeTestSetup, uint64(i), nodeEvents)
require.Nil(err)
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err = state.NodeByID(ws, node.ID)
require.Nil(err)
}
ws := memdb.NewWatchSet()
out, err = state.NodeByID(ws, node.ID)
require.Nil(err)
require.Equal(10, len(out.Events))
require.Equal(uint64(11), out.Events[0].CreateIndex)
require.Equal(uint64(20), out.Events[len(out.Events)-1].CreateIndex)
}
func TestStateStore_UpdateNodeDrain_ResetEligiblity(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
require.Nil(state.UpsertNode(structs.MsgTypeTestSetup, 1000, node))
// Create a watchset so we can test that update node drain fires the watch
ws := memdb.NewWatchSet()
_, err := state.NodeByID(ws, node.ID)
require.Nil(err)
drain := &structs.DrainStrategy{
DrainSpec: structs.DrainSpec{
Deadline: -1 * time.Second,
},
}
event1 := &structs.NodeEvent{
Message: "Drain strategy enabled",
Subsystem: structs.NodeEventSubsystemDrain,
Timestamp: time.Now(),
}
require.Nil(state.UpdateNodeDrain(structs.MsgTypeTestSetup, 1001, node.ID, drain, false, 7, event1, nil, ""))
require.True(watchFired(ws))
// Remove the drain
event2 := &structs.NodeEvent{
Message: "Drain strategy disabled",
Subsystem: structs.NodeEventSubsystemDrain,
Timestamp: time.Now(),
}
require.Nil(state.UpdateNodeDrain(structs.MsgTypeTestSetup, 1002, node.ID, nil, true, 9, event2, nil, ""))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.Nil(err)
require.Nil(out.DrainStrategy)
require.Equal(out.SchedulingEligibility, structs.NodeSchedulingEligible)
require.NotNil(out.LastDrain)
require.Equal(structs.DrainStatusCanceled, out.LastDrain.Status)
require.Equal(time.Unix(7, 0), out.LastDrain.StartedAt)
require.Equal(time.Unix(9, 0), out.LastDrain.UpdatedAt)
require.Len(out.Events, 3)
require.EqualValues(1002, out.ModifyIndex)
require.EqualValues(9, out.StatusUpdatedAt)
index, err := state.Index("nodes")
require.Nil(err)
require.EqualValues(1002, index)
require.False(watchFired(ws))
}
func TestStateStore_UpdateNodeEligibility(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
node := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node)
if err != nil {
t.Fatalf("err: %v", err)
}
expectedEligibility := structs.NodeSchedulingIneligible
// Create a watchset so we can test that update node drain fires the watch
ws := memdb.NewWatchSet()
if _, err := state.NodeByID(ws, node.ID); err != nil {
t.Fatalf("bad: %v", err)
}
event := &structs.NodeEvent{
Message: "Node marked as ineligible",
Subsystem: structs.NodeEventSubsystemCluster,
Timestamp: time.Now(),
}
require.Nil(state.UpdateNodeEligibility(structs.MsgTypeTestSetup, 1001, node.ID, expectedEligibility, 7, event))
require.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.NodeByID(ws, node.ID)
require.Nil(err)
require.Equal(out.SchedulingEligibility, expectedEligibility)
require.Len(out.Events, 2)
require.Equal(out.Events[1], event)
require.EqualValues(1001, out.ModifyIndex)
require.EqualValues(7, out.StatusUpdatedAt)
index, err := state.Index("nodes")
require.Nil(err)
require.EqualValues(1001, index)
require.False(watchFired(ws))
// Set a drain strategy
expectedDrain := &structs.DrainStrategy{
DrainSpec: structs.DrainSpec{
Deadline: -1 * time.Second,
},
}
require.Nil(state.UpdateNodeDrain(structs.MsgTypeTestSetup, 1002, node.ID, expectedDrain, false, 7, nil, nil, ""))
// Try to set the node to eligible
err = state.UpdateNodeEligibility(structs.MsgTypeTestSetup, 1003, node.ID, structs.NodeSchedulingEligible, 9, nil)
require.NotNil(err)
require.Contains(err.Error(), "while it is draining")
}
func TestStateStore_Nodes(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var nodes []*structs.Node
for i := 0; i < 10; i++ {
node := mock.Node()
nodes = append(nodes, node)
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000+uint64(i), node)
if err != nil {
t.Fatalf("err: %v", err)
}
}
// Create a watchset so we can test that getters don't cause it to fire
ws := memdb.NewWatchSet()
iter, err := state.Nodes(ws)
if err != nil {
t.Fatalf("bad: %v", err)
}
var out []*structs.Node
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Node))
}
sort.Sort(NodeIDSort(nodes))
sort.Sort(NodeIDSort(out))
if !reflect.DeepEqual(nodes, out) {
t.Fatalf("bad: %#v %#v", nodes, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_NodesByIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
node.ID = "11111111-662e-d0ab-d1c9-3e434af7bdb4"
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that getters don't cause it to fire
ws := memdb.NewWatchSet()
iter, err := state.NodesByIDPrefix(ws, node.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
gatherNodes := func(iter memdb.ResultIterator) []*structs.Node {
var nodes []*structs.Node
for {
raw := iter.Next()
if raw == nil {
break
}
node := raw.(*structs.Node)
nodes = append(nodes, node)
}
return nodes
}
nodes := gatherNodes(iter)
if len(nodes) != 1 {
t.Fatalf("err: %v", err)
}
if watchFired(ws) {
t.Fatalf("bad")
}
iter, err = state.NodesByIDPrefix(ws, "11")
if err != nil {
t.Fatalf("err: %v", err)
}
nodes = gatherNodes(iter)
if len(nodes) != 1 {
t.Fatalf("err: %v", err)
}
node = mock.Node()
node.ID = "11222222-662e-d0ab-d1c9-3e434af7bdb4"
err = state.UpsertNode(structs.MsgTypeTestSetup, 1001, node)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
iter, err = state.NodesByIDPrefix(ws, "11")
if err != nil {
t.Fatalf("err: %v", err)
}
nodes = gatherNodes(iter)
if len(nodes) != 2 {
t.Fatalf("err: %v", err)
}
iter, err = state.NodesByIDPrefix(ws, "1111")
if err != nil {
t.Fatalf("err: %v", err)
}
nodes = gatherNodes(iter)
if len(nodes) != 1 {
t.Fatalf("err: %v", err)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_NodesByNodePool(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
pool := mock.NodePool()
err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, []*structs.NodePool{pool})
must.NoError(t, err)
node1 := mock.Node()
node1.NodePool = structs.NodePoolDefault
err = state.UpsertNode(structs.MsgTypeTestSetup, 1001, node1)
must.NoError(t, err)
node2 := mock.Node()
node2.NodePool = pool.Name
err = state.UpsertNode(structs.MsgTypeTestSetup, 1002, node2)
must.NoError(t, err)
testCases := []struct {
name string
pool string
expected []string
}{
{
name: "default",
pool: structs.NodePoolDefault,
expected: []string{
node1.ID,
},
},
{
name: "pool",
pool: pool.Name,
expected: []string{
node2.ID,
},
},
{
name: "empty pool",
pool: "",
expected: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create watcher to test that getters don't cause it to fire.
ws := memdb.NewWatchSet()
iter, err := state.NodesByNodePool(ws, tc.pool)
must.NoError(t, err)
got := []string{}
for raw := iter.Next(); raw != nil; raw = iter.Next() {
got = append(got, raw.(*structs.Node).ID)
}
must.SliceContainsAll(t, tc.expected, got)
must.False(t, watchFired(ws))
})
}
}
func TestStateStore_UpsertJob_Job(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(job, out) {
t.Fatalf("bad: %#v %#v", job, out)
}
index, err := state.Index("jobs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
summary, err := state.JobSummaryByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != job.ID {
t.Fatalf("bad summary id: %v", summary.JobID)
}
_, ok := summary.Summary["web"]
if !ok {
t.Fatalf("nil summary for task group")
}
if watchFired(ws) {
t.Fatalf("bad")
}
// Check the job versions
allVersions, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(allVersions) != 1 {
t.Fatalf("got %d; want 1", len(allVersions))
}
if a := allVersions[0]; a.ID != job.ID || a.Version != 0 {
t.Fatalf("bad: %v", a)
}
// Test the looking up the job by version returns the same results
vout, err := state.JobByIDAndVersion(ws, job.Namespace, job.ID, 0)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(out, vout) {
t.Fatalf("bad: %#v %#v", out, vout)
}
}
func TestStateStore_UpdateUpsertJob_Job(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
job2 := mock.Job()
job2.ID = job.ID
job2.AllAtOnce = true
err = state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job2)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(job2, out) {
t.Fatalf("bad: %#v %#v", job2, out)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1001 {
t.Fatalf("bad: %#v", out)
}
if out.Version != 1 {
t.Fatalf("bad: %#v", out)
}
index, err := state.Index("jobs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
// Test the looking up the job by version returns the same results
vout, err := state.JobByIDAndVersion(ws, job.Namespace, job.ID, 1)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(out, vout) {
t.Fatalf("bad: %#v %#v", out, vout)
}
// Test that the job summary remains the same if the job is updated but
// count remains same
summary, err := state.JobSummaryByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != job.ID {
t.Fatalf("bad summary id: %v", summary.JobID)
}
_, ok := summary.Summary["web"]
if !ok {
t.Fatalf("nil summary for task group")
}
// Check the job versions
allVersions, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(allVersions) != 2 {
t.Fatalf("got %d; want 1", len(allVersions))
}
if a := allVersions[0]; a.ID != job.ID || a.Version != 1 || !a.AllAtOnce {
t.Fatalf("bad: %+v", a)
}
if a := allVersions[1]; a.ID != job.ID || a.Version != 0 || a.AllAtOnce {
t.Fatalf("bad: %+v", a)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpdateUpsertJob_PeriodicJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.PeriodicJob()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
// Create a child and an evaluation
job2 := job.Copy()
job2.Periodic = nil
job2.ID = fmt.Sprintf("%v/%s-1490635020", job.ID, structs.PeriodicLaunchSuffix)
err = state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job2)
if err != nil {
t.Fatalf("err: %v", err)
}
eval := mock.Eval()
eval.JobID = job2.ID
err = state.UpsertEvals(structs.MsgTypeTestSetup, 1002, []*structs.Evaluation{eval})
if err != nil {
t.Fatalf("err: %v", err)
}
job3 := job.Copy()
job3.TaskGroups[0].Tasks[0].Name = "new name"
err = state.UpsertJob(structs.MsgTypeTestSetup, 1003, nil, job3)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if s, e := out.Status, structs.JobStatusRunning; s != e {
t.Fatalf("got status %v; want %v", s, e)
}
}
func TestStateStore_UpsertJob_BadNamespace(t *testing.T) {
ci.Parallel(t)
assert := assert.New(t)
state := testStateStore(t)
job := mock.Job()
job.Namespace = "foo"
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
assert.Contains(err.Error(), "nonexistent namespace")
ws := memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
assert.Nil(err)
assert.Nil(out)
}
func TestStateStore_UpsertJob_NodePool(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
testCases := []struct {
name string
pool string
expectedPool string
expectedErr string
}{
{
name: "empty node pool uses default",
pool: "",
expectedPool: structs.NodePoolDefault,
},
{
name: "job uses pool defined",
pool: structs.NodePoolDefault,
expectedPool: structs.NodePoolDefault,
},
{
name: "error when pool doesn't exist",
pool: "nonexisting",
expectedErr: "nonexistent node pool",
},
}
for i, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
job := mock.Job()
job.NodePool = tc.pool
err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(1000+i), nil, job)
if tc.expectedErr != "" {
must.ErrorContains(t, err, tc.expectedErr)
} else {
must.NoError(t, err)
ws := memdb.NewWatchSet()
got, err := state.JobByID(ws, job.Namespace, job.ID)
must.NoError(t, err)
must.Eq(t, tc.expectedPool, got.NodePool)
}
})
}
}
// Upsert a job that is the child of a parent job and ensures its summary gets
// updated.
func TestStateStore_UpsertJob_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a watchset so we can test that upsert fires the watch
parent := mock.Job()
ws := memdb.NewWatchSet()
_, err := state.JobByID(ws, parent.Namespace, parent.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, parent); err != nil {
t.Fatalf("err: %v", err)
}
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, child); err != nil {
t.Fatalf("err: %v", err)
}
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != parent.ID {
t.Fatalf("bad summary id: %v", parent.ID)
}
if summary.Children == nil {
t.Fatalf("nil children summary")
}
if summary.Children.Pending != 1 || summary.Children.Running != 0 || summary.Children.Dead != 0 {
t.Fatalf("bad children summary: %v", summary.Children)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertJob_submission(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
job.Meta = map[string]string{"version": "1"}
submission := &structs.JobSubmission{
Source: "source",
Version: 0,
}
index := uint64(1000)
// initially non-existent
sub, err := state.JobSubmission(nil, job.Namespace, job.ID, 0)
must.NoError(t, err)
must.Nil(t, sub)
// insert first one, version 0, index 1001
index++
err = state.UpsertJob(structs.JobRegisterRequestType, index, submission, job)
must.NoError(t, err)
// query first one, version 0
sub, err = state.JobSubmission(nil, job.Namespace, job.ID, 0)
must.NoError(t, err)
must.NotNil(t, sub)
must.Eq(t, 0, sub.Version)
must.Eq(t, index, sub.JobModifyIndex)
// insert 6 more, going over the limit
for i := 1; i <= structs.JobDefaultTrackedVersions; i++ {
index++
job2 := job.Copy()
job2.Meta["version"] = strconv.Itoa(i)
sub2 := &structs.JobSubmission{
Source: "source",
Version: uint64(i),
}
err = state.UpsertJob(structs.JobRegisterRequestType, index, sub2, job2)
must.NoError(t, err)
}
// the version 0 submission is now dropped
sub, err = state.JobSubmission(nil, job.Namespace, job.ID, 0)
must.NoError(t, err)
must.Nil(t, sub)
// but we do have version 1
sub, err = state.JobSubmission(nil, job.Namespace, job.ID, 1)
must.NoError(t, err)
must.NotNil(t, sub)
must.Eq(t, 1, sub.Version)
must.Eq(t, 1002, sub.JobModifyIndex)
// and up to version 6
sub, err = state.JobSubmission(nil, job.Namespace, job.ID, 6)
must.NoError(t, err)
must.NotNil(t, sub)
must.Eq(t, 6, sub.Version)
must.Eq(t, 1007, sub.JobModifyIndex)
}
func TestStateStore_UpdateUpsertJob_JobVersion(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a job and mark it as stable
job := mock.Job()
job.Stable = true
job.Name = "0"
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
var finalJob *structs.Job
for i := 1; i < 300; i++ {
finalJob = mock.Job()
finalJob.ID = job.ID
finalJob.Name = fmt.Sprintf("%d", i)
err = state.UpsertJob(structs.MsgTypeTestSetup, uint64(1000+i), nil, finalJob)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(finalJob, out) {
t.Fatalf("bad: %#v %#v", finalJob, out)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1299 {
t.Fatalf("bad: %#v", out)
}
if out.Version != 299 {
t.Fatalf("bad: %#v", out)
}
index, err := state.Index("job_version")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1299 {
t.Fatalf("bad: %d", index)
}
// Check the job versions
allVersions, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(allVersions) != structs.JobDefaultTrackedVersions {
t.Fatalf("got %d; want %d", len(allVersions), structs.JobDefaultTrackedVersions)
}
if a := allVersions[0]; a.ID != job.ID || a.Version != 299 || a.Name != "299" {
t.Fatalf("bad: %+v", a)
}
if a := allVersions[1]; a.ID != job.ID || a.Version != 298 || a.Name != "298" {
t.Fatalf("bad: %+v", a)
}
// Ensure we didn't delete the stable job
if a := allVersions[structs.JobDefaultTrackedVersions-1]; a.ID != job.ID ||
a.Version != 0 || a.Name != "0" || !a.Stable {
t.Fatalf("bad: %+v", a)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteJob_Job(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
if _, err := state.JobByID(ws, job.Namespace, job.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err = state.DeleteJob(1001, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", job, out)
}
index, err := state.Index("jobs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
summary, err := state.JobSummaryByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary != nil {
t.Fatalf("expected summary to be nil, but got: %v", summary)
}
index, err = state.Index("job_summary")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
versions, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(versions) != 0 {
t.Fatalf("expected no job versions")
}
index, err = state.Index("job_summary")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteJobTxn_BatchDeletes(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
const testJobCount = 10
const jobVersionCount = 4
stateIndex := uint64(1000)
jobs := make([]*structs.Job, testJobCount)
for i := 0; i < testJobCount; i++ {
stateIndex++
job := mock.BatchJob()
err := state.UpsertJob(structs.MsgTypeTestSetup, stateIndex, nil, job)
require.NoError(t, err)
jobs[i] = job
// Create some versions
for vi := 1; vi < jobVersionCount; vi++ {
stateIndex++
job := job.Copy()
job.TaskGroups[0].Tasks[0].Env = map[string]string{
"Version": fmt.Sprintf("%d", vi),
}
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, stateIndex, nil, job))
}
}
ws := memdb.NewWatchSet()
// Check that jobs are present in DB
job, err := state.JobByID(ws, jobs[0].Namespace, jobs[0].ID)
require.NoError(t, err)
require.Equal(t, jobs[0].ID, job.ID)
jobVersions, err := state.JobVersionsByID(ws, jobs[0].Namespace, jobs[0].ID)
require.NoError(t, err)
require.Equal(t, jobVersionCount, len(jobVersions))
// Actually delete
const deletionIndex = uint64(10001)
err = state.WithWriteTransaction(structs.MsgTypeTestSetup, deletionIndex, func(txn Txn) error {
for i, job := range jobs {
err := state.DeleteJobTxn(deletionIndex, job.Namespace, job.ID, txn)
require.NoError(t, err, "failed at %d %e", i, err)
}
return nil
})
assert.NoError(t, err)
assert.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, jobs[0].Namespace, jobs[0].ID)
require.NoError(t, err)
require.Nil(t, out)
jobVersions, err = state.JobVersionsByID(ws, jobs[0].Namespace, jobs[0].ID)
require.NoError(t, err)
require.Empty(t, jobVersions)
index, err := state.Index("jobs")
require.NoError(t, err)
require.Equal(t, deletionIndex, index)
}
func TestStateStore_DeleteJob_MultipleVersions(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
assert := assert.New(t)
// Create a job and mark it as stable
job := mock.Job()
job.Stable = true
job.Priority = 0
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
assert.Nil(err)
assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job))
assert.True(watchFired(ws))
var finalJob *structs.Job
for i := 1; i < 20; i++ {
finalJob = mock.Job()
finalJob.ID = job.ID
finalJob.Priority = i
assert.Nil(state.UpsertJob(structs.MsgTypeTestSetup, uint64(1000+i), nil, finalJob))
}
assert.Nil(state.DeleteJob(1020, job.Namespace, job.ID))
assert.True(watchFired(ws))
ws = memdb.NewWatchSet()
out, err := state.JobByID(ws, job.Namespace, job.ID)
assert.Nil(err)
assert.Nil(out)
index, err := state.Index("jobs")
assert.Nil(err)
assert.EqualValues(1020, index)
summary, err := state.JobSummaryByID(ws, job.Namespace, job.ID)
assert.Nil(err)
assert.Nil(summary)
index, err = state.Index("job_version")
assert.Nil(err)
assert.EqualValues(1020, index)
versions, err := state.JobVersionsByID(ws, job.Namespace, job.ID)
assert.Nil(err)
assert.Len(versions, 0)
index, err = state.Index("job_summary")
assert.Nil(err)
assert.EqualValues(1020, index)
assert.False(watchFired(ws))
}
func TestStateStore_DeleteJob_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
parent := mock.Job()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, parent); err != nil {
t.Fatalf("err: %v", err)
}
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, child); err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err := state.DeleteJob(1001, child.Namespace, child.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != parent.ID {
t.Fatalf("bad summary id: %v", parent.ID)
}
if summary.Children == nil {
t.Fatalf("nil children summary")
}
if summary.Children.Pending != 0 || summary.Children.Running != 0 || summary.Children.Dead != 1 {
t.Fatalf("bad children summary: %v", summary.Children)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_Jobs(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var jobs []*structs.Job
for i := 0; i < 10; i++ {
job := mock.Job()
jobs = append(jobs, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.Jobs(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
var out []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Job))
}
sort.Sort(JobIDSort(jobs))
sort.Sort(JobIDSort(out))
if !reflect.DeepEqual(jobs, out) {
t.Fatalf("bad: %#v %#v", jobs, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_JobVersions(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var jobs []*structs.Job
for i := 0; i < 10; i++ {
job := mock.Job()
jobs = append(jobs, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.JobVersions(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
var out []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Job))
}
sort.Sort(JobIDSort(jobs))
sort.Sort(JobIDSort(out))
if !reflect.DeepEqual(jobs, out) {
t.Fatalf("bad: %#v %#v", jobs, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_JobsByIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
job.ID = "redis"
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
iter, err := state.JobsByIDPrefix(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
gatherJobs := func(iter memdb.ResultIterator) []*structs.Job {
var jobs []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
jobs = append(jobs, raw.(*structs.Job))
}
return jobs
}
jobs := gatherJobs(iter)
if len(jobs) != 1 {
t.Fatalf("err: %v", err)
}
iter, err = state.JobsByIDPrefix(ws, job.Namespace, "re")
if err != nil {
t.Fatalf("err: %v", err)
}
jobs = gatherJobs(iter)
if len(jobs) != 1 {
t.Fatalf("err: %v", err)
}
if watchFired(ws) {
t.Fatalf("bad")
}
job = mock.Job()
job.ID = "riak"
err = state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
iter, err = state.JobsByIDPrefix(ws, job.Namespace, "r")
if err != nil {
t.Fatalf("err: %v", err)
}
jobs = gatherJobs(iter)
if len(jobs) != 2 {
t.Fatalf("err: %v", err)
}
iter, err = state.JobsByIDPrefix(ws, job.Namespace, "ri")
if err != nil {
t.Fatalf("err: %v", err)
}
jobs = gatherJobs(iter)
if len(jobs) != 1 {
t.Fatalf("err: %v", err)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_JobsByIDPrefix_Namespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job1 := mock.Job()
job2 := mock.Job()
ns1 := mock.Namespace()
ns1.Name = "namespace1"
ns2 := mock.Namespace()
ns2.Name = "namespace2"
jobID := "redis"
job1.ID = jobID
job2.ID = jobID
job1.Namespace = ns1.Name
job2.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job1))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job2))
gatherJobs := func(iter memdb.ResultIterator) []*structs.Job {
var jobs []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
jobs = append(jobs, raw.(*structs.Job))
}
return jobs
}
// Try full match
ws := memdb.NewWatchSet()
iter1, err := state.JobsByIDPrefix(ws, ns1.Name, jobID)
require.NoError(t, err)
iter2, err := state.JobsByIDPrefix(ws, ns2.Name, jobID)
require.NoError(t, err)
jobsNs1 := gatherJobs(iter1)
require.Len(t, jobsNs1, 1)
jobsNs2 := gatherJobs(iter2)
require.Len(t, jobsNs2, 1)
// Try prefix
iter1, err = state.JobsByIDPrefix(ws, ns1.Name, "re")
require.NoError(t, err)
iter2, err = state.JobsByIDPrefix(ws, ns2.Name, "re")
require.NoError(t, err)
jobsNs1 = gatherJobs(iter1)
jobsNs2 = gatherJobs(iter2)
require.Len(t, jobsNs1, 1)
require.Len(t, jobsNs2, 1)
job3 := mock.Job()
job3.ID = "riak"
job3.Namespace = ns1.Name
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1003, nil, job3))
require.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
iter1, err = state.JobsByIDPrefix(ws, ns1.Name, "r")
require.NoError(t, err)
iter2, err = state.JobsByIDPrefix(ws, ns2.Name, "r")
require.NoError(t, err)
jobsNs1 = gatherJobs(iter1)
jobsNs2 = gatherJobs(iter2)
require.Len(t, jobsNs1, 2)
require.Len(t, jobsNs2, 1)
iter1, err = state.JobsByIDPrefix(ws, ns1.Name, "ri")
require.NoError(t, err)
jobsNs1 = gatherJobs(iter1)
require.Len(t, jobsNs1, 1)
require.False(t, watchFired(ws))
}
func TestStateStore_JobsByNamespace(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns1.Name = "new"
job1 := mock.Job()
job2 := mock.Job()
job1.Namespace = ns1.Name
job2.Namespace = ns1.Name
ns2 := mock.Namespace()
ns2.Name = "new-namespace"
job3 := mock.Job()
job4 := mock.Job()
job3.Namespace = ns2.Name
job4.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
// Create watchsets so we can test that update fires the watch
watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()}
_, err := state.JobsByNamespace(watches[0], ns1.Name)
require.NoError(t, err)
_, err = state.JobsByNamespace(watches[1], ns2.Name)
require.NoError(t, err)
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job1))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1002, nil, job2))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1003, nil, job3))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1004, nil, job4))
require.True(t, watchFired(watches[0]))
require.True(t, watchFired(watches[1]))
ws := memdb.NewWatchSet()
iter1, err := state.JobsByNamespace(ws, ns1.Name)
require.NoError(t, err)
iter2, err := state.JobsByNamespace(ws, ns2.Name)
require.NoError(t, err)
var out1 []*structs.Job
for {
raw := iter1.Next()
if raw == nil {
break
}
out1 = append(out1, raw.(*structs.Job))
}
var out2 []*structs.Job
for {
raw := iter2.Next()
if raw == nil {
break
}
out2 = append(out2, raw.(*structs.Job))
}
require.Len(t, out1, 2)
require.Len(t, out2, 2)
for _, job := range out1 {
require.Equal(t, ns1.Name, job.Namespace)
}
for _, job := range out2 {
require.Equal(t, ns2.Name, job.Namespace)
}
index, err := state.Index("jobs")
require.NoError(t, err)
require.EqualValues(t, 1004, index)
require.False(t, watchFired(ws))
}
func TestStateStore_JobsByPeriodic(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var periodic, nonPeriodic []*structs.Job
for i := 0; i < 10; i++ {
job := mock.Job()
nonPeriodic = append(nonPeriodic, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
for i := 0; i < 10; i++ {
job := mock.PeriodicJob()
periodic = append(periodic, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 2000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.JobsByPeriodic(ws, true)
if err != nil {
t.Fatalf("err: %v", err)
}
var outPeriodic []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
outPeriodic = append(outPeriodic, raw.(*structs.Job))
}
iter, err = state.JobsByPeriodic(ws, false)
if err != nil {
t.Fatalf("err: %v", err)
}
var outNonPeriodic []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
outNonPeriodic = append(outNonPeriodic, raw.(*structs.Job))
}
sort.Sort(JobIDSort(periodic))
sort.Sort(JobIDSort(nonPeriodic))
sort.Sort(JobIDSort(outPeriodic))
sort.Sort(JobIDSort(outNonPeriodic))
if !reflect.DeepEqual(periodic, outPeriodic) {
t.Fatalf("bad: %#v %#v", periodic, outPeriodic)
}
if !reflect.DeepEqual(nonPeriodic, outNonPeriodic) {
t.Fatalf("bad: %#v %#v", nonPeriodic, outNonPeriodic)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_JobsByScheduler(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var serviceJobs []*structs.Job
var sysJobs []*structs.Job
for i := 0; i < 10; i++ {
job := mock.Job()
serviceJobs = append(serviceJobs, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
for i := 0; i < 10; i++ {
job := mock.SystemJob()
job.Status = structs.JobStatusRunning
sysJobs = append(sysJobs, job)
err := state.UpsertJob(structs.MsgTypeTestSetup, 2000+uint64(i), nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.JobsByScheduler(ws, "service")
if err != nil {
t.Fatalf("err: %v", err)
}
var outService []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
outService = append(outService, raw.(*structs.Job))
}
iter, err = state.JobsByScheduler(ws, "system")
if err != nil {
t.Fatalf("err: %v", err)
}
var outSystem []*structs.Job
for {
raw := iter.Next()
if raw == nil {
break
}
outSystem = append(outSystem, raw.(*structs.Job))
}
sort.Sort(JobIDSort(serviceJobs))
sort.Sort(JobIDSort(sysJobs))
sort.Sort(JobIDSort(outService))
sort.Sort(JobIDSort(outSystem))
if !reflect.DeepEqual(serviceJobs, outService) {
t.Fatalf("bad: %#v %#v", serviceJobs, outService)
}
if !reflect.DeepEqual(sysJobs, outSystem) {
t.Fatalf("bad: %#v %#v", sysJobs, outSystem)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_JobsByGC(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
gc, nonGc := make(map[string]struct{}), make(map[string]struct{})
for i := 0; i < 20; i++ {
var job *structs.Job
if i%2 == 0 {
job = mock.Job()
} else {
job = mock.PeriodicJob()
}
nonGc[job.ID] = struct{}{}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000+uint64(i), nil, job); err != nil {
t.Fatalf("err: %v", err)
}
}
for i := 0; i < 20; i += 2 {
job := mock.Job()
job.Type = structs.JobTypeBatch
gc[job.ID] = struct{}{}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 2000+uint64(i), nil, job); err != nil {
t.Fatalf("err: %v", err)
}
// Create an eval for it
eval := mock.Eval()
eval.JobID = job.ID
eval.Status = structs.EvalStatusComplete
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 2000+uint64(i+1), []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.JobsByGC(ws, true)
if err != nil {
t.Fatalf("err: %v", err)
}
outGc := make(map[string]struct{})
for i := iter.Next(); i != nil; i = iter.Next() {
j := i.(*structs.Job)
outGc[j.ID] = struct{}{}
}
iter, err = state.JobsByGC(ws, false)
if err != nil {
t.Fatalf("err: %v", err)
}
outNonGc := make(map[string]struct{})
for i := iter.Next(); i != nil; i = iter.Next() {
j := i.(*structs.Job)
outNonGc[j.ID] = struct{}{}
}
if !reflect.DeepEqual(gc, outGc) {
t.Fatalf("bad: %#v %#v", gc, outGc)
}
if !reflect.DeepEqual(nonGc, outNonGc) {
t.Fatalf("bad: %#v %#v", nonGc, outNonGc)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertPeriodicLaunch(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
launch := &structs.PeriodicLaunch{
ID: job.ID,
Namespace: job.Namespace,
Launch: time.Now(),
}
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
if _, err := state.PeriodicLaunchByID(ws, job.Namespace, launch.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err := state.UpsertPeriodicLaunch(1000, launch)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.PeriodicLaunchByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if !reflect.DeepEqual(launch, out) {
t.Fatalf("bad: %#v %#v", job, out)
}
index, err := state.Index("periodic_launch")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpdateUpsertPeriodicLaunch(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
launch := &structs.PeriodicLaunch{
ID: job.ID,
Namespace: job.Namespace,
Launch: time.Now(),
}
err := state.UpsertPeriodicLaunch(1000, launch)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
if _, err := state.PeriodicLaunchByID(ws, job.Namespace, launch.ID); err != nil {
t.Fatalf("bad: %v", err)
}
launch2 := &structs.PeriodicLaunch{
ID: job.ID,
Namespace: job.Namespace,
Launch: launch.Launch.Add(1 * time.Second),
}
err = state.UpsertPeriodicLaunch(1001, launch2)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.PeriodicLaunchByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1001 {
t.Fatalf("bad: %#v", out)
}
if !reflect.DeepEqual(launch2, out) {
t.Fatalf("bad: %#v %#v", launch2, out)
}
index, err := state.Index("periodic_launch")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeletePeriodicLaunch(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
launch := &structs.PeriodicLaunch{
ID: job.ID,
Namespace: job.Namespace,
Launch: time.Now(),
}
err := state.UpsertPeriodicLaunch(1000, launch)
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
if _, err := state.PeriodicLaunchByID(ws, job.Namespace, launch.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err = state.DeletePeriodicLaunch(1001, launch.Namespace, launch.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.PeriodicLaunchByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", job, out)
}
index, err := state.Index("periodic_launch")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_PeriodicLaunches(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var launches []*structs.PeriodicLaunch
for i := 0; i < 10; i++ {
job := mock.Job()
launch := &structs.PeriodicLaunch{
ID: job.ID,
Namespace: job.Namespace,
Launch: time.Now(),
}
launches = append(launches, launch)
err := state.UpsertPeriodicLaunch(1000+uint64(i), launch)
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.PeriodicLaunches(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
out := make(map[string]*structs.PeriodicLaunch, 10)
for {
raw := iter.Next()
if raw == nil {
break
}
launch := raw.(*structs.PeriodicLaunch)
if _, ok := out[launch.ID]; ok {
t.Fatalf("duplicate: %v", launch.ID)
}
out[launch.ID] = launch
}
for _, launch := range launches {
l, ok := out[launch.ID]
if !ok {
t.Fatalf("bad %v", launch.ID)
}
if !reflect.DeepEqual(launch, l) {
t.Fatalf("bad: %#v %#v", launch, l)
}
delete(out, launch.ID)
}
if len(out) != 0 {
t.Fatalf("leftover: %#v", out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
// TestStateStore_CSIVolume checks register, list and deregister for csi_volumes
func TestStateStore_CSIVolume(t *testing.T) {
state := testStateStore(t)
index := uint64(1000)
// Volume IDs
vol0, vol1 := uuid.Generate(), uuid.Generate()
// Create a node running a healthy instance of the plugin
node := mock.Node()
pluginID := "minnie"
alloc := mock.Alloc()
alloc.DesiredStatus = "run"
alloc.ClientStatus = "running"
alloc.NodeID = node.ID
alloc.Job.TaskGroups[0].Volumes = map[string]*structs.VolumeRequest{
"foo": {
Name: "foo",
Source: vol0,
Type: "csi",
},
}
node.CSINodePlugins = map[string]*structs.CSIInfo{
pluginID: {
PluginID: pluginID,
AllocID: alloc.ID,
Healthy: true,
HealthDescription: "healthy",
RequiresControllerPlugin: false,
RequiresTopologies: false,
NodeInfo: &structs.CSINodeInfo{
ID: node.ID,
MaxVolumes: 64,
RequiresNodeStageVolume: true,
},
},
}
index++
err := state.UpsertNode(structs.MsgTypeTestSetup, index, node)
require.NoError(t, err)
defer state.DeleteNode(structs.MsgTypeTestSetup, 9999, []string{pluginID})
index++
err = state.UpsertAllocs(structs.MsgTypeTestSetup, index, []*structs.Allocation{alloc})
require.NoError(t, err)
ns := structs.DefaultNamespace
v0 := structs.NewCSIVolume("foo", index)
v0.ID = vol0
v0.Namespace = ns
v0.PluginID = "minnie"
v0.Schedulable = true
v0.AccessMode = structs.CSIVolumeAccessModeMultiNodeSingleWriter
v0.AttachmentMode = structs.CSIVolumeAttachmentModeFilesystem
v0.RequestedCapabilities = []*structs.CSIVolumeCapability{{
AccessMode: structs.CSIVolumeAccessModeMultiNodeSingleWriter,
AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
}}
index++
v1 := structs.NewCSIVolume("foo", index)
v1.ID = vol1
v1.Namespace = ns
v1.PluginID = "adam"
v1.Schedulable = true
v1.AccessMode = structs.CSIVolumeAccessModeMultiNodeSingleWriter
v1.AttachmentMode = structs.CSIVolumeAttachmentModeFilesystem
v1.RequestedCapabilities = []*structs.CSIVolumeCapability{{
AccessMode: structs.CSIVolumeAccessModeMultiNodeSingleWriter,
AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
}}
index++
err = state.UpsertCSIVolume(index, []*structs.CSIVolume{v0, v1})
require.NoError(t, err)
// volume registration is idempotent, unless identies are changed
index++
err = state.UpsertCSIVolume(index, []*structs.CSIVolume{v0, v1})
require.NoError(t, err)
index++
v2 := v0.Copy()
v2.PluginID = "new-id"
err = state.UpsertCSIVolume(index, []*structs.CSIVolume{v2})
require.Error(t, err, fmt.Sprintf("volume exists: %s", v0.ID))
ws := memdb.NewWatchSet()
iter, err := state.CSIVolumesByNamespace(ws, ns, "")
require.NoError(t, err)
slurp := func(iter memdb.ResultIterator) (vs []*structs.CSIVolume) {
for {
raw := iter.Next()
if raw == nil {
break
}
vol := raw.(*structs.CSIVolume)
vs = append(vs, vol)
}
return vs
}
vs := slurp(iter)
require.Equal(t, 2, len(vs))
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByPluginID(ws, ns, "", "minnie")
require.NoError(t, err)
vs = slurp(iter)
require.Equal(t, 1, len(vs))
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByNodeID(ws, "", node.ID)
require.NoError(t, err)
vs = slurp(iter)
require.Equal(t, 1, len(vs))
// Allocs
a0 := mock.Alloc()
a1 := mock.Alloc()
index++
err = state.UpsertAllocs(structs.MsgTypeTestSetup, index, []*structs.Allocation{a0, a1})
require.NoError(t, err)
// Claims
r := structs.CSIVolumeClaimRead
w := structs.CSIVolumeClaimWrite
u := structs.CSIVolumeClaimGC
claim0 := &structs.CSIVolumeClaim{
AllocationID: a0.ID,
NodeID: node.ID,
Mode: r,
}
claim1 := &structs.CSIVolumeClaim{
AllocationID: a1.ID,
NodeID: node.ID,
Mode: w,
}
index++
err = state.CSIVolumeClaim(index, ns, vol0, claim0)
require.NoError(t, err)
index++
err = state.CSIVolumeClaim(index, ns, vol0, claim1)
require.NoError(t, err)
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByPluginID(ws, ns, "", "minnie")
require.NoError(t, err)
vs = slurp(iter)
require.False(t, vs[0].HasFreeWriteClaims())
claim2 := new(structs.CSIVolumeClaim)
*claim2 = *claim0
claim2.Mode = u
err = state.CSIVolumeClaim(2, ns, vol0, claim2)
require.NoError(t, err)
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByPluginID(ws, ns, "", "minnie")
require.NoError(t, err)
vs = slurp(iter)
require.True(t, vs[0].ReadSchedulable())
// deregistration is an error when the volume is in use
index++
err = state.CSIVolumeDeregister(index, ns, []string{vol0}, false)
require.Error(t, err, "volume deregistered while in use")
// even if forced, because we have a non-terminal claim
index++
err = state.CSIVolumeDeregister(index, ns, []string{vol0}, true)
require.Error(t, err, "volume force deregistered while in use")
// we use the ID, not a prefix
index++
err = state.CSIVolumeDeregister(index, ns, []string{"fo"}, true)
require.Error(t, err, "volume deregistered by prefix")
// release claims to unblock deregister
index++
claim3 := new(structs.CSIVolumeClaim)
*claim3 = *claim2
claim3.State = structs.CSIVolumeClaimStateReadyToFree
err = state.CSIVolumeClaim(index, ns, vol0, claim3)
require.NoError(t, err)
index++
claim1.Mode = u
claim1.State = structs.CSIVolumeClaimStateReadyToFree
err = state.CSIVolumeClaim(index, ns, vol0, claim1)
require.NoError(t, err)
index++
err = state.CSIVolumeDeregister(index, ns, []string{vol0}, false)
require.NoError(t, err)
// List, now omitting the deregistered volume
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByPluginID(ws, ns, "", "minnie")
require.NoError(t, err)
vs = slurp(iter)
require.Equal(t, 0, len(vs))
ws = memdb.NewWatchSet()
iter, err = state.CSIVolumesByNamespace(ws, ns, "")
require.NoError(t, err)
vs = slurp(iter)
require.Equal(t, 1, len(vs))
}
func TestStateStore_CSIPlugin_Lifecycle(t *testing.T) {
ci.Parallel(t)
store := testStateStore(t)
plugID := "foo"
var err error
var controllerJobID string
var nodeJobID string
allocIDs := []string{}
type pluginCounts struct {
controllerFingerprints int
nodeFingerprints int
controllersHealthy int
nodesHealthy int
controllersExpected int
nodesExpected int
}
// helper function for test assertions
checkPlugin := func(counts pluginCounts) *structs.CSIPlugin {
plug, err := store.CSIPluginByID(memdb.NewWatchSet(), plugID)
require.NotNil(t, plug, "plugin was nil")
require.NoError(t, err)
require.Equal(t, counts.controllerFingerprints, len(plug.Controllers), "controllers fingerprinted")
require.Equal(t, counts.nodeFingerprints, len(plug.Nodes), "nodes fingerprinted")
require.Equal(t, counts.controllersHealthy, plug.ControllersHealthy, "controllers healthy")
require.Equal(t, counts.nodesHealthy, plug.NodesHealthy, "nodes healthy")
require.Equal(t, counts.controllersExpected, plug.ControllersExpected, "controllers expected")
require.Equal(t, counts.nodesExpected, plug.NodesExpected, "nodes expected")
return plug.Copy()
}
type allocUpdateKind int
const (
SERVER allocUpdateKind = iota
CLIENT
)
// helper function calling client-side update with with
// UpsertAllocs and/or UpdateAllocsFromClient, depending on which
// status(es) are set
updateAllocsFn := func(allocIDs []string, kind allocUpdateKind,
transform func(alloc *structs.Allocation)) []*structs.Allocation {
allocs := []*structs.Allocation{}
ws := memdb.NewWatchSet()
for _, id := range allocIDs {
alloc, err := store.AllocByID(ws, id)
require.NoError(t, err)
alloc = alloc.Copy()
transform(alloc)
allocs = append(allocs, alloc)
}
switch kind {
case SERVER:
err = store.UpsertAllocs(structs.MsgTypeTestSetup, nextIndex(store), allocs)
case CLIENT:
// this is somewhat artificial b/c we get alloc updates
// from multiple nodes concurrently but not in a single
// RPC call. But this guarantees we'll trigger any nested
// transaction setup bugs
err = store.UpdateAllocsFromClient(structs.MsgTypeTestSetup, nextIndex(store), allocs)
}
require.NoError(t, err)
return allocs
}
// helper function calling UpsertNode for fingerprinting
updateNodeFn := func(nodeID string, transform func(node *structs.Node)) {
ws := memdb.NewWatchSet()
node, _ := store.NodeByID(ws, nodeID)
node = node.Copy()
transform(node)
err = store.UpsertNode(structs.MsgTypeTestSetup, nextIndex(store), node)
require.NoError(t, err)
}
nodes := []*structs.Node{mock.Node(), mock.Node(), mock.Node()}
for _, node := range nodes {
err = store.UpsertNode(structs.MsgTypeTestSetup, nextIndex(store), node)
require.NoError(t, err)
}
// Note: these are all subtests for clarity but are expected to be
// ordered, because they walk through all the phases of plugin
// instance registration and deregistration
t.Run("register plugin jobs", func(t *testing.T) {
controllerJob := mock.CSIPluginJob(structs.CSIPluginTypeController, plugID)
controllerJobID = controllerJob.ID
err = store.UpsertJob(structs.MsgTypeTestSetup, nextIndex(store), nil, controllerJob)
nodeJob := mock.CSIPluginJob(structs.CSIPluginTypeNode, plugID)
nodeJobID = nodeJob.ID
err = store.UpsertJob(structs.MsgTypeTestSetup, nextIndex(store), nil, nodeJob)
// plugins created, but no fingerprints or allocs yet
// note: there's no job summary yet, but we know the task
// group count for the non-system job
//
// TODO: that's the current code but we really should be able
// to figure out the system jobs too
plug := checkPlugin(pluginCounts{
controllerFingerprints: 0,
nodeFingerprints: 0,
controllersHealthy: 0,
nodesHealthy: 0,
controllersExpected: 2,
nodesExpected: 0,
})
require.False(t, plug.ControllerRequired)
})
t.Run("plan apply upserts allocations", func(t *testing.T) {
allocForJob := func(job *structs.Job) *structs.Allocation {
alloc := mock.Alloc()
alloc.Job = job.Copy()
alloc.JobID = job.ID
alloc.TaskGroup = job.TaskGroups[0].Name
alloc.DesiredStatus = structs.AllocDesiredStatusRun
alloc.ClientStatus = structs.AllocClientStatusPending
return alloc
}
ws := memdb.NewWatchSet()
controllerJob, _ := store.JobByID(ws, structs.DefaultNamespace, controllerJobID)
controllerAlloc0 := allocForJob(controllerJob)
controllerAlloc0.NodeID = nodes[0].ID
allocIDs = append(allocIDs, controllerAlloc0.ID)
controllerAlloc1 := allocForJob(controllerJob)
controllerAlloc1.NodeID = nodes[1].ID
allocIDs = append(allocIDs, controllerAlloc1.ID)
allocs := []*structs.Allocation{controllerAlloc0, controllerAlloc1}
nodeJob, _ := store.JobByID(ws, structs.DefaultNamespace, nodeJobID)
for _, node := range nodes {
nodeAlloc := allocForJob(nodeJob)
nodeAlloc.NodeID = node.ID
allocIDs = append(allocIDs, nodeAlloc.ID)
allocs = append(allocs, nodeAlloc)
}
err = store.UpsertAllocs(structs.MsgTypeTestSetup, nextIndex(store), allocs)
require.NoError(t, err)
// node plugin now has expected counts too
plug := checkPlugin(pluginCounts{
controllerFingerprints: 0,
nodeFingerprints: 0,
controllersHealthy: 0,
nodesHealthy: 0,
controllersExpected: 2,
nodesExpected: 3,
})
require.False(t, plug.ControllerRequired)
})
t.Run("client upserts alloc status", func(t *testing.T) {
updateAllocsFn(allocIDs, CLIENT, func(alloc *structs.Allocation) {
alloc.ClientStatus = structs.AllocClientStatusRunning
})
// plugin still has allocs but no fingerprints
plug := checkPlugin(pluginCounts{
controllerFingerprints: 0,
nodeFingerprints: 0,
controllersHealthy: 0,
nodesHealthy: 0,
controllersExpected: 2,
nodesExpected: 3,
})
require.False(t, plug.ControllerRequired)
})
t.Run("client upserts node fingerprints", func(t *testing.T) {
nodeFingerprint := map[string]*structs.CSIInfo{
plugID: {
PluginID: plugID,
Healthy: true,
UpdateTime: time.Now(),
RequiresControllerPlugin: true,
RequiresTopologies: false,
NodeInfo: &structs.CSINodeInfo{},
},
}
for _, node := range nodes {
updateNodeFn(node.ID, func(node *structs.Node) {
node.CSINodePlugins = nodeFingerprint
})
}
controllerFingerprint := map[string]*structs.CSIInfo{
plugID: {
PluginID: plugID,
Healthy: true,
UpdateTime: time.Now(),
RequiresControllerPlugin: true,
RequiresTopologies: false,
ControllerInfo: &structs.CSIControllerInfo{
SupportsReadOnlyAttach: true,
SupportsListVolumes: true,
},
},
}
for n := 0; n < 2; n++ {
updateNodeFn(nodes[n].ID, func(node *structs.Node) {
node.CSIControllerPlugins = controllerFingerprint
})
}
// plugins have been fingerprinted so we have healthy counts
plug := checkPlugin(pluginCounts{
controllerFingerprints: 2,
nodeFingerprints: 3,
controllersHealthy: 2,
nodesHealthy: 3,
controllersExpected: 2,
nodesExpected: 3,
})
require.True(t, plug.ControllerRequired)
})
t.Run("node marked for drain", func(t *testing.T) {
ws := memdb.NewWatchSet()
nodeAllocs, err := store.AllocsByNode(ws, nodes[0].ID)
require.NoError(t, err)
require.Len(t, nodeAllocs, 2)
updateAllocsFn([]string{nodeAllocs[0].ID, nodeAllocs[1].ID},
SERVER, func(alloc *structs.Allocation) {
alloc.DesiredStatus = structs.AllocDesiredStatusStop
})
plug := checkPlugin(pluginCounts{
controllerFingerprints: 2,
nodeFingerprints: 3,
controllersHealthy: 2,
nodesHealthy: 3,
controllersExpected: 2, // job summary hasn't changed
nodesExpected: 3, // job summary hasn't changed
})
require.True(t, plug.ControllerRequired)
})
t.Run("client removes fingerprints after node drain", func(t *testing.T) {
updateNodeFn(nodes[0].ID, func(node *structs.Node) {
node.CSIControllerPlugins = nil
node.CSINodePlugins = nil
})
plug := checkPlugin(pluginCounts{
controllerFingerprints: 1,
nodeFingerprints: 2,
controllersHealthy: 1,
nodesHealthy: 2,
controllersExpected: 2,
nodesExpected: 3,
})
require.True(t, plug.ControllerRequired)
})
t.Run("client updates alloc status to stopped after node drain", func(t *testing.T) {
nodeAllocs, err := store.AllocsByNode(memdb.NewWatchSet(), nodes[0].ID)
require.NoError(t, err)
require.Len(t, nodeAllocs, 2)
updateAllocsFn([]string{nodeAllocs[0].ID, nodeAllocs[1].ID}, CLIENT,
func(alloc *structs.Allocation) {
alloc.ClientStatus = structs.AllocClientStatusComplete
})
plug := checkPlugin(pluginCounts{
controllerFingerprints: 1,
nodeFingerprints: 2,
controllersHealthy: 1,
nodesHealthy: 2,
controllersExpected: 2, // still 2 because count=2
nodesExpected: 2, // has to use nodes we're actually placed on
})
require.True(t, plug.ControllerRequired)
})
t.Run("job stop with purge", func(t *testing.T) {
vol := &structs.CSIVolume{
ID: uuid.Generate(),
Namespace: structs.DefaultNamespace,
PluginID: plugID,
}
err = store.UpsertCSIVolume(nextIndex(store), []*structs.CSIVolume{vol})
require.NoError(t, err)
err = store.DeleteJob(nextIndex(store), structs.DefaultNamespace, controllerJobID)
require.NoError(t, err)
err = store.DeleteJob(nextIndex(store), structs.DefaultNamespace, nodeJobID)
require.NoError(t, err)
plug := checkPlugin(pluginCounts{
controllerFingerprints: 1, // no changes till we get fingerprint
nodeFingerprints: 2,
controllersHealthy: 1,
nodesHealthy: 2,
controllersExpected: 0,
nodesExpected: 0,
})
require.True(t, plug.ControllerRequired)
require.False(t, plug.IsEmpty())
updateAllocsFn(allocIDs, SERVER,
func(alloc *structs.Allocation) {
alloc.DesiredStatus = structs.AllocDesiredStatusStop
})
updateAllocsFn(allocIDs, CLIENT,
func(alloc *structs.Allocation) {
alloc.ClientStatus = structs.AllocClientStatusComplete
})
plug = checkPlugin(pluginCounts{
controllerFingerprints: 1,
nodeFingerprints: 2,
controllersHealthy: 1,
nodesHealthy: 2,
controllersExpected: 0,
nodesExpected: 0,
})
require.True(t, plug.ControllerRequired)
require.False(t, plug.IsEmpty())
for _, node := range nodes {
updateNodeFn(node.ID, func(node *structs.Node) {
node.CSIControllerPlugins = nil
})
}
plug = checkPlugin(pluginCounts{
controllerFingerprints: 0,
nodeFingerprints: 2, // haven't removed fingerprints yet
controllersHealthy: 0,
nodesHealthy: 2,
controllersExpected: 0,
nodesExpected: 0,
})
require.True(t, plug.ControllerRequired)
require.False(t, plug.IsEmpty())
for _, node := range nodes {
updateNodeFn(node.ID, func(node *structs.Node) {
node.CSINodePlugins = nil
})
}
ws := memdb.NewWatchSet()
plug, err := store.CSIPluginByID(ws, plugID)
require.NoError(t, err)
require.Nil(t, plug, "plugin was not deleted")
vol, err = store.CSIVolumeByID(ws, vol.Namespace, vol.ID)
require.NoError(t, err)
require.NotNil(t, vol, "volume should be queryable even if plugin is deleted")
require.False(t, vol.Schedulable)
})
}
func TestStateStore_Indexes(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, node)
if err != nil {
t.Fatalf("err: %v", err)
}
iter, err := state.Indexes()
if err != nil {
t.Fatalf("err: %v", err)
}
var out []*IndexEntry
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*IndexEntry))
}
expect := &IndexEntry{"nodes", 1000}
if l := len(out); l < 1 {
t.Fatalf("unexpected number of index entries: %v", pretty.Sprint(out))
}
for _, index := range out {
if index.Key != expect.Key {
continue
}
if index.Value != expect.Value {
t.Fatalf("bad index; got %d; want %d", index.Value, expect.Value)
}
// We matched
return
}
t.Fatal("did not find expected index entry")
}
func TestStateStore_LatestIndex(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
if err := state.UpsertNode(structs.MsgTypeTestSetup, 1000, mock.Node()); err != nil {
t.Fatalf("err: %v", err)
}
exp := uint64(2000)
if err := state.UpsertJob(structs.MsgTypeTestSetup, exp, nil, mock.Job()); err != nil {
t.Fatalf("err: %v", err)
}
latest, err := state.LatestIndex()
if err != nil {
t.Fatalf("err: %v", err)
}
if latest != exp {
t.Fatalf("LatestIndex() returned %d; want %d", latest, exp)
}
}
func TestStateStore_UpsertEvals_Eval(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
eval := mock.Eval()
// Create a watchset so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
if _, err := state.EvalByID(ws, eval.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval})
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.EvalByID(ws, eval.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(eval, out) {
t.Fatalf("bad: %#v %#v", eval, out)
}
index, err := state.Index("evals")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertEvals_CancelBlocked(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create two blocked evals for the same job
j := "test-job"
b1, b2 := mock.Eval(), mock.Eval()
b1.JobID = j
b1.Status = structs.EvalStatusBlocked
b2.JobID = j
b2.Status = structs.EvalStatusBlocked
err := state.UpsertEvals(structs.MsgTypeTestSetup, 999, []*structs.Evaluation{b1, b2})
if err != nil {
t.Fatalf("err: %v", err)
}
// Create one complete and successful eval for the job
eval := mock.Eval()
eval.JobID = j
eval.Status = structs.EvalStatusComplete
// Create a watchset so we can test that the upsert of the complete eval
// fires the watch
ws := memdb.NewWatchSet()
if _, err := state.EvalByID(ws, b1.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalByID(ws, b2.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.EvalByID(ws, eval.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(eval, out) {
t.Fatalf("bad: %#v %#v", eval, out)
}
index, err := state.Index("evals")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
// Get b1/b2 and check they are cancelled
out1, err := state.EvalByID(ws, b1.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
out2, err := state.EvalByID(ws, b2.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out1.Status != structs.EvalStatusCancelled || out2.Status != structs.EvalStatusCancelled {
t.Fatalf("bad: %#v %#v", out1, out2)
}
if !strings.Contains(out1.StatusDescription, eval.ID) || !strings.Contains(out2.StatusDescription, eval.ID) {
t.Fatalf("bad status description %#v %#v", out1, out2)
}
if out1.ModifyTime != eval.ModifyTime || out2.ModifyTime != eval.ModifyTime {
t.Fatalf("bad modify time %#v %#v", out1, out2)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertEvals_Namespace(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns1.Name = "new"
eval1 := mock.Eval()
eval2 := mock.Eval()
eval1.Namespace = ns1.Name
eval2.Namespace = ns1.Name
ns2 := mock.Namespace()
ns2.Name = "new-namespace"
eval3 := mock.Eval()
eval4 := mock.Eval()
eval3.Namespace = ns2.Name
eval4.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
// Create watchsets so we can test that update fires the watch
watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()}
_, err := state.EvalsByNamespace(watches[0], ns1.Name)
require.NoError(t, err)
_, err = state.EvalsByNamespace(watches[1], ns2.Name)
require.NoError(t, err)
require.NoError(t, state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{eval1, eval2, eval3, eval4}))
require.True(t, watchFired(watches[0]))
require.True(t, watchFired(watches[1]))
ws := memdb.NewWatchSet()
iter1, err := state.EvalsByNamespace(ws, ns1.Name)
require.NoError(t, err)
iter2, err := state.EvalsByNamespace(ws, ns2.Name)
require.NoError(t, err)
var out1 []*structs.Evaluation
for {
raw := iter1.Next()
if raw == nil {
break
}
out1 = append(out1, raw.(*structs.Evaluation))
}
var out2 []*structs.Evaluation
for {
raw := iter2.Next()
if raw == nil {
break
}
out2 = append(out2, raw.(*structs.Evaluation))
}
require.Len(t, out1, 2)
require.Len(t, out2, 2)
for _, eval := range out1 {
require.Equal(t, ns1.Name, eval.Namespace)
}
for _, eval := range out2 {
require.Equal(t, ns2.Name, eval.Namespace)
}
index, err := state.Index("evals")
require.NoError(t, err)
require.EqualValues(t, 1001, index)
require.False(t, watchFired(ws))
}
func TestStateStore_Update_UpsertEvals_Eval(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
eval := mock.Eval()
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval})
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a watchset so we can test that delete fires the watch
ws := memdb.NewWatchSet()
ws2 := memdb.NewWatchSet()
if _, err := state.EvalByID(ws, eval.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalsByJob(ws2, eval.Namespace, eval.JobID); err != nil {
t.Fatalf("bad: %v", err)
}
eval2 := mock.Eval()
eval2.ID = eval.ID
eval2.JobID = eval.JobID
err = state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{eval2})
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
if !watchFired(ws2) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.EvalByID(ws, eval.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(eval2, out) {
t.Fatalf("bad: %#v %#v", eval2, out)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1001 {
t.Fatalf("bad: %#v", out)
}
index, err := state.Index("evals")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertEvals_Eval_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
parent := mock.Job()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, parent); err != nil {
t.Fatalf("err: %v", err)
}
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, child); err != nil {
t.Fatalf("err: %v", err)
}
eval := mock.Eval()
eval.Status = structs.EvalStatusComplete
eval.JobID = child.ID
// Create watchsets so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
ws2 := memdb.NewWatchSet()
ws3 := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalByID(ws2, eval.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalsByJob(ws3, eval.Namespace, eval.JobID); err != nil {
t.Fatalf("bad: %v", err)
}
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval})
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
if !watchFired(ws2) {
t.Fatalf("bad")
}
if !watchFired(ws3) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.EvalByID(ws, eval.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(eval, out) {
t.Fatalf("bad: %#v %#v", eval, out)
}
index, err := state.Index("evals")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != parent.ID {
t.Fatalf("bad summary id: %v", parent.ID)
}
if summary.Children == nil {
t.Fatalf("nil children summary")
}
if summary.Children.Pending != 0 || summary.Children.Running != 0 || summary.Children.Dead != 1 {
t.Fatalf("bad children summary: %v", summary.Children)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteEval_Eval(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
eval1 := mock.Eval()
eval2 := mock.Eval()
alloc1 := mock.Alloc()
alloc2 := mock.Alloc()
// Create watchsets so we can test that upsert fires the watch
watches := make([]memdb.WatchSet, 12)
for i := 0; i < 12; i++ {
watches[i] = memdb.NewWatchSet()
}
if _, err := state.EvalByID(watches[0], eval1.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalByID(watches[1], eval2.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalsByJob(watches[2], eval1.Namespace, eval1.JobID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.EvalsByJob(watches[3], eval2.Namespace, eval2.JobID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocByID(watches[4], alloc1.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocByID(watches[5], alloc2.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByEval(watches[6], alloc1.EvalID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByEval(watches[7], alloc2.EvalID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByJob(watches[8], alloc1.Namespace, alloc1.JobID, false); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByJob(watches[9], alloc2.Namespace, alloc2.JobID, false); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByNode(watches[10], alloc1.NodeID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByNode(watches[11], alloc2.NodeID); err != nil {
t.Fatalf("bad: %v", err)
}
state.UpsertJobSummary(900, mock.JobSummary(eval1.JobID))
state.UpsertJobSummary(901, mock.JobSummary(eval2.JobID))
state.UpsertJobSummary(902, mock.JobSummary(alloc1.JobID))
state.UpsertJobSummary(903, mock.JobSummary(alloc2.JobID))
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2})
if err != nil {
t.Fatalf("err: %v", err)
}
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1, alloc2})
if err != nil {
t.Fatalf("err: %v", err)
}
err = state.DeleteEval(1002, []string{eval1.ID, eval2.ID}, []string{alloc1.ID, alloc2.ID}, false)
if err != nil {
t.Fatalf("err: %v", err)
}
for i, ws := range watches {
if !watchFired(ws) {
t.Fatalf("bad %d", i)
}
}
ws := memdb.NewWatchSet()
out, err := state.EvalByID(ws, eval1.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", eval1, out)
}
out, err = state.EvalByID(ws, eval2.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", eval1, out)
}
outA, err := state.AllocByID(ws, alloc1.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", alloc1, outA)
}
outA, err = state.AllocByID(ws, alloc2.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", alloc1, outA)
}
index, err := state.Index("evals")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1002 {
t.Fatalf("bad: %d", index)
}
index, err = state.Index("allocs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1002 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
// Call the eval delete function with zero length eval and alloc ID arrays.
// This should result in the table indexes both staying the same, rather
// than updating without cause.
require.NoError(t, state.DeleteEval(1010, []string{}, []string{}, false))
allocsIndex, err := state.Index("allocs")
require.NoError(t, err)
require.Equal(t, uint64(1002), allocsIndex)
evalsIndex, err := state.Index("evals")
require.NoError(t, err)
require.Equal(t, uint64(1002), evalsIndex)
}
func TestStateStore_DeleteEval_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
parent := mock.Job()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, parent); err != nil {
t.Fatalf("err: %v", err)
}
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, child); err != nil {
t.Fatalf("err: %v", err)
}
eval1 := mock.Eval()
eval1.JobID = child.ID
alloc1 := mock.Alloc()
alloc1.JobID = child.ID
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1})
if err != nil {
t.Fatalf("err: %v", err)
}
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1})
if err != nil {
t.Fatalf("err: %v", err)
}
// Create watchsets so we can test that delete fires the watch
ws := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID); err != nil {
t.Fatalf("bad: %v", err)
}
err = state.DeleteEval(1002, []string{eval1.ID}, []string{alloc1.ID}, false)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if summary == nil {
t.Fatalf("nil summary")
}
if summary.JobID != parent.ID {
t.Fatalf("bad summary id: %v", parent.ID)
}
if summary.Children == nil {
t.Fatalf("nil children summary")
}
if summary.Children.Pending != 0 || summary.Children.Running != 0 || summary.Children.Dead != 1 {
t.Fatalf("bad children summary: %v", summary.Children)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteEval_UserInitiated(t *testing.T) {
ci.Parallel(t)
testState := testStateStore(t)
// Upsert a scheduler config object, so we have something to check and
// modify.
schedulerConfig := structs.SchedulerConfiguration{PauseEvalBroker: false}
require.NoError(t, testState.SchedulerSetConfig(10, &schedulerConfig))
// Generate some mock evals and upsert these into state.
mockEval1 := mock.Eval()
mockEval2 := mock.Eval()
require.NoError(t, testState.UpsertEvals(
structs.MsgTypeTestSetup, 20, []*structs.Evaluation{mockEval1, mockEval2}))
mockEvalIDs := []string{mockEval1.ID, mockEval2.ID}
// Try and delete the evals without pausing the eval broker.
err := testState.DeleteEval(30, mockEvalIDs, []string{}, true)
require.ErrorContains(t, err, "eval broker is enabled")
// Pause the eval broker on the scheduler config, and try deleting the
// evals again.
schedulerConfig.PauseEvalBroker = true
require.NoError(t, testState.SchedulerSetConfig(30, &schedulerConfig))
require.NoError(t, testState.DeleteEval(40, mockEvalIDs, []string{}, true))
ws := memdb.NewWatchSet()
mockEval1Lookup, err := testState.EvalByID(ws, mockEval1.ID)
require.NoError(t, err)
require.Nil(t, mockEval1Lookup)
mockEval2Lookup, err := testState.EvalByID(ws, mockEval1.ID)
require.NoError(t, err)
require.Nil(t, mockEval2Lookup)
}
// TestStateStore_DeleteEvalsByFilter_Pagination tests the pagination logic for
// deleting evals by filter; the business logic is tested more fully in the eval
// endpoint tests.
func TestStateStore_DeleteEvalsByFilter_Pagination(t *testing.T) {
evalCount := 100
index := uint64(100)
store := testStateStore(t)
// Create a set of pending evaluations
schedulerConfig := &structs.SchedulerConfiguration{
PauseEvalBroker: true,
CreateIndex: index,
ModifyIndex: index,
}
must.NoError(t, store.SchedulerSetConfig(index, schedulerConfig))
evals := []*structs.Evaluation{}
for i := 0; i < evalCount; i++ {
mockEval := mock.Eval()
evals = append(evals, mockEval)
}
index++
must.NoError(t, store.UpsertEvals(
structs.MsgTypeTestSetup, index, evals))
// Delete one page
index++
must.NoError(t, store.DeleteEvalsByFilter(index, "JobID != \"\"", "", 10))
countRemaining := func() (string, int) {
lastSeen := ""
remaining := 0
iter, err := store.Evals(nil, SortDefault)
must.NoError(t, err)
for {
raw := iter.Next()
if raw == nil {
break
}
eval := raw.(*structs.Evaluation)
lastSeen = eval.ID
remaining++
}
return lastSeen, remaining
}
lastSeen, remaining := countRemaining()
must.Eq(t, 90, remaining)
// Delete starting from lastSeen, which should only delete 1
index++
must.NoError(t, store.DeleteEvalsByFilter(index, "JobID != \"\"", lastSeen, 10))
_, remaining = countRemaining()
must.Eq(t, 89, remaining)
}
func TestStateStore_EvalIsUserDeleteSafe(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
inputAllocs []*structs.Allocation
inputJob *structs.Job
expectedResult bool
name string
}{
{
inputAllocs: nil,
inputJob: nil,
expectedResult: true,
name: "job not in state",
},
{
inputAllocs: nil,
inputJob: &structs.Job{Status: structs.JobStatusDead},
expectedResult: true,
name: "job stopped",
},
{
inputAllocs: nil,
inputJob: &structs.Job{Stop: true},
expectedResult: true,
name: "job dead",
},
{
inputAllocs: []*structs.Allocation{},
inputJob: &structs.Job{Status: structs.JobStatusRunning},
expectedResult: true,
name: "no allocs for eval",
},
{
inputAllocs: []*structs.Allocation{
{ClientStatus: structs.AllocClientStatusComplete},
{ClientStatus: structs.AllocClientStatusRunning},
},
inputJob: &structs.Job{Status: structs.JobStatusRunning},
expectedResult: false,
name: "running alloc for eval",
},
{
inputAllocs: []*structs.Allocation{
{ClientStatus: structs.AllocClientStatusComplete},
{ClientStatus: structs.AllocClientStatusUnknown},
},
inputJob: &structs.Job{Status: structs.JobStatusRunning},
expectedResult: false,
name: "unknown alloc for eval",
},
{
inputAllocs: []*structs.Allocation{
{ClientStatus: structs.AllocClientStatusComplete},
{ClientStatus: structs.AllocClientStatusLost},
},
inputJob: &structs.Job{Status: structs.JobStatusRunning},
expectedResult: true,
name: "complete and lost allocs for eval",
},
{
inputAllocs: []*structs.Allocation{
{
ClientStatus: structs.AllocClientStatusFailed,
TaskGroup: "test",
},
},
inputJob: &structs.Job{
Status: structs.JobStatusPending,
TaskGroups: []*structs.TaskGroup{
{
Name: "test",
ReschedulePolicy: nil,
},
},
},
expectedResult: true,
name: "failed alloc job without reschedule",
},
{
inputAllocs: []*structs.Allocation{
{
ClientStatus: structs.AllocClientStatusFailed,
TaskGroup: "test",
},
},
inputJob: &structs.Job{
Status: structs.JobStatusPending,
TaskGroups: []*structs.TaskGroup{
{
Name: "test",
ReschedulePolicy: &structs.ReschedulePolicy{
Unlimited: false,
Attempts: 0,
},
},
},
},
expectedResult: true,
name: "failed alloc job reschedule disabled",
},
{
inputAllocs: []*structs.Allocation{
{
ClientStatus: structs.AllocClientStatusFailed,
TaskGroup: "test",
},
},
inputJob: &structs.Job{
Status: structs.JobStatusPending,
TaskGroups: []*structs.TaskGroup{
{
Name: "test",
ReschedulePolicy: &structs.ReschedulePolicy{
Unlimited: false,
Attempts: 3,
},
},
},
},
expectedResult: false,
name: "failed alloc next alloc not set",
},
{
inputAllocs: []*structs.Allocation{
{
ClientStatus: structs.AllocClientStatusFailed,
TaskGroup: "test",
NextAllocation: "4aa4930a-8749-c95b-9c67-5ef29b0fc653",
},
},
inputJob: &structs.Job{
Status: structs.JobStatusPending,
TaskGroups: []*structs.TaskGroup{
{
Name: "test",
ReschedulePolicy: &structs.ReschedulePolicy{
Unlimited: false,
Attempts: 3,
},
},
},
},
expectedResult: false,
name: "failed alloc next alloc set",
},
{
inputAllocs: []*structs.Allocation{
{
ClientStatus: structs.AllocClientStatusFailed,
TaskGroup: "test",
},
},
inputJob: &structs.Job{
Status: structs.JobStatusPending,
TaskGroups: []*structs.TaskGroup{
{
Name: "test",
ReschedulePolicy: &structs.ReschedulePolicy{
Unlimited: true,
},
},
},
},
expectedResult: false,
name: "failed alloc job reschedule unlimited",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualResult := isEvalDeleteSafe(tc.inputAllocs, tc.inputJob)
require.Equal(t, tc.expectedResult, actualResult)
})
}
}
func TestStateStore_EvalsByJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
eval1 := mock.Eval()
eval2 := mock.Eval()
eval2.JobID = eval1.JobID
eval3 := mock.Eval()
evals := []*structs.Evaluation{eval1, eval2}
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, evals)
if err != nil {
t.Fatalf("err: %v", err)
}
err = state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{eval3})
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.EvalsByJob(ws, eval1.Namespace, eval1.JobID)
if err != nil {
t.Fatalf("err: %v", err)
}
sort.Sort(EvalIDSort(evals))
sort.Sort(EvalIDSort(out))
if !reflect.DeepEqual(evals, out) {
t.Fatalf("bad: %#v %#v", evals, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_Evals(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var evals []*structs.Evaluation
for i := 0; i < 10; i++ {
eval := mock.Eval()
evals = append(evals, eval)
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000+uint64(i), []*structs.Evaluation{eval})
if err != nil {
t.Fatalf("err: %v", err)
}
}
ws := memdb.NewWatchSet()
iter, err := state.Evals(ws, false)
if err != nil {
t.Fatalf("err: %v", err)
}
var out []*structs.Evaluation
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Evaluation))
}
sort.Sort(EvalIDSort(evals))
sort.Sort(EvalIDSort(out))
if !reflect.DeepEqual(evals, out) {
t.Fatalf("bad: %#v %#v", evals, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_EvalsByIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var evals []*structs.Evaluation
ids := []string{
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
"aaabbbbb-7bfb-395d-eb95-0685af2176b2",
"aabbbbbb-7bfb-395d-eb95-0685af2176b2",
"abbbbbbb-7bfb-395d-eb95-0685af2176b2",
"bbbbbbbb-7bfb-395d-eb95-0685af2176b2",
}
for i := 0; i < 9; i++ {
eval := mock.Eval()
eval.ID = ids[i]
evals = append(evals, eval)
}
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, evals)
if err != nil {
t.Fatalf("err: %v", err)
}
gatherEvals := func(iter memdb.ResultIterator) []*structs.Evaluation {
var evals []*structs.Evaluation
for {
raw := iter.Next()
if raw == nil {
break
}
evals = append(evals, raw.(*structs.Evaluation))
}
return evals
}
t.Run("list by prefix", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.EvalsByIDPrefix(ws, structs.DefaultNamespace, "aaaa", SortDefault)
require.NoError(t, err)
got := []string{}
for _, e := range gatherEvals(iter) {
got = append(got, e.ID)
}
expected := []string{
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
}
require.Len(t, got, 5, "expected five evaluations")
require.Equal(t, expected, got) // Must be in this order.
})
t.Run("invalid prefix", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.EvalsByIDPrefix(ws, structs.DefaultNamespace, "b-a7bfb", SortDefault)
require.NoError(t, err)
out := gatherEvals(iter)
require.Len(t, out, 0, "expected zero evaluations")
require.False(t, watchFired(ws))
})
t.Run("reverse order", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.EvalsByIDPrefix(ws, structs.DefaultNamespace, "aaaa", SortReverse)
require.NoError(t, err)
got := []string{}
for _, e := range gatherEvals(iter) {
got = append(got, e.ID)
}
expected := []string{
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
}
require.Len(t, got, 5, "expected five evaluations")
require.Equal(t, expected, got) // Must be in this order.
})
}
func TestStateStore_EvalsByIDPrefix_Namespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
eval1 := mock.Eval()
eval1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2"
eval2 := mock.Eval()
eval2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2"
sharedPrefix := "aabb"
ns1 := mock.Namespace()
ns1.Name = "namespace1"
ns2 := mock.Namespace()
ns2.Name = "namespace2"
eval1.Namespace = ns1.Name
eval2.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
require.NoError(t, state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval1, eval2}))
gatherEvals := func(iter memdb.ResultIterator) []*structs.Evaluation {
var evals []*structs.Evaluation
for {
raw := iter.Next()
if raw == nil {
break
}
evals = append(evals, raw.(*structs.Evaluation))
}
return evals
}
ws := memdb.NewWatchSet()
iter1, err := state.EvalsByIDPrefix(ws, ns1.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
iter2, err := state.EvalsByIDPrefix(ws, ns2.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
iter3, err := state.EvalsByIDPrefix(ws, structs.AllNamespacesSentinel, sharedPrefix, SortDefault)
require.NoError(t, err)
evalsNs1 := gatherEvals(iter1)
evalsNs2 := gatherEvals(iter2)
evalsNs3 := gatherEvals(iter3)
require.Len(t, evalsNs1, 1)
require.Len(t, evalsNs2, 1)
require.Len(t, evalsNs3, 2)
iter1, err = state.EvalsByIDPrefix(ws, ns1.Name, eval1.ID[:8], SortDefault)
require.NoError(t, err)
evalsNs1 = gatherEvals(iter1)
require.Len(t, evalsNs1, 1)
require.False(t, watchFired(ws))
}
func TestStateStore_EvalsRelatedToID(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create sample evals.
e1 := mock.Eval()
e2 := mock.Eval()
e3 := mock.Eval()
e4 := mock.Eval()
e5 := mock.Eval()
e6 := mock.Eval()
// Link evals.
// This is not accurate for a real scenario, but it's helpful for testing
// the general approach.
//
// e1 -> e2 -> e3 -> e5
// └─-> e4 (blocked) -> e6
e1.NextEval = e2.ID
e2.PreviousEval = e1.ID
e2.NextEval = e3.ID
e3.PreviousEval = e2.ID
e3.BlockedEval = e4.ID
e4.PreviousEval = e3.ID
e3.NextEval = e5.ID
e5.PreviousEval = e3.ID
e4.NextEval = e6.ID
e6.PreviousEval = e4.ID
// Create eval not in chain.
e7 := mock.Eval()
// Create eval with GC'ed related eval.
e8 := mock.Eval()
e8.NextEval = uuid.Generate()
err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{e1, e2, e3, e4, e5, e6, e7, e8})
require.NoError(t, err)
testCases := []struct {
name string
id string
expected []string
}{
{
name: "linear history",
id: e1.ID,
expected: []string{
e2.ID,
e3.ID,
e4.ID,
e5.ID,
e6.ID,
},
},
{
name: "linear history from middle",
id: e4.ID,
expected: []string{
e1.ID,
e2.ID,
e3.ID,
e5.ID,
e6.ID,
},
},
{
name: "eval not in chain",
id: e7.ID,
expected: []string{},
},
{
name: "eval with gc",
id: e8.ID,
expected: []string{},
},
{
name: "non-existing eval",
id: uuid.Generate(),
expected: []string{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ws := memdb.NewWatchSet()
related, err := state.EvalsRelatedToID(ws, tc.id)
require.NoError(t, err)
got := []string{}
for _, e := range related {
got = append(got, e.ID)
}
require.ElementsMatch(t, tc.expected, got)
})
}
t.Run("blocking query", func(t *testing.T) {
ws := memdb.NewWatchSet()
_, err := state.EvalsRelatedToID(ws, e2.ID)
require.NoError(t, err)
// Update an eval off the chain and make sure watchset doesn't fire.
e7.Status = structs.EvalStatusComplete
state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{e7})
require.False(t, watchFired(ws))
// Update an eval in the chain and make sure watchset does fire.
e3.Status = structs.EvalStatusComplete
state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{e3})
require.True(t, watchFired(ws))
})
}
func TestStateStore_UpdateAllocsFromClient(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 997, node))
parent := mock.Job()
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, parent))
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, child))
alloc := mock.Alloc()
alloc.NodeID = node.ID
alloc.JobID = child.ID
alloc.Job = child
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
ws := memdb.NewWatchSet()
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
must.NoError(t, err)
must.NotNil(t, summary)
must.Eq(t, parent.ID, summary.JobID)
must.NotNil(t, summary.Children)
must.Eq(t, 0, summary.Children.Pending)
must.Eq(t, 1, summary.Children.Running)
must.Eq(t, 0, summary.Children.Dead)
// Create watchsets so we can test that update fires the watch
ws = memdb.NewWatchSet()
_, err = state.JobSummaryByID(ws, parent.Namespace, parent.ID)
must.NoError(t, err)
// Create the delta updates
ts := map[string]*structs.TaskState{"web": {State: structs.TaskStateRunning}}
update := &structs.Allocation{
ID: alloc.ID,
NodeID: alloc.NodeID,
ClientStatus: structs.AllocClientStatusComplete,
TaskStates: ts,
JobID: alloc.JobID,
TaskGroup: alloc.TaskGroup,
}
err = state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{update})
must.NoError(t, err)
must.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
summary, err = state.JobSummaryByID(ws, parent.Namespace, parent.ID)
must.NoError(t, err)
must.NotNil(t, summary)
must.Eq(t, parent.ID, summary.JobID)
must.NotNil(t, summary.Children)
must.Eq(t, 0, summary.Children.Pending)
must.Eq(t, 0, summary.Children.Running)
must.Eq(t, 1, summary.Children.Dead)
must.False(t, watchFired(ws))
}
func TestStateStore_UpdateAllocsFromClient_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
alloc1 := mock.Alloc()
alloc1.NodeID = node.ID
alloc2 := mock.Alloc()
alloc2.NodeID = node.ID
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 998, node))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc1.Job))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc2.Job))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc1, alloc2}))
// Create watchsets so we can test that update fires the watch
watches := make([]memdb.WatchSet, 8)
for i := 0; i < 8; i++ {
watches[i] = memdb.NewWatchSet()
}
_, err := state.AllocByID(watches[0], alloc1.ID)
must.NoError(t, err)
_, err = state.AllocByID(watches[1], alloc2.ID)
must.NoError(t, err)
_, err = state.AllocsByEval(watches[2], alloc1.EvalID)
must.NoError(t, err)
_, err = state.AllocsByEval(watches[3], alloc2.EvalID)
must.NoError(t, err)
_, err = state.AllocsByJob(watches[4], alloc1.Namespace, alloc1.JobID, false)
must.NoError(t, err)
_, err = state.AllocsByJob(watches[5], alloc2.Namespace, alloc2.JobID, false)
must.NoError(t, err)
_, err = state.AllocsByNode(watches[6], alloc1.NodeID)
must.NoError(t, err)
_, err = state.AllocsByNode(watches[7], alloc2.NodeID)
must.NoError(t, err)
// Create the delta updates
ts := map[string]*structs.TaskState{"web": {State: structs.TaskStatePending}}
update := &structs.Allocation{
ID: alloc1.ID,
NodeID: alloc1.NodeID,
ClientStatus: structs.AllocClientStatusFailed,
TaskStates: ts,
JobID: alloc1.JobID,
TaskGroup: alloc1.TaskGroup,
}
update2 := &structs.Allocation{
ID: alloc2.ID,
NodeID: alloc2.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
TaskStates: ts,
JobID: alloc2.JobID,
TaskGroup: alloc2.TaskGroup,
}
err = state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{update, update2})
must.NoError(t, err)
for _, ws := range watches {
must.True(t, watchFired(ws))
}
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc1.ID)
must.NoError(t, err)
alloc1.CreateIndex = 1000
alloc1.ModifyIndex = 1001
alloc1.TaskStates = ts
alloc1.ClientStatus = structs.AllocClientStatusFailed
must.Eq(t, alloc1, out)
out, err = state.AllocByID(ws, alloc2.ID)
must.NoError(t, err)
alloc2.ModifyIndex = 1000
alloc2.ModifyIndex = 1001
alloc2.ClientStatus = structs.AllocClientStatusRunning
alloc2.TaskStates = ts
must.Eq(t, alloc2, out)
index, err := state.Index("allocs")
must.NoError(t, err)
must.Eq(t, 1001, index)
// Ensure summaries have been updated
summary, err := state.JobSummaryByID(ws, alloc1.Namespace, alloc1.JobID)
must.NoError(t, err)
tgSummary := summary.Summary["web"]
must.Eq(t, 1, tgSummary.Failed)
summary2, err := state.JobSummaryByID(ws, alloc2.Namespace, alloc2.JobID)
must.NoError(t, err)
tgSummary2 := summary2.Summary["web"]
must.Eq(t, 1, tgSummary2.Running)
must.False(t, watchFired(ws))
}
func TestStateStore_UpdateMultipleAllocsFromClient(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
alloc := mock.Alloc()
alloc.NodeID = node.ID
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 998, node))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
// Create the delta updates
ts := map[string]*structs.TaskState{"web": {State: structs.TaskStatePending}}
update := &structs.Allocation{
ID: alloc.ID,
NodeID: alloc.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
TaskStates: ts,
JobID: alloc.JobID,
TaskGroup: alloc.TaskGroup,
}
update2 := &structs.Allocation{
ID: alloc.ID,
NodeID: alloc.NodeID,
ClientStatus: structs.AllocClientStatusPending,
TaskStates: ts,
JobID: alloc.JobID,
TaskGroup: alloc.TaskGroup,
}
err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{update, update2})
must.NoError(t, err)
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
must.NoError(t, err)
alloc.CreateIndex = 1000
alloc.ModifyIndex = 1001
alloc.TaskStates = ts
alloc.ClientStatus = structs.AllocClientStatusPending
must.Eq(t, alloc, out)
summary, err := state.JobSummaryByID(ws, alloc.Namespace, alloc.JobID)
expectedSummary := &structs.JobSummary{
JobID: alloc.JobID,
Namespace: alloc.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Starting: 1,
},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: 999,
ModifyIndex: 1001,
}
must.NoError(t, err)
must.Eq(t, summary, expectedSummary)
}
func TestStateStore_UpdateAllocsFromClient_Deployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
alloc := mock.Alloc()
now := time.Now()
alloc.NodeID = node.ID
alloc.CreateTime = now.UnixNano()
pdeadline := 5 * time.Minute
deployment := mock.Deployment()
deployment.TaskGroups[alloc.TaskGroup].ProgressDeadline = pdeadline
alloc.DeploymentID = deployment.ID
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 998, node))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job))
must.NoError(t, state.UpsertDeployment(1000, deployment))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc}))
healthy := now.Add(time.Second)
update := &structs.Allocation{
ID: alloc.ID,
NodeID: alloc.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
JobID: alloc.JobID,
TaskGroup: alloc.TaskGroup,
DeploymentStatus: &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
Timestamp: healthy,
},
}
must.NoError(t, state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{update}))
// Check that the deployment state was updated because the healthy
// deployment
dout, err := state.DeploymentByID(nil, deployment.ID)
must.NoError(t, err)
must.NotNil(t, dout)
must.MapLen(t, 1, dout.TaskGroups)
dstate := dout.TaskGroups[alloc.TaskGroup]
must.NotNil(t, dstate)
must.Eq(t, 1, dstate.PlacedAllocs)
must.True(t, healthy.Add(pdeadline).Equal(dstate.RequireProgressBy))
}
// This tests that the deployment state is merged correctly
func TestStateStore_UpdateAllocsFromClient_DeploymentStateMerges(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
alloc := mock.Alloc()
now := time.Now()
alloc.NodeID = node.ID
alloc.CreateTime = now.UnixNano()
pdeadline := 5 * time.Minute
deployment := mock.Deployment()
deployment.TaskGroups[alloc.TaskGroup].ProgressDeadline = pdeadline
alloc.DeploymentID = deployment.ID
alloc.DeploymentStatus = &structs.AllocDeploymentStatus{
Canary: true,
}
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 998, node))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job))
must.NoError(t, state.UpsertDeployment(1000, deployment))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc}))
update := &structs.Allocation{
ID: alloc.ID,
NodeID: alloc.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
JobID: alloc.JobID,
TaskGroup: alloc.TaskGroup,
DeploymentStatus: &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
Canary: false,
},
}
must.NoError(t, state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{update}))
// Check that the merging of the deployment status was correct
out, err := state.AllocByID(nil, alloc.ID)
must.NoError(t, err)
must.NotNil(t, out)
must.True(t, out.DeploymentStatus.Canary)
must.NotNil(t, out.DeploymentStatus.Healthy)
must.True(t, *out.DeploymentStatus.Healthy)
}
// TestStateStore_UpdateAllocsFromClient_UpdateNodes verifies that the relevant
// node data is updated when clients update their allocs.
func TestStateStore_UpdateAllocsFromClient_UpdateNodes(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node1 := mock.Node()
alloc1 := mock.Alloc()
alloc1.NodeID = node1.ID
node2 := mock.Node()
alloc2 := mock.Alloc()
alloc2.NodeID = node2.ID
node3 := mock.Node()
alloc3 := mock.Alloc()
alloc3.NodeID = node3.ID
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 1000, node1))
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 1001, node2))
must.NoError(t, state.UpsertNode(structs.MsgTypeTestSetup, 1002, node3))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1003, nil, alloc1.Job))
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1004, nil, alloc2.Job))
must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1005, []*structs.Allocation{alloc1, alloc2, alloc3}))
// Create watches to make sure they fire when nodes are updated.
ws1 := memdb.NewWatchSet()
_, err := state.NodeByID(ws1, node1.ID)
must.NoError(t, err)
ws2 := memdb.NewWatchSet()
_, err = state.NodeByID(ws2, node2.ID)
must.NoError(t, err)
ws3 := memdb.NewWatchSet()
_, err = state.NodeByID(ws3, node3.ID)
must.NoError(t, err)
// Create and apply alloc updates.
// Don't update alloc 3.
updateAlloc1 := &structs.Allocation{
ID: alloc1.ID,
NodeID: alloc1.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
JobID: alloc1.JobID,
TaskGroup: alloc1.TaskGroup,
}
updateAlloc2 := &structs.Allocation{
ID: alloc2.ID,
NodeID: alloc2.NodeID,
ClientStatus: structs.AllocClientStatusRunning,
JobID: alloc2.JobID,
TaskGroup: alloc2.TaskGroup,
}
updateAllocNonExisting := &structs.Allocation{
ID: uuid.Generate(),
NodeID: uuid.Generate(),
ClientStatus: structs.AllocClientStatusRunning,
JobID: uuid.Generate(),
TaskGroup: "group",
}
err = state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1005, []*structs.Allocation{
updateAlloc1, updateAlloc2, updateAllocNonExisting,
})
must.NoError(t, err)
// Check that node update watches fired.
must.True(t, watchFired(ws1))
must.True(t, watchFired(ws2))
// Check that node LastAllocUpdateIndex were updated.
ws := memdb.NewWatchSet()
out, err := state.NodeByID(ws, node1.ID)
must.NoError(t, err)
must.NotNil(t, out)
must.Eq(t, 1005, out.LastAllocUpdateIndex)
must.False(t, watchFired(ws))
out, err = state.NodeByID(ws, node2.ID)
must.NoError(t, err)
must.NotNil(t, out)
must.Eq(t, 1005, out.LastAllocUpdateIndex)
must.False(t, watchFired(ws))
// Node 3 should not be updated.
out, err = state.NodeByID(ws, node3.ID)
must.NoError(t, err)
must.NotNil(t, out)
must.Eq(t, 0, out.LastAllocUpdateIndex)
must.False(t, watchFired(ws))
}
func TestStateStore_UpsertAlloc_Alloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job); err != nil {
t.Fatalf("err: %v", err)
}
// Create watchsets so we can test that update fires the watch
watches := make([]memdb.WatchSet, 4)
for i := 0; i < 4; i++ {
watches[i] = memdb.NewWatchSet()
}
if _, err := state.AllocByID(watches[0], alloc.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByEval(watches[1], alloc.EvalID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByJob(watches[2], alloc.Namespace, alloc.JobID, false); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByNode(watches[3], alloc.NodeID); err != nil {
t.Fatalf("bad: %v", err)
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
if err != nil {
t.Fatalf("err: %v", err)
}
for i, ws := range watches {
if !watchFired(ws) {
t.Fatalf("bad %d", i)
}
}
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(alloc, out) {
t.Fatalf("bad: %#v %#v", alloc, out)
}
index, err := state.Index("allocs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
summary, err := state.JobSummaryByID(ws, alloc.Namespace, alloc.JobID)
if err != nil {
t.Fatalf("err: %v", err)
}
tgSummary, ok := summary.Summary["web"]
if !ok {
t.Fatalf("no summary for task group web")
}
if tgSummary.Starting != 1 {
t.Fatalf("expected queued: %v, actual: %v", 1, tgSummary.Starting)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertAlloc_Deployment(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
alloc := mock.Alloc()
now := time.Now()
alloc.CreateTime = now.UnixNano()
alloc.ModifyTime = now.UnixNano()
pdeadline := 5 * time.Minute
deployment := mock.Deployment()
deployment.TaskGroups[alloc.TaskGroup].ProgressDeadline = pdeadline
alloc.DeploymentID = deployment.ID
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job))
require.Nil(state.UpsertDeployment(1000, deployment))
// Create a watch set so we can test that update fires the watch
ws := memdb.NewWatchSet()
require.Nil(state.AllocsByDeployment(ws, alloc.DeploymentID))
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc})
require.Nil(err)
if !watchFired(ws) {
t.Fatalf("watch not fired")
}
ws = memdb.NewWatchSet()
allocs, err := state.AllocsByDeployment(ws, alloc.DeploymentID)
require.Nil(err)
require.Len(allocs, 1)
require.EqualValues(alloc, allocs[0])
index, err := state.Index("allocs")
require.Nil(err)
require.EqualValues(1001, index)
if watchFired(ws) {
t.Fatalf("bad")
}
// Check that the deployment state was updated
dout, err := state.DeploymentByID(nil, deployment.ID)
require.Nil(err)
require.NotNil(dout)
require.Len(dout.TaskGroups, 1)
dstate := dout.TaskGroups[alloc.TaskGroup]
require.NotNil(dstate)
require.Equal(1, dstate.PlacedAllocs)
require.True(now.Add(pdeadline).Equal(dstate.RequireProgressBy))
}
func TestStateStore_UpsertAlloc_AllocsByNamespace(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
ns1 := mock.Namespace()
ns1.Name = "namespaced"
alloc1 := mock.Alloc()
alloc2 := mock.Alloc()
alloc1.Namespace = ns1.Name
alloc1.Job.Namespace = ns1.Name
alloc2.Namespace = ns1.Name
alloc2.Job.Namespace = ns1.Name
ns2 := mock.Namespace()
ns2.Name = "new-namespace"
alloc3 := mock.Alloc()
alloc4 := mock.Alloc()
alloc3.Namespace = ns2.Name
alloc3.Job.Namespace = ns2.Name
alloc4.Namespace = ns2.Name
alloc4.Job.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc1.Job))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, alloc3.Job))
// Create watchsets so we can test that update fires the watch
watches := []memdb.WatchSet{memdb.NewWatchSet(), memdb.NewWatchSet()}
_, err := state.AllocsByNamespace(watches[0], ns1.Name)
require.NoError(t, err)
_, err = state.AllocsByNamespace(watches[1], ns2.Name)
require.NoError(t, err)
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1, alloc2, alloc3, alloc4}))
require.True(t, watchFired(watches[0]))
require.True(t, watchFired(watches[1]))
ws := memdb.NewWatchSet()
iter1, err := state.AllocsByNamespace(ws, ns1.Name)
require.NoError(t, err)
iter2, err := state.AllocsByNamespace(ws, ns2.Name)
require.NoError(t, err)
var out1 []*structs.Allocation
for {
raw := iter1.Next()
if raw == nil {
break
}
out1 = append(out1, raw.(*structs.Allocation))
}
var out2 []*structs.Allocation
for {
raw := iter2.Next()
if raw == nil {
break
}
out2 = append(out2, raw.(*structs.Allocation))
}
require.Len(t, out1, 2)
require.Len(t, out2, 2)
for _, alloc := range out1 {
require.Equal(t, ns1.Name, alloc.Namespace)
}
for _, alloc := range out2 {
require.Equal(t, ns2.Name, alloc.Namespace)
}
index, err := state.Index("allocs")
require.NoError(t, err)
require.EqualValues(t, 1001, index)
require.False(t, watchFired(ws))
}
// Testing to ensure we keep issue
// https://github.com/hashicorp/nomad/issues/2583 fixed
func TestStateStore_UpsertAlloc_No_Job(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
alloc.Job = nil
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 999, []*structs.Allocation{alloc})
if err == nil || !strings.Contains(err.Error(), "without a job") {
t.Fatalf("expect err: %v", err)
}
}
func TestStateStore_UpsertAlloc_ChildJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
parent := mock.Job()
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 998, nil, parent))
child := mock.Job()
child.Status = ""
child.ParentID = parent.ID
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, child))
alloc := mock.Alloc()
alloc.JobID = child.ID
alloc.Job = child
// Create watchsets so we can test that delete fires the watch
ws := memdb.NewWatchSet()
_, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
require.NoError(t, err)
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
require.NoError(t, err)
require.True(t, watchFired(ws))
ws = memdb.NewWatchSet()
summary, err := state.JobSummaryByID(ws, parent.Namespace, parent.ID)
require.NoError(t, err)
require.NotNil(t, summary)
require.Equal(t, parent.ID, summary.JobID)
require.NotNil(t, summary.Children)
require.Equal(t, int64(0), summary.Children.Pending)
require.Equal(t, int64(1), summary.Children.Running)
require.Equal(t, int64(0), summary.Children.Dead)
require.False(t, watchFired(ws))
}
func TestStateStore_UpdateAlloc_Alloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job); err != nil {
t.Fatalf("err: %v", err)
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
summary, err := state.JobSummaryByID(ws, alloc.Namespace, alloc.JobID)
if err != nil {
t.Fatalf("err: %v", err)
}
tgSummary := summary.Summary["web"]
if tgSummary.Starting != 1 {
t.Fatalf("expected starting: %v, actual: %v", 1, tgSummary.Starting)
}
alloc2 := mock.Alloc()
alloc2.ID = alloc.ID
alloc2.NodeID = alloc.NodeID + ".new"
state.UpsertJobSummary(1001, mock.JobSummary(alloc2.JobID))
// Create watchsets so we can test that update fires the watch
watches := make([]memdb.WatchSet, 4)
for i := 0; i < 4; i++ {
watches[i] = memdb.NewWatchSet()
}
if _, err := state.AllocByID(watches[0], alloc2.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByEval(watches[1], alloc2.EvalID); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByJob(watches[2], alloc2.Namespace, alloc2.JobID, false); err != nil {
t.Fatalf("bad: %v", err)
}
if _, err := state.AllocsByNode(watches[3], alloc2.NodeID); err != nil {
t.Fatalf("bad: %v", err)
}
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1002, []*structs.Allocation{alloc2})
if err != nil {
t.Fatalf("err: %v", err)
}
for i, ws := range watches {
if !watchFired(ws) {
t.Fatalf("bad %d", i)
}
}
ws = memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(alloc2, out) {
t.Fatalf("bad: %#v %#v", alloc2, out)
}
if out.CreateIndex != 1000 {
t.Fatalf("bad: %#v", out)
}
if out.ModifyIndex != 1002 {
t.Fatalf("bad: %#v", out)
}
index, err := state.Index("allocs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1002 {
t.Fatalf("bad: %d", index)
}
// Ensure that summary hasb't changed
summary, err = state.JobSummaryByID(ws, alloc.Namespace, alloc.JobID)
if err != nil {
t.Fatalf("err: %v", err)
}
tgSummary = summary.Summary["web"]
if tgSummary.Starting != 1 {
t.Fatalf("expected starting: %v, actual: %v", 1, tgSummary.Starting)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
// This test ensures that the state store will mark the clients status as lost
// when set rather than preferring the existing status.
func TestStateStore_UpdateAlloc_Lost(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
alloc.ClientStatus = "foo"
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job); err != nil {
t.Fatalf("err: %v", err)
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
if err != nil {
t.Fatalf("err: %v", err)
}
alloc2 := new(structs.Allocation)
*alloc2 = *alloc
alloc2.ClientStatus = structs.AllocClientStatusLost
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc2}); err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc2.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out.ClientStatus != structs.AllocClientStatusLost {
t.Fatalf("bad: %#v", out)
}
}
// This test ensures an allocation can be updated when there is no job
// associated with it. This will happen when a job is stopped by an user which
// has non-terminal allocations on clients
func TestStateStore_UpdateAlloc_NoJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
// Upsert a job
state.UpsertJobSummary(998, mock.JobSummary(alloc.JobID))
if err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job); err != nil {
t.Fatalf("err: %v", err)
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
if err != nil {
t.Fatalf("err: %v", err)
}
if err := state.DeleteJob(1001, alloc.Namespace, alloc.JobID); err != nil {
t.Fatalf("err: %v", err)
}
// Update the desired state of the allocation to stop
allocCopy := alloc.Copy()
allocCopy.DesiredStatus = structs.AllocDesiredStatusStop
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1002, []*structs.Allocation{allocCopy}); err != nil {
t.Fatalf("err: %v", err)
}
// Update the client state of the allocation to complete
allocCopy1 := allocCopy.Copy()
allocCopy1.ClientStatus = structs.AllocClientStatusComplete
if err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1003, []*structs.Allocation{allocCopy1}); err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, _ := state.AllocByID(ws, alloc.ID)
// Update the modify index of the alloc before comparing
allocCopy1.ModifyIndex = 1003
if !reflect.DeepEqual(out, allocCopy1) {
t.Fatalf("expected: %#v \n actual: %#v", allocCopy1, out)
}
}
func TestStateStore_UpdateAllocDesiredTransition(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
alloc := mock.Alloc()
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job))
require.Nil(state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}))
t1 := &structs.DesiredTransition{
Migrate: pointer.Of(true),
}
t2 := &structs.DesiredTransition{
Migrate: pointer.Of(false),
}
eval := &structs.Evaluation{
ID: uuid.Generate(),
Namespace: alloc.Namespace,
Priority: alloc.Job.Priority,
Type: alloc.Job.Type,
TriggeredBy: structs.EvalTriggerNodeDrain,
JobID: alloc.Job.ID,
JobModifyIndex: alloc.Job.ModifyIndex,
Status: structs.EvalStatusPending,
}
evals := []*structs.Evaluation{eval}
m := map[string]*structs.DesiredTransition{alloc.ID: t1}
require.Nil(state.UpdateAllocsDesiredTransitions(structs.MsgTypeTestSetup, 1001, m, evals))
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
require.Nil(err)
require.NotNil(out.DesiredTransition.Migrate)
require.True(*out.DesiredTransition.Migrate)
require.EqualValues(1000, out.CreateIndex)
require.EqualValues(1001, out.ModifyIndex)
index, err := state.Index("allocs")
require.Nil(err)
require.EqualValues(1001, index)
// Check the eval is created
eout, err := state.EvalByID(nil, eval.ID)
require.Nil(err)
require.NotNil(eout)
m = map[string]*structs.DesiredTransition{alloc.ID: t2}
require.Nil(state.UpdateAllocsDesiredTransitions(structs.MsgTypeTestSetup, 1002, m, evals))
ws = memdb.NewWatchSet()
out, err = state.AllocByID(ws, alloc.ID)
require.Nil(err)
require.NotNil(out.DesiredTransition.Migrate)
require.False(*out.DesiredTransition.Migrate)
require.EqualValues(1000, out.CreateIndex)
require.EqualValues(1002, out.ModifyIndex)
index, err = state.Index("allocs")
require.Nil(err)
require.EqualValues(1002, index)
// Try with a bogus alloc id
m = map[string]*structs.DesiredTransition{uuid.Generate(): t2}
require.Nil(state.UpdateAllocsDesiredTransitions(structs.MsgTypeTestSetup, 1003, m, evals))
}
func TestStateStore_JobSummary(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Add a job
job := mock.Job()
state.UpsertJob(structs.MsgTypeTestSetup, 900, nil, job)
// Get the job back
ws := memdb.NewWatchSet()
outJob, _ := state.JobByID(ws, job.Namespace, job.ID)
if outJob.CreateIndex != 900 {
t.Fatalf("bad create index: %v", outJob.CreateIndex)
}
summary, _ := state.JobSummaryByID(ws, job.Namespace, job.ID)
if summary.CreateIndex != 900 {
t.Fatalf("bad create index: %v", summary.CreateIndex)
}
// Upsert an allocation
alloc := mock.Alloc()
alloc.JobID = job.ID
alloc.Job = job
state.UpsertAllocs(structs.MsgTypeTestSetup, 910, []*structs.Allocation{alloc})
// Update the alloc from client
alloc1 := alloc.Copy()
alloc1.ClientStatus = structs.AllocClientStatusPending
alloc1.DesiredStatus = ""
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 920, []*structs.Allocation{alloc})
alloc3 := alloc.Copy()
alloc3.ClientStatus = structs.AllocClientStatusRunning
alloc3.DesiredStatus = ""
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 930, []*structs.Allocation{alloc3})
// Upsert the alloc
alloc4 := alloc.Copy()
alloc4.ClientStatus = structs.AllocClientStatusPending
alloc4.DesiredStatus = structs.AllocDesiredStatusRun
state.UpsertAllocs(structs.MsgTypeTestSetup, 950, []*structs.Allocation{alloc4})
// Again upsert the alloc
alloc5 := alloc.Copy()
alloc5.ClientStatus = structs.AllocClientStatusPending
alloc5.DesiredStatus = structs.AllocDesiredStatusRun
state.UpsertAllocs(structs.MsgTypeTestSetup, 970, []*structs.Allocation{alloc5})
if !watchFired(ws) {
t.Fatalf("bad")
}
expectedSummary := structs.JobSummary{
JobID: job.ID,
Namespace: job.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Running: 1,
},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: 900,
ModifyIndex: 930,
}
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
if !reflect.DeepEqual(&expectedSummary, summary) {
t.Fatalf("expected: %#v, actual: %v", expectedSummary, summary)
}
// De-register the job.
state.DeleteJob(980, job.Namespace, job.ID)
// Shouldn't have any effect on the summary
alloc6 := alloc.Copy()
alloc6.ClientStatus = structs.AllocClientStatusRunning
alloc6.DesiredStatus = ""
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 990, []*structs.Allocation{alloc6})
// We shouldn't have any summary at this point
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
if summary != nil {
t.Fatalf("expected nil, actual: %#v", summary)
}
// Re-register the same job
job1 := mock.Job()
job1.ID = job.ID
state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job1)
outJob2, _ := state.JobByID(ws, job1.Namespace, job1.ID)
if outJob2.CreateIndex != 1000 {
t.Fatalf("bad create index: %v", outJob2.CreateIndex)
}
summary, _ = state.JobSummaryByID(ws, job1.Namespace, job1.ID)
if summary.CreateIndex != 1000 {
t.Fatalf("bad create index: %v", summary.CreateIndex)
}
// Upsert an allocation
alloc7 := alloc.Copy()
alloc7.JobID = outJob.ID
alloc7.Job = outJob
alloc7.ClientStatus = structs.AllocClientStatusComplete
alloc7.DesiredStatus = structs.AllocDesiredStatusRun
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1020, []*structs.Allocation{alloc7})
expectedSummary = structs.JobSummary{
JobID: job.ID,
Namespace: job.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: 1000,
ModifyIndex: 1000,
}
summary, _ = state.JobSummaryByID(ws, job1.Namespace, job1.ID)
if !reflect.DeepEqual(&expectedSummary, summary) {
t.Fatalf("expected: %#v, actual: %#v", expectedSummary, summary)
}
}
func TestStateStore_ReconcileJobSummary(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create an alloc
alloc := mock.Alloc()
// Add another task group to the job
tg2 := alloc.Job.TaskGroups[0].Copy()
tg2.Name = "db"
alloc.Job.TaskGroups = append(alloc.Job.TaskGroups, tg2)
state.UpsertJob(structs.MsgTypeTestSetup, 100, nil, alloc.Job)
// Create one more alloc for the db task group
alloc2 := mock.Alloc()
alloc2.TaskGroup = "db"
alloc2.JobID = alloc.JobID
alloc2.Job = alloc.Job
// Upserts the alloc
state.UpsertAllocs(structs.MsgTypeTestSetup, 110, []*structs.Allocation{alloc, alloc2})
// Change the state of the first alloc to running
alloc3 := alloc.Copy()
alloc3.ClientStatus = structs.AllocClientStatusRunning
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 120, []*structs.Allocation{alloc3})
//Add some more allocs to the second tg
alloc4 := mock.Alloc()
alloc4.JobID = alloc.JobID
alloc4.Job = alloc.Job
alloc4.TaskGroup = "db"
alloc5 := alloc4.Copy()
alloc5.ClientStatus = structs.AllocClientStatusRunning
alloc6 := mock.Alloc()
alloc6.JobID = alloc.JobID
alloc6.Job = alloc.Job
alloc6.TaskGroup = "db"
alloc7 := alloc6.Copy()
alloc7.ClientStatus = structs.AllocClientStatusComplete
alloc8 := mock.Alloc()
alloc8.JobID = alloc.JobID
alloc8.Job = alloc.Job
alloc8.TaskGroup = "db"
alloc9 := alloc8.Copy()
alloc9.ClientStatus = structs.AllocClientStatusFailed
alloc10 := mock.Alloc()
alloc10.JobID = alloc.JobID
alloc10.Job = alloc.Job
alloc10.TaskGroup = "db"
alloc11 := alloc10.Copy()
alloc11.ClientStatus = structs.AllocClientStatusLost
alloc12 := mock.Alloc()
alloc12.JobID = alloc.JobID
alloc12.Job = alloc.Job
alloc12.TaskGroup = "db"
alloc12.ClientStatus = structs.AllocClientStatusUnknown
state.UpsertAllocs(structs.MsgTypeTestSetup, 130, []*structs.Allocation{alloc4, alloc6, alloc8, alloc10, alloc12})
state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 150, []*structs.Allocation{alloc5, alloc7, alloc9, alloc11})
// DeleteJobSummary is a helper method and doesn't modify the indexes table
state.DeleteJobSummary(130, alloc.Namespace, alloc.Job.ID)
state.ReconcileJobSummaries(120)
ws := memdb.NewWatchSet()
summary, _ := state.JobSummaryByID(ws, alloc.Namespace, alloc.Job.ID)
expectedSummary := structs.JobSummary{
JobID: alloc.Job.ID,
Namespace: alloc.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Running: 1,
},
"db": {
Starting: 1,
Running: 1,
Failed: 1,
Complete: 1,
Lost: 1,
Unknown: 1,
},
},
CreateIndex: 100,
ModifyIndex: 120,
}
if !reflect.DeepEqual(&expectedSummary, summary) {
t.Fatalf("expected: %v, actual: %v", expectedSummary, summary)
}
}
func TestStateStore_ReconcileParentJobSummary(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
// Add a node
node := mock.Node()
state.UpsertNode(structs.MsgTypeTestSetup, 80, node)
// Make a parameterized job
job1 := mock.BatchJob()
job1.ID = "test"
job1.ParameterizedJob = &structs.ParameterizedJobConfig{
Payload: "random",
}
job1.TaskGroups[0].Count = 1
state.UpsertJob(structs.MsgTypeTestSetup, 100, nil, job1)
// Make a child job
childJob := job1.Copy()
childJob.ID = job1.ID + "dispatch-23423423"
childJob.ParentID = job1.ID
childJob.Dispatched = true
childJob.Status = structs.JobStatusRunning
// Make some allocs for child job
alloc := mock.Alloc()
alloc.NodeID = node.ID
alloc.Job = childJob
alloc.JobID = childJob.ID
alloc.ClientStatus = structs.AllocClientStatusRunning
alloc2 := mock.Alloc()
alloc2.NodeID = node.ID
alloc2.Job = childJob
alloc2.JobID = childJob.ID
alloc2.ClientStatus = structs.AllocClientStatusFailed
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 110, nil, childJob))
require.Nil(state.UpsertAllocs(structs.MsgTypeTestSetup, 111, []*structs.Allocation{alloc, alloc2}))
// Make the summary incorrect in the state store
summary, err := state.JobSummaryByID(nil, job1.Namespace, job1.ID)
require.Nil(err)
summary.Children = nil
summary.Summary = make(map[string]structs.TaskGroupSummary)
summary.Summary["web"] = structs.TaskGroupSummary{
Queued: 1,
}
// Delete the child job summary
state.DeleteJobSummary(125, childJob.Namespace, childJob.ID)
state.ReconcileJobSummaries(120)
ws := memdb.NewWatchSet()
// Verify parent summary is corrected
summary, _ = state.JobSummaryByID(ws, alloc.Namespace, job1.ID)
expectedSummary := structs.JobSummary{
JobID: job1.ID,
Namespace: job1.Namespace,
Summary: make(map[string]structs.TaskGroupSummary),
Children: &structs.JobChildrenSummary{
Running: 1,
},
CreateIndex: 100,
ModifyIndex: 120,
}
require.Equal(&expectedSummary, summary)
// Verify child job summary is also correct
childSummary, _ := state.JobSummaryByID(ws, childJob.Namespace, childJob.ID)
expectedChildSummary := structs.JobSummary{
JobID: childJob.ID,
Namespace: childJob.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Running: 1,
Failed: 1,
},
},
CreateIndex: 110,
ModifyIndex: 120,
}
require.Equal(&expectedChildSummary, childSummary)
}
func TestStateStore_UpdateAlloc_JobNotPresent(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
state.UpsertJob(structs.MsgTypeTestSetup, 100, nil, alloc.Job)
state.UpsertAllocs(structs.MsgTypeTestSetup, 200, []*structs.Allocation{alloc})
// Delete the job
state.DeleteJob(300, alloc.Namespace, alloc.Job.ID)
// Update the alloc
alloc1 := alloc.Copy()
alloc1.ClientStatus = structs.AllocClientStatusRunning
// Updating allocation should not throw any error
if err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 400, []*structs.Allocation{alloc1}); err != nil {
t.Fatalf("expect err: %v", err)
}
// Re-Register the job
state.UpsertJob(structs.MsgTypeTestSetup, 500, nil, alloc.Job)
// Update the alloc again
alloc2 := alloc.Copy()
alloc2.ClientStatus = structs.AllocClientStatusComplete
if err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 400, []*structs.Allocation{alloc1}); err != nil {
t.Fatalf("expect err: %v", err)
}
// Job Summary of the newly registered job shouldn't account for the
// allocation update for the older job
expectedSummary := structs.JobSummary{
JobID: alloc1.JobID,
Namespace: alloc1.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: 500,
ModifyIndex: 500,
}
ws := memdb.NewWatchSet()
summary, _ := state.JobSummaryByID(ws, alloc.Namespace, alloc.Job.ID)
if !reflect.DeepEqual(&expectedSummary, summary) {
t.Fatalf("expected: %v, actual: %v", expectedSummary, summary)
}
}
func TestStateStore_EvictAlloc_Alloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
state.UpsertJobSummary(999, mock.JobSummary(alloc.JobID))
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc})
if err != nil {
t.Fatalf("err: %v", err)
}
alloc2 := new(structs.Allocation)
*alloc2 = *alloc
alloc2.DesiredStatus = structs.AllocDesiredStatusEvict
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc2})
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.AllocByID(ws, alloc.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if out.DesiredStatus != structs.AllocDesiredStatusEvict {
t.Fatalf("bad: %#v %#v", alloc, out)
}
index, err := state.Index("allocs")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
}
func TestStateStore_AllocsByNode(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
for i := 0; i < 10; i++ {
alloc := mock.Alloc()
alloc.NodeID = "foo"
allocs = append(allocs, alloc)
}
for idx, alloc := range allocs {
state.UpsertJobSummary(uint64(900+idx), mock.JobSummary(alloc.JobID))
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.AllocsByNode(ws, "foo")
if err != nil {
t.Fatalf("err: %v", err)
}
sort.Sort(AllocIDSort(allocs))
sort.Sort(AllocIDSort(out))
if !reflect.DeepEqual(allocs, out) {
t.Fatalf("bad: %#v %#v", allocs, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_AllocsByNodeTerminal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs, term, nonterm []*structs.Allocation
for i := 0; i < 10; i++ {
alloc := mock.Alloc()
alloc.NodeID = "foo"
if i%2 == 0 {
alloc.DesiredStatus = structs.AllocDesiredStatusStop
term = append(term, alloc)
} else {
nonterm = append(nonterm, alloc)
}
allocs = append(allocs, alloc)
}
for idx, alloc := range allocs {
state.UpsertJobSummary(uint64(900+idx), mock.JobSummary(alloc.JobID))
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
if err != nil {
t.Fatalf("err: %v", err)
}
// Verify the terminal allocs
ws := memdb.NewWatchSet()
out, err := state.AllocsByNodeTerminal(ws, "foo", true)
if err != nil {
t.Fatalf("err: %v", err)
}
sort.Sort(AllocIDSort(term))
sort.Sort(AllocIDSort(out))
if !reflect.DeepEqual(term, out) {
t.Fatalf("bad: %#v %#v", term, out)
}
// Verify the non-terminal allocs
out, err = state.AllocsByNodeTerminal(ws, "foo", false)
if err != nil {
t.Fatalf("err: %v", err)
}
sort.Sort(AllocIDSort(nonterm))
sort.Sort(AllocIDSort(out))
if !reflect.DeepEqual(nonterm, out) {
t.Fatalf("bad: %#v %#v", nonterm, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_AllocsByJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
for i := 0; i < 10; i++ {
alloc := mock.Alloc()
alloc.JobID = "foo"
allocs = append(allocs, alloc)
}
for i, alloc := range allocs {
state.UpsertJobSummary(uint64(900+i), mock.JobSummary(alloc.JobID))
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.AllocsByJob(ws, mock.Alloc().Namespace, "foo", false)
if err != nil {
t.Fatalf("err: %v", err)
}
sort.Sort(AllocIDSort(allocs))
sort.Sort(AllocIDSort(out))
if !reflect.DeepEqual(allocs, out) {
t.Fatalf("bad: %#v %#v", allocs, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_AllocsForRegisteredJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
var allocs1 []*structs.Allocation
job := mock.Job()
job.ID = "foo"
state.UpsertJob(structs.MsgTypeTestSetup, 100, nil, job)
for i := 0; i < 3; i++ {
alloc := mock.Alloc()
alloc.Job = job
alloc.JobID = job.ID
allocs = append(allocs, alloc)
}
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 200, allocs); err != nil {
t.Fatalf("err: %v", err)
}
if err := state.DeleteJob(250, job.Namespace, job.ID); err != nil {
t.Fatalf("err: %v", err)
}
job1 := mock.Job()
job1.ID = "foo"
job1.CreateIndex = 50
state.UpsertJob(structs.MsgTypeTestSetup, 300, nil, job1)
for i := 0; i < 4; i++ {
alloc := mock.Alloc()
alloc.Job = job1
alloc.JobID = job1.ID
allocs1 = append(allocs1, alloc)
}
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs1); err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.AllocsByJob(ws, job1.Namespace, job1.ID, true)
if err != nil {
t.Fatalf("err: %v", err)
}
expected := len(allocs) + len(allocs1)
if len(out) != expected {
t.Fatalf("expected: %v, actual: %v", expected, len(out))
}
out1, err := state.AllocsByJob(ws, job1.Namespace, job1.ID, false)
if err != nil {
t.Fatalf("bad: %v", err)
}
expected = len(allocs1)
if len(out1) != expected {
t.Fatalf("expected: %v, actual: %v", expected, len(out1))
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_AllocsByIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
ids := []string{
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
"aaabbbbb-7bfb-395d-eb95-0685af2176b2",
"aabbbbbb-7bfb-395d-eb95-0685af2176b2",
"abbbbbbb-7bfb-395d-eb95-0685af2176b2",
"bbbbbbbb-7bfb-395d-eb95-0685af2176b2",
}
for i := 0; i < 9; i++ {
alloc := mock.Alloc()
alloc.ID = ids[i]
allocs = append(allocs, alloc)
}
for i, alloc := range allocs {
state.UpsertJobSummary(uint64(900+i), mock.JobSummary(alloc.JobID))
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
require.NoError(t, err)
gatherAllocs := func(iter memdb.ResultIterator) []*structs.Allocation {
var allocs []*structs.Allocation
for {
raw := iter.Next()
if raw == nil {
break
}
allocs = append(allocs, raw.(*structs.Allocation))
}
return allocs
}
t.Run("allocs by prefix", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.AllocsByIDPrefix(ws, structs.DefaultNamespace, "aaaa", SortDefault)
require.NoError(t, err)
out := gatherAllocs(iter)
require.Len(t, out, 5, "expected five allocations")
got := []string{}
for _, a := range out {
got = append(got, a.ID)
}
expected := []string{
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
}
require.Equal(t, expected, got)
require.False(t, watchFired(ws))
})
t.Run("invalid prefix", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.AllocsByIDPrefix(ws, structs.DefaultNamespace, "b-a7bfb", SortDefault)
require.NoError(t, err)
out := gatherAllocs(iter)
require.Len(t, out, 0)
require.False(t, watchFired(ws))
})
t.Run("reverse", func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.AllocsByIDPrefix(ws, structs.DefaultNamespace, "aaaa", SortReverse)
require.NoError(t, err)
out := gatherAllocs(iter)
require.Len(t, out, 5, "expected five allocations")
got := []string{}
for _, a := range out {
got = append(got, a.ID)
}
expected := []string{
"aaaabbbb-7bfb-395d-eb95-0685af2176b2",
"aaaaabbb-7bfb-395d-eb95-0685af2176b2",
"aaaaaabb-7bfb-395d-eb95-0685af2176b2",
"aaaaaaab-7bfb-395d-eb95-0685af2176b2",
"aaaaaaaa-7bfb-395d-eb95-0685af2176b2",
}
require.Equal(t, expected, got)
require.False(t, watchFired(ws))
})
}
func TestStateStore_AllocsByIDPrefix_Namespaces(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc1 := mock.Alloc()
alloc1.ID = "aabbbbbb-7bfb-395d-eb95-0685af2176b2"
alloc2 := mock.Alloc()
alloc2.ID = "aabbcbbb-7bfb-395d-eb95-0685af2176b2"
sharedPrefix := "aabb"
ns1 := mock.Namespace()
ns1.Name = "namespace1"
ns2 := mock.Namespace()
ns2.Name = "namespace2"
alloc1.Namespace = ns1.Name
alloc2.Namespace = ns2.Name
require.NoError(t, state.UpsertNamespaces(998, []*structs.Namespace{ns1, ns2}))
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc1, alloc2}))
gatherAllocs := func(iter memdb.ResultIterator) []*structs.Allocation {
var allocs []*structs.Allocation
for {
raw := iter.Next()
if raw == nil {
break
}
alloc := raw.(*structs.Allocation)
allocs = append(allocs, alloc)
}
return allocs
}
ws := memdb.NewWatchSet()
iter1, err := state.AllocsByIDPrefix(ws, ns1.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
iter2, err := state.AllocsByIDPrefix(ws, ns2.Name, sharedPrefix, SortDefault)
require.NoError(t, err)
allocsNs1 := gatherAllocs(iter1)
allocsNs2 := gatherAllocs(iter2)
require.Len(t, allocsNs1, 1)
require.Len(t, allocsNs2, 1)
iter1, err = state.AllocsByIDPrefix(ws, ns1.Name, alloc1.ID[:8], SortDefault)
require.NoError(t, err)
allocsNs1 = gatherAllocs(iter1)
require.Len(t, allocsNs1, 1)
require.False(t, watchFired(ws))
}
func TestStateStore_Allocs(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
for i := 0; i < 10; i++ {
alloc := mock.Alloc()
allocs = append(allocs, alloc)
}
for i, alloc := range allocs {
state.UpsertJobSummary(uint64(900+i), mock.JobSummary(alloc.JobID))
}
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
iter, err := state.Allocs(ws, SortDefault)
if err != nil {
t.Fatalf("err: %v", err)
}
var out []*structs.Allocation
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Allocation))
}
sort.Sort(AllocIDSort(allocs))
sort.Sort(AllocIDSort(out))
if !reflect.DeepEqual(allocs, out) {
t.Fatalf("bad: %#v %#v", allocs, out)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_Allocs_PrevAlloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
var allocs []*structs.Allocation
require := require.New(t)
for i := 0; i < 5; i++ {
alloc := mock.Alloc()
allocs = append(allocs, alloc)
}
for i, alloc := range allocs {
state.UpsertJobSummary(uint64(900+i), mock.JobSummary(alloc.JobID))
}
// Set some previous alloc ids
allocs[1].PreviousAllocation = allocs[0].ID
allocs[2].PreviousAllocation = allocs[1].ID
err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, allocs)
require.Nil(err)
ws := memdb.NewWatchSet()
iter, err := state.Allocs(ws, SortDefault)
require.Nil(err)
var out []*structs.Allocation
for {
raw := iter.Next()
if raw == nil {
break
}
out = append(out, raw.(*structs.Allocation))
}
// Set expected NextAllocation fields
allocs[0].NextAllocation = allocs[1].ID
allocs[1].NextAllocation = allocs[2].ID
sort.Sort(AllocIDSort(allocs))
sort.Sort(AllocIDSort(out))
require.Equal(allocs, out)
require.False(watchFired(ws))
// Insert another alloc, verify index of previous alloc also got updated
alloc := mock.Alloc()
alloc.PreviousAllocation = allocs[0].ID
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc})
require.Nil(err)
alloc0, err := state.AllocByID(nil, allocs[0].ID)
require.Nil(err)
require.Equal(alloc0.ModifyIndex, uint64(1001))
}
func TestStateStore_SetJobStatus_ForceStatus(t *testing.T) {
ci.Parallel(t)
index := uint64(0)
state := testStateStore(t)
txn := state.db.WriteTxn(index)
// Create and insert a mock job.
job := mock.Job()
job.Status = ""
job.ModifyIndex = index
if err := txn.Insert("jobs", job); err != nil {
t.Fatalf("job insert failed: %v", err)
}
exp := "foobar"
index = uint64(1000)
if err := state.setJobStatus(index, txn, job, false, exp); err != nil {
t.Fatalf("setJobStatus() failed: %v", err)
}
i, err := txn.First("jobs", "id", job.Namespace, job.ID)
if err != nil {
t.Fatalf("job lookup failed: %v", err)
}
updated := i.(*structs.Job)
if updated.Status != exp {
t.Fatalf("setJobStatus() set %v; expected %v", updated.Status, exp)
}
if updated.ModifyIndex != index {
t.Fatalf("setJobStatus() set %d; expected %d", updated.ModifyIndex, index)
}
}
func TestStateStore_SetJobStatus_NoOp(t *testing.T) {
ci.Parallel(t)
index := uint64(0)
state := testStateStore(t)
txn := state.db.WriteTxn(index)
// Create and insert a mock job that should be pending.
job := mock.Job()
job.Status = structs.JobStatusPending
job.ModifyIndex = 10
if err := txn.Insert("jobs", job); err != nil {
t.Fatalf("job insert failed: %v", err)
}
index = uint64(1000)
if err := state.setJobStatus(index, txn, job, false, ""); err != nil {
t.Fatalf("setJobStatus() failed: %v", err)
}
i, err := txn.First("jobs", "id", job.Namespace, job.ID)
if err != nil {
t.Fatalf("job lookup failed: %v", err)
}
updated := i.(*structs.Job)
if updated.ModifyIndex == index {
t.Fatalf("setJobStatus() should have been a no-op")
}
}
func TestStateStore_SetJobStatus(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
txn := state.db.WriteTxn(uint64(0))
// Create and insert a mock job that should be pending but has an incorrect
// status.
job := mock.Job()
job.Status = "foobar"
job.ModifyIndex = 10
if err := txn.Insert("jobs", job); err != nil {
t.Fatalf("job insert failed: %v", err)
}
index := uint64(1000)
if err := state.setJobStatus(index, txn, job, false, ""); err != nil {
t.Fatalf("setJobStatus() failed: %v", err)
}
i, err := txn.First("jobs", "id", job.Namespace, job.ID)
if err != nil {
t.Fatalf("job lookup failed: %v", err)
}
updated := i.(*structs.Job)
if updated.Status != structs.JobStatusPending {
t.Fatalf("setJobStatus() set %v; expected %v", updated.Status, structs.JobStatusPending)
}
if updated.ModifyIndex != index {
t.Fatalf("setJobStatus() set %d; expected %d", updated.ModifyIndex, index)
}
}
func TestStateStore_GetJobStatus_NoEvalsOrAllocs(t *testing.T) {
ci.Parallel(t)
job := mock.Job()
state := testStateStore(t)
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusPending {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusPending)
}
}
func TestStateStore_GetJobStatus_NoEvalsOrAllocs_Periodic(t *testing.T) {
ci.Parallel(t)
job := mock.PeriodicJob()
state := testStateStore(t)
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusRunning {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusRunning)
}
}
func TestStateStore_GetJobStatus_NoEvalsOrAllocs_EvalDelete(t *testing.T) {
ci.Parallel(t)
job := mock.Job()
state := testStateStore(t)
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, true)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusDead {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusDead)
}
}
func TestStateStore_GetJobStatus_DeadEvalsAndAllocs(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
// Create a mock alloc that is dead.
alloc := mock.Alloc()
alloc.JobID = job.ID
alloc.DesiredStatus = structs.AllocDesiredStatusStop
state.UpsertJobSummary(999, mock.JobSummary(alloc.JobID))
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}); err != nil {
t.Fatalf("err: %v", err)
}
// Create a mock eval that is complete
eval := mock.Eval()
eval.JobID = job.ID
eval.Status = structs.EvalStatusComplete
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1001, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusDead {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusDead)
}
}
func TestStateStore_GetJobStatus_RunningAlloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
// Create a mock alloc that is running.
alloc := mock.Alloc()
alloc.JobID = job.ID
alloc.DesiredStatus = structs.AllocDesiredStatusRun
state.UpsertJobSummary(999, mock.JobSummary(alloc.JobID))
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc}); err != nil {
t.Fatalf("err: %v", err)
}
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, true)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusRunning {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusRunning)
}
}
func TestStateStore_GetJobStatus_PeriodicJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.PeriodicJob()
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusRunning {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusRunning)
}
// Mark it as stopped
job.Stop = true
status, err = state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusDead {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusDead)
}
}
func TestStateStore_GetJobStatus_ParameterizedJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
job.ParameterizedJob = &structs.ParameterizedJobConfig{}
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusRunning {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusRunning)
}
// Mark it as stopped
job.Stop = true
status, err = state.getJobStatus(txn, job, false)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusDead {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusDead)
}
}
func TestStateStore_SetJobStatus_PendingEval(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.Job()
// Create a mock eval that is pending.
eval := mock.Eval()
eval.JobID = job.ID
eval.Status = structs.EvalStatusPending
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, true)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if status != structs.JobStatusPending {
t.Fatalf("getJobStatus() returned %v; expected %v", status, structs.JobStatusPending)
}
}
// TestStateStore_SetJobStatus_SystemJob asserts that system jobs are still
// considered running until explicitly stopped.
func TestStateStore_SetJobStatus_SystemJob(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
job := mock.SystemJob()
// Create a mock eval that is pending.
eval := mock.Eval()
eval.JobID = job.ID
eval.Type = job.Type
eval.Status = structs.EvalStatusComplete
if err := state.UpsertEvals(structs.MsgTypeTestSetup, 1000, []*structs.Evaluation{eval}); err != nil {
t.Fatalf("err: %v", err)
}
txn := state.db.ReadTxn()
status, err := state.getJobStatus(txn, job, true)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if expected := structs.JobStatusRunning; status != expected {
t.Fatalf("getJobStatus() returned %v; expected %v", status, expected)
}
// Stop the job
job.Stop = true
status, err = state.getJobStatus(txn, job, true)
if err != nil {
t.Fatalf("getJobStatus() failed: %v", err)
}
if expected := structs.JobStatusDead; status != expected {
t.Fatalf("getJobStatus() returned %v; expected %v", status, expected)
}
}
func TestStateJobSummary_UpdateJobCount(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
job := alloc.Job
job.TaskGroups[0].Count = 3
// Create watchsets so we can test that upsert fires the watch
ws := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws, job.Namespace, job.ID); err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job); err != nil {
t.Fatalf("err: %v", err)
}
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
summary, _ := state.JobSummaryByID(ws, job.Namespace, job.ID)
expectedSummary := structs.JobSummary{
JobID: job.ID,
Namespace: job.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Starting: 1,
},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: 1000,
ModifyIndex: 1001,
}
if !reflect.DeepEqual(summary, &expectedSummary) {
t.Fatalf("expected: %v, actual: %v", expectedSummary, summary)
}
// Create watchsets so we can test that upsert fires the watch
ws2 := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws2, job.Namespace, job.ID); err != nil {
t.Fatalf("bad: %v", err)
}
alloc2 := mock.Alloc()
alloc2.Job = job
alloc2.JobID = job.ID
alloc3 := mock.Alloc()
alloc3.Job = job
alloc3.JobID = job.ID
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1002, []*structs.Allocation{alloc2, alloc3}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws2) {
t.Fatalf("bad")
}
outA, _ := state.AllocByID(ws, alloc3.ID)
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
expectedSummary = structs.JobSummary{
JobID: job.ID,
Namespace: job.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Starting: 3,
},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: job.CreateIndex,
ModifyIndex: outA.ModifyIndex,
}
if !reflect.DeepEqual(summary, &expectedSummary) {
t.Fatalf("expected summary: %v, actual: %v", expectedSummary, summary)
}
// Create watchsets so we can test that upsert fires the watch
ws3 := memdb.NewWatchSet()
if _, err := state.JobSummaryByID(ws3, job.Namespace, job.ID); err != nil {
t.Fatalf("bad: %v", err)
}
alloc4 := mock.Alloc()
alloc4.ID = alloc2.ID
alloc4.Job = alloc2.Job
alloc4.JobID = alloc2.JobID
alloc4.ClientStatus = structs.AllocClientStatusComplete
alloc5 := mock.Alloc()
alloc5.ID = alloc3.ID
alloc5.Job = alloc3.Job
alloc5.JobID = alloc3.JobID
alloc5.ClientStatus = structs.AllocClientStatusComplete
if err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1004, []*structs.Allocation{alloc4, alloc5}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws2) {
t.Fatalf("bad")
}
outA, _ = state.AllocByID(ws, alloc5.ID)
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
expectedSummary = structs.JobSummary{
JobID: job.ID,
Namespace: job.Namespace,
Summary: map[string]structs.TaskGroupSummary{
"web": {
Complete: 2,
Starting: 1,
},
},
Children: new(structs.JobChildrenSummary),
CreateIndex: job.CreateIndex,
ModifyIndex: outA.ModifyIndex,
}
if !reflect.DeepEqual(summary, &expectedSummary) {
t.Fatalf("expected: %v, actual: %v", expectedSummary, summary)
}
}
func TestJobSummary_UpdateClientStatus(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
job := alloc.Job
job.TaskGroups[0].Count = 3
alloc2 := mock.Alloc()
alloc2.Job = job
alloc2.JobID = job.ID
alloc3 := mock.Alloc()
alloc3.Job = job
alloc3.JobID = job.ID
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
if err != nil {
t.Fatalf("err: %v", err)
}
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc, alloc2, alloc3}); err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
summary, _ := state.JobSummaryByID(ws, job.Namespace, job.ID)
if summary.Summary["web"].Starting != 3 {
t.Fatalf("bad job summary: %v", summary)
}
alloc4 := mock.Alloc()
alloc4.ID = alloc2.ID
alloc4.Job = alloc2.Job
alloc4.JobID = alloc2.JobID
alloc4.ClientStatus = structs.AllocClientStatusComplete
alloc5 := mock.Alloc()
alloc5.ID = alloc3.ID
alloc5.Job = alloc3.Job
alloc5.JobID = alloc3.JobID
alloc5.ClientStatus = structs.AllocClientStatusFailed
alloc6 := mock.Alloc()
alloc6.ID = alloc.ID
alloc6.Job = alloc.Job
alloc6.JobID = alloc.JobID
alloc6.ClientStatus = structs.AllocClientStatusRunning
if err := state.UpdateAllocsFromClient(structs.MsgTypeTestSetup, 1002, []*structs.Allocation{alloc4, alloc5, alloc6}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
if summary.Summary["web"].Running != 1 || summary.Summary["web"].Failed != 1 || summary.Summary["web"].Complete != 1 {
t.Fatalf("bad job summary: %v", summary)
}
alloc7 := mock.Alloc()
alloc7.Job = alloc.Job
alloc7.JobID = alloc.JobID
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1003, []*structs.Allocation{alloc7}); err != nil {
t.Fatalf("err: %v", err)
}
summary, _ = state.JobSummaryByID(ws, job.Namespace, job.ID)
if summary.Summary["web"].Starting != 1 || summary.Summary["web"].Running != 1 || summary.Summary["web"].Failed != 1 || summary.Summary["web"].Complete != 1 {
t.Fatalf("bad job summary: %v", summary)
}
}
// Test that nonexistent deployment can't be updated
func TestStateStore_UpsertDeploymentStatusUpdate_Nonexistent(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Update the nonexistent deployment
req := &structs.DeploymentStatusUpdateRequest{
DeploymentUpdate: &structs.DeploymentStatusUpdate{
DeploymentID: uuid.Generate(),
Status: structs.DeploymentStatusRunning,
},
}
err := state.UpdateDeploymentStatus(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "does not exist") {
t.Fatalf("expected error updating the status because the deployment doesn't exist")
}
}
// Test that terminal deployment can't be updated
func TestStateStore_UpsertDeploymentStatusUpdate_Terminal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a terminal deployment
d := mock.Deployment()
d.Status = structs.DeploymentStatusFailed
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Update the deployment
req := &structs.DeploymentStatusUpdateRequest{
DeploymentUpdate: &structs.DeploymentStatusUpdate{
DeploymentID: d.ID,
Status: structs.DeploymentStatusRunning,
},
}
err := state.UpdateDeploymentStatus(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "has terminal status") {
t.Fatalf("expected error updating the status because the deployment is terminal")
}
}
// Test that a non terminal deployment is updated and that a job and eval are
// created.
func TestStateStore_UpsertDeploymentStatusUpdate_NonTerminal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a deployment
d := mock.Deployment()
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Create an eval and a job
e := mock.Eval()
j := mock.Job()
// Update the deployment
status, desc := structs.DeploymentStatusFailed, "foo"
req := &structs.DeploymentStatusUpdateRequest{
DeploymentUpdate: &structs.DeploymentStatusUpdate{
DeploymentID: d.ID,
Status: status,
StatusDescription: desc,
},
Job: j,
Eval: e,
}
err := state.UpdateDeploymentStatus(structs.MsgTypeTestSetup, 2, req)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check that the status was updated properly
ws := memdb.NewWatchSet()
dout, err := state.DeploymentByID(ws, d.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if dout.Status != status || dout.StatusDescription != desc {
t.Fatalf("bad: %#v", dout)
}
// Check that the evaluation was created
eout, _ := state.EvalByID(ws, e.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if eout == nil {
t.Fatalf("bad: %#v", eout)
}
// Check that the job was created
jout, _ := state.JobByID(ws, j.Namespace, j.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if jout == nil {
t.Fatalf("bad: %#v", jout)
}
}
// Test that when a deployment is updated to successful the job is updated to
// stable
func TestStateStore_UpsertDeploymentStatusUpdate_Successful(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a job
job := mock.Job()
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, job); err != nil {
t.Fatalf("bad: %v", err)
}
// Insert a deployment
d := structs.NewDeployment(job, 50)
if err := state.UpsertDeployment(2, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Update the deployment
req := &structs.DeploymentStatusUpdateRequest{
DeploymentUpdate: &structs.DeploymentStatusUpdate{
DeploymentID: d.ID,
Status: structs.DeploymentStatusSuccessful,
StatusDescription: structs.DeploymentStatusDescriptionSuccessful,
},
}
err := state.UpdateDeploymentStatus(structs.MsgTypeTestSetup, 3, req)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check that the status was updated properly
ws := memdb.NewWatchSet()
dout, err := state.DeploymentByID(ws, d.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if dout.Status != structs.DeploymentStatusSuccessful ||
dout.StatusDescription != structs.DeploymentStatusDescriptionSuccessful {
t.Fatalf("bad: %#v", dout)
}
// Check that the job was created
jout, _ := state.JobByID(ws, job.Namespace, job.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if jout == nil {
t.Fatalf("bad: %#v", jout)
}
if !jout.Stable {
t.Fatalf("job not marked stable %#v", jout)
}
if jout.Version != d.JobVersion {
t.Fatalf("job version changed; got %d; want %d", jout.Version, d.JobVersion)
}
}
func TestStateStore_UpdateJobStability(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a job twice to get two versions
job := mock.Job()
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, job))
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 2, nil, job.Copy()))
// Update the stability to true
err := state.UpdateJobStability(3, job.Namespace, job.ID, 0, true)
require.NoError(t, err)
// Check that the job was updated properly
ws := memdb.NewWatchSet()
jout, err := state.JobByIDAndVersion(ws, job.Namespace, job.ID, 0)
require.NoError(t, err)
require.NotNil(t, jout)
require.True(t, jout.Stable, "job not marked as stable")
// Update the stability to false
err = state.UpdateJobStability(3, job.Namespace, job.ID, 0, false)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check that the job was updated properly
jout, err = state.JobByIDAndVersion(ws, job.Namespace, job.ID, 0)
require.NoError(t, err)
require.NotNil(t, jout)
require.False(t, jout.Stable)
}
// Test that nonexistent deployment can't be promoted
func TestStateStore_UpsertDeploymentPromotion_Nonexistent(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Promote the nonexistent deployment
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: uuid.Generate(),
All: true,
},
}
err := state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "does not exist") {
t.Fatalf("expected error promoting because the deployment doesn't exist")
}
}
// Test that terminal deployment can't be updated
func TestStateStore_UpsertDeploymentPromotion_Terminal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a terminal deployment
d := mock.Deployment()
d.Status = structs.DeploymentStatusFailed
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Promote the deployment
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: d.ID,
All: true,
},
}
err := state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "has terminal status") {
t.Fatalf("expected error updating the status because the deployment is terminal: %v", err)
}
}
// Test promoting unhealthy canaries in a deployment.
func TestStateStore_UpsertDeploymentPromotion_Unhealthy(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
require := require.New(t)
// Create a job
j := mock.Job()
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, j))
// Create a deployment
d := mock.Deployment()
d.JobID = j.ID
d.TaskGroups["web"].DesiredCanaries = 2
require.Nil(state.UpsertDeployment(2, d))
// Create a set of allocations
c1 := mock.Alloc()
c1.JobID = j.ID
c1.DeploymentID = d.ID
d.TaskGroups[c1.TaskGroup].PlacedCanaries = append(d.TaskGroups[c1.TaskGroup].PlacedCanaries, c1.ID)
c2 := mock.Alloc()
c2.JobID = j.ID
c2.DeploymentID = d.ID
d.TaskGroups[c2.TaskGroup].PlacedCanaries = append(d.TaskGroups[c2.TaskGroup].PlacedCanaries, c2.ID)
// Create a healthy but terminal alloc
c3 := mock.Alloc()
c3.JobID = j.ID
c3.DeploymentID = d.ID
c3.DesiredStatus = structs.AllocDesiredStatusStop
c3.DeploymentStatus = &structs.AllocDeploymentStatus{Healthy: pointer.Of(true)}
d.TaskGroups[c3.TaskGroup].PlacedCanaries = append(d.TaskGroups[c3.TaskGroup].PlacedCanaries, c3.ID)
require.Nil(state.UpsertAllocs(structs.MsgTypeTestSetup, 3, []*structs.Allocation{c1, c2, c3}))
// Promote the canaries
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: d.ID,
All: true,
},
}
err := state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 4, req)
require.NotNil(err)
require.Contains(err.Error(), `Task group "web" has 0/2 healthy allocations`)
}
// Test promoting a deployment with no canaries
func TestStateStore_UpsertDeploymentPromotion_NoCanaries(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
require := require.New(t)
// Create a job
j := mock.Job()
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, j))
// Create a deployment
d := mock.Deployment()
d.TaskGroups["web"].DesiredCanaries = 2
d.JobID = j.ID
require.Nil(state.UpsertDeployment(2, d))
// Promote the canaries
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: d.ID,
All: true,
},
}
err := state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 4, req)
require.NotNil(err)
require.Contains(err.Error(), `Task group "web" has 0/2 healthy allocations`)
}
// Test promoting all canaries in a deployment.
func TestStateStore_UpsertDeploymentPromotion_All(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a job with two task groups
j := mock.Job()
tg1 := j.TaskGroups[0]
tg2 := tg1.Copy()
tg2.Name = "foo"
j.TaskGroups = append(j.TaskGroups, tg2)
if err := state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, j); err != nil {
t.Fatalf("bad: %v", err)
}
// Create a deployment
d := mock.Deployment()
d.StatusDescription = structs.DeploymentStatusDescriptionRunningNeedsPromotion
d.JobID = j.ID
d.TaskGroups = map[string]*structs.DeploymentState{
"web": {
DesiredTotal: 10,
DesiredCanaries: 1,
},
"foo": {
DesiredTotal: 10,
DesiredCanaries: 1,
},
}
if err := state.UpsertDeployment(2, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Create a set of allocations
c1 := mock.Alloc()
c1.JobID = j.ID
c1.DeploymentID = d.ID
d.TaskGroups[c1.TaskGroup].PlacedCanaries = append(d.TaskGroups[c1.TaskGroup].PlacedCanaries, c1.ID)
c1.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
}
c2 := mock.Alloc()
c2.JobID = j.ID
c2.DeploymentID = d.ID
d.TaskGroups[c2.TaskGroup].PlacedCanaries = append(d.TaskGroups[c2.TaskGroup].PlacedCanaries, c2.ID)
c2.TaskGroup = tg2.Name
c2.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
}
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 3, []*structs.Allocation{c1, c2}); err != nil {
t.Fatalf("err: %v", err)
}
// Create an eval
e := mock.Eval()
// Promote the canaries
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: d.ID,
All: true,
},
Eval: e,
}
err := state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 4, req)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check that the status per task group was updated properly
ws := memdb.NewWatchSet()
dout, err := state.DeploymentByID(ws, d.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if dout.StatusDescription != structs.DeploymentStatusDescriptionRunning {
t.Fatalf("status description not updated: got %v; want %v", dout.StatusDescription, structs.DeploymentStatusDescriptionRunning)
}
if len(dout.TaskGroups) != 2 {
t.Fatalf("bad: %#v", dout.TaskGroups)
}
for tg, state := range dout.TaskGroups {
if !state.Promoted {
t.Fatalf("bad: group %q not promoted %#v", tg, state)
}
}
// Check that the evaluation was created
eout, _ := state.EvalByID(ws, e.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if eout == nil {
t.Fatalf("bad: %#v", eout)
}
}
// Test promoting a subset of canaries in a deployment.
func TestStateStore_UpsertDeploymentPromotion_Subset(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
// Create a job with two task groups
j := mock.Job()
tg1 := j.TaskGroups[0]
tg2 := tg1.Copy()
tg2.Name = "foo"
j.TaskGroups = append(j.TaskGroups, tg2)
require.Nil(state.UpsertJob(structs.MsgTypeTestSetup, 1, nil, j))
// Create a deployment
d := mock.Deployment()
d.JobID = j.ID
d.TaskGroups = map[string]*structs.DeploymentState{
"web": {
DesiredTotal: 10,
DesiredCanaries: 1,
},
"foo": {
DesiredTotal: 10,
DesiredCanaries: 1,
},
}
require.Nil(state.UpsertDeployment(2, d))
// Create a set of allocations for both groups, including an unhealthy one
c1 := mock.Alloc()
c1.JobID = j.ID
c1.DeploymentID = d.ID
d.TaskGroups[c1.TaskGroup].PlacedCanaries = append(d.TaskGroups[c1.TaskGroup].PlacedCanaries, c1.ID)
c1.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
Canary: true,
}
// Should still be a canary
c2 := mock.Alloc()
c2.JobID = j.ID
c2.DeploymentID = d.ID
d.TaskGroups[c2.TaskGroup].PlacedCanaries = append(d.TaskGroups[c2.TaskGroup].PlacedCanaries, c2.ID)
c2.TaskGroup = tg2.Name
c2.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
Canary: true,
}
c3 := mock.Alloc()
c3.JobID = j.ID
c3.DeploymentID = d.ID
d.TaskGroups[c3.TaskGroup].PlacedCanaries = append(d.TaskGroups[c3.TaskGroup].PlacedCanaries, c3.ID)
c3.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(false),
Canary: true,
}
require.Nil(state.UpsertAllocs(structs.MsgTypeTestSetup, 3, []*structs.Allocation{c1, c2, c3}))
// Create an eval
e := mock.Eval()
// Promote the canaries
req := &structs.ApplyDeploymentPromoteRequest{
DeploymentPromoteRequest: structs.DeploymentPromoteRequest{
DeploymentID: d.ID,
Groups: []string{"web"},
},
Eval: e,
}
require.Nil(state.UpdateDeploymentPromotion(structs.MsgTypeTestSetup, 4, req))
// Check that the status per task group was updated properly
ws := memdb.NewWatchSet()
dout, err := state.DeploymentByID(ws, d.ID)
require.Nil(err)
require.Len(dout.TaskGroups, 2)
require.Contains(dout.TaskGroups, "web")
require.True(dout.TaskGroups["web"].Promoted)
// Check that the evaluation was created
eout, err := state.EvalByID(ws, e.ID)
require.Nil(err)
require.NotNil(eout)
// Check the canary field was set properly
aout1, err1 := state.AllocByID(ws, c1.ID)
aout2, err2 := state.AllocByID(ws, c2.ID)
aout3, err3 := state.AllocByID(ws, c3.ID)
require.Nil(err1)
require.Nil(err2)
require.Nil(err3)
require.NotNil(aout1)
require.NotNil(aout2)
require.NotNil(aout3)
require.False(aout1.DeploymentStatus.Canary)
require.True(aout2.DeploymentStatus.Canary)
require.True(aout3.DeploymentStatus.Canary)
}
// Test that allocation health can't be set against a nonexistent deployment
func TestStateStore_UpsertDeploymentAllocHealth_Nonexistent(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Set health against the nonexistent deployment
req := &structs.ApplyDeploymentAllocHealthRequest{
DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
DeploymentID: uuid.Generate(),
HealthyAllocationIDs: []string{uuid.Generate()},
},
}
err := state.UpdateDeploymentAllocHealth(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "does not exist") {
t.Fatalf("expected error because the deployment doesn't exist: %v", err)
}
}
// Test that allocation health can't be set against a terminal deployment
func TestStateStore_UpsertDeploymentAllocHealth_Terminal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a terminal deployment
d := mock.Deployment()
d.Status = structs.DeploymentStatusFailed
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Set health against the terminal deployment
req := &structs.ApplyDeploymentAllocHealthRequest{
DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
DeploymentID: d.ID,
HealthyAllocationIDs: []string{uuid.Generate()},
},
}
err := state.UpdateDeploymentAllocHealth(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "has terminal status") {
t.Fatalf("expected error because the deployment is terminal: %v", err)
}
}
// Test that allocation health can't be set against a nonexistent alloc
func TestStateStore_UpsertDeploymentAllocHealth_BadAlloc_Nonexistent(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a deployment
d := mock.Deployment()
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Set health against the terminal deployment
req := &structs.ApplyDeploymentAllocHealthRequest{
DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
DeploymentID: d.ID,
HealthyAllocationIDs: []string{uuid.Generate()},
},
}
err := state.UpdateDeploymentAllocHealth(structs.MsgTypeTestSetup, 2, req)
if err == nil || !strings.Contains(err.Error(), "unknown alloc") {
t.Fatalf("expected error because the alloc doesn't exist: %v", err)
}
}
// Test that a deployments PlacedCanaries is properly updated
func TestStateStore_UpsertDeploymentAlloc_Canaries(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a deployment
d1 := mock.Deployment()
require.NoError(t, state.UpsertDeployment(2, d1))
// Create a Job
job := mock.Job()
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 3, nil, job))
// Create alloc with canary status
a := mock.Alloc()
a.JobID = job.ID
a.DeploymentID = d1.ID
a.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(false),
Canary: true,
}
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 4, []*structs.Allocation{a}))
// Pull the deployment from state
ws := memdb.NewWatchSet()
deploy, err := state.DeploymentByID(ws, d1.ID)
require.NoError(t, err)
// Ensure that PlacedCanaries is accurate
require.Equal(t, 1, len(deploy.TaskGroups[job.TaskGroups[0].Name].PlacedCanaries))
// Create alloc without canary status
b := mock.Alloc()
b.JobID = job.ID
b.DeploymentID = d1.ID
b.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(false),
Canary: false,
}
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 4, []*structs.Allocation{b}))
// Pull the deployment from state
ws = memdb.NewWatchSet()
deploy, err = state.DeploymentByID(ws, d1.ID)
require.NoError(t, err)
// Ensure that PlacedCanaries is accurate
require.Equal(t, 1, len(deploy.TaskGroups[job.TaskGroups[0].Name].PlacedCanaries))
// Create a second deployment
d2 := mock.Deployment()
require.NoError(t, state.UpsertDeployment(5, d2))
c := mock.Alloc()
c.JobID = job.ID
c.DeploymentID = d2.ID
c.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(false),
Canary: true,
}
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 6, []*structs.Allocation{c}))
ws = memdb.NewWatchSet()
deploy2, err := state.DeploymentByID(ws, d2.ID)
require.NoError(t, err)
// Ensure that PlacedCanaries is accurate
require.Equal(t, 1, len(deploy2.TaskGroups[job.TaskGroups[0].Name].PlacedCanaries))
}
func TestStateStore_UpsertDeploymentAlloc_NoCanaries(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Create a deployment
d1 := mock.Deployment()
require.NoError(t, state.UpsertDeployment(2, d1))
// Create a Job
job := mock.Job()
require.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 3, nil, job))
// Create alloc with canary status
a := mock.Alloc()
a.JobID = job.ID
a.DeploymentID = d1.ID
a.DeploymentStatus = &structs.AllocDeploymentStatus{
Healthy: pointer.Of(true),
Canary: false,
}
require.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 4, []*structs.Allocation{a}))
// Pull the deployment from state
ws := memdb.NewWatchSet()
deploy, err := state.DeploymentByID(ws, d1.ID)
require.NoError(t, err)
// Ensure that PlacedCanaries is accurate
require.Equal(t, 0, len(deploy.TaskGroups[job.TaskGroups[0].Name].PlacedCanaries))
}
// Test that allocation health can't be set for an alloc with mismatched
// deployment ids
func TestStateStore_UpsertDeploymentAllocHealth_BadAlloc_MismatchDeployment(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert two deployment
d1 := mock.Deployment()
d2 := mock.Deployment()
if err := state.UpsertDeployment(1, d1); err != nil {
t.Fatalf("bad: %v", err)
}
if err := state.UpsertDeployment(2, d2); err != nil {
t.Fatalf("bad: %v", err)
}
// Insert an alloc for a random deployment
a := mock.Alloc()
a.DeploymentID = d1.ID
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 3, []*structs.Allocation{a}); err != nil {
t.Fatalf("bad: %v", err)
}
// Set health against the terminal deployment
req := &structs.ApplyDeploymentAllocHealthRequest{
DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
DeploymentID: d2.ID,
HealthyAllocationIDs: []string{a.ID},
},
}
err := state.UpdateDeploymentAllocHealth(structs.MsgTypeTestSetup, 4, req)
if err == nil || !strings.Contains(err.Error(), "not part of deployment") {
t.Fatalf("expected error because the alloc isn't part of the deployment: %v", err)
}
}
// Test that allocation health is properly set
func TestStateStore_UpsertDeploymentAllocHealth(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
// Insert a deployment
d := mock.Deployment()
d.TaskGroups["web"].ProgressDeadline = 5 * time.Minute
if err := state.UpsertDeployment(1, d); err != nil {
t.Fatalf("bad: %v", err)
}
// Insert two allocations
a1 := mock.Alloc()
a1.DeploymentID = d.ID
a2 := mock.Alloc()
a2.DeploymentID = d.ID
if err := state.UpsertAllocs(structs.MsgTypeTestSetup, 2, []*structs.Allocation{a1, a2}); err != nil {
t.Fatalf("bad: %v", err)
}
// Create a job to roll back to
j := mock.Job()
// Create an eval that should be upserted
e := mock.Eval()
// Create a status update for the deployment
status, desc := structs.DeploymentStatusFailed, "foo"
u := &structs.DeploymentStatusUpdate{
DeploymentID: d.ID,
Status: status,
StatusDescription: desc,
}
// Capture the time for the update
ts := time.Now()
// Set health against the deployment
req := &structs.ApplyDeploymentAllocHealthRequest{
DeploymentAllocHealthRequest: structs.DeploymentAllocHealthRequest{
DeploymentID: d.ID,
HealthyAllocationIDs: []string{a1.ID},
UnhealthyAllocationIDs: []string{a2.ID},
},
Job: j,
Eval: e,
DeploymentUpdate: u,
Timestamp: ts,
}
err := state.UpdateDeploymentAllocHealth(structs.MsgTypeTestSetup, 3, req)
if err != nil {
t.Fatalf("bad: %v", err)
}
// Check that the status was updated properly
ws := memdb.NewWatchSet()
dout, err := state.DeploymentByID(ws, d.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if dout.Status != status || dout.StatusDescription != desc {
t.Fatalf("bad: %#v", dout)
}
// Check that the evaluation was created
eout, _ := state.EvalByID(ws, e.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if eout == nil {
t.Fatalf("bad: %#v", eout)
}
// Check that the job was created
jout, _ := state.JobByID(ws, j.Namespace, j.ID)
if err != nil {
t.Fatalf("bad: %v", err)
}
if jout == nil {
t.Fatalf("bad: %#v", jout)
}
// Check the status of the allocs
out1, err := state.AllocByID(ws, a1.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
out2, err := state.AllocByID(ws, a2.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if !out1.DeploymentStatus.IsHealthy() {
t.Fatalf("bad: alloc %q not healthy", out1.ID)
}
if !out2.DeploymentStatus.IsUnhealthy() {
t.Fatalf("bad: alloc %q not unhealthy", out2.ID)
}
if !out1.DeploymentStatus.Timestamp.Equal(ts) {
t.Fatalf("bad: alloc %q had timestamp %v; want %v", out1.ID, out1.DeploymentStatus.Timestamp, ts)
}
if !out2.DeploymentStatus.Timestamp.Equal(ts) {
t.Fatalf("bad: alloc %q had timestamp %v; want %v", out2.ID, out2.DeploymentStatus.Timestamp, ts)
}
}
func TestStateStore_UpsertVaultAccessors(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
a := mock.VaultAccessor()
a2 := mock.VaultAccessor()
ws := memdb.NewWatchSet()
if _, err := state.VaultAccessor(ws, a.Accessor); err != nil {
t.Fatalf("err: %v", err)
}
if _, err := state.VaultAccessor(ws, a2.Accessor); err != nil {
t.Fatalf("err: %v", err)
}
err := state.UpsertVaultAccessor(1000, []*structs.VaultAccessor{a, a2})
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.VaultAccessor(ws, a.Accessor)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(a, out) {
t.Fatalf("bad: %#v %#v", a, out)
}
out, err = state.VaultAccessor(ws, a2.Accessor)
if err != nil {
t.Fatalf("err: %v", err)
}
if !reflect.DeepEqual(a2, out) {
t.Fatalf("bad: %#v %#v", a2, out)
}
iter, err := state.VaultAccessors(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
accessor := raw.(*structs.VaultAccessor)
if !reflect.DeepEqual(accessor, a) && !reflect.DeepEqual(accessor, a2) {
t.Fatalf("bad: %#v", accessor)
}
}
if count != 2 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("vault_accessors")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteVaultAccessors(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
a1 := mock.VaultAccessor()
a2 := mock.VaultAccessor()
accessors := []*structs.VaultAccessor{a1, a2}
err := state.UpsertVaultAccessor(1000, accessors)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
if _, err := state.VaultAccessor(ws, a1.Accessor); err != nil {
t.Fatalf("err: %v", err)
}
err = state.DeleteVaultAccessors(1001, accessors)
if err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.VaultAccessor(ws, a1.Accessor)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", a1, out)
}
out, err = state.VaultAccessor(ws, a2.Accessor)
if err != nil {
t.Fatalf("err: %v", err)
}
if out != nil {
t.Fatalf("bad: %#v %#v", a2, out)
}
index, err := state.Index("vault_accessors")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_VaultAccessorsByAlloc(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
var accessors []*structs.VaultAccessor
var expected []*structs.VaultAccessor
for i := 0; i < 5; i++ {
accessor := mock.VaultAccessor()
accessor.AllocID = alloc.ID
expected = append(expected, accessor)
accessors = append(accessors, accessor)
}
for i := 0; i < 10; i++ {
accessor := mock.VaultAccessor()
accessors = append(accessors, accessor)
}
err := state.UpsertVaultAccessor(1000, accessors)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.VaultAccessorsByAlloc(ws, alloc.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(expected) != len(out) {
t.Fatalf("bad: %#v %#v", len(expected), len(out))
}
index, err := state.Index("vault_accessors")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_VaultAccessorsByNode(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
node := mock.Node()
var accessors []*structs.VaultAccessor
var expected []*structs.VaultAccessor
for i := 0; i < 5; i++ {
accessor := mock.VaultAccessor()
accessor.NodeID = node.ID
expected = append(expected, accessor)
accessors = append(accessors, accessor)
}
for i := 0; i < 10; i++ {
accessor := mock.VaultAccessor()
accessors = append(accessors, accessor)
}
err := state.UpsertVaultAccessor(1000, accessors)
if err != nil {
t.Fatalf("err: %v", err)
}
ws := memdb.NewWatchSet()
out, err := state.VaultAccessorsByNode(ws, node.ID)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(expected) != len(out) {
t.Fatalf("bad: %#v %#v", len(expected), len(out))
}
index, err := state.Index("vault_accessors")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_UpsertSITokenAccessors(t *testing.T) {
ci.Parallel(t)
r := require.New(t)
state := testStateStore(t)
a1 := mock.SITokenAccessor()
a2 := mock.SITokenAccessor()
ws := memdb.NewWatchSet()
var err error
_, err = state.SITokenAccessor(ws, a1.AccessorID)
r.NoError(err)
_, err = state.SITokenAccessor(ws, a2.AccessorID)
r.NoError(err)
err = state.UpsertSITokenAccessors(1000, []*structs.SITokenAccessor{a1, a2})
r.NoError(err)
wsFired := watchFired(ws)
r.True(wsFired)
noInsertWS := memdb.NewWatchSet()
result1, err := state.SITokenAccessor(noInsertWS, a1.AccessorID)
r.NoError(err)
r.Equal(a1, result1)
result2, err := state.SITokenAccessor(noInsertWS, a2.AccessorID)
r.NoError(err)
r.Equal(a2, result2)
iter, err := state.SITokenAccessors(noInsertWS)
r.NoError(err)
count := 0
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
accessor := raw.(*structs.SITokenAccessor)
// iterator is sorted by dynamic UUID
matches := reflect.DeepEqual(a1, accessor) || reflect.DeepEqual(a2, accessor)
r.True(matches)
}
r.Equal(2, count)
index, err := state.Index(siTokenAccessorTable)
r.NoError(err)
r.Equal(uint64(1000), index)
noInsertWSFired := watchFired(noInsertWS)
r.False(noInsertWSFired)
}
func TestStateStore_DeleteSITokenAccessors(t *testing.T) {
ci.Parallel(t)
r := require.New(t)
state := testStateStore(t)
a1 := mock.SITokenAccessor()
a2 := mock.SITokenAccessor()
accessors := []*structs.SITokenAccessor{a1, a2}
var err error
err = state.UpsertSITokenAccessors(1000, accessors)
r.NoError(err)
ws := memdb.NewWatchSet()
_, err = state.SITokenAccessor(ws, a1.AccessorID)
r.NoError(err)
err = state.DeleteSITokenAccessors(1001, accessors)
r.NoError(err)
wsFired := watchFired(ws)
r.True(wsFired)
wsPostDelete := memdb.NewWatchSet()
result1, err := state.SITokenAccessor(wsPostDelete, a1.AccessorID)
r.NoError(err)
r.Nil(result1) // was deleted
result2, err := state.SITokenAccessor(wsPostDelete, a2.AccessorID)
r.NoError(err)
r.Nil(result2) // was deleted
index, err := state.Index(siTokenAccessorTable)
r.NoError(err)
r.Equal(uint64(1001), index)
wsPostDeleteFired := watchFired(wsPostDelete)
r.False(wsPostDeleteFired)
}
func TestStateStore_SITokenAccessorsByAlloc(t *testing.T) {
ci.Parallel(t)
r := require.New(t)
state := testStateStore(t)
alloc := mock.Alloc()
var accessors []*structs.SITokenAccessor
var expected []*structs.SITokenAccessor
for i := 0; i < 5; i++ {
accessor := mock.SITokenAccessor()
accessor.AllocID = alloc.ID
expected = append(expected, accessor)
accessors = append(accessors, accessor)
}
for i := 0; i < 10; i++ {
accessor := mock.SITokenAccessor()
accessor.AllocID = uuid.Generate() // does not belong to alloc
accessors = append(accessors, accessor)
}
err := state.UpsertSITokenAccessors(1000, accessors)
r.NoError(err)
ws := memdb.NewWatchSet()
result, err := state.SITokenAccessorsByAlloc(ws, alloc.ID)
r.NoError(err)
r.ElementsMatch(expected, result)
index, err := state.Index(siTokenAccessorTable)
r.NoError(err)
r.Equal(uint64(1000), index)
wsFired := watchFired(ws)
r.False(wsFired)
}
func TestStateStore_SITokenAccessorsByNode(t *testing.T) {
ci.Parallel(t)
r := require.New(t)
state := testStateStore(t)
node := mock.Node()
var accessors []*structs.SITokenAccessor
var expected []*structs.SITokenAccessor
var err error
for i := 0; i < 5; i++ {
accessor := mock.SITokenAccessor()
accessor.NodeID = node.ID
expected = append(expected, accessor)
accessors = append(accessors, accessor)
}
for i := 0; i < 10; i++ {
accessor := mock.SITokenAccessor()
accessor.NodeID = uuid.Generate() // does not belong to node
accessors = append(accessors, accessor)
}
err = state.UpsertSITokenAccessors(1000, accessors)
r.NoError(err)
ws := memdb.NewWatchSet()
result, err := state.SITokenAccessorsByNode(ws, node.ID)
r.NoError(err)
r.ElementsMatch(expected, result)
index, err := state.Index(siTokenAccessorTable)
r.NoError(err)
r.Equal(uint64(1000), index)
wsFired := watchFired(ws)
r.False(wsFired)
}
func TestStateStore_UpsertACLPolicy(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
policy := mock.ACLPolicy()
policy2 := mock.ACLPolicy()
ws := memdb.NewWatchSet()
if _, err := state.ACLPolicyByName(ws, policy.Name); err != nil {
t.Fatalf("err: %v", err)
}
if _, err := state.ACLPolicyByName(ws, policy2.Name); err != nil {
t.Fatalf("err: %v", err)
}
if err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy2}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.ACLPolicyByName(ws, policy.Name)
assert.Equal(t, nil, err)
assert.Equal(t, policy, out)
out, err = state.ACLPolicyByName(ws, policy2.Name)
assert.Equal(t, nil, err)
assert.Equal(t, policy2, out)
iter, err := state.ACLPolicies(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see both policies
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
if count != 2 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("acl_policy")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteACLPolicy(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
policy := mock.ACLPolicy()
policy2 := mock.ACLPolicy()
// Create the policy
if err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy2}); err != nil {
t.Fatalf("err: %v", err)
}
// Create a watcher
ws := memdb.NewWatchSet()
if _, err := state.ACLPolicyByName(ws, policy.Name); err != nil {
t.Fatalf("err: %v", err)
}
// Delete the policy
if err := state.DeleteACLPolicies(structs.MsgTypeTestSetup, 1001, []string{policy.Name, policy2.Name}); err != nil {
t.Fatalf("err: %v", err)
}
// Ensure watching triggered
if !watchFired(ws) {
t.Fatalf("bad")
}
// Ensure we don't get the object back
ws = memdb.NewWatchSet()
out, err := state.ACLPolicyByName(ws, policy.Name)
assert.Equal(t, nil, err)
if out != nil {
t.Fatalf("bad: %#v", out)
}
iter, err := state.ACLPolicies(ws)
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see neither policy
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
if count != 0 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("acl_policy")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_ACLPolicyByNamePrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
names := []string{
"foo",
"bar",
"foobar",
"foozip",
"zip",
}
// Create the policies
var baseIndex uint64 = 1000
for _, name := range names {
p := mock.ACLPolicy()
p.Name = name
if err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, baseIndex, []*structs.ACLPolicy{p}); err != nil {
t.Fatalf("err: %v", err)
}
baseIndex++
}
// Scan by prefix
iter, err := state.ACLPolicyByNamePrefix(nil, "foo")
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see both policies
count := 0
out := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
count++
out = append(out, raw.(*structs.ACLPolicy).Name)
}
if count != 3 {
t.Fatalf("bad: %d %v", count, out)
}
sort.Strings(out)
expect := []string{"foo", "foobar", "foozip"}
assert.Equal(t, expect, out)
}
func TestStateStore_BootstrapACLTokens(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
tk1 := mock.ACLToken()
tk2 := mock.ACLToken()
ok, resetIdx, err := state.CanBootstrapACLToken()
assert.Nil(t, err)
assert.Equal(t, true, ok)
assert.EqualValues(t, 0, resetIdx)
if err := state.BootstrapACLTokens(structs.MsgTypeTestSetup, 1000, 0, tk1); err != nil {
t.Fatalf("err: %v", err)
}
out, err := state.ACLTokenByAccessorID(nil, tk1.AccessorID)
assert.Equal(t, nil, err)
assert.Equal(t, tk1, out)
ok, resetIdx, err = state.CanBootstrapACLToken()
assert.Nil(t, err)
assert.Equal(t, false, ok)
assert.EqualValues(t, 1000, resetIdx)
if err := state.BootstrapACLTokens(structs.MsgTypeTestSetup, 1001, 0, tk2); err == nil {
t.Fatalf("expected error")
}
iter, err := state.ACLTokens(nil, SortDefault)
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see both policies
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
if count != 1 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("acl_token")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
index, err = state.Index("acl_token_bootstrap")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
// Should allow bootstrap with reset index
if err := state.BootstrapACLTokens(structs.MsgTypeTestSetup, 1001, 1000, tk2); err != nil {
t.Fatalf("err %v", err)
}
// Check we've modified the index
index, err = state.Index("acl_token")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
index, err = state.Index("acl_token_bootstrap")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
}
func TestStateStore_UpsertACLTokens(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
tk1 := mock.ACLToken()
tk2 := mock.ACLToken()
ws := memdb.NewWatchSet()
if _, err := state.ACLTokenByAccessorID(ws, tk1.AccessorID); err != nil {
t.Fatalf("err: %v", err)
}
if _, err := state.ACLTokenByAccessorID(ws, tk2.AccessorID); err != nil {
t.Fatalf("err: %v", err)
}
if err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{tk1, tk2}); err != nil {
t.Fatalf("err: %v", err)
}
if !watchFired(ws) {
t.Fatalf("bad")
}
ws = memdb.NewWatchSet()
out, err := state.ACLTokenByAccessorID(ws, tk1.AccessorID)
assert.Equal(t, nil, err)
assert.Equal(t, tk1, out)
out, err = state.ACLTokenByAccessorID(ws, tk2.AccessorID)
assert.Equal(t, nil, err)
assert.Equal(t, tk2, out)
out, err = state.ACLTokenBySecretID(ws, tk1.SecretID)
assert.Equal(t, nil, err)
assert.Equal(t, tk1, out)
out, err = state.ACLTokenBySecretID(ws, tk2.SecretID)
assert.Equal(t, nil, err)
assert.Equal(t, tk2, out)
iter, err := state.ACLTokens(ws, SortDefault)
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see both policies
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
if count != 2 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("acl_token")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1000 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_DeleteACLTokens(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
tk1 := mock.ACLToken()
tk2 := mock.ACLToken()
// Create the tokens
if err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{tk1, tk2}); err != nil {
t.Fatalf("err: %v", err)
}
// Create a watcher
ws := memdb.NewWatchSet()
if _, err := state.ACLTokenByAccessorID(ws, tk1.AccessorID); err != nil {
t.Fatalf("err: %v", err)
}
// Delete the token
if err := state.DeleteACLTokens(structs.MsgTypeTestSetup, 1001, []string{tk1.AccessorID, tk2.AccessorID}); err != nil {
t.Fatalf("err: %v", err)
}
// Ensure watching triggered
if !watchFired(ws) {
t.Fatalf("bad")
}
// Ensure we don't get the object back
ws = memdb.NewWatchSet()
out, err := state.ACLTokenByAccessorID(ws, tk1.AccessorID)
assert.Equal(t, nil, err)
if out != nil {
t.Fatalf("bad: %#v", out)
}
iter, err := state.ACLTokens(ws, SortDefault)
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure we see both policies
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
if count != 0 {
t.Fatalf("bad: %d", count)
}
index, err := state.Index("acl_token")
if err != nil {
t.Fatalf("err: %v", err)
}
if index != 1001 {
t.Fatalf("bad: %d", index)
}
if watchFired(ws) {
t.Fatalf("bad")
}
}
func TestStateStore_ACLTokenByAccessorIDPrefix(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
prefixes := []string{
"aaaa",
"aabb",
"bbbb",
"bbcc",
"ffff",
}
// Create the tokens
var baseIndex uint64 = 1000
for _, prefix := range prefixes {
tk := mock.ACLToken()
tk.AccessorID = prefix + tk.AccessorID[4:]
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, baseIndex, []*structs.ACLToken{tk})
require.NoError(t, err)
baseIndex++
}
gatherTokens := func(iter memdb.ResultIterator) []*structs.ACLToken {
var tokens []*structs.ACLToken
for {
raw := iter.Next()
if raw == nil {
break
}
tokens = append(tokens, raw.(*structs.ACLToken))
}
return tokens
}
t.Run("scan by prefix", func(t *testing.T) {
iter, err := state.ACLTokenByAccessorIDPrefix(nil, "aa", SortDefault)
require.NoError(t, err)
// Ensure we see both tokens
out := gatherTokens(iter)
require.Len(t, out, 2)
got := []string{}
for _, t := range out {
got = append(got, t.AccessorID[:4])
}
expect := []string{"aaaa", "aabb"}
require.Equal(t, expect, got)
})
t.Run("reverse order", func(t *testing.T) {
iter, err := state.ACLTokenByAccessorIDPrefix(nil, "aa", SortReverse)
require.NoError(t, err)
// Ensure we see both tokens
out := gatherTokens(iter)
require.Len(t, out, 2)
got := []string{}
for _, t := range out {
got = append(got, t.AccessorID[:4])
}
expect := []string{"aabb", "aaaa"}
require.Equal(t, expect, got)
})
}
func TestStateStore_ACLTokensByGlobal(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
tk1 := mock.ACLToken()
tk1.AccessorID = "aaaa" + tk1.AccessorID[4:]
tk2 := mock.ACLToken()
tk2.AccessorID = "aabb" + tk2.AccessorID[4:]
tk3 := mock.ACLToken()
tk3.AccessorID = "bbbb" + tk3.AccessorID[4:]
tk3.Global = true
tk4 := mock.ACLToken()
tk4.AccessorID = "ffff" + tk4.AccessorID[4:]
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{tk1, tk2, tk3, tk4})
require.NoError(t, err)
gatherTokens := func(iter memdb.ResultIterator) []*structs.ACLToken {
var tokens []*structs.ACLToken
for {
raw := iter.Next()
if raw == nil {
break
}
tokens = append(tokens, raw.(*structs.ACLToken))
}
return tokens
}
t.Run("only global tokens", func(t *testing.T) {
iter, err := state.ACLTokensByGlobal(nil, true, SortDefault)
require.NoError(t, err)
got := gatherTokens(iter)
require.Len(t, got, 1)
require.Equal(t, tk3.AccessorID, got[0].AccessorID)
})
t.Run("reverse order", func(t *testing.T) {
iter, err := state.ACLTokensByGlobal(nil, false, SortReverse)
require.NoError(t, err)
expected := []*structs.ACLToken{tk4, tk2, tk1}
got := gatherTokens(iter)
require.Len(t, got, 3)
require.Equal(t, expected, got)
})
}
func TestStateStore_OneTimeTokens(t *testing.T) {
ci.Parallel(t)
index := uint64(100)
state := testStateStore(t)
// create some ACL tokens
token1 := mock.ACLToken()
token2 := mock.ACLToken()
token3 := mock.ACLToken()
index++
require.Nil(t, state.UpsertACLTokens(
structs.MsgTypeTestSetup, index,
[]*structs.ACLToken{token1, token2, token3}))
otts := []*structs.OneTimeToken{
{
// expired OTT for token1
OneTimeSecretID: uuid.Generate(),
AccessorID: token1.AccessorID,
ExpiresAt: time.Now().Add(-1 * time.Minute),
},
{
// valid OTT for token2
OneTimeSecretID: uuid.Generate(),
AccessorID: token2.AccessorID,
ExpiresAt: time.Now().Add(10 * time.Minute),
},
{
// new but expired OTT for token2; this will be accepted even
// though it's expired and overwrite the other one
OneTimeSecretID: uuid.Generate(),
AccessorID: token2.AccessorID,
ExpiresAt: time.Now().Add(-10 * time.Minute),
},
{
// valid OTT for token3
AccessorID: token3.AccessorID,
OneTimeSecretID: uuid.Generate(),
ExpiresAt: time.Now().Add(10 * time.Minute),
},
{
// new valid OTT for token3
OneTimeSecretID: uuid.Generate(),
AccessorID: token3.AccessorID,
ExpiresAt: time.Now().Add(5 * time.Minute),
},
}
for _, ott := range otts {
index++
require.NoError(t, state.UpsertOneTimeToken(structs.MsgTypeTestSetup, index, ott))
}
// verify that we have exactly one OTT for each AccessorID
txn := state.db.ReadTxn()
iter, err := txn.Get("one_time_token", "id")
require.NoError(t, err)
results := []*structs.OneTimeToken{}
for {
raw := iter.Next()
if raw == nil {
break
}
ott, ok := raw.(*structs.OneTimeToken)
require.True(t, ok)
results = append(results, ott)
}
// results aren't ordered but if we have 3 OTT and all 3 tokens, we know
// we have no duplicate accessors
require.Len(t, results, 3)
accessors := []string{
results[0].AccessorID, results[1].AccessorID, results[2].AccessorID}
require.Contains(t, accessors, token1.AccessorID)
require.Contains(t, accessors, token2.AccessorID)
require.Contains(t, accessors, token3.AccessorID)
// now verify expiration
getExpiredTokens := func(now time.Time) []*structs.OneTimeToken {
txn := state.db.ReadTxn()
iter, err := state.oneTimeTokensExpiredTxn(txn, nil, now)
require.NoError(t, err)
results := []*structs.OneTimeToken{}
for {
raw := iter.Next()
if raw == nil {
break
}
ott, ok := raw.(*structs.OneTimeToken)
require.True(t, ok)
results = append(results, ott)
}
return results
}
results = getExpiredTokens(time.Now())
require.Len(t, results, 2)
// results aren't ordered
expiredAccessors := []string{results[0].AccessorID, results[1].AccessorID}
require.Contains(t, expiredAccessors, token1.AccessorID)
require.Contains(t, expiredAccessors, token2.AccessorID)
require.True(t, time.Now().After(results[0].ExpiresAt))
require.True(t, time.Now().After(results[1].ExpiresAt))
// clear the expired tokens and verify they're gone
index++
require.NoError(t, state.ExpireOneTimeTokens(
structs.MsgTypeTestSetup, index, time.Now()))
results = getExpiredTokens(time.Now())
require.Len(t, results, 0)
// query the unexpired token
ott, err := state.OneTimeTokenBySecret(nil, otts[len(otts)-1].OneTimeSecretID)
require.NoError(t, err)
require.Equal(t, token3.AccessorID, ott.AccessorID)
require.True(t, time.Now().Before(ott.ExpiresAt))
restore, err := state.Restore()
require.NoError(t, err)
err = restore.OneTimeTokenRestore(ott)
require.NoError(t, err)
require.NoError(t, restore.Commit())
ott, err = state.OneTimeTokenBySecret(nil, otts[len(otts)-1].OneTimeSecretID)
require.NoError(t, err)
require.Equal(t, token3.AccessorID, ott.AccessorID)
}
func TestStateStore_ClusterMetadata(t *testing.T) {
require := require.New(t)
state := testStateStore(t)
clusterID := "12345678-1234-1234-1234-1234567890"
now := time.Now().UnixNano()
meta := &structs.ClusterMetadata{ClusterID: clusterID, CreateTime: now}
err := state.ClusterSetMetadata(100, meta)
require.NoError(err)
result, err := state.ClusterMetadata(nil)
require.NoError(err)
require.Equal(clusterID, result.ClusterID)
require.Equal(now, result.CreateTime)
}
func TestStateStore_UpsertScalingPolicy(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
policy := mock.ScalingPolicy()
policy2 := mock.ScalingPolicy()
wsAll := memdb.NewWatchSet()
all, err := state.ScalingPolicies(wsAll)
require.NoError(err)
require.Nil(all.Next())
ws := memdb.NewWatchSet()
out, err := state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.Nil(out)
out, err = state.ScalingPolicyByTargetAndType(ws, policy2.Target, policy2.Type)
require.NoError(err)
require.Nil(out)
err = state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy, policy2})
require.NoError(err)
require.True(watchFired(ws))
require.True(watchFired(wsAll))
ws = memdb.NewWatchSet()
out, err = state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.Equal(policy, out)
out, err = state.ScalingPolicyByTargetAndType(ws, policy2.Target, policy2.Type)
require.NoError(err)
require.Equal(policy2, out)
// Ensure we see both policies
countPolicies := func() (n int, err error) {
iter, err := state.ScalingPolicies(ws)
if err != nil {
return
}
for raw := iter.Next(); raw != nil; raw = iter.Next() {
n++
}
return
}
count, err := countPolicies()
require.NoError(err)
require.Equal(2, count)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.True(1000 == index)
require.False(watchFired(ws))
// Check that we can add policy with same target but different type
policy3 := mock.ScalingPolicy()
for k, v := range policy2.Target {
policy3.Target[k] = v
}
err = state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy3})
require.NoError(err)
// Ensure we see both policies, since target didn't change
count, err = countPolicies()
require.NoError(err)
require.Equal(2, count)
// Change type and check if we see 3
policy3.Type = "other-type"
err = state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy3})
require.NoError(err)
count, err = countPolicies()
require.NoError(err)
require.Equal(3, count)
}
func TestStateStore_UpsertScalingPolicy_Namespace(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
otherNamespace := "not-default-namespace"
state := testStateStore(t)
policy := mock.ScalingPolicy()
policy2 := mock.ScalingPolicy()
policy2.Target[structs.ScalingTargetNamespace] = otherNamespace
ws1 := memdb.NewWatchSet()
iter, err := state.ScalingPoliciesByNamespace(ws1, structs.DefaultNamespace, "")
require.NoError(err)
require.Nil(iter.Next())
ws2 := memdb.NewWatchSet()
iter, err = state.ScalingPoliciesByNamespace(ws2, otherNamespace, "")
require.NoError(err)
require.Nil(iter.Next())
err = state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy, policy2})
require.NoError(err)
require.True(watchFired(ws1))
require.True(watchFired(ws2))
iter, err = state.ScalingPoliciesByNamespace(nil, structs.DefaultNamespace, "")
require.NoError(err)
policiesInDefaultNamespace := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
policiesInDefaultNamespace = append(policiesInDefaultNamespace, raw.(*structs.ScalingPolicy).ID)
}
require.ElementsMatch([]string{policy.ID}, policiesInDefaultNamespace)
iter, err = state.ScalingPoliciesByNamespace(nil, otherNamespace, "")
require.NoError(err)
policiesInOtherNamespace := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
policiesInOtherNamespace = append(policiesInOtherNamespace, raw.(*structs.ScalingPolicy).ID)
}
require.ElementsMatch([]string{policy2.ID}, policiesInOtherNamespace)
}
func TestStateStore_UpsertScalingPolicy_Namespace_PrefixBug(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
ns1 := "name"
ns2 := "name2" // matches prefix "name"
state := testStateStore(t)
policy1 := mock.ScalingPolicy()
policy1.Target[structs.ScalingTargetNamespace] = ns1
policy2 := mock.ScalingPolicy()
policy2.Target[structs.ScalingTargetNamespace] = ns2
ws1 := memdb.NewWatchSet()
iter, err := state.ScalingPoliciesByNamespace(ws1, ns1, "")
require.NoError(err)
require.Nil(iter.Next())
ws2 := memdb.NewWatchSet()
iter, err = state.ScalingPoliciesByNamespace(ws2, ns2, "")
require.NoError(err)
require.Nil(iter.Next())
err = state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy1, policy2})
require.NoError(err)
require.True(watchFired(ws1))
require.True(watchFired(ws2))
iter, err = state.ScalingPoliciesByNamespace(nil, ns1, "")
require.NoError(err)
policiesInNS1 := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
policiesInNS1 = append(policiesInNS1, raw.(*structs.ScalingPolicy).ID)
}
require.ElementsMatch([]string{policy1.ID}, policiesInNS1)
iter, err = state.ScalingPoliciesByNamespace(nil, ns2, "")
require.NoError(err)
policiesInNS2 := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
policiesInNS2 = append(policiesInNS2, raw.(*structs.ScalingPolicy).ID)
}
require.ElementsMatch([]string{policy2.ID}, policiesInNS2)
}
// Scaling Policy IDs are generated randomly during Job.Register
// Subsequent updates of the job should preserve the ID for the scaling policy
// associated with a given target.
func TestStateStore_UpsertJob_PreserveScalingPolicyIDsAndIndex(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job, policy := mock.JobWithScalingPolicy()
var newIndex uint64 = 1000
err := state.UpsertJob(structs.MsgTypeTestSetup, newIndex, nil, job)
require.NoError(err)
ws := memdb.NewWatchSet()
p1, err := state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(p1)
require.Equal(newIndex, p1.CreateIndex)
require.Equal(newIndex, p1.ModifyIndex)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.Equal(newIndex, index)
require.NotEmpty(p1.ID)
// update the job
job.Meta["new-meta"] = "new-value"
newIndex += 100
err = state.UpsertJob(structs.MsgTypeTestSetup, newIndex, nil, job)
require.NoError(err)
require.False(watchFired(ws), "watch should not have fired")
p2, err := state.ScalingPolicyByTargetAndType(nil, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(p2)
require.Equal(p1.ID, p2.ID, "ID should not have changed")
require.Equal(p1.CreateIndex, p2.CreateIndex)
require.Equal(p1.ModifyIndex, p2.ModifyIndex)
index, err = state.Index("scaling_policy")
require.NoError(err)
require.Equal(index, p1.CreateIndex, "table index should not have changed")
}
// Updating the scaling policy for a job should update the index table and fire the watch.
// This test is the converse of TestStateStore_UpsertJob_PreserveScalingPolicyIDsAndIndex
func TestStateStore_UpsertJob_UpdateScalingPolicy(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job, policy := mock.JobWithScalingPolicy()
var oldIndex uint64 = 1000
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, oldIndex, nil, job))
ws := memdb.NewWatchSet()
p1, err := state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(p1)
require.Equal(oldIndex, p1.CreateIndex)
require.Equal(oldIndex, p1.ModifyIndex)
prevId := p1.ID
index, err := state.Index("scaling_policy")
require.NoError(err)
require.Equal(oldIndex, index)
require.NotEmpty(p1.ID)
// update the job with the updated scaling policy; make sure to use a different object
newPolicy := p1.Copy()
newPolicy.Policy["new-field"] = "new-value"
job.TaskGroups[0].Scaling = newPolicy
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, oldIndex+100, nil, job))
require.True(watchFired(ws), "watch should have fired")
p2, err := state.ScalingPolicyByTargetAndType(nil, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(p2)
require.Equal(p2.Policy["new-field"], "new-value")
require.Equal(prevId, p2.ID, "ID should not have changed")
require.Equal(oldIndex, p2.CreateIndex)
require.Greater(p2.ModifyIndex, oldIndex, "ModifyIndex should have advanced")
index, err = state.Index("scaling_policy")
require.NoError(err)
require.Greater(index, oldIndex, "table index should have advanced")
}
func TestStateStore_DeleteScalingPolicies(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
policy := mock.ScalingPolicy()
policy2 := mock.ScalingPolicy()
// Create the policy
err := state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{policy, policy2})
require.NoError(err)
// Create a watcher
ws := memdb.NewWatchSet()
_, err = state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
// Delete the policy
err = state.DeleteScalingPolicies(1001, []string{policy.ID, policy2.ID})
require.NoError(err)
// Ensure watching triggered
require.True(watchFired(ws))
// Ensure we don't get the objects back
ws = memdb.NewWatchSet()
out, err := state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.Nil(out)
ws = memdb.NewWatchSet()
out, err = state.ScalingPolicyByTargetAndType(ws, policy2.Target, policy2.Type)
require.NoError(err)
require.Nil(out)
// Ensure we see both policies
iter, err := state.ScalingPoliciesByNamespace(ws, policy.Target[structs.ScalingTargetNamespace], "")
require.NoError(err)
count := 0
for {
raw := iter.Next()
if raw == nil {
break
}
count++
}
require.Equal(0, count)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.True(1001 == index)
require.False(watchFired(ws))
}
func TestStateStore_StopJob_DeleteScalingPolicies(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job := mock.Job()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
require.NoError(err)
policy := mock.ScalingPolicy()
policy.Target[structs.ScalingTargetJob] = job.ID
err = state.UpsertScalingPolicies(1100, []*structs.ScalingPolicy{policy})
require.NoError(err)
// Ensure the scaling policy is present and start some watches
wsGet := memdb.NewWatchSet()
out, err := state.ScalingPolicyByTargetAndType(wsGet, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(out)
wsList := memdb.NewWatchSet()
_, err = state.ScalingPolicies(wsList)
require.NoError(err)
// Stop the job
job, err = state.JobByID(nil, job.Namespace, job.ID)
require.NoError(err)
job.Stop = true
err = state.UpsertJob(structs.MsgTypeTestSetup, 1200, nil, job)
require.NoError(err)
// Ensure:
// * the scaling policy was deleted
// * the watches were fired
// * the table index was advanced
require.True(watchFired(wsGet))
require.True(watchFired(wsList))
out, err = state.ScalingPolicyByTargetAndType(nil, policy.Target, policy.Type)
require.NoError(err)
require.Nil(out)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.GreaterOrEqual(index, uint64(1200))
}
func TestStateStore_UnstopJob_UpsertScalingPolicies(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job, policy := mock.JobWithScalingPolicy()
job.Stop = true
// establish watcher, verify there are no scaling policies yet
ws := memdb.NewWatchSet()
list, err := state.ScalingPolicies(ws)
require.NoError(err)
require.Nil(list.Next())
// upsert a stopped job, verify that we don't fire the watcher or add any scaling policies
err = state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
require.NoError(err)
require.True(watchFired(ws))
list, err = state.ScalingPolicies(ws)
require.NoError(err)
require.NotNil(list.Next())
// Establish a new watchset
ws = memdb.NewWatchSet()
_, err = state.ScalingPolicies(ws)
require.NoError(err)
// Unstop this job, say you'll run it again...
job.Stop = false
err = state.UpsertJob(structs.MsgTypeTestSetup, 1100, nil, job)
require.NoError(err)
// Ensure the scaling policy still exists, watch was not fired, index was not advanced
out, err := state.ScalingPolicyByTargetAndType(nil, policy.Target, policy.Type)
require.NoError(err)
require.NotNil(out)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.EqualValues(index, 1000)
require.False(watchFired(ws))
}
func TestStateStore_DeleteJob_DeleteScalingPolicies(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job := mock.Job()
err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
require.NoError(err)
policy := mock.ScalingPolicy()
policy.Target[structs.ScalingTargetJob] = job.ID
err = state.UpsertScalingPolicies(1001, []*structs.ScalingPolicy{policy})
require.NoError(err)
// Delete the job
err = state.DeleteJob(1002, job.Namespace, job.ID)
require.NoError(err)
// Ensure the scaling policy was deleted
ws := memdb.NewWatchSet()
out, err := state.ScalingPolicyByTargetAndType(ws, policy.Target, policy.Type)
require.NoError(err)
require.Nil(out)
index, err := state.Index("scaling_policy")
require.NoError(err)
require.True(index > 1001)
}
func TestStateStore_DeleteJob_DeleteScalingPoliciesPrefixBug(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job := mock.Job()
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job))
job2 := job.Copy()
job2.ID = job.ID + "-but-longer"
require.NoError(state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, job2))
policy := mock.ScalingPolicy()
policy.Target[structs.ScalingTargetJob] = job.ID
policy2 := mock.ScalingPolicy()
policy2.Target[structs.ScalingTargetJob] = job2.ID
require.NoError(state.UpsertScalingPolicies(1002, []*structs.ScalingPolicy{policy, policy2}))
// Delete job with the shorter prefix-ID
require.NoError(state.DeleteJob(1003, job.Namespace, job.ID))
// Ensure only the associated scaling policy was deleted, not the one matching the job with the longer ID
out, err := state.ScalingPolicyByID(nil, policy.ID)
require.NoError(err)
require.Nil(out)
out, err = state.ScalingPolicyByID(nil, policy2.ID)
require.NoError(err)
require.NotNil(out)
}
// This test ensures that deleting a job that doesn't have any scaling policies
// will not cause the scaling_policy table index to increase, on either job
// registration or deletion.
func TestStateStore_DeleteJob_ScalingPolicyIndexNoop(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job := mock.Job()
prevIndex, err := state.Index("scaling_policy")
require.NoError(err)
err = state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, job)
require.NoError(err)
newIndex, err := state.Index("scaling_policy")
require.NoError(err)
require.Equal(prevIndex, newIndex)
// Delete the job
err = state.DeleteJob(1002, job.Namespace, job.ID)
require.NoError(err)
newIndex, err = state.Index("scaling_policy")
require.NoError(err)
require.Equal(prevIndex, newIndex)
}
func TestStateStore_ScalingPoliciesByType(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
// Create scaling policies of different types
pHorzA := mock.ScalingPolicy()
pHorzA.Type = structs.ScalingPolicyTypeHorizontal
pHorzB := mock.ScalingPolicy()
pHorzB.Type = structs.ScalingPolicyTypeHorizontal
pOther1 := mock.ScalingPolicy()
pOther1.Type = "other-type-1"
pOther2 := mock.ScalingPolicy()
pOther2.Type = "other-type-2"
// Create search routine
search := func(t string) (found []string) {
found = []string{}
iter, err := state.ScalingPoliciesByTypePrefix(nil, t)
require.NoError(err)
for raw := iter.Next(); raw != nil; raw = iter.Next() {
found = append(found, raw.(*structs.ScalingPolicy).Type)
}
return
}
// Create the policies
var baseIndex uint64 = 1000
err := state.UpsertScalingPolicies(baseIndex, []*structs.ScalingPolicy{pHorzA, pHorzB, pOther1, pOther2})
require.NoError(err)
// Check if we can read horizontal policies
expect := []string{pHorzA.Type, pHorzB.Type}
actual := search(structs.ScalingPolicyTypeHorizontal)
require.ElementsMatch(expect, actual)
// Check if we can read policies of other types
expect = []string{pOther1.Type}
actual = search("other-type-1")
require.ElementsMatch(expect, actual)
// Check that we can read policies by prefix
expect = []string{"other-type-1", "other-type-2"}
actual = search("other-type")
require.Equal(expect, actual)
// Check for empty result
expect = []string{}
actual = search("non-existing")
require.ElementsMatch(expect, actual)
}
func TestStateStore_ScalingPoliciesByTypePrefix(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
// Create scaling policies of different types
pHorzA := mock.ScalingPolicy()
pHorzA.Type = structs.ScalingPolicyTypeHorizontal
pHorzB := mock.ScalingPolicy()
pHorzB.Type = structs.ScalingPolicyTypeHorizontal
pOther1 := mock.ScalingPolicy()
pOther1.Type = "other-type-1"
pOther2 := mock.ScalingPolicy()
pOther2.Type = "other-type-2"
// Create search routine
search := func(t string) (count int, found []string, err error) {
found = []string{}
iter, err := state.ScalingPoliciesByTypePrefix(nil, t)
if err != nil {
return
}
for raw := iter.Next(); raw != nil; raw = iter.Next() {
count++
found = append(found, raw.(*structs.ScalingPolicy).Type)
}
return
}
// Create the policies
var baseIndex uint64 = 1000
err := state.UpsertScalingPolicies(baseIndex, []*structs.ScalingPolicy{pHorzA, pHorzB, pOther1, pOther2})
require.NoError(err)
// Check if we can read horizontal policies
expect := []string{pHorzA.Type, pHorzB.Type}
count, found, err := search("h")
sort.Strings(found)
sort.Strings(expect)
require.NoError(err)
require.Equal(expect, found)
require.Equal(2, count)
// Check if we can read other prefix policies
expect = []string{pOther1.Type, pOther2.Type}
count, found, err = search("other")
sort.Strings(found)
sort.Strings(expect)
require.NoError(err)
require.Equal(expect, found)
require.Equal(2, count)
// Check for empty result
expect = []string{}
count, found, err = search("non-existing")
sort.Strings(found)
sort.Strings(expect)
require.NoError(err)
require.Equal(expect, found)
require.Equal(0, count)
}
func TestStateStore_ScalingPoliciesByJob(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
policyA := mock.ScalingPolicy()
policyB1 := mock.ScalingPolicy()
policyB2 := mock.ScalingPolicy()
policyB1.Target[structs.ScalingTargetJob] = policyB2.Target[structs.ScalingTargetJob]
// Create the policies
var baseIndex uint64 = 1000
err := state.UpsertScalingPolicies(baseIndex, []*structs.ScalingPolicy{policyA, policyB1, policyB2})
require.NoError(err)
iter, err := state.ScalingPoliciesByJob(nil,
policyA.Target[structs.ScalingTargetNamespace],
policyA.Target[structs.ScalingTargetJob], "")
require.NoError(err)
// Ensure we see expected policies
count := 0
found := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
count++
found = append(found, raw.(*structs.ScalingPolicy).Target[structs.ScalingTargetGroup])
}
require.Equal(1, count)
sort.Strings(found)
expect := []string{policyA.Target[structs.ScalingTargetGroup]}
sort.Strings(expect)
require.Equal(expect, found)
iter, err = state.ScalingPoliciesByJob(nil,
policyB1.Target[structs.ScalingTargetNamespace],
policyB1.Target[structs.ScalingTargetJob], "")
require.NoError(err)
// Ensure we see expected policies
count = 0
found = []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
count++
found = append(found, raw.(*structs.ScalingPolicy).Target[structs.ScalingTargetGroup])
}
require.Equal(2, count)
sort.Strings(found)
expect = []string{
policyB1.Target[structs.ScalingTargetGroup],
policyB2.Target[structs.ScalingTargetGroup],
}
sort.Strings(expect)
require.Equal(expect, found)
}
func TestStateStore_ScalingPoliciesByJob_PrefixBug(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
jobPrefix := "job-name-" + uuid.Generate()
state := testStateStore(t)
policy1 := mock.ScalingPolicy()
policy1.Target[structs.ScalingTargetJob] = jobPrefix
policy2 := mock.ScalingPolicy()
policy2.Target[structs.ScalingTargetJob] = jobPrefix + "-more"
// Create the policies
var baseIndex uint64 = 1000
err := state.UpsertScalingPolicies(baseIndex, []*structs.ScalingPolicy{policy1, policy2})
require.NoError(err)
iter, err := state.ScalingPoliciesByJob(nil,
policy1.Target[structs.ScalingTargetNamespace],
jobPrefix, "")
require.NoError(err)
// Ensure we see expected policies
count := 0
found := []string{}
for {
raw := iter.Next()
if raw == nil {
break
}
count++
found = append(found, raw.(*structs.ScalingPolicy).ID)
}
require.Equal(1, count)
expect := []string{policy1.ID}
require.Equal(expect, found)
}
func TestStateStore_ScalingPolicyByTargetAndType(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
// Create scaling policies
policyA := mock.ScalingPolicy()
// Same target, different type
policyB := mock.ScalingPolicy()
policyC := mock.ScalingPolicy()
for k, v := range policyB.Target {
policyC.Target[k] = v
}
policyC.Type = "other-type"
// Create the policies
var baseIndex uint64 = 1000
err := state.UpsertScalingPolicies(baseIndex, []*structs.ScalingPolicy{policyA, policyB, policyC})
require.NoError(err)
// Check if we can retrieve the right policies
found, err := state.ScalingPolicyByTargetAndType(nil, policyA.Target, policyA.Type)
require.NoError(err)
require.Equal(policyA, found)
// Check for wrong type
found, err = state.ScalingPolicyByTargetAndType(nil, policyA.Target, "wrong_type")
require.NoError(err)
require.Nil(found)
// Check for same target but different type
found, err = state.ScalingPolicyByTargetAndType(nil, policyB.Target, policyB.Type)
require.NoError(err)
require.Equal(policyB, found)
found, err = state.ScalingPolicyByTargetAndType(nil, policyB.Target, policyC.Type)
require.NoError(err)
require.Equal(policyC, found)
}
func TestStateStore_UpsertScalingEvent(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
job := mock.Job()
groupName := job.TaskGroups[0].Name
newEvent := structs.NewScalingEvent("message 1").SetMeta(map[string]interface{}{
"a": 1,
})
wsAll := memdb.NewWatchSet()
all, err := state.ScalingEvents(wsAll)
require.NoError(err)
require.Nil(all.Next())
ws := memdb.NewWatchSet()
out, _, err := state.ScalingEventsByJob(ws, job.Namespace, job.ID)
require.NoError(err)
require.Nil(out)
err = state.UpsertScalingEvent(1000, &structs.ScalingEventRequest{
Namespace: job.Namespace,
JobID: job.ID,
TaskGroup: groupName,
ScalingEvent: newEvent,
})
require.NoError(err)
require.True(watchFired(ws))
require.True(watchFired(wsAll))
ws = memdb.NewWatchSet()
out, eventsIndex, err := state.ScalingEventsByJob(ws, job.Namespace, job.ID)
require.NoError(err)
require.Equal(map[string][]*structs.ScalingEvent{
groupName: {newEvent},
}, out)
require.EqualValues(eventsIndex, 1000)
iter, err := state.ScalingEvents(ws)
require.NoError(err)
count := 0
jobsReturned := []string{}
var jobEvents *structs.JobScalingEvents
for {
raw := iter.Next()
if raw == nil {
break
}
jobEvents = raw.(*structs.JobScalingEvents)
jobsReturned = append(jobsReturned, jobEvents.JobID)
count++
}
require.Equal(1, count)
require.EqualValues(jobEvents.ModifyIndex, 1000)
require.EqualValues(jobEvents.ScalingEvents[groupName][0].CreateIndex, 1000)
index, err := state.Index("scaling_event")
require.NoError(err)
require.ElementsMatch([]string{job.ID}, jobsReturned)
require.Equal(map[string][]*structs.ScalingEvent{
groupName: {newEvent},
}, jobEvents.ScalingEvents)
require.EqualValues(1000, index)
require.False(watchFired(ws))
}
func TestStateStore_UpsertScalingEvent_LimitAndOrder(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
state := testStateStore(t)
namespace := uuid.Generate()
jobID := uuid.Generate()
group1 := uuid.Generate()
group2 := uuid.Generate()
index := uint64(1000)
for i := 1; i <= structs.JobTrackedScalingEvents+10; i++ {
newEvent := structs.NewScalingEvent("").SetMeta(map[string]interface{}{
"i": i,
"group": group1,
})
err := state.UpsertScalingEvent(index, &structs.ScalingEventRequest{
Namespace: namespace,
JobID: jobID,
TaskGroup: group1,
ScalingEvent: newEvent,
})
index++
require.NoError(err)
newEvent = structs.NewScalingEvent("").SetMeta(map[string]interface{}{
"i": i,
"group": group2,
})
err = state.UpsertScalingEvent(index, &structs.ScalingEventRequest{
Namespace: namespace,
JobID: jobID,
TaskGroup: group2,
ScalingEvent: newEvent,
})
index++
require.NoError(err)
}
out, _, err := state.ScalingEventsByJob(nil, namespace, jobID)
require.NoError(err)
require.Len(out, 2)
expectedEvents := []int{}
for i := structs.JobTrackedScalingEvents; i > 0; i-- {
expectedEvents = append(expectedEvents, i+10)
}
// checking order and content
require.Len(out[group1], structs.JobTrackedScalingEvents)
actualEvents := []int{}
for _, event := range out[group1] {
require.Equal(group1, event.Meta["group"])
actualEvents = append(actualEvents, event.Meta["i"].(int))
}
require.Equal(expectedEvents, actualEvents)
// checking order and content
require.Len(out[group2], structs.JobTrackedScalingEvents)
actualEvents = []int{}
for _, event := range out[group2] {
require.Equal(group2, event.Meta["group"])
actualEvents = append(actualEvents, event.Meta["i"].(int))
}
require.Equal(expectedEvents, actualEvents)
}
func TestStateStore_RootKeyMetaData_CRUD(t *testing.T) {
ci.Parallel(t)
store := testStateStore(t)
index, err := store.LatestIndex()
require.NoError(t, err)
// create 3 default keys, one of which is active
keyIDs := []string{}
for i := 0; i < 3; i++ {
key := structs.NewRootKeyMeta()
keyIDs = append(keyIDs, key.KeyID)
if i == 0 {
key.SetActive()
}
index++
require.NoError(t, store.UpsertRootKeyMeta(index, key, false))
}
// retrieve the active key
activeKey, err := store.GetActiveRootKeyMeta(nil)
require.NoError(t, err)
require.NotNil(t, activeKey)
// update an inactive key to active and verify the rotation
inactiveKey, err := store.RootKeyMetaByID(nil, keyIDs[1])
require.NoError(t, err)
require.NotNil(t, inactiveKey)
oldCreateIndex := inactiveKey.CreateIndex
newlyActiveKey := inactiveKey.Copy()
newlyActiveKey.SetActive()
index++
require.NoError(t, store.UpsertRootKeyMeta(index, newlyActiveKey, false))
iter, err := store.RootKeyMetas(nil)
require.NoError(t, err)
for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
if key.KeyID == newlyActiveKey.KeyID {
require.True(t, key.Active(), "expected updated key to be active")
require.Equal(t, oldCreateIndex, key.CreateIndex)
} else {
require.False(t, key.Active(), "expected other keys to be inactive")
}
}
// delete the active key and verify it's been deleted
index++
require.NoError(t, store.DeleteRootKeyMeta(index, keyIDs[1]))
iter, err = store.RootKeyMetas(nil)
require.NoError(t, err)
var found int
for {
raw := iter.Next()
if raw == nil {
break
}
key := raw.(*structs.RootKeyMeta)
require.NotEqual(t, keyIDs[1], key.KeyID)
require.False(t, key.Active(), "expected remaining keys to be inactive")
found++
}
require.Equal(t, 2, found, "expected only 2 keys remaining")
}
func TestStateStore_Abandon(t *testing.T) {
ci.Parallel(t)
s := testStateStore(t)
abandonCh := s.AbandonCh()
s.Abandon()
select {
case <-abandonCh:
default:
t.Fatalf("bad")
}
}
// Verifies that an error is returned when an allocation doesn't exist in the state store.
func TestStateSnapshot_DenormalizeAllocationDiffSlice_AllocDoesNotExist(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
alloc := mock.Alloc()
require := require.New(t)
// Insert job
err := state.UpsertJob(structs.MsgTypeTestSetup, 999, nil, alloc.Job)
require.NoError(err)
allocDiffs := []*structs.AllocationDiff{
{
ID: alloc.ID,
},
}
snap, err := state.Snapshot()
require.NoError(err)
denormalizedAllocs, err := snap.DenormalizeAllocationDiffSlice(allocDiffs)
require.EqualError(err, fmt.Sprintf("alloc %v doesn't exist", alloc.ID))
require.Nil(denormalizedAllocs)
}
// TestStateStore_SnapshotMinIndex_OK asserts StateStore.SnapshotMinIndex blocks
// until the StateStore's latest index is >= the requested index.
func TestStateStore_SnapshotMinIndex_OK(t *testing.T) {
ci.Parallel(t)
s := testStateStore(t)
index, err := s.LatestIndex()
require.NoError(t, err)
node := mock.Node()
require.NoError(t, s.UpsertNode(structs.MsgTypeTestSetup, index+1, node))
// Assert SnapshotMinIndex returns immediately if index < latest index
ctx, cancel := context.WithTimeout(context.Background(), 0)
snap, err := s.SnapshotMinIndex(ctx, index)
cancel()
require.NoError(t, err)
snapIndex, err := snap.LatestIndex()
require.NoError(t, err)
if snapIndex <= index {
require.Fail(t, "snapshot index should be greater than index")
}
// Assert SnapshotMinIndex returns immediately if index == latest index
ctx, cancel = context.WithTimeout(context.Background(), 0)
snap, err = s.SnapshotMinIndex(ctx, index+1)
cancel()
require.NoError(t, err)
snapIndex, err = snap.LatestIndex()
require.NoError(t, err)
require.Equal(t, snapIndex, index+1)
// Assert SnapshotMinIndex blocks if index > latest index
errCh := make(chan error, 1)
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
go func() {
defer close(errCh)
waitIndex := index + 2
snap, err := s.SnapshotMinIndex(ctx, waitIndex)
if err != nil {
errCh <- err
return
}
snapIndex, err := snap.LatestIndex()
if err != nil {
errCh <- err
return
}
if snapIndex < waitIndex {
errCh <- fmt.Errorf("snapshot index < wait index: %d < %d", snapIndex, waitIndex)
return
}
}()
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(500 * time.Millisecond):
// Let it block for a bit before unblocking by upserting
}
node.Name = "hal"
require.NoError(t, s.UpsertNode(structs.MsgTypeTestSetup, index+2, node))
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(5 * time.Second):
require.Fail(t, "timed out waiting for SnapshotMinIndex to unblock")
}
}
// TestStateStore_SnapshotMinIndex_Timeout asserts StateStore.SnapshotMinIndex
// returns an error if the desired index is not reached within the deadline.
func TestStateStore_SnapshotMinIndex_Timeout(t *testing.T) {
ci.Parallel(t)
s := testStateStore(t)
index, err := s.LatestIndex()
require.NoError(t, err)
// Assert SnapshotMinIndex blocks if index > latest index
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
snap, err := s.SnapshotMinIndex(ctx, index+1)
require.EqualError(t, err, context.DeadlineExceeded.Error())
require.Nil(t, snap)
}
// watchFired is a helper for unit tests that returns if the given watch set
// fired (it doesn't care which watch actually fired). This uses a fixed
// timeout since we already expect the event happened before calling this and
// just need to distinguish a fire from a timeout. We do need a little time to
// allow the watch to set up any goroutines, though.
func watchFired(ws memdb.WatchSet) bool {
timedOut := ws.Watch(time.After(50 * time.Millisecond))
return !timedOut
}
// NodeIDSort is used to sort nodes by ID
type NodeIDSort []*structs.Node
func (n NodeIDSort) Len() int {
return len(n)
}
func (n NodeIDSort) Less(i, j int) bool {
return n[i].ID < n[j].ID
}
func (n NodeIDSort) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
// JobIDis used to sort jobs by id
type JobIDSort []*structs.Job
func (n JobIDSort) Len() int {
return len(n)
}
func (n JobIDSort) Less(i, j int) bool {
return n[i].ID < n[j].ID
}
func (n JobIDSort) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
// EvalIDis used to sort evals by id
type EvalIDSort []*structs.Evaluation
func (n EvalIDSort) Len() int {
return len(n)
}
func (n EvalIDSort) Less(i, j int) bool {
return n[i].ID < n[j].ID
}
func (n EvalIDSort) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
// AllocIDsort used to sort allocations by id
type AllocIDSort []*structs.Allocation
func (n AllocIDSort) Len() int {
return len(n)
}
func (n AllocIDSort) Less(i, j int) bool {
return n[i].ID < n[j].ID
}
func (n AllocIDSort) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
// nextIndex gets the LatestIndex for this store and assumes no error
// note: this helper is not safe for concurrent use
func nextIndex(s *StateStore) uint64 {
index, _ := s.LatestIndex()
index++
return index
}