mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 18:35:44 +03:00
node pools: add CRUD API (#17384)
This commit is contained in:
@@ -386,6 +386,9 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
||||
s.mux.HandleFunc("/v1/nodes", s.wrap(s.NodesRequest))
|
||||
s.mux.HandleFunc("/v1/node/", s.wrap(s.NodeSpecificRequest))
|
||||
|
||||
s.mux.HandleFunc("/v1/node/pools", s.wrap(s.NodePoolsRequest))
|
||||
s.mux.HandleFunc("/v1/node/pool/", s.wrap(s.NodePoolSpecificRequest))
|
||||
|
||||
s.mux.HandleFunc("/v1/allocations", s.wrap(s.AllocsRequest))
|
||||
s.mux.HandleFunc("/v1/allocation/", s.wrap(s.AllocSpecificRequest))
|
||||
|
||||
|
||||
121
command/agent/node_pool_endpoint.go
Normal file
121
command/agent/node_pool_endpoint.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
func (s *HTTPServer) NodePoolsRequest(resp http.ResponseWriter, req *http.Request) (any, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return s.nodePoolList(resp, req)
|
||||
case "PUT", "POST":
|
||||
return s.nodePoolUpsert(resp, req, "")
|
||||
default:
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) NodePoolSpecificRequest(resp http.ResponseWriter, req *http.Request) (any, error) {
|
||||
path := strings.TrimPrefix(req.URL.Path, "/v1/node/pool/")
|
||||
switch {
|
||||
default:
|
||||
return s.nodePoolCRUD(resp, req, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) nodePoolCRUD(resp http.ResponseWriter, req *http.Request, poolName string) (any, error) {
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
return s.nodePoolQuery(resp, req, poolName)
|
||||
case "PUT", "POST":
|
||||
return s.nodePoolUpsert(resp, req, poolName)
|
||||
case "DELETE":
|
||||
return s.nodePoolDelete(resp, req, poolName)
|
||||
default:
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) nodePoolList(resp http.ResponseWriter, req *http.Request) (any, error) {
|
||||
args := structs.NodePoolListRequest{}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out structs.NodePoolListResponse
|
||||
if err := s.agent.RPC("NodePool.List", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setMeta(resp, &out.QueryMeta)
|
||||
if out.NodePools == nil {
|
||||
out.NodePools = make([]*structs.NodePool, 0)
|
||||
}
|
||||
return out.NodePools, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) nodePoolQuery(resp http.ResponseWriter, req *http.Request, poolName string) (any, error) {
|
||||
args := structs.NodePoolSpecificRequest{
|
||||
Name: poolName,
|
||||
}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var out structs.SingleNodePoolResponse
|
||||
if err := s.agent.RPC("NodePool.GetNodePool", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setMeta(resp, &out.QueryMeta)
|
||||
if out.NodePool == nil {
|
||||
return nil, CodedError(http.StatusNotFound, "node pool not found")
|
||||
}
|
||||
|
||||
return out.NodePool, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) nodePoolUpsert(resp http.ResponseWriter, req *http.Request, poolName string) (any, error) {
|
||||
var pool structs.NodePool
|
||||
if err := decodeBody(req, &pool); err != nil {
|
||||
return nil, CodedError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if poolName != "" && pool.Name != poolName {
|
||||
return nil, CodedError(http.StatusBadRequest, "Node pool name does not match request path")
|
||||
}
|
||||
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{&pool},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.GenericResponse
|
||||
if err := s.agent.RPC("NodePool.UpsertNodePools", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setIndex(resp, out.Index)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) nodePoolDelete(resp http.ResponseWriter, req *http.Request, poolName string) (any, error) {
|
||||
args := structs.NodePoolDeleteRequest{
|
||||
Names: []string{poolName},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.GenericResponse
|
||||
if err := s.agent.RPC("NodePool.DeleteNodePools", &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setIndex(resp, out.Index)
|
||||
return nil, nil
|
||||
}
|
||||
314
command/agent/node_pool_endpoint_test.go
Normal file
314
command/agent/node_pool_endpoint_test.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestHTTP_NodePool_List(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
httpTest(t, nil, func(s *TestAgent) {
|
||||
// Populate state with test data.
|
||||
pool1 := mock.NodePool()
|
||||
pool2 := mock.NodePool()
|
||||
pool3 := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool1, pool2, pool3},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Make HTTP request.
|
||||
req, err := http.NewRequest("GET", "/v1/node/pools", nil)
|
||||
must.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
obj, err := s.Server.NodePoolsRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Expect 5 node pools: 3 created + 2 built-in.
|
||||
must.SliceLen(t, 5, obj.([]*structs.NodePool))
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_NodePool_Info(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
httpTest(t, nil, func(s *TestAgent) {
|
||||
// Populate state with test data.
|
||||
pool := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
t.Run("test pool", func(t *testing.T) {
|
||||
// Make HTTP request for test pool.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil)
|
||||
must.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
obj, err := s.Server.NodePoolSpecificRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify expected pool is returned.
|
||||
must.Eq(t, pool, obj.(*structs.NodePool), must.Cmp(cmpopts.IgnoreFields(
|
||||
structs.NodePool{},
|
||||
"CreateIndex",
|
||||
"ModifyIndex",
|
||||
)))
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
})
|
||||
|
||||
t.Run("built-in pool", func(t *testing.T) {
|
||||
// Make HTTP request for built-in pool.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/v1/node/pool/%s", structs.NodePoolAll), nil)
|
||||
must.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
obj, err := s.Server.NodePoolSpecificRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify expected pool is returned.
|
||||
must.Eq(t, structs.NodePoolAll, obj.(*structs.NodePool).Name)
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
})
|
||||
|
||||
t.Run("invalid pool", func(t *testing.T) {
|
||||
// Make HTTP request for built-in pool.
|
||||
req, err := http.NewRequest("GET", "/v1/node/pool/doesn-exist", nil)
|
||||
must.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Verify error.
|
||||
_, err = s.Server.NodePoolSpecificRequest(respW, req)
|
||||
must.ErrorContains(t, err, "not found")
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_NodePool_Create(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
httpTest(t, nil, func(s *TestAgent) {
|
||||
// Create test node pool.
|
||||
pool := mock.NodePool()
|
||||
buf := encodeReq(pool)
|
||||
req, err := http.NewRequest("PUT", "/v1/node/pools", buf)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
obj, err := s.Server.NodePoolsRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.Nil(t, obj)
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
|
||||
// Verify test node pool is in state.
|
||||
got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields(
|
||||
structs.NodePool{},
|
||||
"CreateIndex",
|
||||
"ModifyIndex",
|
||||
)))
|
||||
must.Eq(t, gotIndex, got.CreateIndex)
|
||||
must.Eq(t, gotIndex, got.ModifyIndex)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_NodePool_Update(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
httpTest(t, nil, func(s *TestAgent) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// Populate state with test node pool.
|
||||
pool := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Update node pool.
|
||||
updated := pool.Copy()
|
||||
updated.Description = "updated node pool"
|
||||
updated.Meta = map[string]string{
|
||||
"updated": "true",
|
||||
}
|
||||
updated.SchedulerConfiguration = &structs.NodePoolSchedulerConfiguration{
|
||||
SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack,
|
||||
}
|
||||
|
||||
buf := encodeReq(updated)
|
||||
req, err := http.NewRequest("PUT", fmt.Sprintf("/v1/node/pool/%s", updated.Name), buf)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
obj, err := s.Server.NodePoolsRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.Nil(t, obj)
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
|
||||
// Verify node pool was updated.
|
||||
got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields(
|
||||
structs.NodePool{},
|
||||
"CreateIndex",
|
||||
"ModifyIndex",
|
||||
)))
|
||||
must.NotEq(t, gotIndex, got.CreateIndex)
|
||||
must.Eq(t, gotIndex, got.ModifyIndex)
|
||||
})
|
||||
|
||||
t.Run("no name in path", func(t *testing.T) {
|
||||
// Populate state with test node pool.
|
||||
pool := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Update node pool with no name in path.
|
||||
updated := pool.Copy()
|
||||
updated.Description = "updated node pool"
|
||||
updated.Meta = map[string]string{
|
||||
"updated": "true",
|
||||
}
|
||||
updated.SchedulerConfiguration = &structs.NodePoolSchedulerConfiguration{
|
||||
SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack,
|
||||
}
|
||||
|
||||
buf := encodeReq(updated)
|
||||
req, err := http.NewRequest("PUT", "/v1/node/pool/", buf)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
obj, err := s.Server.NodePoolsRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.Nil(t, obj)
|
||||
|
||||
// Verify response index.
|
||||
gotIndex, err := strconv.ParseUint(respW.HeaderMap.Get("X-Nomad-Index"), 10, 64)
|
||||
must.NoError(t, err)
|
||||
must.NonZero(t, gotIndex)
|
||||
|
||||
// Verify node pool was updated.
|
||||
got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, updated, got, must.Cmp(cmpopts.IgnoreFields(
|
||||
structs.NodePool{},
|
||||
"CreateIndex",
|
||||
"ModifyIndex",
|
||||
)))
|
||||
})
|
||||
|
||||
t.Run("wrong name in path", func(t *testing.T) {
|
||||
// Populate state with test node pool.
|
||||
pool := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Update node pool.
|
||||
updated := pool.Copy()
|
||||
updated.Description = "updated node pool"
|
||||
updated.Meta = map[string]string{
|
||||
"updated": "true",
|
||||
}
|
||||
updated.SchedulerConfiguration = &structs.NodePoolSchedulerConfiguration{
|
||||
SchedulerAlgorithm: structs.SchedulerAlgorithmBinpack,
|
||||
}
|
||||
|
||||
// Make request with the wrong path.
|
||||
buf := encodeReq(updated)
|
||||
req, err := http.NewRequest("PUT", "/v1/node/pool/wrong", buf)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
_, err = s.Server.NodePoolSpecificRequest(respW, req)
|
||||
must.ErrorContains(t, err, "name does not match request path")
|
||||
|
||||
// Verify node pool was NOT updated.
|
||||
got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, pool, got, must.Cmp(cmpopts.IgnoreFields(
|
||||
structs.NodePool{},
|
||||
"CreateIndex",
|
||||
"ModifyIndex",
|
||||
)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTP_NodePool_Delete(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
httpTest(t, nil, func(s *TestAgent) {
|
||||
// Populate state with test node pool.
|
||||
pool := mock.NodePool()
|
||||
args := structs.NodePoolUpsertRequest{
|
||||
NodePools: []*structs.NodePool{pool},
|
||||
}
|
||||
var resp structs.GenericResponse
|
||||
err := s.Agent.RPC("NodePool.UpsertNodePools", &args, &resp)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Delete test node pool.
|
||||
req, err := http.NewRequest("DELETE", fmt.Sprintf("/v1/node/pool/%s", pool.Name), nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
respW := httptest.NewRecorder()
|
||||
obj, err := s.Server.NodePoolSpecificRequest(respW, req)
|
||||
must.NoError(t, err)
|
||||
must.Nil(t, obj)
|
||||
|
||||
// Verify node pool was deleted.
|
||||
got, err := s.Agent.server.State().NodePoolByName(nil, pool.Name)
|
||||
must.NoError(t, err)
|
||||
must.Nil(t, got)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user