Skip to content

Commit

Permalink
Backport of NET-6784: Adding cli command to list exported services to…
Browse files Browse the repository at this point in the history
… 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
hc-github-team-consul-core and tauhid621 authored Dec 7, 2023
1 parent 1eeae85 commit 90638a4
Show file tree
Hide file tree
Showing 8 changed files with 441 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/19821.txt
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.
```
154 changes: 154 additions & 0 deletions command/peering/exportedservices/exported_services.go
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 command/peering/exportedservices/exported_services_test.go
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)
})
}
4 changes: 4 additions & 0 deletions command/peering/peering.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 90638a4

Please sign in to comment.