github.com/hernad/nomad@v1.6.112/command/agent/node_pool_endpoint_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package agent
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strconv"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/hernad/nomad/ci"
    15  	"github.com/hernad/nomad/nomad/mock"
    16  	"github.com/hernad/nomad/nomad/structs"
    17  	"github.com/shoenig/test/must"
    18  )
    19  
    20  func TestHTTP_NodePool_List(t *testing.T) {
    21  	ci.Parallel(t)
    22  	httpTest(t, nil, func(s *TestAgent) {
    23  		// Populate state with test data.
    24  		pool1 := mock.NodePool()
    25  		pool2 := mock.NodePool()
    26  		pool3 := mock.NodePool()
    27  		args := structs.NodePoolUpsertRequest{
    28  			NodePools: []*structs.NodePool{pool1, pool2, pool3},
    29  		}
    30  		var resp structs.GenericResponse
    31  		err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
    32  		must.NoError(t, err)
    33  
    34  		// Make HTTP request.
    35  		req, err := http.NewRequest("GET", "/v1/node/pools", nil)
    36  		must.NoError(t, err)
    37  		respW := httptest.NewRecorder()
    38  
    39  		obj, err := s.Server.NodePoolsRequest(respW, req)
    40  		must.NoError(t, err)
    41  
    42  		// Expect 5 node pools: 3 created + 2 built-in.
    43  		must.SliceLen(t, 5, obj.([]*structs.NodePool))
    44  
    45  		// Verify response index.
    46  		gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
    47  		must.NoError(t, err)
    48  		must.NonZero(t, gotIndex)
    49  	})
    50  }
    51  
    52  func TestHTTP_NodePool_Info(t *testing.T) {
    53  	ci.Parallel(t)
    54  	httpTest(t, nil, func(s *TestAgent) {
    55  		// Populate state with test data.
    56  		pool := mock.NodePool()
    57  		args := structs.NodePoolUpsertRequest{
    58  			NodePools: []*structs.NodePool{pool},
    59  		}
    60  		var resp structs.GenericResponse
    61  		err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
    62  		must.NoError(t, err)
    63  
    64  		t.Run("test pool", func(t *testing.T) {
    65  			// Make HTTP request for test pool.
    66  			req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil)
    67  			must.NoError(t, err)
    68  			respW := httptest.NewRecorder()
    69  
    70  			obj, err := s.Server.NodePoolSpecificRequest(respW, req)
    71  			must.NoError(t, err)
    72  
    73  			// Verify expected pool is returned.
    74  			must.Eq(t, pool, obj.(*structs.NodePool), must.Cmp(cmpopts.IgnoreFields(
    75  				structs.NodePool{},
    76  				"Hash",
    77  				"CreateIndex",
    78  				"ModifyIndex",
    79  			)))
    80  
    81  			// Verify response index.
    82  			gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
    83  			must.NoError(t, err)
    84  			must.NonZero(t, gotIndex)
    85  		})
    86  
    87  		t.Run("built-in pool", func(t *testing.T) {
    88  			// Make HTTP request for built-in pool.
    89  			req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", structs.NodePoolAll), nil)
    90  			must.NoError(t, err)
    91  			respW := httptest.NewRecorder()
    92  
    93  			obj, err := s.Server.NodePoolSpecificRequest(respW, req)
    94  			must.NoError(t, err)
    95  
    96  			// Verify expected pool is returned.
    97  			must.Eq(t, structs.NodePoolAll, obj.(*structs.NodePool).Name)
    98  
    99  			// Verify response index.
   100  			gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   101  			must.NoError(t, err)
   102  			must.NonZero(t, gotIndex)
   103  		})
   104  
   105  		t.Run("invalid pool", func(t *testing.T) {
   106  			// Make HTTP request for built-in pool.
   107  			req, err := http.NewRequest("GET", "/v1/node/pool/doesn-exist", nil)
   108  			must.NoError(t, err)
   109  			respW := httptest.NewRecorder()
   110  
   111  			// Verify error.
   112  			_, err = s.Server.NodePoolSpecificRequest(respW, req)
   113  			must.ErrorContains(t, err, "not found")
   114  
   115  			// Verify response index.
   116  			gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   117  			must.NoError(t, err)
   118  			must.NonZero(t, gotIndex)
   119  		})
   120  	})
   121  }
   122  
   123  func TestHTTP_NodePool_Create(t *testing.T) {
   124  	ci.Parallel(t)
   125  	httpTest(t, nil, func(s *TestAgent) {
   126  		// Create test node pool.
   127  		pool := mock.NodePool()
   128  		buf := encodeReq(pool)
   129  		req, err := http.NewRequest("PUT", "/v1/node/pools", buf)
   130  		must.NoError(t, err)
   131  
   132  		respW := httptest.NewRecorder()
   133  		obj, err := s.Server.NodePoolsRequest(respW, req)
   134  		must.NoError(t, err)
   135  		must.Nil(t, obj)
   136  
   137  		// Verify response index.
   138  		gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   139  		must.NoError(t, err)
   140  		must.NonZero(t, gotIndex)
   141  
   142  		// Verify test node pool is in state.
   143  		got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
   144  		must.NoError(t, err)
   145  		must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields(
   146  			structs.NodePool{},
   147  			"Hash",
   148  			"CreateIndex",
   149  			"ModifyIndex",
   150  		)))
   151  		must.Eq(t, gotIndex, got.CreateIndex)
   152  		must.Eq(t, gotIndex, got.ModifyIndex)
   153  	})
   154  }
   155  
   156  func TestHTTP_NodePool_Update(t *testing.T) {
   157  	ci.Parallel(t)
   158  	httpTest(t, nil, func(s *TestAgent) {
   159  		t.Run("success", func(t *testing.T) {
   160  			// Populate state with test node pool.
   161  			pool := mock.NodePool()
   162  			args := structs.NodePoolUpsertRequest{
   163  				NodePools: []*structs.NodePool{pool},
   164  			}
   165  			var resp structs.GenericResponse
   166  			err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
   167  			must.NoError(t, err)
   168  
   169  			// Update node pool.
   170  			updated := pool.Copy()
   171  			updated.Description = "updated node pool"
   172  			updated.Meta = map[string]string{
   173  				"updated": "true",
   174  			}
   175  
   176  			buf := encodeReq(updated)
   177  			req, err := http.NewRequest("PUT", fmt.Sprintf("/v1/node/pool/%s", updated.Name), buf)
   178  			must.NoError(t, err)
   179  
   180  			respW := httptest.NewRecorder()
   181  			obj, err := s.Server.NodePoolsRequest(respW, req)
   182  			must.NoError(t, err)
   183  			must.Nil(t, obj)
   184  
   185  			// Verify response index.
   186  			gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   187  			must.NoError(t, err)
   188  			must.NonZero(t, gotIndex)
   189  
   190  			// Verify node pool was updated.
   191  			got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
   192  			must.NoError(t, err)
   193  			must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields(
   194  				structs.NodePool{},
   195  				"Hash",
   196  				"CreateIndex",
   197  				"ModifyIndex",
   198  			)))
   199  			must.NotEq(t, gotIndex, got.CreateIndex)
   200  			must.Eq(t, gotIndex, got.ModifyIndex)
   201  		})
   202  
   203  		t.Run("no name in path", func(t *testing.T) {
   204  			// Populate state with test node pool.
   205  			pool := mock.NodePool()
   206  			args := structs.NodePoolUpsertRequest{
   207  				NodePools: []*structs.NodePool{pool},
   208  			}
   209  			var resp structs.GenericResponse
   210  			err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
   211  			must.NoError(t, err)
   212  
   213  			// Update node pool with no name in path.
   214  			updated := pool.Copy()
   215  			updated.Description = "updated node pool"
   216  			updated.Meta = map[string]string{
   217  				"updated": "true",
   218  			}
   219  
   220  			buf := encodeReq(updated)
   221  			req, err := http.NewRequest("PUT", "/v1/node/pool/", buf)
   222  			must.NoError(t, err)
   223  
   224  			respW := httptest.NewRecorder()
   225  			obj, err := s.Server.NodePoolsRequest(respW, req)
   226  			must.NoError(t, err)
   227  			must.Nil(t, obj)
   228  
   229  			// Verify response index.
   230  			gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   231  			must.NoError(t, err)
   232  			must.NonZero(t, gotIndex)
   233  
   234  			// Verify node pool was updated.
   235  			got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
   236  			must.NoError(t, err)
   237  			must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields(
   238  				structs.NodePool{},
   239  				"Hash",
   240  				"CreateIndex",
   241  				"ModifyIndex",
   242  			)))
   243  		})
   244  
   245  		t.Run("wrong name in path", func(t *testing.T) {
   246  			// Populate state with test node pool.
   247  			pool := mock.NodePool()
   248  			args := structs.NodePoolUpsertRequest{
   249  				NodePools: []*structs.NodePool{pool},
   250  			}
   251  			var resp structs.GenericResponse
   252  			err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
   253  			must.NoError(t, err)
   254  
   255  			// Update node pool.
   256  			updated := pool.Copy()
   257  			updated.Description = "updated node pool"
   258  			updated.Meta = map[string]string{
   259  				"updated": "true",
   260  			}
   261  
   262  			// Make request with the wrong path.
   263  			buf := encodeReq(updated)
   264  			req, err := http.NewRequest("PUT", "/v1/node/pool/wrong", buf)
   265  			must.NoError(t, err)
   266  
   267  			respW := httptest.NewRecorder()
   268  			_, err = s.Server.NodePoolSpecificRequest(respW, req)
   269  			must.ErrorContains(t, err, "name does not match request path")
   270  
   271  			// Verify node pool was NOT updated.
   272  			got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
   273  			must.NoError(t, err)
   274  			must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields(
   275  				structs.NodePool{},
   276  				"Hash",
   277  				"CreateIndex",
   278  				"ModifyIndex",
   279  			)))
   280  		})
   281  	})
   282  }
   283  
   284  func TestHTTP_NodePool_Delete(t *testing.T) {
   285  	ci.Parallel(t)
   286  	httpTest(t, nil, func(s *TestAgent) {
   287  		// Populate state with test node pool.
   288  		pool := mock.NodePool()
   289  		args := structs.NodePoolUpsertRequest{
   290  			NodePools: []*structs.NodePool{pool},
   291  		}
   292  		var resp structs.GenericResponse
   293  		err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
   294  		must.NoError(t, err)
   295  
   296  		// Delete test node pool.
   297  		req, err := http.NewRequest("DELETE", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil)
   298  		must.NoError(t, err)
   299  
   300  		respW := httptest.NewRecorder()
   301  		obj, err := s.Server.NodePoolSpecificRequest(respW, req)
   302  		must.NoError(t, err)
   303  		must.Nil(t, obj)
   304  
   305  		// Verify node pool was deleted.
   306  		got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
   307  		must.NoError(t, err)
   308  		must.Nil(t, got)
   309  	})
   310  }
   311  
   312  func TestHTTP_NodePool_NodesList(t *testing.T) {
   313  	ci.Parallel(t)
   314  	httpTest(t,
   315  		func(c *Config) {
   316  			// Disable client so it doesn't impact tests since we're registering
   317  			// our own test nodes.
   318  			c.Client.Enabled = false
   319  		},
   320  		func(s *TestAgent) {
   321  			// Populate state with test data.
   322  			pool1 := mock.NodePool()
   323  			pool2 := mock.NodePool()
   324  			args := structs.NodePoolUpsertRequest{
   325  				NodePools: []*structs.NodePool{pool1, pool2},
   326  			}
   327  			var resp structs.GenericResponse
   328  			err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
   329  			must.NoError(t, err)
   330  
   331  			// Split test nodes between default, pool1, and pool2.
   332  			nodesByPool := make(map[string][]*structs.Node)
   333  			for i := 0; i < 10; i++ {
   334  				node := mock.Node()
   335  				switch i % 3 {
   336  				case 0:
   337  					// Leave node pool value empty so node goes to default.
   338  				case 1:
   339  					node.NodePool = pool1.Name
   340  				case 2:
   341  					node.NodePool = pool2.Name
   342  				}
   343  				nodeRegReq := structs.NodeRegisterRequest{
   344  					Node: node,
   345  					WriteRequest: structs.WriteRequest{
   346  						Region: "global",
   347  					},
   348  				}
   349  				var nodeRegResp structs.NodeUpdateResponse
   350  				err := s.Agent.RPC("Node.Register", &nodeRegReq, &nodeRegResp)
   351  				must.NoError(t, err)
   352  
   353  				nodesByPool[node.NodePool] = append(nodesByPool[node.NodePool], node)
   354  			}
   355  
   356  			testCases := []struct {
   357  				name          string
   358  				pool          string
   359  				args          string
   360  				expectedNodes []*structs.Node
   361  				expectedErr   string
   362  				validateFn    func(*testing.T, []*structs.NodeListStub)
   363  			}{
   364  				{
   365  					name:          "nodes in default",
   366  					pool:          structs.NodePoolDefault,
   367  					expectedNodes: nodesByPool[structs.NodePoolDefault],
   368  					validateFn: func(t *testing.T, stubs []*structs.NodeListStub) {
   369  						must.Nil(t, stubs[0].NodeResources)
   370  					},
   371  				},
   372  				{
   373  					name:          "nodes in pool1 with resources",
   374  					pool:          pool1.Name,
   375  					args:          "resources=true",
   376  					expectedNodes: nodesByPool[pool1.Name],
   377  					validateFn: func(t *testing.T, stubs []*structs.NodeListStub) {
   378  						must.NotNil(t, stubs[0].NodeResources)
   379  					},
   380  				},
   381  			}
   382  			for _, tc := range testCases {
   383  				t.Run(tc.name, func(t *testing.T) {
   384  					// Make HTTP request.
   385  					path := fmt.Sprintf("/v1/node/pool/%s/nodes?%s", tc.pool, tc.args)
   386  					req, err := http.NewRequest("GET", path, nil)
   387  					must.NoError(t, err)
   388  					respW := httptest.NewRecorder()
   389  
   390  					obj, err := s.Server.NodePoolSpecificRequest(respW, req)
   391  					if tc.expectedErr != "" {
   392  						must.ErrorContains(t, err, tc.expectedErr)
   393  						return
   394  					}
   395  					must.NoError(t, err)
   396  
   397  					// Verify request only has expected nodes.
   398  					stubs := obj.([]*structs.NodeListStub)
   399  					must.Len(t, len(tc.expectedNodes), stubs)
   400  					for _, node := range tc.expectedNodes {
   401  						must.SliceContainsFunc(t, stubs, node, func(s *structs.NodeListStub, n *structs.Node) bool {
   402  							return s.ID == n.ID
   403  						})
   404  					}
   405  
   406  					// Verify respose.
   407  					if tc.validateFn != nil {
   408  						tc.validateFn(t, stubs)
   409  					}
   410  
   411  					// Verify response index.
   412  					gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   413  					must.NoError(t, err)
   414  					must.NonZero(t, gotIndex)
   415  				})
   416  			}
   417  		})
   418  }
   419  
   420  func TestHTTP_NodePool_JobsList(t *testing.T) {
   421  	ci.Parallel(t)
   422  	httpTest(t, nil, func(s *TestAgent) {
   423  
   424  		pool1, pool2 := mock.NodePool(), mock.NodePool()
   425  		npUpReq := structs.NodePoolUpsertRequest{
   426  			NodePools: []*structs.NodePool{pool1, pool2},
   427  		}
   428  		var npUpResp structs.GenericResponse
   429  		err := s.Agent.RPC("NodePool.UpsertNodePools", &npUpReq, &npUpResp)
   430  		must.NoError(t, err)
   431  
   432  		for _, poolName := range []string{pool1.Name, "default", "all"} {
   433  			for i := 0; i < 2; i++ {
   434  				job := mock.MinJob()
   435  				job.NodePool = poolName
   436  				jobRegReq := structs.JobRegisterRequest{
   437  					Job: job,
   438  					WriteRequest: structs.WriteRequest{
   439  						Region:    "global",
   440  						Namespace: structs.DefaultNamespace,
   441  					},
   442  				}
   443  				var jobRegResp structs.JobRegisterResponse
   444  				must.NoError(t, s.Agent.RPC("Job.Register", &jobRegReq, &jobRegResp))
   445  			}
   446  		}
   447  
   448  		// Make HTTP request to occupied pool
   449  		req, err := http.NewRequest(http.MethodGet,
   450  			fmt.Sprintf("/v1/node/pool/%s/jobs", pool1.Name), nil)
   451  		must.NoError(t, err)
   452  		respW := httptest.NewRecorder()
   453  
   454  		obj, err := s.Server.NodePoolSpecificRequest(respW, req)
   455  		must.NoError(t, err)
   456  		must.SliceLen(t, 2, obj.([]*structs.JobListStub))
   457  
   458  		// Verify response index.
   459  		gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
   460  		must.NoError(t, err)
   461  		must.NonZero(t, gotIndex)
   462  
   463  		// Make HTTP request to empty pool
   464  		req, err = http.NewRequest(http.MethodGet,
   465  			fmt.Sprintf("/v1/node/pool/%s/jobs", pool2.Name), nil)
   466  		must.NoError(t, err)
   467  		respW = httptest.NewRecorder()
   468  
   469  		obj, err = s.Server.NodePoolSpecificRequest(respW, req)
   470  		must.NoError(t, err)
   471  		must.SliceLen(t, 0, obj.([]*structs.JobListStub))
   472  
   473  		// Make HTTP request to the "all"" pool
   474  		req, err = http.NewRequest(http.MethodGet, "/v1/node/pool/all/jobs", nil)
   475  		must.NoError(t, err)
   476  		respW = httptest.NewRecorder()
   477  
   478  		obj, err = s.Server.NodePoolSpecificRequest(respW, req)
   479  		must.NoError(t, err)
   480  		must.SliceLen(t, 2, obj.([]*structs.JobListStub))
   481  
   482  	})
   483  }