mirror of https://github.com/status-im/consul.git
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:
parent
3a78446114
commit
ab68ddff91
|
@ -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.
|
||||
```
|
|
@ -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
|
||||
`
|
||||
)
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 }},
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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)
|
|
@ -472,6 +472,10 @@
|
|||
"title": "establish",
|
||||
"path": "peering/establish"
|
||||
},
|
||||
{
|
||||
"title": "exported-services",
|
||||
"path": "peering/exported-services"
|
||||
},
|
||||
{
|
||||
"title": "generate-token",
|
||||
"path": "peering/generate-token"
|
||||
|
|
Loading…
Reference in New Issue