node pools: implement CLI (#17388)

This commit is contained in:
Luiz Aoqui 2023-06-02 15:49:57 -04:00 committed by GitHub
parent 6758379e48
commit b770f2b1ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2057 additions and 3 deletions

View File

@ -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"

View File

@ -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.

105
api/node_pools.go Normal file
View File

@ -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"`
}

210
api/node_pools_test.go Normal file
View File

@ -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")
})
}

View File

@ -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,

101
command/node_pool.go Normal file
View File

@ -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
})
}

139
command/node_pool_apply.go Normal file
View File

@ -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"`
}

View File

@ -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)
})
}
}

View File

@ -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
}

View File

@ -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)
})
}
}

167
command/node_pool_info.go Normal file
View File

@ -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
}
}

View File

@ -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))
})
}
}

141
command/node_pool_list.go Normal file
View File

@ -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
}

View File

@ -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))
})
}
}

89
command/node_pool_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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"!
```

View File

@ -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"!
```

View File

@ -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

View File

@ -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.
```

View File

@ -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

View File

@ -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": [