Skip to content

Commit

Permalink
Exported services CLI and docs (#20331)
Browse files Browse the repository at this point in the history
* Exported services CLI and docs

* Changelog added

* Added format option for pretty print

* Update command/exportedservices/exported_services.go

Co-authored-by: Ashesh Vidyut <134911583+absolutelightning@users.noreply.github.com>

* Addressing PR comments, moving the command under services category

* Add consumer peer and partition filter

* Adding bexpr filter, change format of data

---------

Co-authored-by: Ashesh Vidyut <134911583+absolutelightning@users.noreply.github.com>
  • Loading branch information
tauhid621 and absolutelightning authored Feb 6, 2024
1 parent 922844b commit 0c509a6
Show file tree
Hide file tree
Showing 8 changed files with 712 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/20331.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
cli: Adds new command `exported-services` to list all services exported and their consumers. Refer to the [CLI docs](https://developer.hashicorp.com/consul/commands/exported-services) for more information.
```
2 changes: 2 additions & 0 deletions command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ import (
"github.com/hashicorp/consul/command/services"
svcsderegister "github.com/hashicorp/consul/command/services/deregister"
svcsexport "github.com/hashicorp/consul/command/services/export"
exportedservices "github.com/hashicorp/consul/command/services/exportedservices"
svcsregister "github.com/hashicorp/consul/command/services/register"
"github.com/hashicorp/consul/command/snapshot"
snapinspect "github.com/hashicorp/consul/command/snapshot/inspect"
Expand Down Expand Up @@ -264,6 +265,7 @@ func RegisteredCommands(ui cli.Ui) map[string]mcli.CommandFactory {
entry{"services register", func(ui cli.Ui) (cli.Command, error) { return svcsregister.New(ui), nil }},
entry{"services deregister", func(ui cli.Ui) (cli.Command, error) { return svcsderegister.New(ui), nil }},
entry{"services export", func(ui cli.Ui) (cli.Command, error) { return svcsexport.New(ui), nil }},
entry{"services exported-services", func(ui cli.Ui) (cli.Command, error) { return exportedservices.New(ui), nil }},
entry{"snapshot", func(cli.Ui) (cli.Command, error) { return snapshot.New(), nil }},
entry{"snapshot inspect", func(ui cli.Ui) (cli.Command, error) { return snapinspect.New(ui), nil }},
entry{"snapshot restore", func(ui cli.Ui) (cli.Command, error) { return snaprestore.New(ui), nil }},
Expand Down
176 changes: 176 additions & 0 deletions command/services/exportedservices/exported_services.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package exportedservices

import (
"encoding/json"
"flag"
"fmt"
"strings"

"github.com/mitchellh/cli"

"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/go-bexpr"
"github.com/ryanuber/columnize"
)

const (
PrettyFormat string = "pretty"
JSONFormat string = "json"
)

func getSupportedFormats() []string {
return []string{PrettyFormat, JSONFormat}
}

func formatIsValid(f string) bool {
for _, format := range getSupportedFormats() {
if f == format {
return true
}
}
return false

}

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
filter string
}

func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)

c.flags.StringVar(
&c.format,
"format",
PrettyFormat,
fmt.Sprintf("Output format {%s} (default: %s)", strings.Join(getSupportedFormats(), "|"), PrettyFormat),
)

c.flags.StringVar(&c.filter, "filter", "", "go-bexpr filter string to filter the response")

c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
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 !formatIsValid(c.format) {
c.UI.Error(fmt.Sprintf("Invalid format, valid formats are {%s}", strings.Join(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
}

exportedServices, _, err := client.ExportedServices(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading exported services: %v", err))
return 1
}

var filterType []api.ResolvedExportedService
filter, err := bexpr.CreateFilter(c.filter, nil, filterType)
if err != nil {
c.UI.Error(fmt.Sprintf("Error while creating filter: %s", err))
return 1
}

raw, err := filter.Execute(exportedServices)
if err != nil {
c.UI.Error(fmt.Sprintf("Error while filtering response: %s", err))
return 1
}

filteredServices := raw.([]api.ResolvedExportedService)

if len(filteredServices) == 0 {
c.UI.Info("No exported services found")
return 0
}

if c.format == JSONFormat {
output, err := json.MarshalIndent(filteredServices, "", " ")
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(filteredServices))

return 0
}

func formatExportedServices(services []api.ResolvedExportedService) string {
result := make([]string, 0, len(services)+1)

if services[0].Partition != "" {
result = append(result, "Service\x1fPartition\x1fNamespace\x1fConsumer Peers\x1fConsumer Partitions")
} else {
result = append(result, "Service\x1fConsumer Peers")
}

for _, expService := range services {
row := ""
peers := strings.Join(expService.Consumers.Peers, ", ")
partitions := strings.Join(expService.Consumers.Partitions, ", ")
if expService.Partition != "" {
row = fmt.Sprintf("%s\x1f%s\x1f%s\x1f%s\x1f%s", expService.Service, expService.Partition, expService.Namespace, peers, partitions)
} else {
row = fmt.Sprintf("%s\x1f%s", expService.Service, peers)
}

result = append(result, row)

}

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"
help = `
Usage: consul services exported-services [options]
Lists all the exported services and their consumers. Wildcards and sameness groups(Enterprise) are expanded.
Example:
$ consul services exported-services
`
)
Loading

0 comments on commit 0c509a6

Please sign in to comment.