mirror of
https://github.com/status-im/consul.git
synced 2025-01-12 14:55:02 +00:00
cli: update consul members output to display partitions and sort the results usefully (#11446)
This commit is contained in:
parent
c8cafb7654
commit
61361c2e5d
3
.changelog/11446.txt
Normal file
3
.changelog/11446.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
cli: update consul members output to display partitions and sort the results usefully
|
||||||
|
```
|
12
api/agent.go
12
api/agent.go
@ -144,11 +144,23 @@ const (
|
|||||||
// that the member represents a Consul server.
|
// that the member represents a Consul server.
|
||||||
MemberTagValueRoleServer = "consul"
|
MemberTagValueRoleServer = "consul"
|
||||||
|
|
||||||
|
// MemberTagValueRoleClient is the value of the MemberTagKeyRole used to indicate
|
||||||
|
// that the member represents a Consul client.
|
||||||
|
MemberTagValueRoleClient = "node"
|
||||||
|
|
||||||
|
// MemberTagKeyDatacenter is the key used to indicate which datacenter this member is in.
|
||||||
|
MemberTagKeyDatacenter = "dc"
|
||||||
|
|
||||||
// MemberTagKeySegment is the key name of the tag used to indicate which network
|
// MemberTagKeySegment is the key name of the tag used to indicate which network
|
||||||
// segment this member is in.
|
// segment this member is in.
|
||||||
// Network Segments are a Consul Enterprise feature.
|
// Network Segments are a Consul Enterprise feature.
|
||||||
MemberTagKeySegment = "segment"
|
MemberTagKeySegment = "segment"
|
||||||
|
|
||||||
|
// MemberTagKeyPartition is the key name of the tag used to indicate which partition
|
||||||
|
// this member is in.
|
||||||
|
// Partitions are a Consul Enterprise feature.
|
||||||
|
MemberTagKeyPartition = "ap"
|
||||||
|
|
||||||
// MemberTagKeyBootstrap is the key name of the tag used to indicate whether this
|
// MemberTagKeyBootstrap is the key name of the tag used to indicate whether this
|
||||||
// agent was started with the "bootstrap" configuration enabled
|
// agent was started with the "bootstrap" configuration enabled
|
||||||
MemberTagKeyBootstrap = "bootstrap"
|
MemberTagKeyBootstrap = "bootstrap"
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/ryanuber/columnize"
|
"github.com/ryanuber/columnize"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
consulapi "github.com/hashicorp/consul/api"
|
consulapi "github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/command/flags"
|
"github.com/hashicorp/consul/command/flags"
|
||||||
)
|
)
|
||||||
@ -90,11 +91,17 @@ func (c *cmd) Run(args []string) int {
|
|||||||
n := len(members)
|
n := len(members)
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
member := members[i]
|
member := members[i]
|
||||||
if member.Tags["segment"] == "" {
|
if member.Tags[consulapi.MemberTagKeyPartition] == "" {
|
||||||
member.Tags["segment"] = "<default>"
|
member.Tags[consulapi.MemberTagKeyPartition] = "default"
|
||||||
}
|
}
|
||||||
if c.segment == consulapi.AllSegments && member.Tags["role"] == "consul" {
|
if structs.IsDefaultPartition(member.Tags[consulapi.MemberTagKeyPartition]) {
|
||||||
member.Tags["segment"] = "<all>"
|
if c.segment == consulapi.AllSegments && member.Tags[consulapi.MemberTagKeyRole] == consulapi.MemberTagValueRoleServer {
|
||||||
|
member.Tags[consulapi.MemberTagKeySegment] = "<all>"
|
||||||
|
} else if member.Tags[consulapi.MemberTagKeySegment] == "" {
|
||||||
|
member.Tags[consulapi.MemberTagKeySegment] = "<default>"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
member.Tags[consulapi.MemberTagKeySegment] = ""
|
||||||
}
|
}
|
||||||
statusString := serf.MemberStatus(member.Status).String()
|
statusString := serf.MemberStatus(member.Status).String()
|
||||||
if !statusRe.MatchString(statusString) {
|
if !statusRe.MatchString(statusString) {
|
||||||
@ -111,7 +118,7 @@ func (c *cmd) Run(args []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(ByMemberNameAndSegment(members))
|
sort.Sort(ByMemberNamePartitionAndSegment(members))
|
||||||
|
|
||||||
// Generate the output
|
// Generate the output
|
||||||
var result []string
|
var result []string
|
||||||
@ -128,29 +135,69 @@ func (c *cmd) Run(args []string) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// so we can sort members by name
|
// ByMemberNamePartitionAndSegment sorts members by name with a stable sort.
|
||||||
type ByMemberNameAndSegment []*consulapi.AgentMember
|
//
|
||||||
|
// 1. servers go at the top
|
||||||
|
// 2. members of the default partition go next (including segments)
|
||||||
|
// 3. members of partitions follow
|
||||||
|
type ByMemberNamePartitionAndSegment []*consulapi.AgentMember
|
||||||
|
|
||||||
func (m ByMemberNameAndSegment) Len() int { return len(m) }
|
func (m ByMemberNamePartitionAndSegment) Len() int { return len(m) }
|
||||||
func (m ByMemberNameAndSegment) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
func (m ByMemberNamePartitionAndSegment) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
||||||
func (m ByMemberNameAndSegment) Less(i, j int) bool {
|
func (m ByMemberNamePartitionAndSegment) Less(i, j int) bool {
|
||||||
|
tags_i := parseTags(m[i].Tags)
|
||||||
|
tags_j := parseTags(m[j].Tags)
|
||||||
|
|
||||||
|
// put role=consul first
|
||||||
switch {
|
switch {
|
||||||
case m[i].Tags["segment"] < m[j].Tags["segment"]:
|
case tags_i.role == consulapi.MemberTagValueRoleServer && tags_j.role != consulapi.MemberTagValueRoleServer:
|
||||||
return true
|
return true
|
||||||
case m[i].Tags["segment"] > m[j].Tags["segment"]:
|
case tags_i.role != consulapi.MemberTagValueRoleServer && tags_j.role == consulapi.MemberTagValueRoleServer:
|
||||||
return false
|
return false
|
||||||
default:
|
|
||||||
return m[i].Name < m[j].Name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// then the default partitions
|
||||||
|
switch {
|
||||||
|
case isDefault(tags_i.partition) && !isDefault(tags_j.partition):
|
||||||
|
return true
|
||||||
|
case !isDefault(tags_i.partition) && isDefault(tags_j.partition):
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// then by segments within the default
|
||||||
|
switch {
|
||||||
|
case tags_i.segment < tags_j.segment:
|
||||||
|
return true
|
||||||
|
case tags_i.segment > tags_j.segment:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// then by partitions
|
||||||
|
switch {
|
||||||
|
case tags_i.partition < tags_j.partition:
|
||||||
|
return true
|
||||||
|
case tags_i.partition > tags_j.partition:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally by name
|
||||||
|
return m[i].Name < m[j].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDefault(s string) bool {
|
||||||
|
// NOTE: we can't use structs.IsDefaultPartition since that discards the input
|
||||||
|
return s == "" || s == "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
// standardOutput is used to dump the most useful information about nodes
|
// standardOutput is used to dump the most useful information about nodes
|
||||||
// in a more human-friendly format
|
// in a more human-friendly format
|
||||||
func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
|
func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
|
||||||
result := make([]string, 0, len(members))
|
result := make([]string, 0, len(members))
|
||||||
header := "Node\x1fAddress\x1fStatus\x1fType\x1fBuild\x1fProtocol\x1fDC\x1fSegment"
|
header := "Node\x1fAddress\x1fStatus\x1fType\x1fBuild\x1fProtocol\x1fDC\x1fPartition\x1fSegment"
|
||||||
result = append(result, header)
|
result = append(result, header)
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
|
tags := parseTags(member.Tags)
|
||||||
|
|
||||||
addr := net.TCPAddr{IP: net.ParseIP(member.Addr), Port: int(member.Port)}
|
addr := net.TCPAddr{IP: net.ParseIP(member.Addr), Port: int(member.Port)}
|
||||||
protocol := member.Tags["vsn"]
|
protocol := member.Tags["vsn"]
|
||||||
build := member.Tags["build"]
|
build := member.Tags["build"]
|
||||||
@ -159,21 +206,21 @@ func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
|
|||||||
} else if idx := strings.Index(build, ":"); idx != -1 {
|
} else if idx := strings.Index(build, ":"); idx != -1 {
|
||||||
build = build[:idx]
|
build = build[:idx]
|
||||||
}
|
}
|
||||||
dc := member.Tags["dc"]
|
|
||||||
segment := member.Tags["segment"]
|
|
||||||
|
|
||||||
statusString := serf.MemberStatus(member.Status).String()
|
statusString := serf.MemberStatus(member.Status).String()
|
||||||
switch member.Tags["role"] {
|
switch tags.role {
|
||||||
case "node":
|
case consulapi.MemberTagValueRoleClient:
|
||||||
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fclient\x1f%s\x1f%s\x1f%s\x1f%s",
|
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fclient\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s",
|
||||||
member.Name, addr.String(), statusString, build, protocol, dc, segment)
|
member.Name, addr.String(), statusString, build, protocol, tags.datacenter, tags.partition, tags.segment)
|
||||||
result = append(result, line)
|
result = append(result, line)
|
||||||
case "consul":
|
|
||||||
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fserver\x1f%s\x1f%s\x1f%s\x1f%s",
|
case consulapi.MemberTagValueRoleServer:
|
||||||
member.Name, addr.String(), statusString, build, protocol, dc, segment)
|
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fserver\x1f%s\x1f%s\x1f%s\x1f%s\x1f%s",
|
||||||
|
member.Name, addr.String(), statusString, build, protocol, tags.datacenter, tags.partition, tags.segment)
|
||||||
result = append(result, line)
|
result = append(result, line)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1funknown\x1f\x1f\x1f\x1f",
|
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1funknown\x1f\x1f\x1f\x1f\x1f",
|
||||||
member.Name, addr.String(), statusString)
|
member.Name, addr.String(), statusString)
|
||||||
result = append(result, line)
|
result = append(result, line)
|
||||||
}
|
}
|
||||||
@ -181,6 +228,22 @@ func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type decodedTags struct {
|
||||||
|
role string
|
||||||
|
segment string
|
||||||
|
partition string
|
||||||
|
datacenter string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTags(tags map[string]string) decodedTags {
|
||||||
|
return decodedTags{
|
||||||
|
role: tags[consulapi.MemberTagKeyRole],
|
||||||
|
segment: tags[consulapi.MemberTagKeySegment],
|
||||||
|
partition: tags[consulapi.MemberTagKeyPartition],
|
||||||
|
datacenter: tags[consulapi.MemberTagKeyDatacenter],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// detailedOutput is used to dump all known information about nodes in
|
// detailedOutput is used to dump all known information about nodes in
|
||||||
// their raw format
|
// their raw format
|
||||||
func (c *cmd) detailedOutput(members []*consulapi.AgentMember) []string {
|
func (c *cmd) detailedOutput(members []*consulapi.AgentMember) []string {
|
||||||
|
@ -2,12 +2,17 @@ package members
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent"
|
"github.com/hashicorp/consul/agent"
|
||||||
|
consulapi "github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/lib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(partitions): split these tests
|
// TODO(partitions): split these tests
|
||||||
@ -169,3 +174,63 @@ func TestMembersCommand_verticalBar(t *testing.T) {
|
|||||||
t.Fatalf("bad: %#v", ui.OutputWriter.String())
|
t.Fatalf("bad: %#v", ui.OutputWriter.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSortByMemberNamePartitionAndSegment(t *testing.T) {
|
||||||
|
lib.SeedMathRand()
|
||||||
|
|
||||||
|
// For the test data we'll give them names that would sort them backwards
|
||||||
|
// if we only sorted by name.
|
||||||
|
newData := func() []*consulapi.AgentMember {
|
||||||
|
// NOTE: This should be sorted for assertions.
|
||||||
|
return []*consulapi.AgentMember{
|
||||||
|
// servers
|
||||||
|
{Name: "p-betty", Tags: map[string]string{"role": "consul"}},
|
||||||
|
{Name: "q-bob", Tags: map[string]string{"role": "consul"}},
|
||||||
|
{Name: "r-bonnie", Tags: map[string]string{"role": "consul"}},
|
||||||
|
// default clients
|
||||||
|
{Name: "m-betty", Tags: map[string]string{}},
|
||||||
|
{Name: "n-bob", Tags: map[string]string{}},
|
||||||
|
{Name: "o-bonnie", Tags: map[string]string{}},
|
||||||
|
// segment 1 clients
|
||||||
|
{Name: "j-betty", Tags: map[string]string{"segment": "alpha"}},
|
||||||
|
{Name: "k-bob", Tags: map[string]string{"segment": "alpha"}},
|
||||||
|
{Name: "l-bonnie", Tags: map[string]string{"segment": "alpha"}},
|
||||||
|
// segment 2 clients
|
||||||
|
{Name: "g-betty", Tags: map[string]string{"segment": "beta"}},
|
||||||
|
{Name: "h-bob", Tags: map[string]string{"segment": "beta"}},
|
||||||
|
{Name: "i-bonnie", Tags: map[string]string{"segment": "beta"}},
|
||||||
|
// partition 1 clients
|
||||||
|
{Name: "d-betty", Tags: map[string]string{"ap": "part1"}},
|
||||||
|
{Name: "e-bob", Tags: map[string]string{"ap": "part1"}},
|
||||||
|
{Name: "f-bonnie", Tags: map[string]string{"ap": "part1"}},
|
||||||
|
// partition 2 clients
|
||||||
|
{Name: "a-betty", Tags: map[string]string{"ap": "part2"}},
|
||||||
|
{Name: "b-bob", Tags: map[string]string{"ap": "part2"}},
|
||||||
|
{Name: "c-bonnie", Tags: map[string]string{"ap": "part2"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stringify := func(data []*consulapi.AgentMember) []string {
|
||||||
|
var out []string
|
||||||
|
for _, m := range data {
|
||||||
|
out = append(out, fmt.Sprintf("<%s, %s, %s, %s>",
|
||||||
|
m.Tags["role"],
|
||||||
|
m.Tags["ap"],
|
||||||
|
m.Tags["segment"],
|
||||||
|
m.Name))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
expect := newData()
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
data := newData()
|
||||||
|
rand.Shuffle(len(data), func(i, j int) {
|
||||||
|
data[i], data[j] = data[j], data[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Sort(ByMemberNamePartitionAndSegment(data))
|
||||||
|
|
||||||
|
require.Equal(t, stringify(expect), stringify(data), "iteration #%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user