Add catalog CLI functions (#3204)

pull/3221/merge
Seth Vargo 2017-07-14 15:45:08 -04:00 committed by James Phillips
parent a5461ce368
commit afd83a9705
17 changed files with 1317 additions and 25 deletions

View File

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

View File

@ -0,0 +1,8 @@
package command
import "testing"
func TestCatalogCommand_noTabs(t *testing.T) {
t.Parallel()
assertNoTabs(t, new(CatalogCommand))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
package command
import (
"fmt"
"os"
"os/signal"
"sort"
"strings"
"syscall"
"github.com/hashicorp/consul/version"
@ -30,6 +33,42 @@ func init() {
}, 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) {
return &ConfigTestCommand{
BaseCommand: BaseCommand{
@ -363,3 +402,19 @@ func makeShutdownCh() <-chan struct{} {
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)
}

View File

@ -49,28 +49,3 @@ Usage: consul kv <subcommand> [options] [args]
func (c *KVCommand) Synopsis() string {
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.
`)

View File

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

View File

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

View File

@ -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:

View File

@ -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" %>

View File

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

View File

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

View File

@ -56,6 +56,20 @@
<li<%= sidebar_current("docs-commands-agent") %>>
<a href="/docs/commands/agent.html">agent</a>
</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") %>>
<a href="/docs/commands/event.html">event</a>
</li>