add -list-primary to `consul keyring` command (#8692)

* add -list-primary

* add docs

* use builder

* fix multiple actions
This commit is contained in:
Hans Hasselberg 2020-09-24 20:04:20 +02:00 committed by GitHub
parent e2223b84b8
commit 6467eb08dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 146 additions and 38 deletions

View File

@ -3,6 +3,7 @@ package keyring
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/agent"
consulapi "github.com/hashicorp/consul/api"
@ -23,12 +24,13 @@ type cmd struct {
help string
// flags
installKey string
useKey string
removeKey string
listKeys bool
relay int
local bool
installKey string
useKey string
removeKey string
listKeys bool
listPrimaryKeys bool
relay int
local bool
}
func (c *cmd) init() {
@ -45,6 +47,8 @@ func (c *cmd) init() {
"performed on keys which are not currently the primary key.")
c.flags.BoolVar(&c.listKeys, "list", false,
"List all keys currently in use within the cluster.")
c.flags.BoolVar(&c.listPrimaryKeys, "list-primary", false,
"List all primary keys currently in use within the cluster.")
c.flags.IntVar(&c.relay, "relay-factor", 0,
"Setting this to a non-zero value will cause nodes to relay their response "+
"to the operation through this many randomly-chosen other nodes in the "+
@ -58,6 +62,22 @@ func (c *cmd) init() {
c.help = flags.Usage(help, c.flags)
}
func numberActions(listKeys, listPrimaryKeys bool, installKey, useKey, removeKey string) int {
count := 0
if listKeys {
count++
}
if listPrimaryKeys {
count++
}
for _, arg := range []string{installKey, useKey, removeKey} {
if len(arg) > 0 {
count++
}
}
return count
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
@ -70,21 +90,15 @@ func (c *cmd) Run(args []string) int {
Ui: c.UI,
}
// Only accept a single argument
found := c.listKeys
for _, arg := range []string{c.installKey, c.useKey, c.removeKey} {
if found && len(arg) > 0 {
c.UI.Error("Only a single action is allowed")
return 1
}
found = found || len(arg) > 0
}
// Fail fast if no actionable args were passed
if !found {
num := numberActions(c.listKeys, c.listPrimaryKeys, c.installKey, c.useKey, c.removeKey)
if num == 0 {
c.UI.Error(c.Help())
return 1
}
if num > 1 {
c.UI.Error("Only a single action is allowed")
return 1
}
// Validate the relay factor
relayFactor, err := agent.ParseRelayFactor(c.relay)
@ -114,7 +128,22 @@ func (c *cmd) Run(args []string) int {
c.UI.Error(fmt.Sprintf("error: %s", err))
return 1
}
c.handleList(responses)
for _, response := range responses {
c.UI.Output(formatResponse(response, response.Keys))
}
return 0
}
if c.listPrimaryKeys {
c.UI.Info("Gathering installed primary encryption keys...")
responses, err := client.Operator().KeyringList(&consulapi.QueryOptions{RelayFactor: relayFactor, LocalOnly: c.local})
if err != nil {
c.UI.Error(fmt.Sprintf("error: %s", err))
return 1
}
for _, response := range responses {
c.UI.Output(formatResponse(response, response.PrimaryKeys))
}
return 0
}
@ -153,27 +182,40 @@ func (c *cmd) Run(args []string) int {
return 0
}
func (c *cmd) handleList(responses []*consulapi.KeyringResponse) {
for _, response := range responses {
pool := response.Datacenter + " (LAN)"
if response.Segment != "" {
pool += fmt.Sprintf(" [%s]", response.Segment)
}
if response.WAN {
pool = "WAN"
}
func formatResponse(response *consulapi.KeyringResponse, keys map[string]int) string {
b := new(strings.Builder)
b.WriteString("\n")
b.WriteString(poolName(response.Datacenter, response.WAN, response.Segment))
b.WriteString(formatMessages(response.Messages))
b.WriteString(formatKeys(keys, response.NumNodes))
return strings.TrimRight(b.String(), "\n")
}
c.UI.Output("")
c.UI.Output(pool + ":")
for from, msg := range response.Messages {
c.UI.Output(fmt.Sprintf(" ===> %s: %s", from, msg))
}
for key, num := range response.Keys {
c.UI.Output(fmt.Sprintf(" %s [%d/%d]", key, num, response.NumNodes))
}
func poolName(dc string, wan bool, segment string) string {
pool := fmt.Sprintf("%s (LAN)", dc)
if wan {
pool = "WAN"
}
if segment != "" {
segment = fmt.Sprintf(" [%s]", segment)
}
return fmt.Sprintf("%s%s:\n", pool, segment)
}
func formatMessages(messages map[string]string) string {
b := new(strings.Builder)
for from, msg := range messages {
b.WriteString(fmt.Sprintf(" ===> %s: %s\n", from, msg))
}
return b.String()
}
func formatKeys(keys map[string]int, total int) string {
b := new(strings.Builder)
for key, num := range keys {
b.WriteString(fmt.Sprintf(" %s [%d/%d]\n", key, num, total))
}
return b.String()
}
func (c *cmd) Synopsis() string {

View File

@ -5,7 +5,9 @@ import (
"testing"
"github.com/hashicorp/consul/agent"
consulapi "github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestKeyringCommand_noTabs(t *testing.T) {
@ -51,6 +53,16 @@ func TestKeyringCommand(t *testing.T) {
// Rotate to key2, remove key1
useKey(t, a1.HTTPAddr(), key2)
// New key should be present
out = listPrimaryKeys(t, a1.HTTPAddr())
if strings.Contains(out, key1) {
t.Fatalf("bad: %#v", out)
}
if !strings.Contains(out, key2) {
t.Fatalf("bad: %#v", out)
}
removeKey(t, a1.HTTPAddr(), key1)
// Only key2 is present now
@ -132,6 +144,19 @@ func listKeys(t *testing.T, addr string) string {
return ui.OutputWriter.String()
}
func listPrimaryKeys(t *testing.T, addr string) string {
ui := cli.NewMockUi()
c := New(ui)
args := []string{"-list-primary", "-http-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
return ui.OutputWriter.String()
}
func installKey(t *testing.T, addr string, key string) {
ui := cli.NewMockUi()
c := New(ui)
@ -164,3 +189,42 @@ func removeKey(t *testing.T, addr string, key string) {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
}
func TestKeyringCommand_poolName(t *testing.T) {
require.Equal(t, "dc1 (LAN):\n", poolName("dc1", false, ""))
require.Equal(t, "dc1 (LAN) [segment1]:\n", poolName("dc1", false, "segment1"))
require.Equal(t, "WAN:\n", poolName("dc1", true, ""))
}
func TestKeyringCommand_formatKeys(t *testing.T) {
require.Equal(t, "", formatKeys(map[string]int{}, 0))
keys := formatKeys(map[string]int{"key1": 1, "key2": 2}, 2)
require.Contains(t, keys, " key1 [1/2]\n")
require.Contains(t, keys, " key2 [2/2]\n")
}
func TestKeyringCommand_formatMessages(t *testing.T) {
require.Equal(t, "", formatMessages(map[string]string{}))
messages := formatMessages(map[string]string{"n1": "hello", "n2": "world"})
require.Contains(t, messages, " ===> n1: hello\n")
require.Contains(t, messages, " ===> n2: world\n")
}
func TestKeyringCommand_formatResponse(t *testing.T) {
response := &consulapi.KeyringResponse{Datacenter: "dc1", NumNodes: 1}
keys := map[string]int{"key1": 1}
require.Equal(t, "\ndc1 (LAN):\n key1 [1/1]", formatResponse(response, keys))
response = &consulapi.KeyringResponse{WAN: true, Datacenter: "dc1", NumNodes: 1}
keys = map[string]int{"key1": 1}
require.Equal(t, "\nWAN:\n key1 [1/1]", formatResponse(response, keys))
}
func TestKeyringCommand_numActions(t *testing.T) {
require.Equal(t, 0, numberActions(false, false, "", "", ""))
require.Equal(t, 1, numberActions(true, false, "", "", ""))
require.Equal(t, 1, numberActions(false, true, "", "", ""))
require.Equal(t, 1, numberActions(false, false, "1", "", ""))
require.Equal(t, 2, numberActions(true, false, "1", "", ""))
require.Equal(t, 2, numberActions(false, false, "1", "1", ""))
}

View File

@ -43,6 +43,8 @@ Only one actionable argument may be specified per run, including `-list`,
- `-list` - List all keys currently in use within the cluster.
- `-list-primary` - List all primary keys currently in use within the cluster.
- `-install` - Install a new encryption key. This will broadcast the new key to
all members in the cluster.