node pool: implement `nomad node pool nodes` CLI (#17444)

This commit is contained in:
Luiz Aoqui 2023-06-07 10:37:27 -04:00 committed by GitHub
parent 06fc284644
commit 5878113c41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 456 additions and 5 deletions

View File

@ -101,6 +101,18 @@ func (n *NodePools) ListJobs(poolName string, q *QueryOptions) ([]*JobListStub,
return resp, qm, nil
}
// ListNodes is used to list all the nodes in a node pool.
func (n *NodePools) ListNodes(poolName string, q *QueryOptions) ([]*NodeListStub, *QueryMeta, error) {
var resp []*NodeListStub
qm, err := n.client.query(
fmt.Sprintf("/v1/node/pool/%s/nodes", url.PathEscape(poolName)),
&resp, q)
if err != nil {
return nil, nil, err
}
return resp, qm, nil
}
// NodePool is used to serialize a node pool.
type NodePool struct {
Name string `hcl:"name,label"`

View File

@ -646,6 +646,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"node pool nodes": func() (cli.Command, error) {
return &NodePoolNodesCommand{
Meta: meta,
}, nil
},
"operator": func() (cli.Command, error) {
return &OperatorCommand{
Meta: meta,

164
command/node_pool_nodes.go Normal file
View File

@ -0,0 +1,164 @@
// 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 NodePoolNodesCommand struct {
Meta
}
func (c *NodePoolNodesCommand) Name() string {
return "node pool nodes"
}
func (c *NodePoolNodesCommand) Synopsis() string {
return "Fetch a list of nodes in a node pool"
}
func (c *NodePoolNodesCommand) Help() string {
helpText := `
Usage: nomad node pool nodes <node-pool>
Node pool nodes is used to list nodes in a given node pool.
If ACLs are enabled, this command requires a token with the 'node:read'
capability and the 'read' capability in a 'node_pool' policy that matches the
node pool being targeted.
General Options:
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
Node Pool Nodes Options:
-filter
Specifies an expression used to filter nodes from the results. The filter
is applied to the nodes and not the node pool.
-json
Output the list 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 nodes list using a Go template.
-verbose
Display full information.
`
return strings.TrimSpace(helpText)
}
func (c *NodePoolNodesCommand) 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,
"-verbose": complete.PredictNothing,
})
}
func (c *NodePoolNodesCommand) AutocompleteArgs() complete.Predictor {
return nodePoolPredictor(c.Client, nil)
}
func (c *NodePoolNodesCommand) Run(args []string) int {
var json, verbose bool
var perPage int
var pageToken, filter, tmpl string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&filter, "filter", "", "")
flags.StringVar(&pageToken, "page-token", "", "")
flags.IntVar(&perPage, "per-page", 0, "")
flags.StringVar(&tmpl, "t", "", "")
flags.BoolVar(&verbose, "verbose", 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: <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 := 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
}
opts := &api.QueryOptions{
Filter: filter,
PerPage: int32(perPage),
NextToken: pageToken,
}
nodes, qm, err := client.NodePools().ListNodes(pool.Name, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying nodes: %s", err))
return 1
}
if len(nodes) == 0 {
c.Ui.Output("No nodes")
return 0
}
// Format output if requested.
if json || tmpl != "" {
out, err := Format(json, tmpl, nodes)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(out)
return 0
}
c.Ui.Output(formatNodeStubList(nodes, verbose))
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,187 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"encoding/json"
"sort"
"strings"
"testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli"
"github.com/shoenig/test/must"
)
func TestNodePoolNodesCommand_Implements(t *testing.T) {
ci.Parallel(t)
var _ cli.Command = &NodePoolNodesCommand{}
}
func TestNodePoolNodesCommand_Run(t *testing.T) {
ci.Parallel(t)
// Start test server.
srv, client, url := testServer(t, true, func(c *agent.Config) {
c.Client.Enabled = false
})
testutil.WaitForLeader(t, srv.Agent.RPC)
// Start some test clients.
rpcAddr := srv.GetConfig().AdvertiseAddrs.RPC
clientNodePoolConfig := func(pool string) func(*agent.Config) {
return func(c *agent.Config) {
c.Client.NodePool = pool
c.Client.Servers = []string{rpcAddr}
c.Client.Enabled = true
c.Server.Enabled = false
}
}
testClient(t, "client-default", clientNodePoolConfig(""))
testClient(t, "client-dev", clientNodePoolConfig("dev"))
testClient(t, "client-prod-1", clientNodePoolConfig("prod"))
testClient(t, "client-prod-2", clientNodePoolConfig("prod"))
waitForNodes(t, client)
nodes, _, err := client.Nodes().List(nil)
must.NoError(t, err)
// Nodes().List() sort results by CreateIndex, but for pagination we need
// nodes sorted by ID.
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].ID < nodes[j].ID
})
testCases := []struct {
name string
args []string
expectedCode int
expectedNodes []string
expectedErr string
}{
{
name: "nodes in prod",
args: []string{"prod"},
expectedCode: 0,
expectedNodes: []string{
"client-prod-1",
"client-prod-2",
},
},
{
name: "nodes in all",
args: []string{"all"},
expectedCode: 0,
expectedNodes: []string{
"client-default",
"client-dev",
"client-prod-1",
"client-prod-2",
},
},
{
name: "filter nodes",
args: []string{"-filter", `Name matches "dev"`, "all"},
expectedCode: 0,
expectedNodes: []string{
"client-dev",
},
},
{
name: "pool by prefix",
args: []string{"def"},
expectedNodes: []string{
"client-default",
},
},
{
name: "paginate page 1",
args: []string{"-per-page=2", "all"},
expectedCode: 0,
expectedNodes: []string{
nodes[0].Name,
nodes[1].Name,
},
},
{
name: "paginate page 2",
args: []string{"-per-page", "2", "-page-token", nodes[2].ID, "all"},
expectedCode: 0,
expectedNodes: []string{
nodes[2].Name,
nodes[3].Name,
},
},
{
name: "missing pool name",
args: []string{},
expectedCode: 1,
expectedErr: "This command takes one argument",
},
{
name: "prefix match multiple",
args: []string{"de"},
expectedCode: 1,
expectedErr: "Prefix matched multiple node pools",
},
{
name: "json and template not allowed",
args: []string{"-t", "{{.}}", "all"},
expectedCode: 1,
expectedErr: "Both json and template formatting are not allowed",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Initialize UI and command.
ui := cli.NewMockUi()
cmd := &NodePoolNodesCommand{Meta: Meta{Ui: ui}}
// Run command.
// Add -json to help parse and validate results.
args := []string{"-address", url, "-json"}
args = append(args, tc.args...)
code := cmd.Run(args)
if tc.expectedErr != "" {
must.StrContains(t, ui.ErrorWriter.String(), strings.TrimSpace(tc.expectedErr))
} else {
must.Eq(t, "", ui.ErrorWriter.String())
var nodes []*api.NodeListStub
err := json.Unmarshal(ui.OutputWriter.Bytes(), &nodes)
must.NoError(t, err)
gotNodes := helper.ConvertSlice(nodes,
func(n *api.NodeListStub) string { return n.Name })
must.SliceContainsAll(t, tc.expectedNodes, gotNodes)
}
must.Eq(t, tc.expectedCode, code)
})
}
t.Run("template formatting", func(t *testing.T) {
// Initialize UI and command.
ui := cli.NewMockUi()
cmd := &NodePoolNodesCommand{Meta: Meta{Ui: ui}}
// Run command.
args := []string{"-address", url, "-t", `{{range .}}{{.ID}} {{end}}`, "all"}
code := cmd.Run(args)
must.Zero(t, code)
var expected string
for _, n := range nodes {
expected += n.ID + " "
}
got := ui.OutputWriter.String()
must.Eq(t, strings.TrimSpace(expected), strings.TrimSpace(got))
})
}

View File

@ -349,15 +349,15 @@ This endpoint lists the jobs in a node pool.
| Method | Path | Produces |
| ------ | -------------------------------- | ------------------ |
| `GET` | `/v1/node/pool/:node_pool/jobs` | `application/json` |
| `GET` | `/v1/node/pool/:node_pool/jobs` | `application/json` |
The table below shows this endpoint's support for
[blocking queries](/nomad/api-docs#blocking-queries) and
[required ACLs](/nomad/api-docs#acls).
| Blocking Queries | ACL Required |
| ---------------- | ----------------------------------- |
| `YES` | `namespace:read` <br /> `node_pool:read` |
| Blocking Queries | ACL Required |
| ---------------- | -------------------------------------------- |
| `YES` | `namespace:read-job` <br /> `node_pool:read` |
### Parameters

View File

@ -26,8 +26,11 @@ following subcommands are available:
- [`node pool list`][list] - Retrieve a list of node pools.
- [`node pool nodes`][nodes] - Retrieve a list of nodes in a node pool.
[apply]: /nomad/docs/commands/node-pool/apply
[delete]: /nomad/docs/commands/node-pool/delete
[info]: /nomad/docs/commands/node-pool/info
[jobs]: /nomad/docs/commands/node-pool/jobs
[list]: /nomad/docs/commands/node-pool/list
[nodes]: /nomad/docs/commands/node-pool/nodes

View File

@ -23,7 +23,7 @@ token has the 'read' capability.
@include 'general_options_no_namespace.mdx'
## Info Options
## List Options
- `-filter`: Specifies an expression used to [filter results][api_filtering].

View File

@ -0,0 +1,76 @@
---
layout: docs
page_title: 'Commands: node pool nodes'
description: |
The node pool nodes command is used to list nodes in node pool.
---
# Command: node pool nodes
The `node pool nodes` command is used to list nodes in a node pool.
## Usage
```plaintext
nomad node pool nodes [options] <node-pool>
```
If ACLs are enabled, this command requires a token with the `node:read`
capability and the `read` capability in a `node_pool` policy that matches the
node pool being targeted.
## General Options
@include 'general_options_no_namespace.mdx'
## Nodes Options
- `-filter`: Specifies an expression used to [filter results][api_filtering].
- `-json`: Output the nodes 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 nodes using a Go template.
- `-verbose`: Display full information.
## Examples
List nodes in the `prod` node pool:
```shell-session
$ nomad node pool nodes prod
ID DC Name Class Drain Eligibility Status
31c5347f dc2 client-3 <none> false eligible ready
3ed547cd dc1 client-1 <none> false eligible ready
6e98e378 dc2 client-2 <none> false eligible ready
```
Filter nodes with an expression:
```shell-session
$ nomad node pool nodes -filter='Datacenter == "dc2"' prod
ID DC Name Class Drain Eligibility Status
31c5347f dc2 client-3 <none> false eligible ready
6e98e378 dc2 client-2 <none> false eligible ready
```
Paginate list:
```shell-session
$ nomad node pool nodes -per-page=2 prod
ID DC Name Class Drain Eligibility Status
31c5347f dc2 us-client-3 <none> false eligible ready
3ed547cd dc1 us-client-1 <none> false eligible ready
Results have been paginated. To get the next page run:
nomad node pool nodes -per-page=2 prod -page-token 6e98e378-b41a-86ac-8e5e-5ca8ab66236d
```
[api_filtering]: /nomad/api-docs#filtering
[api_pagination]: /nomad/api-docs#pagination

View File

@ -698,6 +698,10 @@
{
"title": "list",
"path": "commands/node-pool/list"
},
{
"title": "nodes",
"path": "commands/node-pool/nodes"
}
]
},