open-nomad/nomad/state/state_store_node_pools_test.go

462 lines
9.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package state
import (
"fmt"
"testing"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/shoenig/test/must"
)
func TestStateStore_NodePools(t *testing.T) {
ci.Parallel(t)
// Create test node pools.
state := testStateStore(t)
pools := make([]*structs.NodePool, 10)
for i := 0; i < 10; i++ {
pools[i] = mock.NodePool()
}
must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
// Create a watchset to test that getters don't cause it to fire.
ws := memdb.NewWatchSet()
iter, err := state.NodePools(ws, SortDefault)
must.NoError(t, err)
// Verify all pools are returned.
foundBuiltIn := map[string]bool{
structs.NodePoolAll: false,
structs.NodePoolDefault: false,
}
got := make([]*structs.NodePool, 0, 10)
for raw := iter.Next(); raw != nil; raw = iter.Next() {
pool := raw.(*structs.NodePool)
if pool.IsBuiltIn() {
must.False(t, foundBuiltIn[pool.Name])
foundBuiltIn[pool.Name] = true
continue
}
got = append(got, pool)
}
must.SliceContainsAll(t, got, pools)
must.False(t, watchFired(ws))
for k, v := range foundBuiltIn {
must.True(t, v, must.Sprintf("built-in pool %q not found", k))
}
}
func TestStateStore_NodePools_Ordering(t *testing.T) {
ci.Parallel(t)
// Create test node pools with stable sortable names.
state := testStateStore(t)
pools := make([]*structs.NodePool, 10)
for i := 0; i < 5; i++ {
pool := mock.NodePool()
pool.Name = fmt.Sprintf("%02d", i+1)
pools[i] = pool
}
must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
testCases := []struct {
name string
order SortOption
expected []string
}{
{
name: "default order",
order: SortDefault,
expected: []string{"01", "02", "03", "04", "05"},
},
{
name: "reverse order",
order: SortReverse,
expected: []string{"05", "04", "03", "02", "01"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.NodePools(ws, tc.order)
must.NoError(t, err)
var got []string
for raw := iter.Next(); raw != nil; raw = iter.Next() {
pool := raw.(*structs.NodePool)
if pool.IsBuiltIn() {
continue
}
got = append(got, pool.Name)
}
must.Eq(t, got, tc.expected)
})
}
}
func TestStateStore_NodePool_ByName(t *testing.T) {
ci.Parallel(t)
// Create test node pools.
state := testStateStore(t)
pools := make([]*structs.NodePool, 10)
for i := 0; i < 10; i++ {
pools[i] = mock.NodePool()
}
must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
testCases := []struct {
name string
pool string
expected *structs.NodePool
}{
{
name: "find a pool",
pool: pools[3].Name,
expected: pools[3],
},
{
name: "find built-in pool all",
pool: structs.NodePoolAll,
expected: &structs.NodePool{
Name: structs.NodePoolAll,
Description: structs.NodePoolAllDescription,
CreateIndex: 1,
ModifyIndex: 1,
},
},
{
name: "find built-in pool default",
pool: structs.NodePoolDefault,
expected: &structs.NodePool{
Name: structs.NodePoolDefault,
Description: structs.NodePoolDefaultDescription,
CreateIndex: 1,
ModifyIndex: 1,
},
},
{
name: "pool not found",
pool: "no-pool",
expected: nil,
},
{
name: "must be exact match",
pool: pools[2].Name[:4],
expected: nil,
},
{
name: "empty search",
pool: "",
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ws := memdb.NewWatchSet()
got, err := state.NodePoolByName(ws, tc.pool)
must.NoError(t, err)
must.Eq(t, tc.expected, got)
must.False(t, watchFired(ws))
})
}
}
func TestStateStore_NodePool_ByNamePrefix(t *testing.T) {
ci.Parallel(t)
// Create test node pools.
state := testStateStore(t)
existingPools := []*structs.NodePool{
{Name: "prod-1"},
{Name: "prod-2"},
{Name: "prod-3"},
{Name: "dev-1"},
{Name: "dev-2"},
{Name: "qa"},
}
err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools)
must.NoError(t, err)
testCases := []struct {
name string
prefix string
expected []string
order SortOption
}{
{
name: "multiple prefix match",
prefix: "prod",
order: SortDefault,
expected: []string{"prod-1", "prod-2", "prod-3"},
},
{
name: "single prefix match",
prefix: "qa",
order: SortDefault,
expected: []string{"qa"},
},
{
name: "no match",
prefix: "nope",
order: SortDefault,
expected: []string{},
},
{
name: "empty prefix",
prefix: "",
order: SortDefault,
expected: []string{
"all",
"default",
"prod-1",
"prod-2",
"prod-3",
"dev-1",
"dev-2",
"qa",
},
},
{
name: "reverse order",
prefix: "prod",
order: SortReverse,
expected: []string{"prod-3", "prod-2", "prod-1"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.NodePoolsByNamePrefix(ws, tc.prefix, tc.order)
must.NoError(t, err)
got := []string{}
for raw := iter.Next(); raw != nil; raw = iter.Next() {
got = append(got, raw.(*structs.NodePool).Name)
}
must.SliceContainsAll(t, tc.expected, got)
})
}
}
func TestStateStore_NodePool_Upsert(t *testing.T) {
ci.Parallel(t)
existingPools := make([]*structs.NodePool, 10)
for i := 0; i < 10; i++ {
existingPools[i] = mock.NodePool()
}
testCases := []struct {
name string
input []*structs.NodePool
expectedErr string
}{
{
name: "add single pool",
input: []*structs.NodePool{
mock.NodePool(),
},
},
{
name: "add multiple pools",
input: []*structs.NodePool{
mock.NodePool(),
mock.NodePool(),
mock.NodePool(),
},
},
{
name: "update existing pools",
input: []*structs.NodePool{
{
Name: existingPools[0].Name,
Description: "updated",
Meta: map[string]string{
"updated": "true",
},
SchedulerConfiguration: &structs.NodePoolSchedulerConfiguration{
SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack,
},
},
{
Name: existingPools[1].Name,
Description: "use global scheduler config",
},
},
},
{
name: "update with nil",
input: []*structs.NodePool{
nil,
},
},
{
name: "empty name",
input: []*structs.NodePool{
{
Name: "",
},
},
expectedErr: "missing primary index",
},
{
name: "update bulit-in pool all",
input: []*structs.NodePool{
{
Name: structs.NodePoolAll,
Description: "changed",
},
},
expectedErr: "not allowed",
},
{
name: "update built-in pool default",
input: []*structs.NodePool{
{
Name: structs.NodePoolDefault,
Description: "changed",
},
},
expectedErr: "not allowed",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create test pools.
state := testStateStore(t)
must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools))
// Update pools from test case.
err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1001, tc.input)
if tc.expectedErr != "" {
must.ErrorContains(t, err, tc.expectedErr)
} else {
must.NoError(t, err)
ws := memdb.NewWatchSet()
for _, pool := range tc.input {
if pool == nil {
continue
}
got, err := state.NodePoolByName(ws, pool.Name)
must.NoError(t, err)
must.Eq(t, pool, got)
}
}
})
}
}
func TestStateStore_NodePool_Delete(t *testing.T) {
ci.Parallel(t)
pools := make([]*structs.NodePool, 10)
for i := 0; i < 10; i++ {
pools[i] = mock.NodePool()
}
testCases := []struct {
name string
del []string
expectedErr string
}{
{
name: "delete one",
del: []string{pools[0].Name},
},
{
name: "delete multiple",
del: []string{pools[0].Name, pools[3].Name},
},
{
name: "delete non-existing",
del: []string{"nope"},
expectedErr: "not found",
},
{
name: "delete is atomic",
del: []string{pools[0].Name, "nope"},
expectedErr: "not found",
},
{
name: "delete built-in pool all",
del: []string{structs.NodePoolAll},
expectedErr: "not allowed",
},
{
name: "delete built-in pool default",
del: []string{structs.NodePoolDefault},
expectedErr: "not allowed",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
state := testStateStore(t)
must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
err := state.DeleteNodePools(structs.MsgTypeTestSetup, 1001, tc.del)
if tc.expectedErr != "" {
must.ErrorContains(t, err, tc.expectedErr)
// Make sure delete is atomic and nothing is removed if an
// error happens.
for _, p := range pools {
got, err := state.NodePoolByName(nil, p.Name)
must.NoError(t, err)
must.Eq(t, p, got)
}
} else {
must.NoError(t, err)
// Check that the node pools is deleted.
for _, p := range tc.del {
got, err := state.NodePoolByName(nil, p)
must.NoError(t, err)
must.Nil(t, got)
}
}
})
}
}
func TestStateStore_NodePool_Restore(t *testing.T) {
ci.Parallel(t)
state := testStateStore(t)
pool := mock.NodePool()
restore, err := state.Restore()
must.NoError(t, err)
err = restore.NodePoolRestore(pool)
must.NoError(t, err)
restore.Commit()
ws := memdb.NewWatchSet()
out, err := state.NodePoolByName(ws, pool.Name)
must.NoError(t, err)
must.Eq(t, out, pool)
}