github.com/hernad/nomad@v1.6.112/nomad/state/state_store_node_pools_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package state
     5  
     6  import (
     7  	"fmt"
     8  	"testing"
     9  
    10  	memdb "github.com/hashicorp/go-memdb"
    11  	"github.com/hernad/nomad/ci"
    12  	"github.com/hernad/nomad/nomad/mock"
    13  	"github.com/hernad/nomad/nomad/structs"
    14  	"github.com/shoenig/test/must"
    15  )
    16  
    17  func TestStateStore_NodePools(t *testing.T) {
    18  	ci.Parallel(t)
    19  
    20  	// Create test node pools.
    21  	state := testStateStore(t)
    22  	pools := make([]*structs.NodePool, 10)
    23  	for i := 0; i < 10; i++ {
    24  		pools[i] = mock.NodePool()
    25  	}
    26  	must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
    27  
    28  	// Create a watchset to test that getters don't cause it to fire.
    29  	ws := memdb.NewWatchSet()
    30  	iter, err := state.NodePools(ws, SortDefault)
    31  	must.NoError(t, err)
    32  
    33  	// Verify all pools are returned.
    34  	foundBuiltIn := map[string]bool{
    35  		structs.NodePoolAll:     false,
    36  		structs.NodePoolDefault: false,
    37  	}
    38  	got := make([]*structs.NodePool, 0, 10)
    39  
    40  	for raw := iter.Next(); raw != nil; raw = iter.Next() {
    41  		pool := raw.(*structs.NodePool)
    42  
    43  		if pool.IsBuiltIn() {
    44  			must.False(t, foundBuiltIn[pool.Name])
    45  			foundBuiltIn[pool.Name] = true
    46  			continue
    47  		}
    48  
    49  		got = append(got, pool)
    50  	}
    51  
    52  	must.SliceContainsAll(t, got, pools)
    53  	must.False(t, watchFired(ws))
    54  	for k, v := range foundBuiltIn {
    55  		must.True(t, v, must.Sprintf("built-in pool %q not found", k))
    56  	}
    57  }
    58  
    59  func TestStateStore_NodePools_Ordering(t *testing.T) {
    60  	ci.Parallel(t)
    61  
    62  	// Create test node pools with stable sortable names.
    63  	state := testStateStore(t)
    64  	pools := make([]*structs.NodePool, 10)
    65  	for i := 0; i < 5; i++ {
    66  		pool := mock.NodePool()
    67  		pool.Name = fmt.Sprintf("%02d", i+1)
    68  		pools[i] = pool
    69  	}
    70  	must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
    71  
    72  	testCases := []struct {
    73  		name     string
    74  		order    SortOption
    75  		expected []string
    76  	}{
    77  		{
    78  			name:     "default order",
    79  			order:    SortDefault,
    80  			expected: []string{"01", "02", "03", "04", "05"},
    81  		},
    82  		{
    83  			name:     "reverse order",
    84  			order:    SortReverse,
    85  			expected: []string{"05", "04", "03", "02", "01"},
    86  		},
    87  	}
    88  
    89  	for _, tc := range testCases {
    90  		t.Run(tc.name, func(t *testing.T) {
    91  			ws := memdb.NewWatchSet()
    92  			iter, err := state.NodePools(ws, tc.order)
    93  			must.NoError(t, err)
    94  
    95  			var got []string
    96  			for raw := iter.Next(); raw != nil; raw = iter.Next() {
    97  				pool := raw.(*structs.NodePool)
    98  				if pool.IsBuiltIn() {
    99  					continue
   100  				}
   101  
   102  				got = append(got, pool.Name)
   103  			}
   104  
   105  			must.Eq(t, got, tc.expected)
   106  		})
   107  	}
   108  }
   109  
   110  func TestStateStore_NodePool_ByName(t *testing.T) {
   111  	ci.Parallel(t)
   112  
   113  	// Create test node pools.
   114  	state := testStateStore(t)
   115  	pools := make([]*structs.NodePool, 10)
   116  	for i := 0; i < 10; i++ {
   117  		pools[i] = mock.NodePool()
   118  	}
   119  	must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
   120  
   121  	testCases := []struct {
   122  		name     string
   123  		pool     string
   124  		expected *structs.NodePool
   125  	}{
   126  		{
   127  			name:     "find a pool",
   128  			pool:     pools[3].Name,
   129  			expected: pools[3],
   130  		},
   131  		{
   132  			name: "find built-in pool all",
   133  			pool: structs.NodePoolAll,
   134  			expected: &structs.NodePool{
   135  				Name:        structs.NodePoolAll,
   136  				Description: structs.NodePoolAllDescription,
   137  				CreateIndex: 1,
   138  				ModifyIndex: 1,
   139  			},
   140  		},
   141  		{
   142  			name: "find built-in pool default",
   143  			pool: structs.NodePoolDefault,
   144  			expected: &structs.NodePool{
   145  				Name:        structs.NodePoolDefault,
   146  				Description: structs.NodePoolDefaultDescription,
   147  				CreateIndex: 1,
   148  				ModifyIndex: 1,
   149  			},
   150  		},
   151  		{
   152  			name:     "pool not found",
   153  			pool:     "no-pool",
   154  			expected: nil,
   155  		},
   156  		{
   157  			name:     "must be exact match",
   158  			pool:     pools[2].Name[:4],
   159  			expected: nil,
   160  		},
   161  		{
   162  			name:     "empty search",
   163  			pool:     "",
   164  			expected: nil,
   165  		},
   166  	}
   167  
   168  	for _, tc := range testCases {
   169  		t.Run(tc.name, func(t *testing.T) {
   170  			ws := memdb.NewWatchSet()
   171  			got, err := state.NodePoolByName(ws, tc.pool)
   172  
   173  			must.NoError(t, err)
   174  			must.Eq(t, tc.expected, got)
   175  			must.False(t, watchFired(ws))
   176  		})
   177  	}
   178  }
   179  
   180  func TestStateStore_NodePool_ByNamePrefix(t *testing.T) {
   181  	ci.Parallel(t)
   182  
   183  	// Create test node pools.
   184  	state := testStateStore(t)
   185  	existingPools := []*structs.NodePool{
   186  		{Name: "prod-1"},
   187  		{Name: "prod-2"},
   188  		{Name: "prod-3"},
   189  		{Name: "dev-1"},
   190  		{Name: "dev-2"},
   191  		{Name: "qa"},
   192  	}
   193  	err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools)
   194  	must.NoError(t, err)
   195  
   196  	testCases := []struct {
   197  		name     string
   198  		prefix   string
   199  		expected []string
   200  		order    SortOption
   201  	}{
   202  		{
   203  			name:     "multiple prefix match",
   204  			prefix:   "prod",
   205  			order:    SortDefault,
   206  			expected: []string{"prod-1", "prod-2", "prod-3"},
   207  		},
   208  		{
   209  			name:     "single prefix match",
   210  			prefix:   "qa",
   211  			order:    SortDefault,
   212  			expected: []string{"qa"},
   213  		},
   214  		{
   215  			name:     "no match",
   216  			prefix:   "nope",
   217  			order:    SortDefault,
   218  			expected: []string{},
   219  		},
   220  		{
   221  			name:   "empty prefix",
   222  			prefix: "",
   223  			order:  SortDefault,
   224  			expected: []string{
   225  				"all",
   226  				"default",
   227  				"prod-1",
   228  				"prod-2",
   229  				"prod-3",
   230  				"dev-1",
   231  				"dev-2",
   232  				"qa",
   233  			},
   234  		},
   235  		{
   236  			name:     "reverse order",
   237  			prefix:   "prod",
   238  			order:    SortReverse,
   239  			expected: []string{"prod-3", "prod-2", "prod-1"},
   240  		},
   241  	}
   242  
   243  	for _, tc := range testCases {
   244  		t.Run(tc.name, func(t *testing.T) {
   245  			ws := memdb.NewWatchSet()
   246  			iter, err := state.NodePoolsByNamePrefix(ws, tc.prefix, tc.order)
   247  			must.NoError(t, err)
   248  
   249  			got := []string{}
   250  			for raw := iter.Next(); raw != nil; raw = iter.Next() {
   251  				got = append(got, raw.(*structs.NodePool).Name)
   252  			}
   253  			must.SliceContainsAll(t, tc.expected, got)
   254  		})
   255  	}
   256  }
   257  
   258  func TestStateStore_NodePool_Upsert(t *testing.T) {
   259  	ci.Parallel(t)
   260  
   261  	existingPools := make([]*structs.NodePool, 10)
   262  	for i := 0; i < 10; i++ {
   263  		existingPools[i] = mock.NodePool()
   264  	}
   265  
   266  	testCases := []struct {
   267  		name        string
   268  		input       []*structs.NodePool
   269  		expectedErr string
   270  	}{
   271  		{
   272  			name: "add single pool",
   273  			input: []*structs.NodePool{
   274  				mock.NodePool(),
   275  			},
   276  		},
   277  		{
   278  			name: "add multiple pools",
   279  			input: []*structs.NodePool{
   280  				mock.NodePool(),
   281  				mock.NodePool(),
   282  				mock.NodePool(),
   283  			},
   284  		},
   285  		{
   286  			name: "update existing pools",
   287  			input: []*structs.NodePool{
   288  				{
   289  					Name:        existingPools[0].Name,
   290  					Description: "updated",
   291  					Meta: map[string]string{
   292  						"updated": "true",
   293  					},
   294  					SchedulerConfiguration: &structs.NodePoolSchedulerConfiguration{
   295  						SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack,
   296  					},
   297  				},
   298  				{
   299  					Name:        existingPools[1].Name,
   300  					Description: "use global scheduler config",
   301  				},
   302  			},
   303  		},
   304  		{
   305  			name: "update with nil",
   306  			input: []*structs.NodePool{
   307  				nil,
   308  			},
   309  		},
   310  		{
   311  			name: "empty name",
   312  			input: []*structs.NodePool{
   313  				{
   314  					Name: "",
   315  				},
   316  			},
   317  			expectedErr: "missing primary index",
   318  		},
   319  		{
   320  			name: "update bulit-in pool all",
   321  			input: []*structs.NodePool{
   322  				{
   323  					Name:        structs.NodePoolAll,
   324  					Description: "changed",
   325  				},
   326  			},
   327  			expectedErr: "not allowed",
   328  		},
   329  		{
   330  			name: "update built-in pool default",
   331  			input: []*structs.NodePool{
   332  				{
   333  					Name:        structs.NodePoolDefault,
   334  					Description: "changed",
   335  				},
   336  			},
   337  			expectedErr: "not allowed",
   338  		},
   339  	}
   340  
   341  	for _, tc := range testCases {
   342  		t.Run(tc.name, func(t *testing.T) {
   343  			// Create test pools.
   344  			state := testStateStore(t)
   345  			must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, existingPools))
   346  
   347  			// Update pools from test case.
   348  			err := state.UpsertNodePools(structs.MsgTypeTestSetup, 1001, tc.input)
   349  
   350  			if tc.expectedErr != "" {
   351  				must.ErrorContains(t, err, tc.expectedErr)
   352  			} else {
   353  				must.NoError(t, err)
   354  
   355  				ws := memdb.NewWatchSet()
   356  				for _, pool := range tc.input {
   357  					if pool == nil {
   358  						continue
   359  					}
   360  
   361  					got, err := state.NodePoolByName(ws, pool.Name)
   362  					must.NoError(t, err)
   363  					must.Eq(t, pool, got)
   364  				}
   365  			}
   366  		})
   367  	}
   368  }
   369  
   370  func TestStateStore_NodePool_Delete(t *testing.T) {
   371  	ci.Parallel(t)
   372  
   373  	pools := make([]*structs.NodePool, 10)
   374  	for i := 0; i < 10; i++ {
   375  		pools[i] = mock.NodePool()
   376  	}
   377  
   378  	testCases := []struct {
   379  		name        string
   380  		del         []string
   381  		expectedErr string
   382  	}{
   383  		{
   384  			name: "delete one",
   385  			del:  []string{pools[0].Name},
   386  		},
   387  		{
   388  			name: "delete multiple",
   389  			del:  []string{pools[0].Name, pools[3].Name},
   390  		},
   391  		{
   392  			name:        "delete non-existing",
   393  			del:         []string{"nope"},
   394  			expectedErr: "not found",
   395  		},
   396  		{
   397  			name:        "delete is atomic",
   398  			del:         []string{pools[0].Name, "nope"},
   399  			expectedErr: "not found",
   400  		},
   401  		{
   402  			name:        "delete built-in pool all",
   403  			del:         []string{structs.NodePoolAll},
   404  			expectedErr: "not allowed",
   405  		},
   406  		{
   407  			name:        "delete built-in pool default",
   408  			del:         []string{structs.NodePoolDefault},
   409  			expectedErr: "not allowed",
   410  		},
   411  	}
   412  
   413  	for _, tc := range testCases {
   414  		t.Run(tc.name, func(t *testing.T) {
   415  			state := testStateStore(t)
   416  			must.NoError(t, state.UpsertNodePools(structs.MsgTypeTestSetup, 1000, pools))
   417  
   418  			err := state.DeleteNodePools(structs.MsgTypeTestSetup, 1001, tc.del)
   419  			if tc.expectedErr != "" {
   420  				must.ErrorContains(t, err, tc.expectedErr)
   421  
   422  				// Make sure delete is atomic and nothing is removed if an
   423  				// error happens.
   424  				for _, p := range pools {
   425  					got, err := state.NodePoolByName(nil, p.Name)
   426  					must.NoError(t, err)
   427  					must.Eq(t, p, got)
   428  				}
   429  			} else {
   430  				must.NoError(t, err)
   431  
   432  				// Check that the node pools is deleted.
   433  				for _, p := range tc.del {
   434  					got, err := state.NodePoolByName(nil, p)
   435  					must.NoError(t, err)
   436  					must.Nil(t, got)
   437  				}
   438  			}
   439  		})
   440  	}
   441  }
   442  
   443  func TestStateStore_NodePool_Restore(t *testing.T) {
   444  	ci.Parallel(t)
   445  
   446  	state := testStateStore(t)
   447  	pool := mock.NodePool()
   448  
   449  	restore, err := state.Restore()
   450  	must.NoError(t, err)
   451  
   452  	err = restore.NodePoolRestore(pool)
   453  	must.NoError(t, err)
   454  
   455  	restore.Commit()
   456  
   457  	ws := memdb.NewWatchSet()
   458  	out, err := state.NodePoolByName(ws, pool.Name)
   459  	must.NoError(t, err)
   460  	must.Eq(t, out, pool)
   461  }