mirror of https://github.com/hashicorp/consul
Add catalog CLI functions (#3204)
parent
a5461ce368
commit
afd83a9705
|
@ -0,0 +1,50 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cli.Command = (*CatalogCommand)(nil)
|
||||||
|
|
||||||
|
type CatalogCommand struct {
|
||||||
|
BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogCommand) Run(args []string) int {
|
||||||
|
return cli.RunResultHelp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: consul catalog <subcommand> [options] [args]
|
||||||
|
|
||||||
|
This command has subcommands for interacting with Consul's catalog. The
|
||||||
|
catalog should not be confused with the agent, although the APIs and
|
||||||
|
responses may be similar.
|
||||||
|
|
||||||
|
Here are some simple examples, and more detailed examples are available
|
||||||
|
in the subcommands or the documentation.
|
||||||
|
|
||||||
|
List all datacenters:
|
||||||
|
|
||||||
|
$ consul catalog datacenters
|
||||||
|
|
||||||
|
List all nodes:
|
||||||
|
|
||||||
|
$ consul catalog nodes
|
||||||
|
|
||||||
|
List all services:
|
||||||
|
|
||||||
|
$ consul catalog services
|
||||||
|
|
||||||
|
For more examples, ask for subcommand help or view the documentation.
|
||||||
|
|
||||||
|
`
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogCommand) Synopsis() string {
|
||||||
|
return "Interact with the catalog"
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCatalogCommand_noTabs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertNoTabs(t, new(CatalogCommand))
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cli.Command = (*CatalogListDatacentersCommand)(nil)
|
||||||
|
|
||||||
|
type CatalogListDatacentersCommand struct {
|
||||||
|
BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListDatacentersCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: consul catalog datacenters [options]
|
||||||
|
|
||||||
|
Retrieves the list of all known datacenters. This datacenters are sorted in
|
||||||
|
ascending order based on the estimated median round trip time from the servers
|
||||||
|
in this datacenter to the servers in the other datacenters.
|
||||||
|
|
||||||
|
To retrieve the list of datacenters:
|
||||||
|
|
||||||
|
$ consul catalog datacenters
|
||||||
|
|
||||||
|
For a full list of options and examples, please see the Consul documentation.
|
||||||
|
|
||||||
|
` + c.BaseCommand.Help()
|
||||||
|
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListDatacentersCommand) Run(args []string) int {
|
||||||
|
f := c.BaseCommand.NewFlagSet(c)
|
||||||
|
|
||||||
|
if err := c.BaseCommand.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(f.Args()); l > 0 {
|
||||||
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and test the HTTP client
|
||||||
|
client, err := c.BaseCommand.HTTPClient()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
dcs, err := client.Catalog().Datacenters()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error listing datacenters: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dc := range dcs {
|
||||||
|
c.UI.Info(dc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListDatacentersCommand) Synopsis() string {
|
||||||
|
return "Lists all known datacenters"
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCatalogListDatacentersCommand(t *testing.T) (*cli.MockUi, *CatalogListDatacentersCommand) {
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
return ui, &CatalogListDatacentersCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListDatacentersCommand_noTabs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertNoTabs(t, new(CatalogListDatacentersCommand))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListDatacentersCommand_Validation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ui, c := testCatalogListDatacentersCommand(t)
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
"args": {
|
||||||
|
[]string{"foo"},
|
||||||
|
"Too many arguments",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
// Ensure our buffer is always clear
|
||||||
|
if ui.ErrorWriter != nil {
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
|
}
|
||||||
|
if ui.OutputWriter != nil {
|
||||||
|
ui.OutputWriter.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
code := c.Run(tc.args)
|
||||||
|
if code == 0 {
|
||||||
|
t.Errorf("%s: expected non-zero exit", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, tc.output) {
|
||||||
|
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListDatacentersCommand_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
a := agent.NewTestAgent(t.Name(), nil)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
ui, c := testCatalogListDatacentersCommand(t)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
}
|
||||||
|
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if !strings.Contains(output, "dc") {
|
||||||
|
t.Errorf("bad: %#v", output)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/configutil"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/ryanuber/columnize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cli.Command = (*CatalogListNodesCommand)(nil)
|
||||||
|
|
||||||
|
// CatalogListNodesCommand is a Command implementation that is used to fetch all the
|
||||||
|
// nodes in the catalog.
|
||||||
|
type CatalogListNodesCommand struct {
|
||||||
|
BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListNodesCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: consul catalog nodes [options]
|
||||||
|
|
||||||
|
Retrieves the list nodes registered in a given datacenter. By default, the
|
||||||
|
datacenter of the local agent is queried.
|
||||||
|
|
||||||
|
To retrieve the list of nodes:
|
||||||
|
|
||||||
|
$ consul catalog nodes
|
||||||
|
|
||||||
|
To print detailed information including full node IDs, tagged addresses, and
|
||||||
|
metadata information:
|
||||||
|
|
||||||
|
$ consul catalog nodes -detailed
|
||||||
|
|
||||||
|
To list nodes which are running a particular service:
|
||||||
|
|
||||||
|
$ consul catalog nodes -service=web
|
||||||
|
|
||||||
|
To filter by node metadata:
|
||||||
|
|
||||||
|
$ consul catalog nodes -node-meta="foo=bar"
|
||||||
|
|
||||||
|
To sort nodes by estimated round-trip time from node-web:
|
||||||
|
|
||||||
|
$ consul catalog nodes -near=node-web
|
||||||
|
|
||||||
|
For a full list of options and examples, please see the Consul documentation.
|
||||||
|
|
||||||
|
` + c.BaseCommand.Help()
|
||||||
|
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListNodesCommand) Run(args []string) int {
|
||||||
|
f := c.BaseCommand.NewFlagSet(c)
|
||||||
|
|
||||||
|
detailed := f.Bool("detailed", false, "Output detailed information about "+
|
||||||
|
"the nodes including their addresses and metadata.")
|
||||||
|
|
||||||
|
near := f.String("near", "", "Node name to sort the node list in ascending "+
|
||||||
|
"order based on estimated round-trip time from that node. "+
|
||||||
|
"Passing \"_agent\" will use this agent's node for sorting.")
|
||||||
|
|
||||||
|
nodeMeta := make(map[string]string)
|
||||||
|
f.Var((*configutil.FlagMapValue)(&nodeMeta), "node-meta", "Metadata to "+
|
||||||
|
"filter nodes with the given `key=value` pairs. This flag may be "+
|
||||||
|
"specified multiple times to filter on multiple sources of metadata.")
|
||||||
|
|
||||||
|
service := f.String("service", "", "Service `id or name` to filter nodes. "+
|
||||||
|
"Only nodes which are providing the given service will be returned.")
|
||||||
|
|
||||||
|
if err := c.BaseCommand.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(f.Args()); l > 0 {
|
||||||
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and test the HTTP client
|
||||||
|
client, err := c.BaseCommand.HTTPClient()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodes []*api.Node
|
||||||
|
if *service != "" {
|
||||||
|
services, _, err := client.Catalog().Service(*service, "", &api.QueryOptions{
|
||||||
|
Near: *near,
|
||||||
|
NodeMeta: nodeMeta,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error listing nodes for service: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes = make([]*api.Node, len(services))
|
||||||
|
for i, s := range services {
|
||||||
|
nodes[i] = &api.Node{
|
||||||
|
ID: s.ID,
|
||||||
|
Node: s.Node,
|
||||||
|
Address: s.Address,
|
||||||
|
Datacenter: s.Datacenter,
|
||||||
|
TaggedAddresses: s.TaggedAddresses,
|
||||||
|
Meta: s.NodeMeta,
|
||||||
|
CreateIndex: s.CreateIndex,
|
||||||
|
ModifyIndex: s.ModifyIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nodes, _, err = client.Catalog().Nodes(&api.QueryOptions{
|
||||||
|
Near: *near,
|
||||||
|
NodeMeta: nodeMeta,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error listing nodes: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the edge case where there are no nodes that match the query.
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
c.UI.Error("No nodes match the given query - try expanding your search.")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := printNodes(nodes, *detailed)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error printing nodes: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
c.UI.Info(output)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListNodesCommand) Synopsis() string {
|
||||||
|
return "Lists all nodes in the given datacenter"
|
||||||
|
}
|
||||||
|
|
||||||
|
// printNodes accepts a list of nodes and prints information in a tabular
|
||||||
|
// format about the nodes.
|
||||||
|
func printNodes(nodes []*api.Node, detailed bool) (string, error) {
|
||||||
|
var result []string
|
||||||
|
if detailed {
|
||||||
|
result = detailedNodes(nodes)
|
||||||
|
} else {
|
||||||
|
result = simpleNodes(nodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnize.SimpleFormat(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detailedNodes(nodes []*api.Node) []string {
|
||||||
|
result := make([]string, 0, len(nodes)+1)
|
||||||
|
header := "Node|ID|Address|DC|TaggedAddresses|Meta"
|
||||||
|
result = append(result, header)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
result = append(result, fmt.Sprintf("%s|%s|%s|%s|%s|%s",
|
||||||
|
node.Node, node.ID, node.Address, node.Datacenter,
|
||||||
|
mapToKV(node.TaggedAddresses, ", "), mapToKV(node.Meta, ", ")))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleNodes(nodes []*api.Node) []string {
|
||||||
|
result := make([]string, 0, len(nodes)+1)
|
||||||
|
header := "Node|ID|Address|DC"
|
||||||
|
result = append(result, header)
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
// Shorten the ID in non-detailed mode to just the first octet.
|
||||||
|
id := node.ID
|
||||||
|
idx := strings.Index(id, "-")
|
||||||
|
if idx > 0 {
|
||||||
|
id = id[0:idx]
|
||||||
|
}
|
||||||
|
result = append(result, fmt.Sprintf("%s|%s|%s|%s",
|
||||||
|
node.Node, id, node.Address, node.Datacenter))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCatalogListNodesCommand(t *testing.T) (*cli.MockUi, *CatalogListNodesCommand) {
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
return ui, &CatalogListNodesCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListNodesCommand_noTabs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertNoTabs(t, new(CatalogListNodesCommand))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListNodesCommand_Validation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
"args": {
|
||||||
|
[]string{"foo"},
|
||||||
|
"Too many arguments",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
// Ensure our buffer is always clear
|
||||||
|
if ui.ErrorWriter != nil {
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
|
}
|
||||||
|
if ui.OutputWriter != nil {
|
||||||
|
ui.OutputWriter.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
code := c.Run(tc.args)
|
||||||
|
if code == 0 {
|
||||||
|
t.Errorf("%s: expected non-zero exit", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, tc.output) {
|
||||||
|
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListNodesCommand_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
a := agent.NewTestAgent(t.Name(), nil)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
for _, s := range []string{"Node", "ID", "Address", "DC"} {
|
||||||
|
if !strings.Contains(output, s) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{"TaggedAddresses", "Meta"} {
|
||||||
|
if strings.Contains(output, s) {
|
||||||
|
t.Errorf("expected %q to NOT contain %q", output, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("detailed", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-detailed",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
for _, s := range []string{"Node", "ID", "Address", "DC", "TaggedAddresses", "Meta"} {
|
||||||
|
if !strings.Contains(output, s) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("node-meta", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-node-meta", "foo=bar",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if expected := "No nodes match the given query"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("near", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-near", "_agent",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if expected := "127.0.0.1"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service_present", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-service", "consul",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if expected := "127.0.0.1"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service_missing", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListNodesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-service", "this-service-will-literally-never-exist",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if expected := "No nodes match the given query"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/configutil"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cli.Command = (*CatalogListServicesCommand)(nil)
|
||||||
|
|
||||||
|
// CatalogListServicesCommand is a Command implementation that is used to fetch all the
|
||||||
|
// datacenters the agent knows about.
|
||||||
|
type CatalogListServicesCommand struct {
|
||||||
|
BaseCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListServicesCommand) Help() string {
|
||||||
|
helpText := `
|
||||||
|
Usage: consul catalog services [options]
|
||||||
|
|
||||||
|
Retrieves the list services registered in a given datacenter. By default, the
|
||||||
|
datacenter of the local agent is queried.
|
||||||
|
|
||||||
|
To retrieve the list of services:
|
||||||
|
|
||||||
|
$ consul catalog services
|
||||||
|
|
||||||
|
To include the services' tags in the output:
|
||||||
|
|
||||||
|
$ consul catalog services -tags
|
||||||
|
|
||||||
|
To list services which run on a particular node:
|
||||||
|
|
||||||
|
$ consul catalog services -node=web
|
||||||
|
|
||||||
|
To filter services on node metadata:
|
||||||
|
|
||||||
|
$ consul catalog services -node-meta="foo=bar"
|
||||||
|
|
||||||
|
For a full list of options and examples, please see the Consul documentation.
|
||||||
|
|
||||||
|
` + c.BaseCommand.Help()
|
||||||
|
|
||||||
|
return strings.TrimSpace(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListServicesCommand) Run(args []string) int {
|
||||||
|
f := c.BaseCommand.NewFlagSet(c)
|
||||||
|
|
||||||
|
node := f.String("node", "", "Node `id or name` for which to list services.")
|
||||||
|
|
||||||
|
nodeMeta := make(map[string]string)
|
||||||
|
f.Var((*configutil.FlagMapValue)(&nodeMeta), "node-meta", "Metadata to "+
|
||||||
|
"filter nodes with the given `key=value` pairs. If specified, only "+
|
||||||
|
"services running on nodes matching the given metadata will be returned. "+
|
||||||
|
"This flag may be specified multiple times to filter on multiple sources "+
|
||||||
|
"of metadata.")
|
||||||
|
|
||||||
|
tags := f.Bool("tags", false, "Display each service's tags as a "+
|
||||||
|
"comma-separated list beside each service entry.")
|
||||||
|
|
||||||
|
if err := c.BaseCommand.Parse(args); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if l := len(f.Args()); l > 0 {
|
||||||
|
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", l))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and test the HTTP client
|
||||||
|
client, err := c.BaseCommand.HTTPClient()
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var services map[string][]string
|
||||||
|
if *node != "" {
|
||||||
|
catalogNode, _, err := client.Catalog().Node(*node, &api.QueryOptions{
|
||||||
|
NodeMeta: nodeMeta,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error listing services for node: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if catalogNode != nil {
|
||||||
|
services = make(map[string][]string, len(catalogNode.Services))
|
||||||
|
for _, s := range catalogNode.Services {
|
||||||
|
services[s.Service] = append(services[s.Service], s.Tags...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
services, _, err = client.Catalog().Services(&api.QueryOptions{
|
||||||
|
NodeMeta: nodeMeta,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error listing services: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the edge case where there are no services that match the query.
|
||||||
|
if len(services) == 0 {
|
||||||
|
c.UI.Error("No services match the given query - try expanding your search.")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order the map for consistent output
|
||||||
|
order := make([]string, 0, len(services))
|
||||||
|
for k, _ := range services {
|
||||||
|
order = append(order, k)
|
||||||
|
}
|
||||||
|
sort.Strings(order)
|
||||||
|
|
||||||
|
if *tags {
|
||||||
|
var b bytes.Buffer
|
||||||
|
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
|
||||||
|
for _, s := range order {
|
||||||
|
fmt.Fprintf(tw, "%s\t%s\n", s, strings.Join(services[s], ","))
|
||||||
|
}
|
||||||
|
if err := tw.Flush(); err != nil {
|
||||||
|
c.UI.Error(fmt.Sprintf("Error flushing tabwriter: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
c.UI.Output(strings.TrimSpace(b.String()))
|
||||||
|
} else {
|
||||||
|
for _, s := range order {
|
||||||
|
c.UI.Output(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CatalogListServicesCommand) Synopsis() string {
|
||||||
|
return "Lists all registered services in a datacenter"
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent"
|
||||||
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/mitchellh/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testCatalogListServicesCommand(t *testing.T) (*cli.MockUi, *CatalogListServicesCommand) {
|
||||||
|
ui := cli.NewMockUi()
|
||||||
|
return ui, &CatalogListServicesCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListServicesCommand_noTabs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assertNoTabs(t, new(CatalogListServicesCommand))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListServicesCommand_Validation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
args []string
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
"args": {
|
||||||
|
[]string{"foo"},
|
||||||
|
"Too many arguments",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
// Ensure our buffer is always clear
|
||||||
|
if ui.ErrorWriter != nil {
|
||||||
|
ui.ErrorWriter.Reset()
|
||||||
|
}
|
||||||
|
if ui.OutputWriter != nil {
|
||||||
|
ui.OutputWriter.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
code := c.Run(tc.args)
|
||||||
|
if code == 0 {
|
||||||
|
t.Errorf("%s: expected non-zero exit", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if !strings.Contains(output, tc.output) {
|
||||||
|
t.Errorf("%s: expected %q to contain %q", name, output, tc.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogListServicesCommand_Run(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
a := agent.NewTestAgent(t.Name(), nil)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
// Add another service with tags for testing
|
||||||
|
if err := a.Client().Agent().ServiceRegister(&api.AgentServiceRegistration{
|
||||||
|
Name: "testing",
|
||||||
|
Tags: []string{"foo", "bar"},
|
||||||
|
Port: 8080,
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if expected := "consul\ntesting\n"; output != expected {
|
||||||
|
t.Errorf("expected %q to be %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tags", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-tags",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if expected := "foo,bar"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("node_missing", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-node", "not-a-real-node",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if expected := "No services match the given query"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("node_present", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-node", a.Config.NodeName,
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.OutputWriter.String()
|
||||||
|
if expected := "consul\ntesting\n"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("node-meta", func(t *testing.T) {
|
||||||
|
ui, c := testCatalogListServicesCommand(t)
|
||||||
|
args := []string{
|
||||||
|
"-http-addr=" + a.HTTPAddr(),
|
||||||
|
"-node-meta", "foo=bar",
|
||||||
|
}
|
||||||
|
code := c.Run(args)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("bad exit code %d: %s", code, ui.ErrorWriter.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
output := ui.ErrorWriter.String()
|
||||||
|
if expected := "No services match the given query"; !strings.Contains(output, expected) {
|
||||||
|
t.Errorf("expected %q to contain %q", output, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/version"
|
"github.com/hashicorp/consul/version"
|
||||||
|
@ -30,6 +33,42 @@ func init() {
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"catalog": func() (cli.Command, error) {
|
||||||
|
return &CatalogCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
UI: ui,
|
||||||
|
Flags: FlagSetNone,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
"catalog datacenters": func() (cli.Command, error) {
|
||||||
|
return &CatalogListDatacentersCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
"catalog nodes": func() (cli.Command, error) {
|
||||||
|
return &CatalogListNodesCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
|
"catalog services": func() (cli.Command, error) {
|
||||||
|
return &CatalogListServicesCommand{
|
||||||
|
BaseCommand: BaseCommand{
|
||||||
|
Flags: FlagSetHTTP,
|
||||||
|
UI: ui,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
|
||||||
"configtest": func() (cli.Command, error) {
|
"configtest": func() (cli.Command, error) {
|
||||||
return &ConfigTestCommand{
|
return &ConfigTestCommand{
|
||||||
BaseCommand: BaseCommand{
|
BaseCommand: BaseCommand{
|
||||||
|
@ -363,3 +402,19 @@ func makeShutdownCh() <-chan struct{} {
|
||||||
|
|
||||||
return resultCh
|
return resultCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mapToKV converts a map[string]string into a human-friendly key=value list,
|
||||||
|
// sorted by name.
|
||||||
|
func mapToKV(m map[string]string, joiner string) string {
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
r := make([]string, len(keys))
|
||||||
|
for i, k := range keys {
|
||||||
|
r[i] = fmt.Sprintf("%s=%s", k, m[k])
|
||||||
|
}
|
||||||
|
return strings.Join(r, joiner)
|
||||||
|
}
|
||||||
|
|
|
@ -49,28 +49,3 @@ Usage: consul kv <subcommand> [options] [args]
|
||||||
func (c *KVCommand) Synopsis() string {
|
func (c *KVCommand) Synopsis() string {
|
||||||
return "Interact with the key-value store"
|
return "Interact with the key-value store"
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiOptsText = strings.TrimSpace(`
|
|
||||||
API Options:
|
|
||||||
|
|
||||||
-http-addr=<addr> Address of the Consul agent with the port. This can
|
|
||||||
be an IP address or DNS address, but it must include
|
|
||||||
the port. This can also be specified via the
|
|
||||||
CONSUL_HTTP_ADDR environment variable. The default
|
|
||||||
value is 127.0.0.1:8500.
|
|
||||||
|
|
||||||
-datacenter=<name> Name of the datacenter to query. If unspecified, the
|
|
||||||
query will default to the datacenter of the Consul
|
|
||||||
agent at the HTTP address.
|
|
||||||
|
|
||||||
-token=<value> ACL token to use in the request. This can also be
|
|
||||||
specified via the CONSUL_HTTP_TOKEN environment
|
|
||||||
variable. If unspecified, the query will default to
|
|
||||||
the token of the Consul agent at the HTTP address.
|
|
||||||
|
|
||||||
-stale Permit any Consul server (non-leader) to respond to
|
|
||||||
this request. This allows for lower latency and higher
|
|
||||||
throughput, but can result in stale data. This option
|
|
||||||
has no effect on non-read operations. The default
|
|
||||||
value is false.
|
|
||||||
`)
|
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package configutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure implements
|
||||||
|
var _ flag.Value = (*FlagMapValue)(nil)
|
||||||
|
|
||||||
|
// FlagMapValue is a flag implementation used to provide key=value semantics
|
||||||
|
// multiple times.
|
||||||
|
type FlagMapValue map[string]string
|
||||||
|
|
||||||
|
func (h *FlagMapValue) String() string {
|
||||||
|
return fmt.Sprintf("%v", *h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *FlagMapValue) Set(value string) error {
|
||||||
|
idx := strings.Index(value, "=")
|
||||||
|
if idx == -1 {
|
||||||
|
return fmt.Errorf("Missing \"=\" value in argument: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, value := value[0:idx], value[idx+1:]
|
||||||
|
|
||||||
|
if *h == nil {
|
||||||
|
*h = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := *h
|
||||||
|
headers[key] = value
|
||||||
|
*h = headers
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package configutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFlagMapValueSet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("missing =", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := new(FlagMapValue)
|
||||||
|
if err := f.Set("foo"); err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := new(FlagMapValue)
|
||||||
|
if err := f.Set("foo=bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := (*f)["foo"]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing value: %#v", f)
|
||||||
|
}
|
||||||
|
if exp := "bar"; r != exp {
|
||||||
|
t.Errorf("expected %q to be %q", r, exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("sets multiple", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := new(FlagMapValue)
|
||||||
|
|
||||||
|
r := map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"zip": "zap",
|
||||||
|
"cat": "dog",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r {
|
||||||
|
if err := f.Set(fmt.Sprintf("%s=%s", k, v)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range r {
|
||||||
|
r, ok := (*f)[k]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing value %q: %#v", k, f)
|
||||||
|
}
|
||||||
|
if exp := v; r != exp {
|
||||||
|
t.Errorf("expected %q to be %q", r, exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("overwrites", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
f := new(FlagMapValue)
|
||||||
|
if err := f.Set("foo=bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := f.Set("foo=zip"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, ok := (*f)["foo"]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing value: %#v", f)
|
||||||
|
}
|
||||||
|
if exp := "zip"; r != exp {
|
||||||
|
t.Errorf("expected %q to be %q", r, exp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Commands: Catalog"
|
||||||
|
sidebar_current: "docs-commands-catalog"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Consul Catalog
|
||||||
|
|
||||||
|
Command: `consul catalog`
|
||||||
|
|
||||||
|
The `catalog` command is used to interact with Consul's catalog via the command
|
||||||
|
line. It exposes top-level commands for reading and filtering data from the
|
||||||
|
registry.
|
||||||
|
|
||||||
|
The catalog is also accessible via the [HTTP API](/api/catalog.html).
|
||||||
|
|
||||||
|
## Basic Examples
|
||||||
|
|
||||||
|
List all datacenters:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog datacenters
|
||||||
|
dc1
|
||||||
|
dc2
|
||||||
|
dc3
|
||||||
|
```
|
||||||
|
|
||||||
|
List all nodes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes
|
||||||
|
Node ID Address DC
|
||||||
|
worker-01 1b662d97 10.4.5.31 dc1
|
||||||
|
```
|
||||||
|
|
||||||
|
List all nodes which provide a particular service:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes -service=redis
|
||||||
|
Node ID Address DC
|
||||||
|
worker-01 1b662d97 10.4.5.31 dc1
|
||||||
|
worker-02 d407a592 10.4.4.158 dc1
|
||||||
|
```
|
||||||
|
|
||||||
|
List all services:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog services
|
||||||
|
consul
|
||||||
|
postgresql
|
||||||
|
redis
|
||||||
|
```
|
||||||
|
|
||||||
|
List all services on a node:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog services -node=worker-01
|
||||||
|
consul
|
||||||
|
postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
For more examples, ask for subcommand help or view the subcommand documentation
|
||||||
|
by clicking on one of the links in the sidebar.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `consul catalog <subcommand>`
|
||||||
|
|
||||||
|
For the exact documentation for your Consul version, run `consul catalog -h` to
|
||||||
|
view the complete list of subcommands.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: consul catalog <subcommand> [options] [args]
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
datacenters Lists all known datacenters for this agent
|
||||||
|
nodes Lists all nodes in the given datacenter
|
||||||
|
services Lists all registered services in a datacenter
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, examples, and usage about a subcommand, click on the name
|
||||||
|
of the subcommand in the sidebar or one of the links below:
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Commands: Catalog List Datacenters"
|
||||||
|
sidebar_current: "docs-commands-catalog-datacenters"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Consul Catalog List Datacenters
|
||||||
|
|
||||||
|
Command: `consul catalog datacenters`
|
||||||
|
|
||||||
|
The `catalog datacenters` command prints all known datacenters.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
List all datacenters:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ consul catalog datacenters
|
||||||
|
dc1
|
||||||
|
dc2
|
||||||
|
dc3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `consul catalog datacenters [options]`
|
||||||
|
|
||||||
|
#### API Options
|
||||||
|
|
||||||
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
<%= partial "docs/commands/http_api_options_server" %>
|
|
@ -0,0 +1,73 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Commands: Catalog List Nodes"
|
||||||
|
sidebar_current: "docs-commands-catalog-nodes"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Consul Catalog List Nodes
|
||||||
|
|
||||||
|
Command: `consul catalog nodes`
|
||||||
|
|
||||||
|
The `catalog nodes` command prints all known nodes and metadata about them.
|
||||||
|
It can also query for nodes that match a particular metadata or provide a
|
||||||
|
particular service.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
List all nodes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes
|
||||||
|
Node ID Address DC
|
||||||
|
worker-01 1b662d97 10.4.5.31 dc1
|
||||||
|
```
|
||||||
|
|
||||||
|
Print detailed node information such as tagged addresses and node metadata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes -detailed
|
||||||
|
Node ID Address DC TaggedAddresses Meta
|
||||||
|
worker-01 1b662d97-8b5c-3cc2-0ac0-96f55ad423b5 10.4.5.31 dc1 lan=10.4.5.31, wan=10.4.5.31
|
||||||
|
```
|
||||||
|
|
||||||
|
List nodes which provide the service name "web":
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes -service=web
|
||||||
|
Node ID Address DC TaggedAddresses Meta
|
||||||
|
worker-01 1b662d97-8b5c-3cc2-0ac0-96f55ad423b5 10.4.5.31 dc1 lan=10.4.5.31, wan=10.4.5.31
|
||||||
|
```
|
||||||
|
|
||||||
|
Sort the resulting node list by estimated round trip time to worker-05:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog nodes -near=web-05
|
||||||
|
Node ID Address DC TaggedAddresses Meta
|
||||||
|
worker-01 1b662d97-8b5c-3cc2-0ac0-96f55ad423b5 10.4.5.31 dc1 lan=10.4.5.31, wan=10.4.5.31
|
||||||
|
worker-02 d407a592-e93c-4d8e-8a6d-aba853d1e067 10.4.4.158 dc1 lan=10.4.4.158, wan=10.4.4.158
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `consul catalog nodes [options]`
|
||||||
|
|
||||||
|
#### API Options
|
||||||
|
|
||||||
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
<%= partial "docs/commands/http_api_options_server" %>
|
||||||
|
|
||||||
|
#### Catalog List Nodes Options
|
||||||
|
|
||||||
|
- `-detailed` - Output detailed information about the nodes including their
|
||||||
|
addresses and metadata.
|
||||||
|
|
||||||
|
- `-near=<string>`- Node name to sort the node list in ascending order based on
|
||||||
|
estimated round-trip time from that node. Passing `"_agent"` will use this
|
||||||
|
agent's node for sorting.
|
||||||
|
|
||||||
|
- `-node-meta=<key=value>` - Metadata to filter nodes with the given key=value
|
||||||
|
pairs. This flag may be specified multiple times to filter on multiple sources
|
||||||
|
of metadata.
|
||||||
|
|
||||||
|
- `-service=<id or name>` - Service id or name to filter nodes. Only nodes
|
||||||
|
which are providing the given service will be returned.
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
page_title: "Commands: Catalog List Services"
|
||||||
|
sidebar_current: "docs-commands-catalog-services"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Consul Catalog List Services
|
||||||
|
|
||||||
|
Command: `consul catalog services`
|
||||||
|
|
||||||
|
The `catalog services` command prints all known services. It can also query
|
||||||
|
for services that match particular metadata or list the services that a
|
||||||
|
particular node provides.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
List all services:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog services
|
||||||
|
consul
|
||||||
|
postgresql
|
||||||
|
redis
|
||||||
|
```
|
||||||
|
|
||||||
|
Show all services with their tags:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog services -tags
|
||||||
|
consul
|
||||||
|
postgresql leader
|
||||||
|
redis primary,v1
|
||||||
|
```
|
||||||
|
|
||||||
|
List services for the node "worker-01":
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ consul catalog services -node=worker-01
|
||||||
|
consul
|
||||||
|
redis
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Usage: `consul catalog services [options]`
|
||||||
|
|
||||||
|
#### API Options
|
||||||
|
|
||||||
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
<%= partial "docs/commands/http_api_options_server" %>
|
||||||
|
|
||||||
|
#### Catalog List Nodes Options
|
||||||
|
|
||||||
|
- `-node=<id or name>` - Node `id or name` for which to list services.
|
||||||
|
|
||||||
|
- `-node-meta=<key=value>` - Metadata to filter nodes with the given
|
||||||
|
`key=value` pairs. If specified, only services running on nodes matching the
|
||||||
|
given metadata will be returned. This flag may be specified multiple times to
|
||||||
|
filter on multiple sources of metadata.
|
||||||
|
|
||||||
|
- `-tags` - Display each service's tags as a comma-separated list beside each
|
||||||
|
service entry.
|
|
@ -56,6 +56,20 @@
|
||||||
<li<%= sidebar_current("docs-commands-agent") %>>
|
<li<%= sidebar_current("docs-commands-agent") %>>
|
||||||
<a href="/docs/commands/agent.html">agent</a>
|
<a href="/docs/commands/agent.html">agent</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-commands-catalog") %>>
|
||||||
|
<a href="/docs/commands/catalog.html">catalog</a>
|
||||||
|
<ul class="nav">
|
||||||
|
<li<%= sidebar_current("docs-commands-catalog-datacenters") %>>
|
||||||
|
<a href="/docs/commands/catalog/datacenters.html">datacenters</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-commands-catalog-nodes") %>>
|
||||||
|
<a href="/docs/commands/catalog/nodes.html">nodes</a>
|
||||||
|
</li>
|
||||||
|
<li<%= sidebar_current("docs-commands-catalog-services") %>>
|
||||||
|
<a href="/docs/commands/catalog/services.html">services</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li<%= sidebar_current("docs-commands-event") %>>
|
<li<%= sidebar_current("docs-commands-event") %>>
|
||||||
<a href="/docs/commands/event.html">event</a>
|
<a href="/docs/commands/event.html">event</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
Loading…
Reference in New Issue