-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Backport of NET-6784: Adding cli command to list exported services to…
… a peer into release/1.17.x (#19851) * backport of commit 53f3d35 * backport of commit f75f976 * backport of commit f6c7fce * backport of commit 6d95618 --------- Co-authored-by: Tauhid <tauhidanjum@gmail.com>
- Loading branch information
1 parent
1eeae85
commit 90638a4
Showing
8 changed files
with
441 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
```release-note:feature | ||
cli: Adds new subcommand `peering exported-services` to list services exported to a peer . Refer to the [CLI docs](https://developer.hashicorp.com/consul/commands/peering) for more information. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package exportedservices | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/ryanuber/columnize" | ||
|
||
"github.com/hashicorp/consul/agent/structs" | ||
"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(fmt.Sprintf("Error reading peering: %s", err)) | ||
return 1 | ||
} | ||
|
||
if res == nil { | ||
c.UI.Error(fmt.Sprintf("No peering with name %s found.", c.name)) | ||
return 1 | ||
} | ||
|
||
// Convert service to serviceID | ||
services := make([]structs.ServiceID, 0, len(res.StreamStatus.ExportedServices)) | ||
for _, svc := range res.StreamStatus.ExportedServices { | ||
services = append(services, structs.ServiceIDFromString(svc)) | ||
} | ||
|
||
if c.format == peering.PeeringFormatJSON { | ||
output, err := json.Marshal(services) | ||
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(formatExportedServices(services)) | ||
|
||
return 0 | ||
} | ||
|
||
func formatExportedServices(services []structs.ServiceID) string { | ||
if len(services) == 0 { | ||
return "" | ||
} | ||
|
||
result := make([]string, 0, len(services)+1) | ||
|
||
if services[0].EnterpriseMeta.ToEnterprisePolicyMeta() != nil { | ||
result = append(result, "Partition\x1fNamespace\x1fService Name") | ||
} | ||
|
||
for _, svc := range services { | ||
if svc.EnterpriseMeta.ToEnterprisePolicyMeta() == nil { | ||
result = append(result, svc.ID) | ||
} else { | ||
result = append(result, fmt.Sprintf("%s\x1f%s\x1f%s", svc.EnterpriseMeta.PartitionOrDefault(), svc.EnterpriseMeta.NamespaceOrDefault(), svc.ID)) | ||
} | ||
} | ||
|
||
return columnize.Format(result, &columnize.Config{Delim: string([]byte{0x1f})}) | ||
} | ||
|
||
func (c *cmd) Synopsis() string { | ||
return synopsis | ||
} | ||
|
||
func (c *cmd) Help() string { | ||
return flags.Usage(c.help, nil) | ||
} | ||
|
||
const ( | ||
synopsis = "Lists exported services to a peer" | ||
help = ` | ||
Usage: consul peering exported-services [options] -name <peer name> | ||
Lists services exported to the peer with the provided name. If the peer is not found, | ||
the command exits with a non-zero code. The result is filtered according | ||
to ACL policy configuration. | ||
Example: | ||
$ consul peering exported-services -name west-dc | ||
` | ||
) |
216 changes: 216 additions & 0 deletions
216
command/peering/exportedservices/exported_services_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package exportedservices | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/mitchellh/cli" | ||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/hashicorp/consul/agent" | ||
"github.com/hashicorp/consul/agent/structs" | ||
"github.com/hashicorp/consul/api" | ||
"github.com/hashicorp/consul/sdk/testutil/retry" | ||
"github.com/hashicorp/consul/testrpc" | ||
) | ||
|
||
func TestExportedServicesCommand_noTabs(t *testing.T) { | ||
t.Parallel() | ||
|
||
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { | ||
t.Fatal("help has tabs") | ||
} | ||
} | ||
|
||
func TestExportedServicesCommand(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, `datacenter = "dc2"`) | ||
t.Cleanup(func() { _ = dialer.Shutdown() }) | ||
|
||
testrpc.WaitForTestAgent(t, acceptor.RPC, "dc1") | ||
testrpc.WaitForTestAgent(t, dialer.RPC, "dc2") | ||
|
||
acceptingClient := acceptor.Client() | ||
dialingClient := dialer.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("peering exist but no exported services", func(t *testing.T) { | ||
// Generate token | ||
generateReq := api.PeeringGenerateTokenRequest{ | ||
PeerName: "foo", | ||
Meta: map[string]string{ | ||
"env": "production", | ||
}, | ||
} | ||
|
||
res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) | ||
require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") | ||
|
||
// Establish peering | ||
establishReq := api.PeeringEstablishRequest{ | ||
PeerName: "bar", | ||
PeeringToken: res.PeeringToken, | ||
Meta: map[string]string{ | ||
"env": "production", | ||
}, | ||
} | ||
|
||
_, _, err = dialingClient.Peerings().Establish(context.Background(), establishReq, &api.WriteOptions{}) | ||
require.NoError(t, err, "Could not establish peering for \"bar\"") | ||
|
||
ui := cli.NewMockUi() | ||
cmd := New(ui) | ||
|
||
args := []string{ | ||
"-http-addr=" + acceptor.HTTPAddr(), | ||
"-name=foo", | ||
} | ||
|
||
code := cmd.Run(args) | ||
require.Equal(t, 0, code) | ||
require.Equal(t, ui.ErrorWriter.String(), "") | ||
}) | ||
|
||
t.Run("exported-services with pretty print", func(t *testing.T) { | ||
// Generate token | ||
generateReq := api.PeeringGenerateTokenRequest{ | ||
PeerName: "foo", | ||
Meta: map[string]string{ | ||
"env": "production", | ||
}, | ||
} | ||
|
||
res, _, err := acceptingClient.Peerings().GenerateToken(context.Background(), generateReq, &api.WriteOptions{}) | ||
require.NoError(t, err, "Could not generate peering token at acceptor for \"foo\"") | ||
|
||
// Establish peering | ||
establishReq := api.PeeringEstablishRequest{ | ||
PeerName: "bar", | ||
PeeringToken: res.PeeringToken, | ||
Meta: map[string]string{ | ||
"env": "production", | ||
}, | ||
} | ||
|
||
_, _, err = dialingClient.Peerings().Establish(context.Background(), establishReq, &api.WriteOptions{}) | ||
require.NoError(t, err, "Could not establish peering for \"bar\"") | ||
|
||
_, _, err = acceptingClient.ConfigEntries().Set(&api.ExportedServicesConfigEntry{ | ||
Name: "default", | ||
Services: []api.ExportedService{ | ||
{ | ||
Name: "web", | ||
Consumers: []api.ServiceConsumer{ | ||
{ | ||
Peer: "foo", | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "db", | ||
Consumers: []api.ServiceConsumer{ | ||
{ | ||
Peer: "foo", | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, nil) | ||
require.NoError(t, err) | ||
|
||
ui := cli.NewMockUi() | ||
cmd := New(ui) | ||
|
||
args := []string{ | ||
"-http-addr=" + acceptor.HTTPAddr(), | ||
"-name=foo", | ||
} | ||
|
||
retry.Run(t, func(r *retry.R) { | ||
code := cmd.Run(args) | ||
require.Equal(r, 0, code) | ||
output := ui.OutputWriter.String() | ||
|
||
// Spot check some fields and values | ||
require.Contains(r, output, "web") | ||
require.Contains(r, output, "db") | ||
}) | ||
}) | ||
|
||
t.Run("exported-services 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 services []structs.ServiceID | ||
require.NoError(t, json.Unmarshal(output, &services)) | ||
|
||
require.Equal(t, "db", services[0].ID) | ||
require.Equal(t, "web", services[1].ID) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.