cli: Add `json` and `-t` flags to `server members` command (#16444)

* cli: Add  and  flags to server members

* Update website/content/docs/commands/server/members.mdx

Co-authored-by: James Rasell <jrasell@users.noreply.github.com>

* Update website/content/docs/commands/server/members.mdx

Co-authored-by: James Rasell <jrasell@users.noreply.github.com>

* cli: update the server memebers tests to use must

* cli: add flags addition to changelog

---------

Co-authored-by: James Rasell <jrasell@users.noreply.github.com>
This commit is contained in:
Juana De La Cuesta 2023-03-20 10:39:24 +01:00 committed by GitHub
parent e4e53872be
commit eeb3766575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 126 additions and 46 deletions

3
.changelog/16444.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Added `-json` and `-t` flag to `server members` command
```

View File

@ -35,6 +35,12 @@ Server Members Options:
-verbose
Show detailed information about each member. This dumps a raw set of tags
which shows more information than the default output format.
-json
Output the latest information about each member in a JSON format.
-t
Format and display latest information about each member using a Go template.
`
return strings.TrimSpace(helpText)
}
@ -43,6 +49,9 @@ func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-detailed": complete.PredictNothing,
"-verbose": complete.PredictNothing,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
})
}
@ -57,12 +66,15 @@ func (c *ServerMembersCommand) Synopsis() string {
func (c *ServerMembersCommand) Name() string { return "server members" }
func (c *ServerMembersCommand) Run(args []string) int {
var detailed, verbose bool
var detailed, verbose, json bool
var tmpl string
flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
flags.BoolVar(&verbose, "verbose", false, "Show detailed output")
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&tmpl, "t", "", "")
if err := flags.Parse(args); err != nil {
return 1
@ -103,6 +115,17 @@ func (c *ServerMembersCommand) Run(args []string) int {
// Sort the members
sort.Sort(api.AgentMembersNameSort(srvMembers.Members))
if json || len(tmpl) > 0 {
out, err := Format(json, tmpl, srvMembers.Members)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
c.Ui.Output(out)
return 0
}
// Determine the leaders per region.
leaders, leaderErr := regionLeaders(client, srvMembers.Members)

View File

@ -1,13 +1,16 @@
package command
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/mitchellh/cli"
"github.com/shoenig/test/must"
)
func TestServerMembersCommand_Implements(t *testing.T) {
@ -25,30 +28,46 @@ func TestServerMembersCommand_Run(t *testing.T) {
// Get our own node name
name, err := client.Agent().NodeName()
if err != nil {
t.Fatalf("err: %s", err)
}
must.NoError(t, err)
// Query the members
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
code := cmd.Run([]string{"-address=" + url})
if out := ui.OutputWriter.String(); !strings.Contains(out, name) {
t.Fatalf("expected %q in output, got: %s", name, out)
}
ui.OutputWriter.Reset()
// Query members with verbose output
if code := cmd.Run([]string{"-address=" + url, "-verbose"}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
code = cmd.Run([]string{"-address=" + url, "-verbose"})
must.Zero(t, code)
// Still support previous detailed flag
if code := cmd.Run([]string{"-address=" + url, "-detailed"}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
if out := ui.OutputWriter.String(); !strings.Contains(out, "Tags") {
t.Fatalf("expected tags in output, got: %s", out)
}
code = cmd.Run([]string{"-address=" + url, "-detailed"})
must.Zero(t, code)
must.StrContains(t, ui.OutputWriter.String(), "Tags")
ui.OutputWriter.Reset()
// List json
code = cmd.Run([]string{"-address=" + url, "-json"})
must.Zero(t, code)
outJson := []api.AgentMember{}
err = json.Unmarshal(ui.OutputWriter.Bytes(), &outJson)
must.NoError(t, err)
ui.OutputWriter.Reset()
// Go template to format the output
code = cmd.Run([]string{"-address=" + url, "-t", "{{range .}}{{ .Status }}{{end}}"})
must.Zero(t, code)
out := ui.OutputWriter.String()
must.StrContains(t, out, "alive")
ui.ErrorWriter.Reset()
}
func TestMembersCommand_Fails(t *testing.T) {
@ -57,21 +76,17 @@ func TestMembersCommand_Fails(t *testing.T) {
cmd := &ServerMembersCommand{Meta: Meta{Ui: ui}}
// Fails on misuse
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
t.Fatalf("expected help output, got: %s", out)
}
code := cmd.Run([]string{"some", "bad", "args"})
must.One(t, code)
must.StrContains(t, ui.ErrorWriter.String(), commandErrorText(cmd))
ui.ErrorWriter.Reset()
// Fails on connection failure
if code := cmd.Run([]string{"-address=nope"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying servers") {
t.Fatalf("expected failed query error, got: %s", out)
}
code = cmd.Run([]string{"-address=nope"})
must.One(t, code)
must.StrContains(t, ui.ErrorWriter.String(), "Error querying servers")
}
// Tests that a single server region that left should still
@ -99,35 +114,29 @@ func TestServerMembersCommand_MultiRegion_Leave(t *testing.T) {
addr := fmt.Sprintf("127.0.0.1:%d",
srv1.Agent.Server().GetConfig().SerfConfig.MemberlistConfig.BindPort)
if _, err := srv2.Agent.Server().Join([]string{addr}); err != nil {
t.Fatalf("Join err: %v", err)
}
_, err := srv2.Agent.Server().Join([]string{addr})
must.NoError(t, err)
ui := cli.NewMockUi()
cmd := &ServerMembersCommand{Meta: Meta{Ui: ui}}
// Get our own node name
name, err := client1.Agent().NodeName()
if err != nil {
t.Fatalf("err: %s", err)
}
must.NoError(t, err)
// Query the members
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
if out := ui.OutputWriter.String(); !strings.Contains(out, name) {
t.Fatalf("expected %q in output, got: %s", name, out)
}
code := cmd.Run([]string{"-address=" + url})
must.Zero(t, code)
must.StrContains(t, ui.OutputWriter.String(), name)
ui.OutputWriter.Reset()
// Make one of the servers leave
srv2.Agent.Leave()
// Query again, should still contain expected output
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
if out := ui.OutputWriter.String(); !strings.Contains(out, name) {
t.Fatalf("expected %q in output, got: %s", name, out)
}
code = cmd.Run([]string{"-address=" + url})
must.Zero(t, code)
must.StrContains(t, ui.OutputWriter.String(), name)
}

View File

@ -36,6 +36,10 @@ capability.
for each member. This mode reveals additional information not displayed in
the standard output format.
- `-json`: Output the server memebers in its JSON format.
- `-t`: Format and display the server memebers using a Go template.
## Examples
Default view:
@ -57,3 +61,44 @@ server-1.global 10.0.0.8 4648 alive true 2 3 1.3.0
server-2.global 10.0.0.9 4648 alive false 2 3 1.3.0 dc1 global id=04594bee-fec9-4cec-f308-eebe82025ae7,dc=dc1,expect=3,rpc_addr=10.0.0.9,raft_vsn=3,port=4647,role=nomad,region=global,build=1.3.0
server-3.global 10.0.0.10 4648 alive false 2 3 1.3.0 dc1 global region=global,dc=dc1,rpc_addr=10.0.0.10,raft_vsn=3,build=1.3.0,expect=3,id=59542f6c-fb0e-50f1-4c9f-98bb593e9fe8,role=nomad,port=4647
```
The `-json` flag can be used to get the latest server members information in json format:
```shell-session
$ nomad server members -json
[
{
"Addr": "127.0.0.1",
"DelegateCur": 4,
"DelegateMax": 5,
"DelegateMin": 2,
"Name": "bacon-mac.global",
"Port": 4648,
"ProtocolCur": 2,
"ProtocolMax": 5,
"ProtocolMin": 1,
"Status": "alive",
"Tags": {
"build": "1.5.0",
"region": "global",
"rpc_addr": "127.0.0.1",
"role": "nomad",
"raft_vsn": "3",
"bootstrap": "1",
"expect": "1",
"dc": "dc1",
"port": "4647",
"revision": "fc40c491cacec3d8ec3f2f98cd82b9068a50797c",
"vsn": "1",
"id": "354fd9a8-4228-ca8e-8b93-3cf002514910"
}
}
]
```
Or use the `-t` flag to format and display the server members information using a Go template:
```shell-session
$ nomad server members -t '{{range .}}{{printf "%s: %s" .Name .Status }}{{end}}'
bacon-mac.global: alive
```