diff --git a/command/server_members.go b/command/server_members.go index 769b20b817a..3f8e14942f7 100644 --- a/command/server_members.go +++ b/command/server_members.go @@ -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) } @@ -43,6 +46,7 @@ func (c *ServerMembersCommand) AutocompleteFlags() complete.Flags { return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), complete.Flags{ "-detailed": complete.PredictNothing, + "-json": complete.PredictNothing, }) } @@ -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 @@ -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 { @@ -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 +} diff --git a/command/server_members_test.go b/command/server_members_test.go index 9959157b588..1557a9798d0 100644 --- a/command/server_members_test.go +++ b/command/server_members_test.go @@ -1,6 +1,7 @@ package command import ( + "encoding/json" "fmt" "strings" "testing" @@ -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) { diff --git a/website/content/docs/commands/server/members.mdx b/website/content/docs/commands/server/members.mdx index bfd9ce255b1..6d23d0066b2 100644 --- a/website/content/docs/commands/server/members.mdx +++ b/website/content/docs/commands/server/members.mdx @@ -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: @@ -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" + } +] +``` \ No newline at end of file