open-nomad/nomad/state/state_store_node_pools_test.go
Luiz Aoqui 389212bfda
node pool: initial base work (#17163)
Implementation of the base work for the new node pools feature. It includes a new `NodePool` struct and its corresponding state store table.

Upon start the state store is populated with two built-in node pools that cannot be modified nor deleted:

  * `all` is a node pool that always includes all nodes in the cluster.
  * `default` is the node pool where nodes that don't specify a node pool in their configuration are placed.
2023-05-15 10:49:08 -04:00

399 lines
8.3 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package state
import (
"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)
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_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
}{
{
name: "multiple prefix match",
prefix: "prod",
expected: []string{"prod-1", "prod-2", "prod-3"},
},
{
name: "single prefix match",
prefix: "qa",
expected: []string{"qa"},
},
{
name: "no match",
prefix: "nope",
expected: []string{},
},
{
name: "empty prefix",
prefix: "",
expected: []string{
"all",
"default",
"prod-1",
"prod-2",
"prod-3",
"dev-1",
"dev-2",
"qa",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ws := memdb.NewWatchSet()
iter, err := state.NodePoolsByNamePrefix(ws, tc.prefix)
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)
}