NET-6784: Adding cli command to list exported services to a peer (#19821)

* Adding cli command to list exported services to a peer

* Changelog added

* Addressing docs comments

* Adding test case for no exported services scenario
This commit is contained in:
Tauhid Anjum 2023-12-07 12:55:15 +05:30 committed by GitHub
parent 3a78446114
commit ab68ddff91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 441 additions and 6 deletions

3
.changelog/19821.txt Normal file
View File

@ -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.
```

View File

@ -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
`
)

View File

@ -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)
})
}

View File

@ -64,6 +64,10 @@ Usage: consul peering <subcommand> [options] [args]
$ consul peering read -name west-dc
Lists services exported to a peering connection:
$ consul peering exported-services -name west-dc
Delete and close a peering connection:
$ consul peering delete -name west-dc

View File

@ -108,6 +108,7 @@ import (
"github.com/hashicorp/consul/command/peering"
peerdelete "github.com/hashicorp/consul/command/peering/delete"
peerestablish "github.com/hashicorp/consul/command/peering/establish"
peerexported "github.com/hashicorp/consul/command/peering/exportedservices"
peergenerate "github.com/hashicorp/consul/command/peering/generate"
peerlist "github.com/hashicorp/consul/command/peering/list"
peerread "github.com/hashicorp/consul/command/peering/read"
@ -247,6 +248,7 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory {
entry{"operator usage instances", func(ui cli.Ui) (cli.Command, error) { return instances.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 exported-services", func(ui cli.Ui) (cli.Command, error) { return peerexported.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 }},

View File

@ -0,0 +1,50 @@
---
layout: commands
page_title: 'Commands: Peering Exported Services'
description: |
The `consul peering exported-services` command outputs a list of services exported to a cluster peer.
---
# Consul Peering Exported Services
Command: `consul peering exported-services`
Corresponding HTTP API Endpoint: [\[GET\] /v1/peering/:name](/consul/api-docs/peering#read-a-peering-connection)
The `peering exported-services` command displays all of the services that were exported to the cluster peer using an [`exported-services` configuration entry](/consul/docs/connect/config-entries/exported-services).
The table below shows this command's [required ACLs](/consul/api-docs/api-structure#authentication).
| ACL Required |
| ------------ |
| `peering:read` |
## Usage
Usage: `consul peering exported-services [options] -name <peer name>`
#### Command Options
- `-name=<string>` - (Required) The name of the peer associated with a connection.
- `-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 the exported services to a peering connection locally referred to as "cluster-02":
```shell-session hideClipboard
$ consul peering exported-services -name cluster-02
backend
frontend
web
```

View File

@ -22,11 +22,12 @@ Usage: consul peering <subcommand> [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
delete Close and delete a peering connection
establish Consume a peering token and establish a connection with the accepting cluster
exported-services Lists the services exported to the peer
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
@ -37,3 +38,4 @@ of the subcommand in the sidebar or one of the links below:
- [generate-token](/consul/commands/peering/generate-token)
- [list](/consul/commands/peering/list)
- [read](/consul/commands/peering/read)
- [exported-services](/consul/commands/peering/exported-services)

View File

@ -472,6 +472,10 @@
"title": "establish",
"path": "peering/establish"
},
{
"title": "exported-services",
"path": "peering/exported-services"
},
{
"title": "generate-token",
"path": "peering/generate-token"