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 -verbose
Show detailed information about each member. This dumps a raw set of tags Show detailed information about each member. This dumps a raw set of tags
which shows more information than the default output format. 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) return strings.TrimSpace(helpText)
} }
@ -43,6 +49,9 @@ func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{ complete.Flags{
"-detailed": complete.PredictNothing, "-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) Name() string { return "server members" }
func (c *ServerMembersCommand) Run(args []string) int { 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 := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) } flags.Usage = func() { c.Ui.Output(c.Help()) }
flags.BoolVar(&detailed, "detailed", false, "Show detailed output") flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
flags.BoolVar(&verbose, "verbose", 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 { if err := flags.Parse(args); err != nil {
return 1 return 1
@ -103,6 +115,17 @@ func (c *ServerMembersCommand) Run(args []string) int {
// Sort the members // Sort the members
sort.Sort(api.AgentMembersNameSort(srvMembers.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. // Determine the leaders per region.
leaders, leaderErr := regionLeaders(client, srvMembers.Members) leaders, leaderErr := regionLeaders(client, srvMembers.Members)

View File

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

View File

@ -36,6 +36,10 @@ capability.
for each member. This mode reveals additional information not displayed in for each member. This mode reveals additional information not displayed in
the standard output format. 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 ## Examples
Default view: 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-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 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
```