node pools: implement CLI (#17388)
This commit is contained in:
parent
6758379e48
commit
b770f2b1ef
|
@ -15,6 +15,7 @@ const (
|
|||
Evals Context = "evals"
|
||||
Jobs Context = "jobs"
|
||||
Nodes Context = "nodes"
|
||||
NodePools Context = "node_pools"
|
||||
Namespaces Context = "namespaces"
|
||||
Quotas Context = "quotas"
|
||||
Recommendations Context = "recommendations"
|
||||
|
|
|
@ -38,9 +38,6 @@ const (
|
|||
// DefaultNamespace is the default namespace.
|
||||
DefaultNamespace = "default"
|
||||
|
||||
// NodePoolDefault is the default node pool.
|
||||
NodePoolDefault = "default"
|
||||
|
||||
// For Job configuration, GlobalRegion is a sentinel region value
|
||||
// that users may specify to indicate the job should be run on
|
||||
// the region of the node that the job was submitted to.
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const (
|
||||
// NodePoolAll is the node pool that always includes all nodes.
|
||||
NodePoolAll = "all"
|
||||
|
||||
// NodePoolDefault is the default node pool.
|
||||
NodePoolDefault = "default"
|
||||
)
|
||||
|
||||
// NodePools is used to access node pools endpoints.
|
||||
type NodePools struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// NodePools returns a handle on the node pools endpoints.
|
||||
func (c *Client) NodePools() *NodePools {
|
||||
return &NodePools{client: c}
|
||||
}
|
||||
|
||||
// List is used to list all node pools.
|
||||
func (n *NodePools) List(q *QueryOptions) ([]*NodePool, *QueryMeta, error) {
|
||||
var resp []*NodePool
|
||||
qm, err := n.client.query("/v1/node/pools", &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, qm, nil
|
||||
}
|
||||
|
||||
// PrefixList is used to list node pools that match a given prefix.
|
||||
func (n *NodePools) PrefixList(prefix string, q *QueryOptions) ([]*NodePool, *QueryMeta, error) {
|
||||
if q == nil {
|
||||
q = &QueryOptions{}
|
||||
}
|
||||
q.Prefix = prefix
|
||||
return n.List(q)
|
||||
}
|
||||
|
||||
// Info is used to fetch details of a specific node pool.
|
||||
func (n *NodePools) Info(name string, q *QueryOptions) (*NodePool, *QueryMeta, error) {
|
||||
if name == "" {
|
||||
return nil, nil, errors.New("missing node pool name")
|
||||
}
|
||||
|
||||
var resp NodePool
|
||||
qm, err := n.client.query("/v1/node/pool/"+url.PathEscape(name), &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, qm, nil
|
||||
}
|
||||
|
||||
// Register is used to create or update a node pool.
|
||||
func (n *NodePools) Register(pool *NodePool, w *WriteOptions) (*WriteMeta, error) {
|
||||
if pool == nil {
|
||||
return nil, errors.New("missing node pool")
|
||||
}
|
||||
if pool.Name == "" {
|
||||
return nil, errors.New("missing node pool name")
|
||||
}
|
||||
|
||||
wm, err := n.client.put("/v1/node/pools", pool, nil, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete a node pool.
|
||||
func (n *NodePools) Delete(name string, w *WriteOptions) (*WriteMeta, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("missing node pool name")
|
||||
}
|
||||
|
||||
wm, err := n.client.delete("/v1/node/pool/"+url.PathEscape(name), nil, nil, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// NodePool is used to serialize a node pool.
|
||||
type NodePool struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Description string `hcl:"description,optional"`
|
||||
Meta map[string]string `hcl:"meta,block"`
|
||||
SchedulerConfiguration *NodePoolSchedulerConfiguration `hcl:"scheduler_configuration,block"`
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// NodePoolSchedulerConfiguration is used to serialize the scheduler
|
||||
// configuration of a node pool.
|
||||
type NodePoolSchedulerConfiguration struct {
|
||||
SchedulerAlgorithm SchedulerAlgorithm `hcl:"scheduler_algorithm,optional"`
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api/internal/testutil"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodePools_List(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
nodePools := c.NodePools()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
q *QueryOptions
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "list all",
|
||||
q: nil,
|
||||
expected: []string{
|
||||
NodePoolAll,
|
||||
NodePoolDefault,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with query param",
|
||||
q: &QueryOptions{
|
||||
PerPage: 1,
|
||||
},
|
||||
expected: []string{NodePoolAll},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp, _, err := nodePools.List(tc.q)
|
||||
must.NoError(t, err)
|
||||
|
||||
got := make([]string, len(resp))
|
||||
for i, pool := range resp {
|
||||
got[i] = pool.Name
|
||||
}
|
||||
must.SliceContainsAll(t, got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodePools_PrefixList(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
nodePools := c.NodePools()
|
||||
|
||||
// Create test node pool.
|
||||
dev1 := &NodePool{Name: "dev-1"}
|
||||
_, err := nodePools.Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
prefix string
|
||||
q *QueryOptions
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "prefix",
|
||||
prefix: "d",
|
||||
q: nil,
|
||||
expected: []string{
|
||||
NodePoolDefault,
|
||||
dev1.Name,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with query param",
|
||||
prefix: "d",
|
||||
q: &QueryOptions{
|
||||
PerPage: 1,
|
||||
},
|
||||
expected: []string{NodePoolDefault},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
resp, _, err := nodePools.PrefixList(tc.prefix, tc.q)
|
||||
must.NoError(t, err)
|
||||
|
||||
got := make([]string, len(resp))
|
||||
for i, pool := range resp {
|
||||
got[i] = pool.Name
|
||||
}
|
||||
must.SliceContainsAll(t, got, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodePools_Info(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
nodePools := c.NodePools()
|
||||
|
||||
t.Run("default node pool", func(t *testing.T) {
|
||||
pool, _, err := nodePools.Info(NodePoolDefault, nil)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, NodePoolDefault, pool.Name)
|
||||
})
|
||||
|
||||
t.Run("missing node pool name", func(t *testing.T) {
|
||||
pool, _, err := nodePools.Info("", nil)
|
||||
must.ErrorContains(t, err, "missing node pool name")
|
||||
must.Nil(t, pool)
|
||||
})
|
||||
|
||||
t.Run("node pool name with special charaters", func(t *testing.T) {
|
||||
pool, _, err := nodePools.Info("node/pool", nil)
|
||||
must.ErrorContains(t, err, "not found")
|
||||
must.Nil(t, pool)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodePools_Register(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
nodePools := c.NodePools()
|
||||
|
||||
// Create test node pool.
|
||||
t.Run("create and update node pool", func(t *testing.T) {
|
||||
dev1 := &NodePool{Name: "dev-1"}
|
||||
_, err := nodePools.Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify node pool was persisted.
|
||||
got, _, err := nodePools.Info(dev1.Name, nil)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, dev1.Name, got.Name)
|
||||
|
||||
// Update test node pool.
|
||||
dev1.Description = "test"
|
||||
_, err = nodePools.Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify node pool was updated.
|
||||
got, _, err = nodePools.Info(dev1.Name, nil)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, dev1.Name, got.Name)
|
||||
must.Eq(t, dev1.Description, got.Description)
|
||||
})
|
||||
|
||||
t.Run("missing node pool", func(t *testing.T) {
|
||||
_, err := nodePools.Register(nil, nil)
|
||||
must.ErrorContains(t, err, "missing node pool")
|
||||
})
|
||||
|
||||
t.Run("missing node pool name", func(t *testing.T) {
|
||||
_, err := nodePools.Register(&NodePool{}, nil)
|
||||
must.ErrorContains(t, err, "missing node pool name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestNodePools_Delete(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
nodePools := c.NodePools()
|
||||
|
||||
// Create test node pool.
|
||||
t.Run("delete node pool", func(t *testing.T) {
|
||||
dev1 := &NodePool{Name: "dev-1"}
|
||||
_, err := nodePools.Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify node pool was persisted.
|
||||
got, _, err := nodePools.Info(dev1.Name, nil)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, dev1.Name, got.Name)
|
||||
|
||||
// Delete test node pool.
|
||||
_, err = nodePools.Delete(dev1.Name, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Verify node pool is gone.
|
||||
got, _, err = nodePools.Info(dev1.Name, nil)
|
||||
must.ErrorContains(t, err, "not found")
|
||||
})
|
||||
|
||||
t.Run("missing node pool name", func(t *testing.T) {
|
||||
_, err := nodePools.Delete("", nil)
|
||||
must.ErrorContains(t, err, "missing node pool name")
|
||||
})
|
||||
|
||||
t.Run("node pool name with special charaters", func(t *testing.T) {
|
||||
_, err := nodePools.Delete("node/pool", nil)
|
||||
must.ErrorContains(t, err, "not found")
|
||||
})
|
||||
}
|
|
@ -616,6 +616,31 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
|||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool": func() (cli.Command, error) {
|
||||
return &NodePoolCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool apply": func() (cli.Command, error) {
|
||||
return &NodePoolApplyCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool delete": func() (cli.Command, error) {
|
||||
return &NodePoolDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool info": func() (cli.Command, error) {
|
||||
return &NodePoolInfoCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"node pool list": func() (cli.Command, error) {
|
||||
return &NodePoolListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"operator": func() (cli.Command, error) {
|
||||
return &OperatorCommand{
|
||||
Meta: meta,
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-set"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/api/contexts"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodePoolCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolCommand) Name() string {
|
||||
return "node pool"
|
||||
}
|
||||
|
||||
func (c *NodePoolCommand) Synopsis() string {
|
||||
return "Interact with node pools"
|
||||
}
|
||||
|
||||
func (c *NodePoolCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool <subcommand> [options] [args]
|
||||
|
||||
This command groups subcommands for interacting with node pools. Node pools
|
||||
are used to partition and control access to a group of nodes. This command
|
||||
can be used to create, update, list, and delete node pools.
|
||||
|
||||
Create or update a node pool:
|
||||
|
||||
$ nomad node pool apply <path>
|
||||
|
||||
List all node pools:
|
||||
|
||||
$ nomad node pool list
|
||||
|
||||
Fetch information on an existing node pool:
|
||||
|
||||
$ nomad node info <name>
|
||||
|
||||
Delete a node pool:
|
||||
|
||||
$ nomad node pool delete <name>
|
||||
|
||||
Please refer to individual subcommand help for detailed usage information.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolCommand) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
func formatNodePoolList(pools []*api.NodePool) string {
|
||||
out := make([]string, len(pools)+1)
|
||||
out[0] = "Name|Description"
|
||||
for i, p := range pools {
|
||||
out[i+1] = fmt.Sprintf("%s|%s",
|
||||
p.Name,
|
||||
p.Description,
|
||||
)
|
||||
}
|
||||
return formatList(out)
|
||||
}
|
||||
|
||||
func nodePoolPredictor(factory ApiClientFactory, filter *set.Set[string]) complete.Predictor {
|
||||
return complete.PredictFunc(func(a complete.Args) []string {
|
||||
client, err := factory()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, _, err := client.Search().PrefixSearch(a.Last, contexts.NodePools, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
results := resp.Matches[contexts.NodePools]
|
||||
if filter == nil {
|
||||
return results
|
||||
}
|
||||
|
||||
filtered := []string{}
|
||||
for _, pool := range resp.Matches[contexts.NodePools] {
|
||||
if filter.Contains(pool) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, pool)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2/hclsimple"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodePoolApplyCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) Name() string {
|
||||
return "node pool apply"
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) Synopsis() string {
|
||||
return "Create or update a node pool"
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool apply [options] <input>
|
||||
|
||||
Apply is used to create or update a node pool. The specification file is read
|
||||
from stdin by specifying "-", otherwise a path to the file is expected.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'write'
|
||||
capability in a 'node_pool' policy that matches the node pool being targeted.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
Apply Options:
|
||||
|
||||
-json
|
||||
Parse the input as a JSON node pool specification.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictOr(
|
||||
complete.PredictFiles("*.hcl"),
|
||||
complete.PredictFiles("*.json"),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *NodePoolApplyCommand) Run(args []string) int {
|
||||
var jsonInput bool
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.BoolVar(&jsonInput, "json", false, "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we only have one argument.
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error("This command takes one argument: <input>")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read input content.
|
||||
path := args[0]
|
||||
var content []byte
|
||||
var err error
|
||||
switch path {
|
||||
case "-":
|
||||
content, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to read stdin: %v", err))
|
||||
return 1
|
||||
}
|
||||
// Set .hcl extension so the decoder doesn't fail.
|
||||
if !jsonInput {
|
||||
path = "stdin.nomad.hcl"
|
||||
}
|
||||
default:
|
||||
content, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to read file %q: %v", path, err))
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Parse input.
|
||||
var poolSpec nodePoolSpec
|
||||
if jsonInput {
|
||||
err = json.Unmarshal(content, &poolSpec.NodePool)
|
||||
} else {
|
||||
err = hclsimple.Decode(path, content, nil, &poolSpec)
|
||||
}
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Failed to parse input content: %v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Make API request.
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
_, err = client.NodePools().Register(poolSpec.NodePool, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error applying node pool: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(fmt.Sprintf("Successfully applied node pool %q!", poolSpec.NodePool.Name))
|
||||
return 0
|
||||
}
|
||||
|
||||
type nodePoolSpec struct {
|
||||
NodePool *api.NodePool `hcl:"node_pool,block"`
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodePoolApplyCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolApplyCommand{}
|
||||
}
|
||||
|
||||
func TestNodePoolApplyCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolApplyCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Create node pool with HCL file.
|
||||
hclTestFile := `
|
||||
node_pool "dev" {
|
||||
description = "dev node pool"
|
||||
}`
|
||||
file, err := os.CreateTemp(t.TempDir(), "node-pool-test-*.hcl")
|
||||
must.NoError(t, err)
|
||||
_, err = file.WriteString(hclTestFile)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Run command.
|
||||
args := []string{"-address", url, file.Name()}
|
||||
code := cmd.Run(args)
|
||||
must.Eq(t, 0, code)
|
||||
|
||||
// Verify node pool was created.
|
||||
got, err := srv.Agent.Server().State().NodePoolByName(nil, "dev")
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, got)
|
||||
|
||||
// Update node pool.
|
||||
file.Truncate(0)
|
||||
file.Seek(0, 0)
|
||||
hclTestFile = `
|
||||
node_pool "dev" {
|
||||
description = "dev node pool"
|
||||
|
||||
meta {
|
||||
test = "true"
|
||||
}
|
||||
}`
|
||||
_, err = file.WriteString(hclTestFile)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Run command.
|
||||
code = cmd.Run(args)
|
||||
must.Eq(t, 0, code)
|
||||
|
||||
// Verify node pool was updated.
|
||||
got, err = srv.Agent.Server().State().NodePoolByName(nil, "dev")
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, got)
|
||||
must.NotNil(t, got.Meta)
|
||||
must.Eq(t, "true", got.Meta["test"])
|
||||
|
||||
// Create node pool with JSON file.
|
||||
jsonTestFile := `
|
||||
{
|
||||
"Name": "prod",
|
||||
"Description": "prod node pool"
|
||||
}`
|
||||
|
||||
file, err = os.CreateTemp(t.TempDir(), "node-pool-test-*.json")
|
||||
must.NoError(t, err)
|
||||
_, err = file.WriteString(jsonTestFile)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Run command.
|
||||
args = []string{"-address", url, "-json", file.Name()}
|
||||
code = cmd.Run(args)
|
||||
must.Eq(t, 0, code)
|
||||
|
||||
// Verify node pool was created.
|
||||
got, err = srv.Agent.Server().State().NodePoolByName(nil, "prod")
|
||||
must.NoError(t, err)
|
||||
must.NotNil(t, got)
|
||||
}
|
||||
|
||||
func TestNodePoolApplyCommand_Run_fail(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
input string
|
||||
expectedOutput string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "missing file",
|
||||
args: []string{},
|
||||
expectedOutput: "This command takes one argument",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "file doesn't exist",
|
||||
args: []string{"doesn-exist.hcl"},
|
||||
expectedOutput: "no such file",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
args: []string{"-json", "invalid.json"},
|
||||
input: "not json",
|
||||
expectedOutput: "Failed to parse input",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid hcl",
|
||||
args: []string{"invalid.hcl"},
|
||||
input: "not HCL",
|
||||
expectedOutput: "Failed to parse input",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "valid json without json flag",
|
||||
args: []string{"valid.json"},
|
||||
input: `{"Name": "dev"}`,
|
||||
expectedOutput: "Failed to parse input",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "valid hcl with json flag",
|
||||
args: []string{"-json", "valid.hcl"},
|
||||
input: `node_pool "dev" {}`,
|
||||
expectedOutput: "Failed to parse input",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid node pool hcl",
|
||||
args: []string{"invalid.hcl"},
|
||||
input: `not_a_node_pool "dev" {}`,
|
||||
expectedOutput: "Failed to parse input",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid node pool",
|
||||
args: []string{"-address", url, "invalid_node_pool.hcl"},
|
||||
input: `node_pool "invalid name" {}`,
|
||||
expectedOutput: "Error applying node pool",
|
||||
expectedCode: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolApplyCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Write input to file.
|
||||
if tc.input != "" {
|
||||
// Split the filename and extension from the last argument to
|
||||
// add a "*" between them so os.CreateTemp retains the file
|
||||
// extension.
|
||||
filename := tc.args[len(tc.args)-1]
|
||||
ext := filepath.Ext(filename)
|
||||
name, _ := strings.CutSuffix(filename, ext)
|
||||
|
||||
file, err := os.CreateTemp(t.TempDir(), fmt.Sprintf("%s-*%s", name, ext))
|
||||
must.NoError(t, err)
|
||||
_, err = file.WriteString(tc.input)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Update last arg with full test file path.
|
||||
tc.args[len(tc.args)-1] = file.Name()
|
||||
}
|
||||
|
||||
got := cmd.Run(tc.args)
|
||||
test.Eq(t, tc.expectedCode, got)
|
||||
test.StrContains(t, ui.ErrorWriter.String(), tc.expectedOutput)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-set"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodePoolDeleteCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) Name() string {
|
||||
return "node pool delete"
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) Synopsis() string {
|
||||
return "Delete a node pool"
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool delete [options] <node-pool>
|
||||
|
||||
Delete is used to remove a node pool.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'delete'
|
||||
capability in a 'node_pool' policy that matches the node pool being targeted.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault)
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Meta.AutocompleteFlags(FlagSetClient)
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) AutocompleteArgs() complete.Predictor {
|
||||
return nodePoolPredictor(c.Client, set.From([]string{
|
||||
api.NodePoolAll,
|
||||
api.NodePoolDefault,
|
||||
}))
|
||||
}
|
||||
|
||||
func (c *NodePoolDeleteCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we only have one argument.
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error("This command takes one argument: <node-pool>")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
pool := args[0]
|
||||
|
||||
// Make API equest.
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
_, err = client.NodePools().Delete(pool, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error deleting node pool: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(fmt.Sprintf("Successfully deleted node pool %q!", pool))
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodePoolDeleteCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolDeleteCommand{}
|
||||
}
|
||||
|
||||
func TestNodePoolDeleteCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
// Register test node pools.
|
||||
dev1 := &api.NodePool{Name: "dev-1"}
|
||||
_, err := client.NodePools().Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolDeleteCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Delete test node pool.
|
||||
args := []string{"-address", url, dev1.Name}
|
||||
code := cmd.Run(args)
|
||||
must.Eq(t, 0, code)
|
||||
must.StrContains(t, ui.OutputWriter.String(), "Successfully deleted")
|
||||
|
||||
// Verify node pool was delete.
|
||||
got, _, err := client.NodePools().Info(dev1.Name, nil)
|
||||
must.ErrorContains(t, err, "404")
|
||||
must.Nil(t, got)
|
||||
}
|
||||
|
||||
func TestNodePoolDeleteCommand_Run_fail(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "missing pool",
|
||||
args: []string{"-address", url},
|
||||
expectedCode: 1,
|
||||
expectedErr: "This command takes one argument",
|
||||
},
|
||||
{
|
||||
name: "invalid pool",
|
||||
args: []string{"-address", url, "invalid"},
|
||||
expectedCode: 1,
|
||||
expectedErr: "not found",
|
||||
},
|
||||
{
|
||||
name: "built-in pool",
|
||||
args: []string{"-address", url, "all"},
|
||||
expectedCode: 1,
|
||||
expectedErr: "not allowed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolDeleteCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(tc.args)
|
||||
must.Eq(t, tc.expectedCode, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodePoolInfoCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) Name() string {
|
||||
return "node pool info"
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) Synopsis() string {
|
||||
return "Fetch information about an existing node pool"
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool info <node-pool>
|
||||
|
||||
Info is used to fetch information about an existing node pool.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'read'
|
||||
capability in a 'node_pool' policy that matches the node pool being targeted.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
Info Options:
|
||||
|
||||
-json
|
||||
Output the node pool in its JSON format.
|
||||
|
||||
-t
|
||||
Format and display node pool using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) AutocompleteArgs() complete.Predictor {
|
||||
return nodePoolPredictor(c.Client, nil)
|
||||
}
|
||||
|
||||
func (c *NodePoolInfoCommand) Run(args []string) int {
|
||||
var json bool
|
||||
var tmpl string
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we only have one argument.
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error("This command takes one argument: <node-pool>")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Lookup node pool by prefix.
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
pool, possible, err := c.nodePoolByPrefix(client, args[0])
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error retrieving node pool: %s", err))
|
||||
return 1
|
||||
}
|
||||
if len(possible) != 0 {
|
||||
c.Ui.Error(fmt.Sprintf("Prefix matched multiple node pools\n\n%s", formatNodePoolList(possible)))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format output if requested.
|
||||
if json || tmpl != "" {
|
||||
out, err := Format(json, tmpl, pool)
|
||||
if err != nil {
|
||||
c.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Print node pool information.
|
||||
basic := []string{
|
||||
fmt.Sprintf("Name|%s", pool.Name),
|
||||
fmt.Sprintf("Description|%s", pool.Description),
|
||||
}
|
||||
c.Ui.Output(formatKV(basic))
|
||||
|
||||
c.Ui.Output(c.Colorize().Color("\n[bold]Metadata[reset]"))
|
||||
if len(pool.Meta) > 0 {
|
||||
var meta []string
|
||||
for k, v := range pool.Meta {
|
||||
meta = append(meta, fmt.Sprintf("%s|%s", k, v))
|
||||
}
|
||||
sort.Strings(meta)
|
||||
c.Ui.Output(formatKV(meta))
|
||||
} else {
|
||||
c.Ui.Output("No metadata")
|
||||
}
|
||||
|
||||
c.Ui.Output(c.Colorize().Color("\n[bold]Scheduler Configuration[reset]"))
|
||||
if pool.SchedulerConfiguration != nil {
|
||||
schedConfig := []string{
|
||||
fmt.Sprintf("Scheduler Algorithm|%s", pool.SchedulerConfiguration.SchedulerAlgorithm),
|
||||
}
|
||||
c.Ui.Output(formatKV(schedConfig))
|
||||
} else {
|
||||
c.Ui.Output("No scheduler configuration")
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// nodePoolByPrefix returns a node pool that matches the given prefix or a list
|
||||
// of all matches if an exact match is not found.
|
||||
func (c *NodePoolInfoCommand) nodePoolByPrefix(client *api.Client, prefix string) (*api.NodePool, []*api.NodePool, error) {
|
||||
pools, _, err := client.NodePools().PrefixList(prefix, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch len(pools) {
|
||||
case 0:
|
||||
return nil, nil, fmt.Errorf("No node pool with prefix %q found", prefix)
|
||||
case 1:
|
||||
return pools[0], nil, nil
|
||||
default:
|
||||
for _, pool := range pools {
|
||||
if pool.Name == prefix {
|
||||
return pool, nil, nil
|
||||
}
|
||||
}
|
||||
return nil, pools, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodePoolInfoCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolInfoCommand{}
|
||||
}
|
||||
|
||||
func TestNodePoolInfoCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
// Register test node pools.
|
||||
dev1 := &api.NodePool{
|
||||
Name: "dev-1",
|
||||
Description: "Test pool",
|
||||
Meta: map[string]string{
|
||||
"env": "test",
|
||||
},
|
||||
SchedulerConfiguration: &api.NodePoolSchedulerConfiguration{
|
||||
SchedulerAlgorithm: api.SchedulerAlgorithmSpread,
|
||||
},
|
||||
}
|
||||
_, err := client.NodePools().Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
dev1Output := `
|
||||
Name = dev-1
|
||||
Description = Test pool
|
||||
|
||||
Metadata
|
||||
env = test
|
||||
|
||||
Scheduler Configuration
|
||||
Scheduler Algorithm = spread`
|
||||
|
||||
dev1JsonOutput := `
|
||||
{
|
||||
"Description": "Test pool",
|
||||
"Meta": {
|
||||
"env": "test"
|
||||
},
|
||||
"Name": "dev-1",
|
||||
"SchedulerConfiguration": {
|
||||
"SchedulerAlgorithm": "spread"
|
||||
}
|
||||
}`
|
||||
|
||||
// These two node pools are used to test exact prefix match.
|
||||
prod1 := &api.NodePool{Name: "prod-1"}
|
||||
_, err = client.NodePools().Register(prod1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
prod12 := &api.NodePool{Name: "prod-12"}
|
||||
_, err = client.NodePools().Register(prod12, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "basic info",
|
||||
args: []string{"dev-1"},
|
||||
expectedOut: dev1Output,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "basic info by prefix",
|
||||
args: []string{"dev"},
|
||||
expectedOut: dev1Output,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "exact prefix match",
|
||||
args: []string{"prod-1"},
|
||||
expectedOut: `
|
||||
Name = prod-1
|
||||
Description = <none>
|
||||
|
||||
Metadata
|
||||
No metadata
|
||||
|
||||
Scheduler Configuration
|
||||
No scheduler configuration`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
args: []string{"-json", "dev"},
|
||||
expectedOut: dev1JsonOutput,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "template",
|
||||
args: []string{
|
||||
"-t", "{{.Name}} -> {{.Meta.env}}",
|
||||
"dev-1",
|
||||
},
|
||||
expectedOut: "dev-1 -> test",
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "fail because of missing node pool arg",
|
||||
args: []string{},
|
||||
expectedErr: "This command takes one argument",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "fail because no match",
|
||||
args: []string{"invalid"},
|
||||
expectedErr: `No node pool with prefix "invalid" found`,
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "fail because of multiple matches",
|
||||
args: []string{"de"}, // Matches default and dev-1.
|
||||
expectedErr: "Prefix matched multiple node pools",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "fail because of invalid template",
|
||||
args: []string{
|
||||
"-t", "{{.NotValid}}",
|
||||
"dev-1",
|
||||
},
|
||||
expectedErr: "Error formatting the data",
|
||||
expectedCode: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolInfoCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Run command.
|
||||
args := []string{"-address", url}
|
||||
args = append(args, tc.args...)
|
||||
code := cmd.Run(args)
|
||||
|
||||
gotStdout := ui.OutputWriter.String()
|
||||
gotStdout = jsonOutputRaftIndexes.ReplaceAllString(gotStdout, "")
|
||||
|
||||
test.Eq(t, tc.expectedCode, code)
|
||||
test.StrContains(t, gotStdout, strings.TrimSpace(tc.expectedOut))
|
||||
test.StrContains(t, ui.ErrorWriter.String(), strings.TrimSpace(tc.expectedErr))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
type NodePoolListCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) Name() string {
|
||||
return "node pool list"
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) Synopsis() string {
|
||||
return "List node pools"
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node pool list [options]
|
||||
|
||||
List is used to list existing node pools.
|
||||
|
||||
If ACLs are enabled, this command requires a management token to view all
|
||||
node pools. A non-management token can be used to list node pools for which
|
||||
the token has the 'read' capability.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault) + `
|
||||
|
||||
List Options:
|
||||
|
||||
-filter
|
||||
Specifies an expression used to filter results.
|
||||
|
||||
-json
|
||||
Output the node pools in JSON format.
|
||||
|
||||
-page-token
|
||||
Where to start pagination.
|
||||
|
||||
-per-page
|
||||
How many results to show per page. If not specified, or set to 0, all
|
||||
results are returned.
|
||||
|
||||
-t
|
||||
Format and display the node pools using a Go template.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-filter": complete.PredictAnything,
|
||||
"-json": complete.PredictNothing,
|
||||
"-page-token": complete.PredictAnything,
|
||||
"-per-page": complete.PredictAnything,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictNothing
|
||||
}
|
||||
|
||||
func (c *NodePoolListCommand) Run(args []string) int {
|
||||
var json bool
|
||||
var perPage int
|
||||
var tmpl, pageToken, filter string
|
||||
|
||||
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.StringVar(&filter, "filter", "", "")
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&pageToken, "page-token", "", "")
|
||||
flags.IntVar(&perPage, "per-page", 0, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we don't have any arguments.
|
||||
if len(flags.Args()) != 0 {
|
||||
c.Ui.Error("This command takes no arguments")
|
||||
c.Ui.Error(commandErrorText(c))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Make list request.
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
opts := &api.QueryOptions{
|
||||
Filter: filter,
|
||||
PerPage: int32(perPage),
|
||||
NextToken: pageToken,
|
||||
}
|
||||
pools, qm, err := client.NodePools().List(opts)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying node pools: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format output if requested.
|
||||
if json || tmpl != "" {
|
||||
out, err := Format(json, tmpl, pools)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error formatting output: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
c.Ui.Output(formatNodePoolList(pools))
|
||||
|
||||
if qm.NextToken != "" {
|
||||
c.Ui.Output(fmt.Sprintf(`
|
||||
Results have been paginated. To get the next page run:
|
||||
|
||||
%s -page-token %s`, argsWithoutPageToken(os.Args), qm.NextToken))
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestNodePoolListCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolListCommand{}
|
||||
}
|
||||
|
||||
func TestNodePoolListCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
// Register test node pools.
|
||||
dev1 := &api.NodePool{Name: "dev-1", Description: "Pool dev-1"}
|
||||
_, err := client.NodePools().Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
prod1 := &api.NodePool{Name: "prod-1"}
|
||||
_, err = client.NodePools().Register(prod1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
prod2 := &api.NodePool{Name: "prod-2", Description: "Pool prod-2"}
|
||||
_, err = client.NodePools().Register(prod2, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "list all",
|
||||
args: []string{},
|
||||
expectedOut: `
|
||||
Name Description
|
||||
all Node pool with all nodes in the cluster.
|
||||
default Default node pool.
|
||||
dev-1 Pool dev-1
|
||||
prod-1 <none>
|
||||
prod-2 Pool prod-2`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "filter",
|
||||
args: []string{
|
||||
"-filter", `Name contains "prod"`,
|
||||
},
|
||||
expectedOut: `
|
||||
Name Description
|
||||
prod-1 <none>
|
||||
prod-2 Pool prod-2`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "paginate",
|
||||
args: []string{
|
||||
"-per-page", "2",
|
||||
},
|
||||
expectedOut: `
|
||||
Name Description
|
||||
all Node pool with all nodes in the cluster.
|
||||
default Default node pool.`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "paginate page 2",
|
||||
args: []string{
|
||||
"-per-page", "2",
|
||||
"-page-token", "dev-1",
|
||||
},
|
||||
expectedOut: `
|
||||
Name Description
|
||||
dev-1 Pool dev-1
|
||||
prod-1 <none>`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
args: []string{
|
||||
"-json",
|
||||
"-filter", `Name == "prod-1"`,
|
||||
},
|
||||
expectedOut: `
|
||||
[
|
||||
{
|
||||
"Description": "",
|
||||
"Meta": null,
|
||||
"Name": "prod-1",
|
||||
"SchedulerConfiguration": null
|
||||
}
|
||||
]`,
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "template",
|
||||
args: []string{
|
||||
"-t", "{{range .}}{{.Name}} {{end}}",
|
||||
},
|
||||
expectedOut: "all default dev-1 prod-1 prod-2",
|
||||
expectedCode: 0,
|
||||
},
|
||||
{
|
||||
name: "fail because of arg",
|
||||
args: []string{"invalid"},
|
||||
expectedErr: "This command takes no arguments",
|
||||
expectedCode: 1,
|
||||
},
|
||||
{
|
||||
name: "fail because of invalid template",
|
||||
args: []string{
|
||||
"-t", "{{.NotValid}}",
|
||||
},
|
||||
expectedErr: "Error formatting the data",
|
||||
expectedCode: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Initialize UI and command.
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &NodePoolListCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Run command.
|
||||
args := []string{"-address", url}
|
||||
args = append(args, tc.args...)
|
||||
code := cmd.Run(args)
|
||||
|
||||
gotStdout := ui.OutputWriter.String()
|
||||
gotStdout = jsonOutputRaftIndexes.ReplaceAllString(gotStdout, "")
|
||||
|
||||
test.Eq(t, tc.expectedCode, code)
|
||||
test.StrContains(t, gotStdout, strings.TrimSpace(tc.expectedOut))
|
||||
test.StrContains(t, ui.ErrorWriter.String(), strings.TrimSpace(tc.expectedErr))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-set"
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
var (
|
||||
// jsonOutputRaftIndexes is a regex that matches raft index fields in JSON
|
||||
// strings. It can be used to remove them to make test results more
|
||||
// consistent.
|
||||
jsonOutputRaftIndexes = regexp.MustCompile(`(?m)\s*"(?:CreateIndex|ModifyIndex)".*`)
|
||||
)
|
||||
|
||||
func TestNodePoolCommand_Implements(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
var _ cli.Command = &NodePoolCommand{}
|
||||
}
|
||||
|
||||
func TestMeta_NodePoolPredictor(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start test server.
|
||||
srv, client, url := testServer(t, true, nil)
|
||||
defer srv.Shutdown()
|
||||
|
||||
waitForNodes(t, client)
|
||||
|
||||
// Register some test node pools.
|
||||
dev1 := &api.NodePool{Name: "dev-1"}
|
||||
_, err := client.NodePools().Register(dev1, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
dev2 := &api.NodePool{Name: "dev-2"}
|
||||
_, err = client.NodePools().Register(dev2, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
prod := &api.NodePool{Name: "prod"}
|
||||
_, err = client.NodePools().Register(prod, nil)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args complete.Args
|
||||
filter *set.Set[string]
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "find with prefix",
|
||||
args: complete.Args{
|
||||
Last: "de",
|
||||
},
|
||||
expected: []string{"default", "dev-1", "dev-2"},
|
||||
},
|
||||
{
|
||||
name: "filter",
|
||||
args: complete.Args{
|
||||
Last: "de",
|
||||
},
|
||||
filter: set.From([]string{"default"}),
|
||||
expected: []string{"dev-1", "dev-2"},
|
||||
},
|
||||
{
|
||||
name: "find all",
|
||||
args: complete.Args{
|
||||
Last: "",
|
||||
},
|
||||
expected: []string{"all", "default", "dev-1", "dev-2", "prod"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m := &Meta{flagAddress: url}
|
||||
got := nodePoolPredictor(m.Client, tc.filter).Predict(tc.args)
|
||||
must.SliceContainsAll(t, tc.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool apply'
|
||||
description: |
|
||||
The node pool apply command is used to create or update a node pool.
|
||||
---
|
||||
|
||||
# Command: node pool apply
|
||||
|
||||
The `node pool apply` command is used to create or update a node pool.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad node pool apply [options] <input>
|
||||
```
|
||||
|
||||
Apply is used to create or update a node pool. The specification file is read
|
||||
from stdin by specifying `-`, otherwise a path to the file is expected.
|
||||
|
||||
If ACLs are enabled, this command requires a token with the `write` capability
|
||||
in a `node_pool` policy that matches the node pool being targeted.
|
||||
|
||||
## General Options
|
||||
|
||||
@include 'general_options_no_namespace.mdx'
|
||||
|
||||
## Apply Options
|
||||
|
||||
- `-json`: Parse the input as a JSON node pool specification.
|
||||
|
||||
## Examples
|
||||
|
||||
Create a node pool from a file:
|
||||
|
||||
```hcl
|
||||
# prod_pool.nomad.hcl
|
||||
node_pool "prod" {
|
||||
description = "Node pool for production workloads."
|
||||
|
||||
meta {
|
||||
env = "prod"
|
||||
}
|
||||
|
||||
# Available only in Nomad Enterprise.
|
||||
scheduler_configuration {
|
||||
scheduler_algorithm = "spread"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool apply prod_pool.nomad.hcl
|
||||
Successfully applied node pool "prod"!
|
||||
```
|
||||
|
||||
Create a node pool from stdin:
|
||||
|
||||
```shell-session
|
||||
$ cat prod_pool.nomad.hcl | nomad node pool apply -
|
||||
Successfully applied node pool "prod"!
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ cat <<EOF | nomad node pool apply -
|
||||
node_pool "dev" {
|
||||
description = "Node pool for dev workloads."
|
||||
}
|
||||
EOF
|
||||
Successfully applied node pool "dev"!
|
||||
```
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool delete'
|
||||
description: |
|
||||
The node pool delete command is used to delete a node pool.
|
||||
---
|
||||
|
||||
# Command: node pool delete
|
||||
|
||||
The `node pool delete` command is used delete a node pool.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad node pool delete [options] <node-pool>
|
||||
```
|
||||
|
||||
If ACLs are enabled, this command requires a token with the 'delete'
|
||||
capability in a `node_pool` policy that matches the node pool being targeted.
|
||||
|
||||
## General Options
|
||||
|
||||
@include 'general_options_no_namespace.mdx'
|
||||
|
||||
## Examples
|
||||
|
||||
Delete a node pool:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool delete dev
|
||||
Successfully deleted node pool "dev"!
|
||||
```
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool'
|
||||
description: |
|
||||
The node pool command is used to interact with node pools.
|
||||
---
|
||||
|
||||
# Command: node pool
|
||||
|
||||
The `node pool` command is used to interact with node pools.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: `nomad node pool <subcommand> [options]`
|
||||
|
||||
Run `nomad node pool <subcommand> -h` for help on that subcommand. The
|
||||
following subcommands are available:
|
||||
|
||||
- [`node pool apply`][apply] - Create or update a node pool.
|
||||
|
||||
- [`node pool delete`][delete] - Delete a node pool.
|
||||
|
||||
- [`node pool info`][info] - Fetch information on an existing node pool.
|
||||
|
||||
- [`node pool list`][list] - Retrieve a list of node pools.
|
||||
|
||||
[apply]: /nomad/docs/commands/node-pool/apply
|
||||
[delete]: /nomad/docs/commands/node-pool/delete
|
||||
[info]: /nomad/docs/commands/node-pool/info
|
||||
[list]: /nomad/docs/commands/node-pool/list
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool info'
|
||||
description: |
|
||||
The node pool info command is used to fetch information about a node pool.
|
||||
---
|
||||
|
||||
# Command: node pool info
|
||||
|
||||
The `node pool info` command is used to fetch information about an existing
|
||||
node pool.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad node pool info [options] <node-pool>
|
||||
```
|
||||
|
||||
If ACLs are enabled, this command requires a token with the `read` capability
|
||||
in a `node_pool` policy that matches the node pool being targeted.
|
||||
|
||||
## General Options
|
||||
|
||||
@include 'general_options_no_namespace.mdx'
|
||||
|
||||
## Info Options
|
||||
|
||||
- `-json`: Output the node pool in its JSON format.
|
||||
|
||||
- `-t`: Format and display node pool using a Go template.
|
||||
|
||||
## Examples
|
||||
|
||||
Retrieve information on a node pool:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool info prod
|
||||
Name = prod
|
||||
Description = Node pool for production workloads.
|
||||
|
||||
Metadata
|
||||
env = production
|
||||
|
||||
Scheduler Configuration
|
||||
Scheduler Algorithm = spread
|
||||
```
|
||||
|
||||
Retrieve information in JSON format:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool info -json prod
|
||||
{
|
||||
"CreateIndex": 39,
|
||||
"Description": "Node pool for production workloads.",
|
||||
"Meta": {
|
||||
"env": "production"
|
||||
},
|
||||
"ModifyIndex": 39,
|
||||
"Name": "prod",
|
||||
"SchedulerConfiguration": {
|
||||
"SchedulerAlgorithm": "spread"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Customize output with a Go template:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool info -t "{{.Name}} [{{.Meta.env}}] - {{.Description}}" prod
|
||||
prod [production] - Node pool for production workloads.
|
||||
```
|
|
@ -0,0 +1,132 @@
|
|||
---
|
||||
layout: docs
|
||||
page_title: 'Commands: node pool list'
|
||||
description: |
|
||||
The node pool list command is used to list node pools.
|
||||
---
|
||||
|
||||
# Command: node pool list
|
||||
|
||||
The `node pool list` command is used to list existing node pools.
|
||||
|
||||
## Usage
|
||||
|
||||
```plaintext
|
||||
nomad node pool list [options] <node-pool>
|
||||
```
|
||||
|
||||
If ACLs are enabled, this command requires a management token to view all node
|
||||
pools. A non-management token can be used to list node pools for which the
|
||||
token has the 'read' capability.
|
||||
|
||||
## General Options
|
||||
|
||||
@include 'general_options_no_namespace.mdx'
|
||||
|
||||
## Info Options
|
||||
|
||||
- `-filter`: Specifies an expression used to [filter results][api_filtering].
|
||||
|
||||
- `-json`: Output the node pools in JSON format.
|
||||
|
||||
- `-page-token`: Where to start [pagination][api_pagination].
|
||||
|
||||
- `-per-page`: How many results to show per page. If not specified, or set to
|
||||
`0`, all results are returned.
|
||||
|
||||
- `-t`: Format and display node pools using a Go template.
|
||||
|
||||
## Examples
|
||||
|
||||
List all node pools:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool list
|
||||
Name Description
|
||||
all Node pool with all nodes in the cluster.
|
||||
default Default node pool.
|
||||
dev Node pool for dev workloads.
|
||||
prod Node pool for production workloads.
|
||||
```
|
||||
|
||||
Retrieve information in JSON format:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool list -json
|
||||
[
|
||||
{
|
||||
"CreateIndex": 1,
|
||||
"Description": "Node pool with all nodes in the cluster.",
|
||||
"Meta": null,
|
||||
"ModifyIndex": 1,
|
||||
"Name": "all",
|
||||
"SchedulerConfiguration": null
|
||||
},
|
||||
{
|
||||
"CreateIndex": 1,
|
||||
"Description": "Default node pool.",
|
||||
"Meta": null,
|
||||
"ModifyIndex": 1,
|
||||
"Name": "default",
|
||||
"SchedulerConfiguration": null
|
||||
},
|
||||
{
|
||||
"CreateIndex": 21,
|
||||
"Description": "Node pool for dev workloads.",
|
||||
"Meta": {
|
||||
"env": "development"
|
||||
},
|
||||
"ModifyIndex": 21,
|
||||
"Name": "dev",
|
||||
"SchedulerConfiguration": null
|
||||
},
|
||||
{
|
||||
"CreateIndex": 39,
|
||||
"Description": "Node pool for production workloads.",
|
||||
"Meta": {
|
||||
"env": "production"
|
||||
},
|
||||
"ModifyIndex": 39,
|
||||
"Name": "prod",
|
||||
"SchedulerConfiguration": {
|
||||
"SchedulerAlgorithm": "spread"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Customize output with a Go template:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool list -t '{{range .}}{{.Name}} [{{.Meta.env}}] - {{.Description}}{{println}}{{end}}'
|
||||
all [<no value>] - Node pool with all nodes in the cluster.
|
||||
default [<no value>] - Default node pool.
|
||||
dev [development] - Node pool for dev workloads.
|
||||
prod [production] - Node pool for production workloads.
|
||||
```
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool info -t "{{.Name}} [{{.Meta.env}}] - {{.Description}}" prod
|
||||
prod [production] - Node pool for production workloads.
|
||||
```
|
||||
|
||||
Paginate list:
|
||||
|
||||
```shell-session
|
||||
$ nomad node pool list -per-page 2
|
||||
Name Description
|
||||
all Node pool with all nodes in the cluster.
|
||||
default Default node pool.
|
||||
|
||||
Results have been paginated. To get the next page run:
|
||||
|
||||
nomad node pool list -per-page 2 -page-token dev
|
||||
|
||||
$ nomad node pool list -per-page 2 -page-token dev
|
||||
Name Description
|
||||
dev Node pool for dev workloads.
|
||||
prod Node pool for production workloads.
|
||||
```
|
||||
|
||||
[api_filtering]: /nomad/api-docs#filtering
|
||||
[api_pagination]: /nomad/api-docs#pagination
|
|
@ -672,6 +672,31 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "node pool",
|
||||
"routes": [
|
||||
{
|
||||
"title": "Overview",
|
||||
"path": "commands/node-pool"
|
||||
},
|
||||
{
|
||||
"title": "apply",
|
||||
"path": "commands/node-pool/apply"
|
||||
},
|
||||
{
|
||||
"title": "delete",
|
||||
"path": "commands/node-pool/delete"
|
||||
},
|
||||
{
|
||||
"title": "info",
|
||||
"path": "commands/node-pool/info"
|
||||
},
|
||||
{
|
||||
"title": "list",
|
||||
"path": "commands/node-pool/list"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "operator",
|
||||
"routes": [
|
||||
|
|
Loading…
Reference in New Issue