From 1fe98bbe0b354c6991844d72251e55e726121c4d Mon Sep 17 00:00:00 2001 From: DanStough Date: Wed, 31 Aug 2022 12:58:41 -0400 Subject: [PATCH] feat(cli): add initial peering cli commands --- .changelog/14423.txt | 3 + command/flags/http.go | 4 + command/peering/delete/delete.go | 91 ++++++++++ command/peering/delete/delete_test.go | 70 ++++++++ command/peering/establish/establish.go | 109 ++++++++++++ command/peering/establish/establish_test.go | 127 ++++++++++++++ command/peering/generate/generate.go | 139 +++++++++++++++ command/peering/generate/generate_test.go | 141 +++++++++++++++ command/peering/list/list.go | 139 +++++++++++++++ command/peering/list/list_test.go | 133 ++++++++++++++ command/peering/peering.go | 69 ++++++++ command/peering/read/read.go | 164 ++++++++++++++++++ command/peering/read/read_test.go | 135 ++++++++++++++ command/registry.go | 12 ++ testrpc/wait.go | 7 +- website/content/api-docs/peering.mdx | 18 +- website/content/commands/index.mdx | 1 + website/content/commands/peering/delete.mdx | 50 ++++++ .../content/commands/peering/establish.mdx | 52 ++++++ .../commands/peering/generate-token.mdx | 68 ++++++++ website/content/commands/peering/index.mdx | 40 +++++ website/content/commands/peering/list.mdx | 47 +++++ website/content/commands/peering/read.mdx | 62 +++++++ .../cluster-peering/create-manage-peering.mdx | 86 +++++++++ website/data/commands-nav-data.json | 29 ++++ 25 files changed, 1780 insertions(+), 16 deletions(-) create mode 100644 .changelog/14423.txt create mode 100644 command/peering/delete/delete.go create mode 100644 command/peering/delete/delete_test.go create mode 100644 command/peering/establish/establish.go create mode 100644 command/peering/establish/establish_test.go create mode 100644 command/peering/generate/generate.go create mode 100644 command/peering/generate/generate_test.go create mode 100644 command/peering/list/list.go create mode 100644 command/peering/list/list_test.go create mode 100644 command/peering/peering.go create mode 100644 command/peering/read/read.go create mode 100644 command/peering/read/read_test.go create mode 100644 website/content/commands/peering/delete.mdx create mode 100644 website/content/commands/peering/establish.mdx create mode 100644 website/content/commands/peering/generate-token.mdx create mode 100644 website/content/commands/peering/index.mdx create mode 100644 website/content/commands/peering/list.mdx create mode 100644 website/content/commands/peering/read.mdx diff --git a/.changelog/14423.txt b/.changelog/14423.txt new file mode 100644 index 000000000..fd4033945 --- /dev/null +++ b/.changelog/14423.txt @@ -0,0 +1,3 @@ +```release-note:feature +cli: Adds new subcommands for `peering` workflows. Refer to the [CLI docs](https://www.consul.io/commands/peering) for more information. +``` diff --git a/command/flags/http.go b/command/flags/http.go index 139ab7ed0..e82e024fb 100644 --- a/command/flags/http.go +++ b/command/flags/http.go @@ -98,6 +98,10 @@ func (f *HTTPFlags) Datacenter() string { return f.datacenter.String() } +func (f *HTTPFlags) Partition() string { + return f.partition.String() +} + func (f *HTTPFlags) Stale() bool { if f.stale.v == nil { return false diff --git a/command/peering/delete/delete.go b/command/peering/delete/delete.go new file mode 100644 index 000000000..cb9818900 --- /dev/null +++ b/command/peering/delete/delete.go @@ -0,0 +1,91 @@ +package delete + +import ( + "context" + "flag" + "fmt" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + _, err = peerings.Delete(context.Background(), c.name, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error deleting peering for %s: %v", c.name, err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully submitted peering connection, %s, for deletion", c.name)) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Delete a peering connection" + help = ` +Usage: consul peering delete [options] -name + + Delete a peering connection. Consul deletes all data imported from the peer + in the background. The peering connection is removed after all associated + data has been deleted. Operators can still read the peering connections + while the data is being removed. A 'DeletedAt' field will be populated with + the timestamp of when the peering was marked for deletion. + + Example: + + $ consul peering delete -name west-dc +` +) diff --git a/command/peering/delete/delete_test.go b/command/peering/delete/delete_test.go new file mode 100644 index 000000000..984e773f5 --- /dev/null +++ b/command/peering/delete/delete_test.go @@ -0,0 +1,70 @@ +package delete + +import ( + "context" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestDeleteCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestDeleteCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("delete connection", func(t *testing.T) { + + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + }) +} diff --git a/command/peering/establish/establish.go b/command/peering/establish/establish.go new file mode 100644 index 000000000..14cd0e310 --- /dev/null +++ b/command/peering/establish/establish.go @@ -0,0 +1,109 @@ +package establish + +import ( + "context" + "flag" + "fmt" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + peeringToken string + meta map[string]string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.StringVar(&c.peeringToken, "peering-token", "", "(Required) The peering token from the accepting cluster.") + + c.flags.Var((*flags.FlagMapValue)(&c.meta), "meta", + "Metadata to associate with the peering, formatted as key=value. This flag "+ + "may be specified multiple times to set multiple meta fields.") + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if c.peeringToken == "" { + c.UI.Error("Missing the required -peering-token flag") + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + req := api.PeeringEstablishRequest{ + PeerName: c.name, + PeeringToken: c.peeringToken, + Partition: c.http.Partition(), + Meta: c.meta, + } + + _, _, err = peerings.Establish(context.Background(), req, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error establishing peering for %s: %v", req.PeerName, err)) + return 1 + } + + c.UI.Info(fmt.Sprintf("Successfully established peering connection with %s", req.PeerName)) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Establish a peering connection" + help = ` +Usage: consul peering establish [options] -name -peering-token + + Establish a peering connection. The name provided will be used locally by + this cluster to refer to the peering connection. The peering token can + only be used once to establish the connection. + + Example: + + $ consul peering establish -name west-dc -peering-token +` +) diff --git a/command/peering/establish/establish_test.go b/command/peering/establish/establish_test.go new file mode 100644 index 000000000..95e7da505 --- /dev/null +++ b/command/peering/establish/establish_test.go @@ -0,0 +1,127 @@ +package establish + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestEstablishCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestEstablishCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + dialer := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = dialer.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + testrpc.WaitForTestAgent(t, dialer.RPC, "dc1") + + acceptingClient := acceptor.Client() + dialingClient := dialer.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-peering-token=1234abcde", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("peering token is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -peering-token flag") + }) + + t.Run("establish connection", func(t *testing.T) { + // Grab the token from the acceptor + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + fmt.Sprintf("-peering-token=%s", res.PeeringToken), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + }) + + t.Run("establish connection with options", func(t *testing.T) { + // Grab the token from the acceptor + req := api.PeeringGenerateTokenRequest{PeerName: "foo"} + res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), req, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + dialer.HTTPAddr(), + "-name=bar", + fmt.Sprintf("-peering-token=%s", res.PeeringToken), + "-meta=env=production", + "-meta=region=us-west-1", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "Success") + + //Meta + peering, _, err := dialingClient.Peerings().Read(context.Background(), "bar", &api.QueryOptions{}) + require.NoError(t, err) + + actual, ok := peering.Meta["env"] + require.True(t, ok) + require.Equal(t, "production", actual) + + actual, ok = peering.Meta["region"] + require.True(t, ok) + require.Equal(t, "us-west-1", actual) + }) +} diff --git a/command/peering/generate/generate.go b/command/peering/generate/generate.go new file mode 100644 index 000000000..cbbb23009 --- /dev/null +++ b/command/peering/generate/generate.go @@ -0,0 +1,139 @@ +package generate + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "strings" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + externalAddresses []string + meta map[string]string + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.Var((*flags.FlagMapValue)(&c.meta), "meta", + "Metadata to associate with the peering, formatted as key=value. This flag "+ + "may be specified multiple times to set multiple metadata fields.") + + c.flags.Var((*flags.AppendSliceValue)(&c.externalAddresses), "server-external-addresses", + "A list of addresses to put into the generated token, formatted as a comma-separate list. "+ + "Addresses are the form of :port. "+ + "This could be used to specify load balancer(s) or external IPs to reach the servers from "+ + "the dialing side, and will override any server addresses obtained from the \"consul\" service.") + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + req := api.PeeringGenerateTokenRequest{ + PeerName: c.name, + Partition: c.http.Partition(), + Meta: c.meta, + ServerExternalAddresses: c.externalAddresses, + } + + res, _, err := peerings.GenerateToken(context.Background(), req, &api.WriteOptions{}) + if err != nil { + c.UI.Error(fmt.Sprintf("Error generating peering token for %s: %v", req.PeerName, err)) + return 1 + } + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(res) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + c.UI.Info(res.PeeringToken) + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Generate a peering token" + help = ` +Usage: consul peering generate-token [options] -name + + Generate a peering token. The name provided will be used locally by + this cluster to refer to the peering connection. Re-generating a token + for a given name will not interrupt any active connection, but will + invalidate any unused token for that name. + + Example: + + $ consul peering generate-token -name west-dc + + Example using a load balancer in front of Consul servers: + + $ consul peering generate-token -name west-dc -server-external-addresses load-balancer.elb.us-west-1.amazonaws.com:8502 +` +) diff --git a/command/peering/generate/generate_test.go b/command/peering/generate/generate_test.go new file mode 100644 index 000000000..c74597610 --- /dev/null +++ b/command/peering/generate/generate_test.go @@ -0,0 +1,141 @@ +package generate + +import ( + "context" + "encoding/base64" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestGenerateCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestGenerateCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = a.Shutdown() }) + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + client := a.Client() + + t.Run("name is required", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=foo", + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("generate token", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + token, err := base64.StdEncoding.DecodeString(ui.OutputWriter.String()) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + }) + + t.Run("generate token with options", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=bar", + "-server-external-addresses=1.2.3.4,5.6.7.8", + "-meta=env=production", + "-meta=region=us-east-1", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + token, err := base64.StdEncoding.DecodeString(ui.OutputWriter.String()) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + + //ServerExternalAddresses + require.Contains(t, string(token), "1.2.3.4") + require.Contains(t, string(token), "5.6.7.8") + + //Meta + peering, _, err := client.Peerings().Read(context.Background(), "bar", &api.QueryOptions{}) + require.NoError(t, err) + + actual, ok := peering.Meta["env"] + require.True(t, ok) + require.Equal(t, "production", actual) + + actual, ok = peering.Meta["region"] + require.True(t, ok) + require.Equal(t, "us-east-1", actual) + }) + + t.Run("read with json", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-name=baz", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputRes api.PeeringGenerateTokenResponse + require.NoError(t, json.Unmarshal(output, &outputRes)) + + token, err := base64.StdEncoding.DecodeString(outputRes.PeeringToken) + require.NoError(t, err, "error decoding token") + require.Contains(t, string(token), "\"ServerName\":\"server.dc1.consul\"") + }) +} diff --git a/command/peering/list/list.go b/command/peering/list/list.go new file mode 100644 index 000000000..c445e3d57 --- /dev/null +++ b/command/peering/list/list.go @@ -0,0 +1,139 @@ +package list + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "sort" + "strings" + + "github.com/mitchellh/cli" + "github.com/ryanuber/columnize" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + res, _, err := peerings.List(context.Background(), &api.QueryOptions{}) + if err != nil { + c.UI.Error("Error listing peerings") + return 1 + } + + list := peeringList(res) + sort.Sort(list) + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(list) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + if len(res) == 0 { + c.UI.Info(fmt.Sprintf("There are no peering connections.")) + return 0 + } + + result := make([]string, 0, len(list)) + header := "Name\x1fState\x1fImported Svcs\x1fExported Svcs\x1fMeta" + result = append(result, header) + for _, peer := range list { + metaPairs := make([]string, 0, len(peer.Meta)) + for k, v := range peer.Meta { + metaPairs = append(metaPairs, fmt.Sprintf("%s=%s", k, v)) + } + meta := strings.Join(metaPairs, ",") + line := fmt.Sprintf("%s\x1f%s\x1f%d\x1f%d\x1f%s", + peer.Name, peer.State, peer.ImportedServiceCount, peer.ExportedServiceCount, meta) + result = append(result, line) + } + + output := columnize.Format(result, &columnize.Config{Delim: string([]byte{0x1f})}) + c.UI.Output(output) + + return 0 +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "List peering connections" + help = ` +Usage: consul peering list [options] + + List all peering connections. The results will be filtered according + to ACL policy configuration. + + Example: + + $ consul peering list +` +) + +// peeringList applies sort.Interface to a list of peering connections for sorting by name. +type peeringList []*api.Peering + +func (d peeringList) Len() int { return len(d) } +func (d peeringList) Less(i, j int) bool { return d[i].Name < d[j].Name } +func (d peeringList) Swap(i, j int) { d[i], d[j] = d[j], d[i] } diff --git a/command/peering/list/list_test.go b/command/peering/list/list_test.go new file mode 100644 index 000000000..06f9248b0 --- /dev/null +++ b/command/peering/list/list_test.go @@ -0,0 +1,133 @@ +package list + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestListCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestListCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("no results - pretty", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "no peering connections") + }) + + t.Run("two results for pretty print", func(t *testing.T) { + + generateReq := api.PeeringGenerateTokenRequest{PeerName: "foo"} + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") + + generateReq = api.PeeringGenerateTokenRequest{PeerName: "bar"} + _, _, err = acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"bar\"") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Equal(t, 3, strings.Count(output, "\n")) // There should be three lines including the header + + lines := strings.Split(output, "\n") + + require.Contains(t, lines[0], "Name") + require.Contains(t, lines[1], "bar") + require.Contains(t, lines[2], "foo") + }) + + t.Run("no results - json", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Contains(t, output, "[]") + }) + + t.Run("two results for JSON print", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputList []*api.Peering + require.NoError(t, json.Unmarshal(output, &outputList)) + + require.Len(t, outputList, 2) + require.Equal(t, "bar", outputList[0].Name) + require.Equal(t, "foo", outputList[1].Name) + }) +} diff --git a/command/peering/peering.go b/command/peering/peering.go new file mode 100644 index 000000000..1872f3738 --- /dev/null +++ b/command/peering/peering.go @@ -0,0 +1,69 @@ +package peering + +import ( + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/command/flags" +) + +const ( + PeeringFormatJSON = "json" + PeeringFormatPretty = "pretty" +) + +func GetSupportedFormats() []string { + return []string{PeeringFormatJSON, PeeringFormatPretty} +} + +func FormatIsValid(f string) bool { + return f == PeeringFormatPretty || f == PeeringFormatJSON +} + +func New() *cmd { + return &cmd{} +} + +type cmd struct{} + +func (c *cmd) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(help, nil) +} + +const synopsis = "Create and manage peering connections between Consul clusters" +const help = ` +Usage: consul peering [options] [args] + + This command has subcommands for interacting with Cluster Peering + connections. Here are some simple examples, and more detailed + examples are available in the subcommands or the documentation. + + Generate a peering token: + + $ consul peering generate-token -name west-dc + + Establish a peering connection: + + $ consul peering establish -name east-dc -peering-token + + List all the local peering connections: + + $ consul peering list + + Print the status of a peering connection: + + $ consul peering read -name west-dc + + Delete and close a peering connection: + + $ consul peering delete -name west-dc + + For more examples, ask for subcommand help or view the documentation. +` diff --git a/command/peering/read/read.go b/command/peering/read/read.go new file mode 100644 index 000000000..c8340e19b --- /dev/null +++ b/command/peering/read/read.go @@ -0,0 +1,164 @@ +package read + +import ( + "bytes" + "context" + "encoding/json" + "flag" + "fmt" + "strings" + "time" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/command/flags" + "github.com/hashicorp/consul/command/peering" +) + +func New(ui cli.Ui) *cmd { + c := &cmd{UI: ui} + c.init() + return c +} + +type cmd struct { + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + + name string + format string +} + +func (c *cmd) init() { + c.flags = flag.NewFlagSet("", flag.ContinueOnError) + + c.flags.StringVar(&c.name, "name", "", "(Required) The local name assigned to the peer cluster.") + + c.flags.StringVar( + &c.format, + "format", + peering.PeeringFormatPretty, + fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(peering.GetSupportedFormats(), "|"), peering.PeeringFormatPretty), + ) + + c.http = &flags.HTTPFlags{} + flags.Merge(c.flags, c.http.ClientFlags()) + flags.Merge(c.flags, c.http.PartitionFlag()) + c.help = flags.Usage(help, c.flags) +} + +func (c *cmd) Run(args []string) int { + if err := c.flags.Parse(args); err != nil { + return 1 + } + + if c.name == "" { + c.UI.Error("Missing the required -name flag") + return 1 + } + + if !peering.FormatIsValid(c.format) { + c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(peering.GetSupportedFormats(), "|"))) + return 1 + } + + client, err := c.http.APIClient() + if err != nil { + c.UI.Error(fmt.Sprintf("Error connect to Consul agent: %s", err)) + return 1 + } + + peerings := client.Peerings() + + res, _, err := peerings.Read(context.Background(), c.name, &api.QueryOptions{}) + if err != nil { + c.UI.Error("Error reading peerings") + return 1 + } + + if res == nil { + c.UI.Error(fmt.Sprintf("No peering with name %s found.", c.name)) + return 1 + } + + if c.format == peering.PeeringFormatJSON { + output, err := json.Marshal(res) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshalling JSON: %s", err)) + return 1 + } + c.UI.Output(string(output)) + return 0 + } + + c.UI.Output(formatPeering(res)) + + return 0 +} + +func formatPeering(peering *api.Peering) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("Name: %s\n", peering.Name)) + buffer.WriteString(fmt.Sprintf("ID: %s\n", peering.ID)) + if peering.Partition != "" { + buffer.WriteString(fmt.Sprintf("Partition: %s\n", peering.Partition)) + } + if peering.DeletedAt != nil { + buffer.WriteString(fmt.Sprintf("DeletedAt: %s\n", peering.DeletedAt.Format(time.RFC3339))) + } + buffer.WriteString(fmt.Sprintf("State: %s\n", peering.State)) + if peering.Meta != nil && len(peering.Meta) > 0 { + buffer.WriteString("Meta:\n") + for k, v := range peering.Meta { + buffer.WriteString(fmt.Sprintf(" %s=%s\n", k, v)) + } + } + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Peer ID: %s\n", peering.PeerID)) + buffer.WriteString(fmt.Sprintf("Peer Server Name: %s\n", peering.PeerServerName)) + buffer.WriteString(fmt.Sprintf("Peer CA Pems: %d\n", len(peering.PeerCAPems))) + if peering.PeerServerAddresses != nil && len(peering.PeerServerAddresses) > 0 { + buffer.WriteString("Peer Server Addresses:\n") + for _, v := range peering.PeerServerAddresses { + buffer.WriteString(fmt.Sprintf(" %s", v)) + } + } + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Imported Services: %d\n", peering.ImportedServiceCount)) + buffer.WriteString(fmt.Sprintf("Exported Services: %d\n", peering.ExportedServiceCount)) + + buffer.WriteString("\n") + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", peering.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", peering.ModifyIndex)) + + return buffer.String() +} + +func (c *cmd) Synopsis() string { + return synopsis +} + +func (c *cmd) Help() string { + return flags.Usage(c.help, nil) +} + +const ( + synopsis = "Read a peering connection" + help = ` +Usage: consul peering read [options] -name + + Read a peering connection with the provided name. If one is not found, + the command will exit with a non-zero code. The result will be filtered according + to ACL policy configuration. + + Example: + + $ consul peering read -name west-dc +` +) diff --git a/command/peering/read/read_test.go b/command/peering/read/read_test.go new file mode 100644 index 000000000..fe19e1100 --- /dev/null +++ b/command/peering/read/read_test.go @@ -0,0 +1,135 @@ +package read + +import ( + "context" + "encoding/json" + "strings" + "testing" + + "github.com/mitchellh/cli" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/agent" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/testrpc" +) + +func TestReadCommand_noTabs(t *testing.T) { + t.Parallel() + + if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { + t.Fatal("help has tabs") + } +} + +func TestReadCommand(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + acceptor := agent.NewTestAgent(t, ``) + t.Cleanup(func() { _ = acceptor.Shutdown() }) + + testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") + + acceptingClient := acceptor.Client() + + t.Run("no name flag", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "Missing the required -name flag") + }) + + t.Run("invalid format", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + "-format=toml", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "exited successfully when it should have failed") + output := ui.ErrorWriter.String() + require.Contains(t, output, "Invalid format") + }) + + t.Run("peering does not exist", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) + require.Contains(t, ui.ErrorWriter.String(), "No peering with name") + }) + + t.Run("read with pretty print", func(t *testing.T) { + + generateReq := api.PeeringGenerateTokenRequest{ + PeerName: "foo", + Meta: map[string]string{ + "env": "production", + }, + } + _, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) + require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.String() + require.Greater(t, strings.Count(output, "\n"), 0) // Checking for some kind of empty output + + // Spot check some fields and values + require.Contains(t, output, "foo") + require.Contains(t, output, api.PeeringStatePending) + require.Contains(t, output, "env=production") + require.Contains(t, output, "Imported Services") + require.Contains(t, output, "Exported Services") + }) + + t.Run("read with json", func(t *testing.T) { + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + acceptor.HTTPAddr(), + "-name=foo", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + output := ui.OutputWriter.Bytes() + + var outputPeering api.Peering + require.NoError(t, json.Unmarshal(output, &outputPeering)) + + require.Equal(t, "foo", outputPeering.Name) + require.Equal(t, "production", outputPeering.Meta["env"]) + }) +} diff --git a/command/registry.go b/command/registry.go index 28e441e87..b35ac2e42 100644 --- a/command/registry.go +++ b/command/registry.go @@ -96,6 +96,12 @@ import ( operraft "github.com/hashicorp/consul/command/operator/raft" operraftlist "github.com/hashicorp/consul/command/operator/raft/listpeers" operraftremove "github.com/hashicorp/consul/command/operator/raft/removepeer" + "github.com/hashicorp/consul/command/peering" + peerdelete "github.com/hashicorp/consul/command/peering/delete" + peerestablish "github.com/hashicorp/consul/command/peering/establish" + peergenerate "github.com/hashicorp/consul/command/peering/generate" + peerlist "github.com/hashicorp/consul/command/peering/list" + peerread "github.com/hashicorp/consul/command/peering/read" "github.com/hashicorp/consul/command/reload" "github.com/hashicorp/consul/command/rtt" "github.com/hashicorp/consul/command/services" @@ -214,6 +220,12 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory { entry{"operator raft", func(cli.Ui) (cli.Command, error) { return operraft.New(), nil }}, entry{"operator raft list-peers", func(ui cli.Ui) (cli.Command, error) { return operraftlist.New(ui), nil }}, entry{"operator raft remove-peer", func(ui cli.Ui) (cli.Command, error) { return operraftremove.New(ui), nil }}, + entry{"peering", func(cli.Ui) (cli.Command, error) { return peering.New(), nil }}, + entry{"peering delete", func(ui cli.Ui) (cli.Command, error) { return peerdelete.New(ui), nil }}, + entry{"peering generate-token", func(ui cli.Ui) (cli.Command, error) { return peergenerate.New(ui), nil }}, + entry{"peering establish", func(ui cli.Ui) (cli.Command, error) { return peerestablish.New(ui), nil }}, + entry{"peering list", func(ui cli.Ui) (cli.Command, error) { return peerlist.New(ui), nil }}, + entry{"peering read", func(ui cli.Ui) (cli.Command, error) { return peerread.New(ui), nil }}, entry{"reload", func(ui cli.Ui) (cli.Command, error) { return reload.New(ui), nil }}, entry{"rtt", func(ui cli.Ui) (cli.Command, error) { return rtt.New(ui), nil }}, entry{"services", func(cli.Ui) (cli.Command, error) { return services.New(), nil }}, diff --git a/testrpc/wait.go b/testrpc/wait.go index d6b72749e..39e3d6592 100644 --- a/testrpc/wait.go +++ b/testrpc/wait.go @@ -11,7 +11,9 @@ import ( type rpcFn func(string, interface{}, interface{}) error -// WaitForLeader ensures we have a leader and a node registration. +// WaitForLeader ensures we have a leader and a node registration. It +// does not wait for the Consul (node) service to be ready. Use `WaitForTestAgent` +// to make sure the Consul service is ready. // // Most uses of this would be better served in the agent/consul package by // using waitForLeaderEstablishment() instead. @@ -91,7 +93,8 @@ func flattenOptions(options []waitOption) waitOption { return flat } -// WaitForTestAgent ensures we have a node with serfHealth check registered +// WaitForTestAgent ensures we have a node with serfHealth check registered. +// You'll want to use this if you expect the Consul (node) service to be ready. func WaitForTestAgent(t *testing.T, rpc rpcFn, dc string, options ...waitOption) { t.Helper() diff --git a/website/content/api-docs/peering.mdx b/website/content/api-docs/peering.mdx index ef50fcb87..102161319 100644 --- a/website/content/api-docs/peering.mdx +++ b/website/content/api-docs/peering.mdx @@ -42,12 +42,9 @@ The table below shows this endpoint's support for - `Partition` `(string: "")` - The admin partition that the peering token is generated from. Uses `default` when not specified. -- `Datacenter` `(string: "")` - Specifies the datacenter where the peering token is generated. Defaults to the - agent's datacenter when not specified. - -- `Token` `(string: "")` - Specifies the ACL token to use in the request. Takes precedence - over the token specified in the `token` query parameter, `X-Consul-Token` request header, - and `CONSUL_HTTP_TOKEN` environment variable. +- `ServerExternalAddresses` `([]string: )` - A list of addresses to put +into the generated token. Addresses are the form of `{host or IP}:port`. +You can specify one or more load balancers or external IPs that route external traffic to this cluster's Consul servers. - `Meta` `(map: )` - Specifies KV metadata to associate with the peering. This parameter is not required and does not directly impact the cluster @@ -116,13 +113,6 @@ The table below shows this endpoint's support for - `PeeringToken` `(string: )` - The peering token fetched from the peer cluster. -- `Datacenter` `(string: "")` - Specifies the datacenter where the peering token is generated. Defaults to the - agent's datacenter when not specified. - -- `Token` `(string: "")` - Specifies the ACL token to use in the request. Takes precedence - over the token specified in the `token` query parameter, `X-Consul-Token` request header, - and `CONSUL_HTTP_TOKEN` environment variable. - - `Meta` `(map: )` - Specifies KV metadata to associate with the peering. This parameter is not required and does not directly impact the cluster peering process. @@ -314,6 +304,6 @@ $ curl --header "X-Consul-Token: 0137db51-5895-4c25-b6cd-d9ed992f4a52" \ }, "CreateIndex": 109, "ModifyIndex": 119 - }, + } ] ``` diff --git a/website/content/commands/index.mdx b/website/content/commands/index.mdx index 7b0b9c2b0..d805d4eb2 100644 --- a/website/content/commands/index.mdx +++ b/website/content/commands/index.mdx @@ -50,6 +50,7 @@ Available commands are: members Lists the members of a Consul cluster monitor Stream logs from a Consul agent operator Provides cluster-level tools for Consul operators + peering Create and manage peering connections between Consul clusters reload Triggers the agent to reload configuration files rtt Estimates network round trip time between nodes services Interact with services diff --git a/website/content/commands/peering/delete.mdx b/website/content/commands/peering/delete.mdx new file mode 100644 index 000000000..04a7e16ba --- /dev/null +++ b/website/content/commands/peering/delete.mdx @@ -0,0 +1,50 @@ +--- +layout: commands +page_title: 'Commands: Peering Delete' +description: Learn how to use the consul peering delete command to remove a peering connection between Consul clusters. +--- + +# Consul Peering Delete + +Command: `consul peering delete` + +Corresponding HTTP API Endpoint: [\[DELETE\] /v1/peering/:name](/api-docs/peering#delete-a-peering-connection) + +The `peering delete` removes a peering connection with another cluster. +Consul deletes all data imported from the peer in the background. +The peering connection is removed after all associated data has been deleted. +Operators can still read the peering connections while the data is being removed. +The command adds a `DeletedAt` field to the peering connection object with the timestamp of when the peering was marked for deletion. +You can only use a peering token to establish the connection once. If you need to reestablish a peering connection, you must generate a new token. + +The table below shows this command's [required ACLs](/api#authentication). + +| ACL Required | +| ------------ | +| `peering:write` | + +## Usage + +Usage: `consul peering delete [options] -name ` + +#### Command Options + +- `-name=` - (Required) The name of the peer. + +#### Enterprise Options + +@include 'http_api_partition_options.mdx' + +#### API Options + +@include 'http_api_options_client.mdx' + +## Examples + +The following examples deletes a peering connection to a cluster locally referred to as "cluster-02": + +```shell-session hideClipboard +$ consul peering delete -name cluster-02 +Successfully submitted peering connection, cluster-02, for deletion +``` + diff --git a/website/content/commands/peering/establish.mdx b/website/content/commands/peering/establish.mdx new file mode 100644 index 000000000..d9906e068 --- /dev/null +++ b/website/content/commands/peering/establish.mdx @@ -0,0 +1,52 @@ +--- +layout: commands +page_title: 'Commands: Peering Establish' +description: Learn how to use the consul peering establish command to establish a peering connection between Consul clusters. +--- + +# Consul Peering Establish + +Command: `consul peering establish` + +Corresponding HTTP API Endpoint: [\[POST\] /v1/peering/establish](/api-docs/peering#establish-a-peering-connection) + +The `peering establish` starts a peering connection with the cluster that generated the peering token. +You can generate cluster peering tokens using the [`consul peering generate-token`](/commands/operator/generate-token) command or the [HTTP API](https://www.consul.io/api-docs/peering#generate-a-peering-token). + +You can only use a peering token to establish the connection once. If you need to reestablish a peering connection, you must generate a new token. + +The table below shows this command's [required ACLs](/api#authentication). + +| ACL Required | +| ------------ | +| `peering:write` | + +## Usage + +Usage: `consul peering establish [options] -name -peering-token ` + +#### Command Options + +- `-name=` - (Required) Specifies a local name for the cluster you are establishing a connection with. The `name` is only used to identify the connection with the peer. + +- `-peering-token=` - (Required) Specifies the peering token from the cluster that generated the token. + +- `-meta==` - Specifies key/value pairs to associate with the peering connection in `-meta="key"="value"` format. You can use the flag multiple times to set multiple metadata fields. + +#### Enterprise Options + +@include 'http_api_partition_options.mdx' + +#### API Options + +@include 'http_api_options_client.mdx' + +## Examples + +The following examples establishes a peering connection with a cluster locally referred to as "cluster-01": + +```shell-session hideClipboard +$ consul peering establish -name cluster-01 -peering-token eyJDQSI6bnVs...5Yi0wNzk5NTA1YTRmYjYifQ== +Successfully established peering connection with cluster-01 +``` + diff --git a/website/content/commands/peering/generate-token.mdx b/website/content/commands/peering/generate-token.mdx new file mode 100644 index 000000000..961122fc6 --- /dev/null +++ b/website/content/commands/peering/generate-token.mdx @@ -0,0 +1,68 @@ +--- +layout: commands +page_title: 'Commands: Peering Generate Token' +description: Learn how to use the consul peering generate-token command to generate token that enables you to peer Consul clusters. +--- + +# Consul Peering Generate Token + +Command: `consul peering generate-token` + +Corresponding HTTP API Endpoint: [\[POST\] /v1/peering/token](/api-docs/peering#generate-a-peering-token) + +The `peering generate-token` generates a peering token. The token is base 64-encoded string containing the token details. +This token should be transferred to the other cluster being peered and consumed using [`consul peering establish`](/commands/peering/establish). + +Generating a token and specifying the same local name associated with a previously-generated token does not affect active connections established with the original token. If the previously-generated token is not actively being used for a peer connection, however, it will become invalid when the new token with the same local name is generated. + +The table below shows this command's [required ACLs](/api#authentication). + +| ACL Required | +| ------------ | +| `peering:write` | + +## Usage + +Usage: `consul peering generate-token [options] -name ` + +#### Command Options + +- `-name=` - (Required) Specifies a local name for the cluster that the token is intended for. +The `name` is only used to identify the connection with the peer. +Generating a token and specifying the same local name associated with a previously-generated token does not affect active connections established with the original token. +If the previously-generated token is not actively being used for a peer connection, however, it will become invalid when the new token with the same local name is generated. + +- `-meta==` - Specifies key/value pairs to associate with the peering connection token in `-meta="key"="value"` format. You can use the flag multiple times to set multiple metadata fields. + +<<<<<<< HEAD +- `-server-external-addresses=[,string,...]` - Specifies a comma-separated list of addresses +to put into the generated token. Addresses are of the form of `{host or IP}:port`. +You can specify one or more load balancers or external IPs that route external traffic to this cluster's Consul servers. + +- `-format={pretty|json}` - Command output format. The default value is `pretty`. + +#### Enterprise Options + +@include 'http_api_partition_options.mdx' + +#### API Options + +@include 'http_api_options_client.mdx' + +## Examples + +The following example generates a peering token for a cluster called "cluster-02": + +```shell-session hideClipboard +$ consul peering generate-token -name cluster-02 +eyJDQSI6bnVs...5Yi0wNzk5NTA1YTRmYjYifQ== +``` + +### Using a Load Balancer for Consul Servers + +The following example generates a token for a cluster where servers are proxied by a load balancer: + +```shell-session hideClipboard +$ consul peering generate-token -server-external-addresses my-load-balancer-1234567890abcdef.elb.us-east-2.amazonaws.com -name cluster-02 +eyJDQSI6bnVs...5Yi0wNzk5NTA1YTRmYjYifQ== +``` diff --git a/website/content/commands/peering/index.mdx b/website/content/commands/peering/index.mdx new file mode 100644 index 000000000..47311a444 --- /dev/null +++ b/website/content/commands/peering/index.mdx @@ -0,0 +1,40 @@ +--- +layout: commands +page_title: 'Commands: Peering' +--- + +# Consul Peering + +Command: `consul peering` + +Use the `peering` command to create and manage peering connections between Consul clusters, including token generation and consumption. Refer to +[Create and Manage Peerings Connections](/docs/connect/cluster-peering/create-manage-peering) for an +overview of the CLI workflow for cluster peering. + +## Usage + +```text +Usage: consul peering [options] + + # ... + +Subcommands: + + delete Close and delete a peering connection + establish Consume a peering token and establish a connection with the accepting cluster + generate-token Generate a peering token for use by a dialing cluster + list List the local cluster's peering connections + read Read detailed information on a peering connection +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar or one of the links below: + +- [delete](/commands/peering/delete) +- [establish](/commands/peering/establish) +- [generate-token](/commands/peering/generate-token) +- [list](/commands/peering/list) +- [read](/commands/peering/read) + + + diff --git a/website/content/commands/peering/list.mdx b/website/content/commands/peering/list.mdx new file mode 100644 index 000000000..27f9f748f --- /dev/null +++ b/website/content/commands/peering/list.mdx @@ -0,0 +1,47 @@ +--- +layout: commands +page_title: 'Commands: Peering List' +--- + +# Consul Peering List + +Command: `consul peering List` + +Corresponding HTTP API Endpoint: [\[GET\] /v1/peerings](/api-docs/peering#list-all-peerings) + +The `peering list` lists all peering connections. +The results are filtered according to ACL policy configuration. + +The table below shows this command's [required ACLs](/api#authentication). + +| ACL Required | +| ------------ | +| `peering:read` | + +## Usage + +Usage: `consul peering list [options]` + +#### Command Options + +- `-format={pretty|json}` - Command output format. The default value is `pretty`. + +#### Enterprise Options + +@include 'http_api_partition_options.mdx' + +#### API Options + +@include 'http_api_options_client.mdx' + +## Examples + +The following example lists all peering connections associated with the cluster: + +```shell-session hideClipboard +$ consul peering list +Name State Imported Svcs Exported Svcs Meta +cluster-02 ACTIVE 0 2 env=production +cluster-03 PENDING 0 0 +``` + diff --git a/website/content/commands/peering/read.mdx b/website/content/commands/peering/read.mdx new file mode 100644 index 000000000..59d3cc74f --- /dev/null +++ b/website/content/commands/peering/read.mdx @@ -0,0 +1,62 @@ +--- +layout: commands +page_title: 'Commands: Peering Read' +--- + +# Consul Peering Read + +Command: `consul peering read` + +Corresponding HTTP API Endpoint: [\[GET\] /v1/peering/:name](/api-docs/peering#read-a-peering-connection) + +The `peering read` displays information on the status of a peering connection. + +The table below shows this command's [required ACLs](/api#authentication). + +| ACL Required | +| ------------ | +| `peering:read` | + +## Usage + +Usage: `consul peering read [options] -name ` + +#### Command Options + +- `-name=` - (Required) The name of the peer associated with a connection that you want to read. + +- `-format={pretty|json}` - Command output format. The default value is `pretty`. + +#### Enterprise Options + +@include 'http_api_partition_options.mdx' + +#### API Options + +@include 'http_api_options_client.mdx' + +## Examples + +The following example outputs information about a peering connection locally referred to as "cluster-02": + +```shell-session hideClipboard +$ consul peering read -name cluster-02 +Name: cluster-02 +ID: 3b001063-8079-b1a6-764c-738af5a39a97 +State: ACTIVE +Meta: + env=production + +Peer ID: e83a315c-027e-bcb1-7c0c-a46650904a05 +Peer Server Name: server.dc1.consul +Peer CA Pems: 0 +Peer Server Addresses: + 10.0.0.1:8300 + +Imported Services: 0 +Exported Services: 2 + +Create Index: 89 +Modify Index: 89 +``` + diff --git a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx index ee0a69a94..5af8324e2 100644 --- a/website/content/docs/connect/cluster-peering/create-manage-peering.mdx +++ b/website/content/docs/connect/cluster-peering/create-manage-peering.mdx @@ -57,6 +57,19 @@ Create a JSON file that contains the first cluster's name and the peering token. + + +In `cluster-01`, use the [`consul peering generate-token` command](/commands/operator/generate-token) to issue a request for a peering token. + +```shell-session +$ consul peering generate-token -name cluster-02 +``` + +The CLI outputs the peering token, which is a base64-encoded string containing the token details. +Save this value to a file or clipboard to be used in the next step on `cluster-02`. + + + 1. In the Consul UI for the datacenter associated with `cluster-01`, click **Peers**. @@ -88,6 +101,25 @@ You can dial the `peering/establish` endpoint once per peering token. Peering to + + +In one of the client agents in "cluster-02," issue the [`consul peering establish` command](/commands/peering/establish) and specify the token generated in the previous step. The command establishes the peering connection. +The commands prints "Successfully established peering connection with cluster-01" after the connection is established. + +```shell-session +$ consul peering establish -name cluster-01 -peering-token token-from-generate +``` + +When you connect server agents through cluster peering, they peer their default partitions. +To establish peering connections for other partitions through server agents, you must add the `-partition` flag to the `establish` command and specify the partitions you want to peer. +For additional configuration information, refer to [`consul peering establish` command](/commands/peering/establish) . + +You can run the `peering establish` command once per peering token. +Peering tokens cannot be reused after being used to establish a connection. +If you need to re-establish a connection, you must generate a new peering token. + + + 1. In the Consul UI for the datacenter associated with `cluster 02`, click **Peers** and then **Add peer connection**. @@ -213,6 +245,20 @@ $ curl http://127.0.0.1:8500/v1/peerings ``` + + +After you establish a peering connection, run the [`consul peering list`](/commands/peering/list) command to get a list of all peering connections. +For example, the following command requests a list of all peering connections and returns the information in a table: + +```shell-session +$ consul peerings list + +Name State Imported Svcs Exported Svcs Meta +cluster-02 ACTIVE 0 2 env=production +cluster-03 PENDING 0 0 + ``` + + In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in a datacenter. @@ -248,6 +294,35 @@ $ curl http://127.0.0.1:8500/v1/peering/cluster-02 ``` + + +After you establish a peering connection, run the [`consul peering read`](/commands/peering/list) command to get peering information about for a specific cluster. +For example, the following command requests peering connection information for "cluster-02": + +```shell-session +$ consul peering read -name cluster-02 + +Name: cluster-02 +ID: 3b001063-8079-b1a6-764c-738af5a39a97 +State: ACTIVE +Meta: + env=production + +Peer ID: e83a315c-027e-bcb1-7c0c-a46650904a05 +Peer Server Name: server.dc1.consul +Peer CA Pems: 0 +Peer Server Addresses: + 10.0.0.1:8300 + +Imported Services: 0 +Exported Services: 2 + +Create Index: 89 +Modify Index: 89 + +``` + + In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. Click the name of a peered cluster to view additional details about the peering connection. @@ -281,6 +356,17 @@ $ curl --request DELETE http://127.0.0.1:8500/v1/peering/cluster-02 ``` + + +In "cluster-01," request the deletion through the [`consul peering delete`](/commands/peering/list) command. + +```shell-session +$ consul peering delete -name cluster-02 + +Successfully submitted peering connection, cluster-02, for deletion +``` + + In the Consul UI, click **Peers**. The UI lists peering connections you created for clusters in that datacenter. diff --git a/website/data/commands-nav-data.json b/website/data/commands-nav-data.json index 565851223..3a3bb0609 100644 --- a/website/data/commands-nav-data.json +++ b/website/data/commands-nav-data.json @@ -436,6 +436,35 @@ "title": "partition", "path": "partition" }, + { + "title": "peering", + "routes": [ + { + "title": "Overview", + "path": "peering" + }, + { + "title": "delete", + "path": "peering/delete" + }, + { + "title": "establish", + "path": "peering/establish" + }, + { + "title": "generate-token", + "path": "peering/generate-token" + }, + { + "title": "list", + "path": "peering/list" + }, + { + "title": "read", + "path": "peering/read" + } + ] + }, { "title": "reload", "path": "reload"