From eeb376657584b04b5f3fc45b0e812e12670a7e94 Mon Sep 17 00:00:00 2001 From: Juana De La Cuesta Date: Mon, 20 Mar 2023 10:39:24 +0100 Subject: [PATCH] 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 * Update website/content/docs/commands/server/members.mdx Co-authored-by: James Rasell * cli: update the server memebers tests to use must * cli: add flags addition to changelog --------- Co-authored-by: James Rasell --- .changelog/16444.txt | 3 + command/server_members.go | 25 ++++- command/server_members_test.go | 99 ++++++++++--------- .../content/docs/commands/server/members.mdx | 45 +++++++++ 4 files changed, 126 insertions(+), 46 deletions(-) create mode 100644 .changelog/16444.txt diff --git a/.changelog/16444.txt b/.changelog/16444.txt new file mode 100644 index 000000000..45550cc83 --- /dev/null +++ b/.changelog/16444.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Added `-json` and `-t` flag to `server members` command +``` diff --git a/command/server_members.go b/command/server_members.go index 769b20b81..108ab9a7a 100644 --- a/command/server_members.go +++ b/command/server_members.go @@ -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) diff --git a/command/server_members_test.go b/command/server_members_test.go index 9959157b5..cec2b6281 100644 --- a/command/server_members_test.go +++ b/command/server_members_test.go @@ -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) } diff --git a/website/content/docs/commands/server/members.mdx b/website/content/docs/commands/server/members.mdx index bfd9ce255..3ace4d8db 100644 --- a/website/content/docs/commands/server/members.mdx +++ b/website/content/docs/commands/server/members.mdx @@ -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 +``` \ No newline at end of file