cli: update consul members output to display partitions and sort the results usefully (#11446)

This commit is contained in:
R.B. Boyer 2021-10-28 17:27:31 -05:00 committed by GitHub
parent d40d098321
commit 2353d59413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 25 deletions

3
.changelog/11446.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: update consul members output to display partitions and sort the results usefully
```

View File

@ -144,11 +144,23 @@ const (
// that the member represents a Consul server.
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
// segment this member is in.
// Network Segments are a Consul Enterprise feature.
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
// agent was started with the "bootstrap" configuration enabled
MemberTagKeyBootstrap = "bootstrap"

View File

@ -12,6 +12,7 @@ import (
"github.com/mitchellh/cli"
"github.com/ryanuber/columnize"
"github.com/hashicorp/consul/agent/structs"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
)
@ -90,11 +91,17 @@ func (c *cmd) Run(args []string) int {
n := len(members)
for i := 0; i < n; i++ {
member := members[i]
if member.Tags["segment"] == "" {
member.Tags["segment"] = "<default>"
if member.Tags[consulapi.MemberTagKeyPartition] == "" {
member.Tags[consulapi.MemberTagKeyPartition] = "default"
}
if c.segment == consulapi.AllSegments && member.Tags["role"] == "consul" {
member.Tags["segment"] = "<all>"
if structs.IsDefaultPartition(member.Tags[consulapi.MemberTagKeyPartition]) {
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()
if !statusRe.MatchString(statusString) {
@ -111,7 +118,7 @@ func (c *cmd) Run(args []string) int {
return 2
}
sort.Sort(ByMemberNameAndSegment(members))
sort.Sort(ByMemberNamePartitionAndSegment(members))
// Generate the output
var result []string
@ -128,29 +135,69 @@ func (c *cmd) Run(args []string) int {
return 0
}
// so we can sort members by name
type ByMemberNameAndSegment []*consulapi.AgentMember
// ByMemberNamePartitionAndSegment sorts members by name with a stable sort.
//
// 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 ByMemberNameAndSegment) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m ByMemberNameAndSegment) Less(i, j int) bool {
func (m ByMemberNamePartitionAndSegment) Len() int { return len(m) }
func (m ByMemberNamePartitionAndSegment) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
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 {
case m[i].Tags["segment"] < m[j].Tags["segment"]:
case tags_i.role == consulapi.MemberTagValueRoleServer && tags_j.role != consulapi.MemberTagValueRoleServer:
return true
case m[i].Tags["segment"] > m[j].Tags["segment"]:
case tags_i.role != consulapi.MemberTagValueRoleServer && tags_j.role == consulapi.MemberTagValueRoleServer:
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
// in a more human-friendly format
func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
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)
for _, member := range members {
tags := parseTags(member.Tags)
addr := net.TCPAddr{IP: net.ParseIP(member.Addr), Port: int(member.Port)}
protocol := member.Tags["vsn"]
build := member.Tags["build"]
@ -159,21 +206,21 @@ func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
} else if idx := strings.Index(build, ":"); idx != -1 {
build = build[:idx]
}
dc := member.Tags["dc"]
segment := member.Tags["segment"]
statusString := serf.MemberStatus(member.Status).String()
switch member.Tags["role"] {
case "node":
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fclient\x1f%s\x1f%s\x1f%s\x1f%s",
member.Name, addr.String(), statusString, build, protocol, dc, segment)
switch tags.role {
case consulapi.MemberTagValueRoleClient:
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, tags.datacenter, tags.partition, tags.segment)
result = append(result, line)
case "consul":
line := fmt.Sprintf("%s\x1f%s\x1f%s\x1fserver\x1f%s\x1f%s\x1f%s\x1f%s",
member.Name, addr.String(), statusString, build, protocol, dc, segment)
case consulapi.MemberTagValueRoleServer:
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)
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)
result = append(result, line)
}
@ -181,6 +228,22 @@ func (c *cmd) standardOutput(members []*consulapi.AgentMember) []string {
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
// their raw format
func (c *cmd) detailedOutput(members []*consulapi.AgentMember) []string {

View File

@ -2,12 +2,17 @@ package members
import (
"fmt"
"math/rand"
"sort"
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
)
// TODO(partitions): split these tests
@ -169,3 +174,63 @@ func TestMembersCommand_verticalBar(t *testing.T) {
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)
}
}