From ad4e1893548c03664d1c9cc71c37bdad0e005ead Mon Sep 17 00:00:00 2001 From: Hans Hasselberg Date: Thu, 24 Sep 2020 20:04:20 +0200 Subject: [PATCH] add -list-primary to `consul keyring` command (#8692) * add -list-primary * add docs * use builder * fix multiple actions --- command/keyring/keyring.go | 118 +++++++++++++++++++---------- command/keyring/keyring_test.go | 64 ++++++++++++++++ website/pages/commands/keyring.mdx | 2 + 3 files changed, 146 insertions(+), 38 deletions(-) diff --git a/command/keyring/keyring.go b/command/keyring/keyring.go index 6ef618f2a8..acdb0476f7 100644 --- a/command/keyring/keyring.go +++ b/command/keyring/keyring.go @@ -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 { diff --git a/command/keyring/keyring_test.go b/command/keyring/keyring_test.go index 710f1cb731..7d3b5baf87 100644 --- a/command/keyring/keyring_test.go +++ b/command/keyring/keyring_test.go @@ -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", "")) +} diff --git a/website/pages/commands/keyring.mdx b/website/pages/commands/keyring.mdx index cc61b619a4..1b415ad052 100644 --- a/website/pages/commands/keyring.mdx +++ b/website/pages/commands/keyring.mdx @@ -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.