Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add JSON output to server members command #16334

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 76 additions & 9 deletions command/server_members.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ 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 Server members information in JSON format.
`
return strings.TrimSpace(helpText)
}
Expand All @@ -43,6 +46,7 @@ func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-detailed": complete.PredictNothing,
"-json": complete.PredictNothing,
})
}

Expand All @@ -57,12 +61,13 @@ 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

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, "Show output in JSON format")

if err := flags.Parse(args); err != nil {
return 1
Expand Down Expand Up @@ -106,16 +111,34 @@ func (c *ServerMembersCommand) Run(args []string) int {
// Determine the leaders per region.
leaders, leaderErr := regionLeaders(client, srvMembers.Members)

// Format the list
var out []string
if verbose {
out = verboseOutput(srvMembers.Members, leaders)
if !json {
// Format the list
var out []string
if verbose {
out = verboseOutput(srvMembers.Members, leaders)
} else {
out = standardOutput(srvMembers.Members, leaders)
}

// Dump the list
c.Ui.Output(columnize.SimpleFormat(out))
} else {
out = standardOutput(srvMembers.Members, leaders)
}
var output_map []map[string]any
if verbose {
output_map = mapVerboseOutput(srvMembers.Members, leaders)
} else {
output_map = mapStandardOutput(srvMembers.Members, leaders)
}

// Dump the list
c.Ui.Output(columnize.SimpleFormat(out))
json_output, jsonErr := Format(true, "", output_map)
if jsonErr != nil {
c.Ui.Output("")
c.Ui.Warn(fmt.Sprintf("Error formating json: %s", jsonErr))
return 1
}

c.Ui.Output(json_output)
}

// If there were leader errors display a warning
if leaderErr != nil {
Expand Down Expand Up @@ -216,3 +239,47 @@ func isLeader(member *api.AgentMember, leaders map[string]string) bool {
regLeader, ok := leaders[reg]
return ok && regLeader == addr
}

// mapStandardOutput returns the standard output in a map so it can be used to create JSON output
func mapStandardOutput(members []*api.AgentMember, leaders map[string]string) []map[string]any {
membersMap := make([]map[string]any, len(members)-1, len(members))

for _, member := range members {
membersMap = append(membersMap, map[string]any{
"Name": member.Name,
"Address": member.Addr,
"Port": member.Port,
"Status": member.Status,
"Leader": isLeader(member, leaders),
"Raft Version": member.Tags["raft_vsn"],
"Build": member.Tags["build"],
"Datacenter": member.Tags["dc"],
"Region": member.Tags["region"],
})
}

return membersMap
}

// mapStandardOutput returns the verbose output in a map so it can be used to create JSON output
func mapVerboseOutput(members []*api.AgentMember, leaders map[string]string) []map[string]any {
membersMap := make([]map[string]any, len(members)-1, len(members))

for _, member := range members {
membersMap = append(membersMap, map[string]any{
"Name": member.Name,
"Address": member.Addr,
"Port": member.Port,
"Status": member.Status,
"Leader": isLeader(member, leaders),
"Protocol": member.ProtocolCur,
"Raft Version": member.Tags["raft_vsn"],
"Build": member.Tags["build"],
"Datacenter": member.Tags["dc"],
"Region": member.Tags["region"],
"Tags": member.Tags,
})
}

return membersMap
}
24 changes: 24 additions & 0 deletions command/server_members_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package command

import (
"encoding/json"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -49,6 +50,29 @@ func TestServerMembersCommand_Run(t *testing.T) {
if out := ui.OutputWriter.String(); !strings.Contains(out, "Tags") {
t.Fatalf("expected tags in output, got: %s", out)
}
// Query members with JSON normal output
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
var jsonMap []map[string]interface{}
var out string
if code := cmd.Run([]string{"-address=" + url, "-json"}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
out = ui.OutputWriter.String()
if err = json.Unmarshal([]byte(out), &jsonMap); err != nil {
t.Fatalf("failed to parse JSON: %s", err)
}

// Query members with JSON verbose output
ui.OutputWriter.Reset()
ui.ErrorWriter.Reset()
if code := cmd.Run([]string{"-address=" + url, "-detailed", "-json"}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
out = ui.OutputWriter.String()
if err = json.Unmarshal([]byte(out), &jsonMap); err != nil {
t.Fatalf("failed to parse JSON: %s", err)
}
}

func TestMembersCommand_Fails(t *testing.T) {
Expand Down
56 changes: 56 additions & 0 deletions website/content/docs/commands/server/members.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ capability.
for each member. This mode reveals additional information not displayed in
the standard output format.

- `-json`: Output the Server members information in JSON format. Can be used both
for normal output and detailed output.

## Examples

Default view:
Expand All @@ -57,3 +60,56 @@ 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
```

JSON view:

```shell-session
$ nomad server members -json
[
{
"Leader": true,
"Raft Version": "3",
"Build": "1.5.1-dev",
"Region": "global",
"Name": "server-1.global",
"Status": "alive",
"Datacenter": "dc1",
"Address": "172.18.1.253",
"Port": 4648
}
]
```

JSON verbose view:

```shell-session
$ nomad server members -verbose -json
[
{
"Port": 4648,
"Leader": true,
"Protocol": 2,
"Datacenter": "dc1",
"Tags": {
"raft_vsn": "3",
"region": "global",
"build": "1.5.1-dev",
"bootstrap": "1",
"role": "nomad",
"expect": "1",
"dc": "dc1",
"rpc_addr": "172.18.1.253",
"revision": "b07af5761846a59ac91e4826c9a0f8e38c91f70b+CHANGES",
"id": "34385ed2-1dc8-ad7d-1a3a-ba665fda91c8",
"vsn": "1",
"port": "4647"
},
"Region": "global",
"Name": "server-1.global",
"Address": "172.18.1.253",
"Status": "alive",
"Raft Version": "3",
"Build": "1.5.1-dev"
}
]
```