From b770f2b1ef4758d2edbaf04bfceb1f4657af495f Mon Sep 17 00:00:00 2001 From: Luiz Aoqui Date: Fri, 2 Jun 2023 15:49:57 -0400 Subject: [PATCH] node pools: implement CLI (#17388) --- api/contexts/contexts.go | 1 + api/jobs.go | 3 - api/node_pools.go | 105 +++++++++ api/node_pools_test.go | 210 ++++++++++++++++++ command/commands.go | 25 +++ command/node_pool.go | 101 +++++++++ command/node_pool_apply.go | 139 ++++++++++++ command/node_pool_apply_test.go | 206 +++++++++++++++++ command/node_pool_delete.go | 86 +++++++ command/node_pool_delete_test.go | 97 ++++++++ command/node_pool_info.go | 167 ++++++++++++++ command/node_pool_info_test.go | 172 ++++++++++++++ command/node_pool_list.go | 141 ++++++++++++ command/node_pool_list_test.go | 157 +++++++++++++ command/node_pool_test.go | 89 ++++++++ .../content/docs/commands/node-pool/apply.mdx | 71 ++++++ .../docs/commands/node-pool/delete.mdx | 32 +++ .../content/docs/commands/node-pool/index.mdx | 30 +++ .../content/docs/commands/node-pool/info.mdx | 71 ++++++ .../content/docs/commands/node-pool/list.mdx | 132 +++++++++++ website/data/docs-nav-data.json | 25 +++ 21 files changed, 2057 insertions(+), 3 deletions(-) create mode 100644 api/node_pools.go create mode 100644 api/node_pools_test.go create mode 100644 command/node_pool.go create mode 100644 command/node_pool_apply.go create mode 100644 command/node_pool_apply_test.go create mode 100644 command/node_pool_delete.go create mode 100644 command/node_pool_delete_test.go create mode 100644 command/node_pool_info.go create mode 100644 command/node_pool_info_test.go create mode 100644 command/node_pool_list.go create mode 100644 command/node_pool_list_test.go create mode 100644 command/node_pool_test.go create mode 100644 website/content/docs/commands/node-pool/apply.mdx create mode 100644 website/content/docs/commands/node-pool/delete.mdx create mode 100644 website/content/docs/commands/node-pool/index.mdx create mode 100644 website/content/docs/commands/node-pool/info.mdx create mode 100644 website/content/docs/commands/node-pool/list.mdx diff --git a/api/contexts/contexts.go b/api/contexts/contexts.go index 2ce523a72..5176f5b82 100644 --- a/api/contexts/contexts.go +++ b/api/contexts/contexts.go @@ -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" diff --git a/api/jobs.go b/api/jobs.go index 8d5f6373b..f768b3510 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -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. diff --git a/api/node_pools.go b/api/node_pools.go new file mode 100644 index 000000000..37ecf1555 --- /dev/null +++ b/api/node_pools.go @@ -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"` +} diff --git a/api/node_pools_test.go b/api/node_pools_test.go new file mode 100644 index 000000000..bd8d81cef --- /dev/null +++ b/api/node_pools_test.go @@ -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") + }) +} diff --git a/command/commands.go b/command/commands.go index 9f1d225e4..942acef0f 100644 --- a/command/commands.go +++ b/command/commands.go @@ -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, diff --git a/command/node_pool.go b/command/node_pool.go new file mode 100644 index 000000000..10e0b2e77 --- /dev/null +++ b/command/node_pool.go @@ -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 [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 + + List all node pools: + + $ nomad node pool list + + Fetch information on an existing node pool: + + $ nomad node info + + Delete a node pool: + + $ nomad node pool delete + + 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 + }) +} diff --git a/command/node_pool_apply.go b/command/node_pool_apply.go new file mode 100644 index 000000000..5ce976d83 --- /dev/null +++ b/command/node_pool_apply.go @@ -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] + + 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: ") + 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"` +} diff --git a/command/node_pool_apply_test.go b/command/node_pool_apply_test.go new file mode 100644 index 000000000..4f1be6807 --- /dev/null +++ b/command/node_pool_apply_test.go @@ -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) + }) + } +} diff --git a/command/node_pool_delete.go b/command/node_pool_delete.go new file mode 100644 index 000000000..20398ac20 --- /dev/null +++ b/command/node_pool_delete.go @@ -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] + + 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: ") + 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 +} diff --git a/command/node_pool_delete_test.go b/command/node_pool_delete_test.go new file mode 100644 index 000000000..13fe23928 --- /dev/null +++ b/command/node_pool_delete_test.go @@ -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) + }) + } +} diff --git a/command/node_pool_info.go b/command/node_pool_info.go new file mode 100644 index 000000000..6c848d3c5 --- /dev/null +++ b/command/node_pool_info.go @@ -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 + + 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: ") + 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 + } +} diff --git a/command/node_pool_info_test.go b/command/node_pool_info_test.go new file mode 100644 index 000000000..b8607ab9d --- /dev/null +++ b/command/node_pool_info_test.go @@ -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 = + +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)) + }) + } +} diff --git a/command/node_pool_list.go b/command/node_pool_list.go new file mode 100644 index 000000000..9a08e05e7 --- /dev/null +++ b/command/node_pool_list.go @@ -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 +} diff --git a/command/node_pool_list_test.go b/command/node_pool_list_test.go new file mode 100644 index 000000000..0dbe893d9 --- /dev/null +++ b/command/node_pool_list_test.go @@ -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 +prod-2 Pool prod-2`, + expectedCode: 0, + }, + { + name: "filter", + args: []string{ + "-filter", `Name contains "prod"`, + }, + expectedOut: ` +Name Description +prod-1 +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 `, + 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)) + }) + } +} diff --git a/command/node_pool_test.go b/command/node_pool_test.go new file mode 100644 index 000000000..be72d0ce6 --- /dev/null +++ b/command/node_pool_test.go @@ -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) + }) + } +} diff --git a/website/content/docs/commands/node-pool/apply.mdx b/website/content/docs/commands/node-pool/apply.mdx new file mode 100644 index 000000000..b98652c80 --- /dev/null +++ b/website/content/docs/commands/node-pool/apply.mdx @@ -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] +``` + +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 < +``` + +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"! +``` diff --git a/website/content/docs/commands/node-pool/index.mdx b/website/content/docs/commands/node-pool/index.mdx new file mode 100644 index 000000000..14a28d4fc --- /dev/null +++ b/website/content/docs/commands/node-pool/index.mdx @@ -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 [options]` + +Run `nomad node pool -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 diff --git a/website/content/docs/commands/node-pool/info.mdx b/website/content/docs/commands/node-pool/info.mdx new file mode 100644 index 000000000..e15231aef --- /dev/null +++ b/website/content/docs/commands/node-pool/info.mdx @@ -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] +``` + +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. +``` diff --git a/website/content/docs/commands/node-pool/list.mdx b/website/content/docs/commands/node-pool/list.mdx new file mode 100644 index 000000000..0e1889004 --- /dev/null +++ b/website/content/docs/commands/node-pool/list.mdx @@ -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] +``` + +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 [] - Node pool with all nodes in the cluster. +default [] - 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 diff --git a/website/data/docs-nav-data.json b/website/data/docs-nav-data.json index 3db88c626..4ef8ae9e8 100644 --- a/website/data/docs-nav-data.json +++ b/website/data/docs-nav-data.json @@ -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": [