Merge pull request #2723 from hashicorp/f-cli-rework-2

Centralize command-line parsing part 2
This commit is contained in:
Kyle Havlovitz 2017-02-09 21:09:35 -05:00 committed by GitHub
commit 8ecbd91341
33 changed files with 543 additions and 612 deletions

View File

@ -79,6 +79,11 @@ type QueryOptions struct {
// metadata key/value pairs. Currently, only one key/value pair can
// be provided for filtering.
NodeMeta map[string]string
// RelayFactor is used in keyring operations to cause reponses to be
// relayed back to the sender through N other random nodes. Must be
// a value from 0 to 5 (inclusive).
RelayFactor uint8
}
// WriteOptions are used to parameterize a write
@ -90,6 +95,11 @@ type WriteOptions struct {
// Token is used to provide a per-request ACL token
// which overrides the agent's default token.
Token string
// RelayFactor is used in keyring operations to cause reponses to be
// relayed back to the sender through N other random nodes. Must be
// a value from 0 to 5 (inclusive).
RelayFactor uint8
}
// QueryMeta is used to return meta data about a query
@ -396,6 +406,9 @@ func (r *request) setQueryOptions(q *QueryOptions) {
r.params.Add("node-meta", key+":"+value)
}
}
if q.RelayFactor != 0 {
r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
}
}
// durToMsec converts a duration to a millisecond specified string. If the
@ -437,6 +450,9 @@ func (r *request) setWriteOptions(q *WriteOptions) {
if q.Token != "" {
r.header.Set("X-Consul-Token", q.Token)
}
if q.RelayFactor != 0 {
r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
}
}
// toHTTP converts the request to an HTTP request

View File

@ -154,7 +154,14 @@ func keyringErrorsOrNil(responses []*structs.KeyringResponse) error {
var errs error
for _, response := range responses {
if response.Error != "" {
errs = multierror.Append(errs, fmt.Errorf(response.Error))
pool := response.Datacenter + " (LAN)"
if response.WAN {
pool = "WAN"
}
errs = multierror.Append(errs, fmt.Errorf("%s error: %s", pool, response.Error))
for key, message := range response.Messages {
errs = multierror.Append(errs, fmt.Errorf("%s: %s", key, message))
}
}
}
return errs

View File

@ -58,6 +58,16 @@ func (c *Command) HTTPClient() (*api.Client, error) {
return api.NewClient(config)
}
func (c *Command) HTTPDatacenter() string {
return c.datacenter.String()
}
func (c *Command) HTTPStale() bool {
var stale bool
c.stale.Merge(&stale)
return stale
}
// httpFlagsClient is the list of flags that apply to HTTP connections.
func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
if f == nil {
@ -65,7 +75,7 @@ func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
}
f.Var(&c.httpAddr, "http-addr",
"Address and port to the Consul HTTP agent. The value can be an IP "+
"The `address` and port of the Consul HTTP agent. The value can be an IP "+
"address or DNS address, but it must also include the port. This can "+
"also be specified via the CONSUL_HTTP_ADDR environment variable. The "+
"default value is 127.0.0.1:8500.")
@ -130,6 +140,11 @@ func (c *Command) Parse(args []string) error {
// Help returns the help for this flagSet.
func (c *Command) Help() string {
// Some commands with subcommands (kv/snapshot) call this without initializing
// any flags first, so exit early to avoid a panic
if c.flagSet == nil {
return ""
}
return c.helpFlagsFor(c.flagSet)
}

View File

@ -1,19 +1,18 @@
package command
import (
"flag"
"fmt"
"regexp"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// EventCommand is a Command implementation that is used to
// fire new events
type EventCommand struct {
Ui cli.Ui
base.Command
}
func (c *EventCommand) Help() string {
@ -24,33 +23,25 @@ Usage: consul event [options] [payload]
a name, but a payload is optional. Events support filtering using
regular expressions on node name, service, and tag definitions.
Options:
` + c.Command.Help()
-http-addr=127.0.0.1:8500 HTTP address of the Consul agent.
-datacenter="" Datacenter to dispatch in. Defaults to that of agent.
-name="" Name of the event.
-node="" Regular expression to filter on node names
-service="" Regular expression to filter on service instances
-tag="" Regular expression to filter on service tags. Must be used
with -service.
-token="" ACL token to use during requests. Defaults to that
of the agent.
`
return strings.TrimSpace(helpText)
}
func (c *EventCommand) Run(args []string) int {
var datacenter, name, node, service, tag, token string
cmdFlags := flag.NewFlagSet("event", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.StringVar(&datacenter, "datacenter", "", "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.StringVar(&node, "node", "", "")
cmdFlags.StringVar(&service, "service", "", "")
cmdFlags.StringVar(&tag, "tag", "", "")
cmdFlags.StringVar(&token, "token", "", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
var name, node, service, tag string
f := c.Command.NewFlagSet(c)
f.StringVar(&name, "name", "",
"Name of the event.")
f.StringVar(&node, "node", "",
"Regular expression to filter on node names.")
f.StringVar(&service, "service", "",
"Regular expression to filter on service instances.")
f.StringVar(&tag, "tag", "",
"Regular expression to filter on service tags. Must be used with -service.")
if err := c.Command.Parse(args); err != nil {
return 1
}
@ -88,7 +79,7 @@ func (c *EventCommand) Run(args []string) int {
// Check for a payload
var payload []byte
args = cmdFlags.Args()
args = f.Args()
switch len(args) {
case 0:
case 1:
@ -101,7 +92,7 @@ func (c *EventCommand) Run(args []string) int {
}
// Create and test the HTTP client
client, err := HTTPClient(*httpAddr)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
@ -121,13 +112,9 @@ func (c *EventCommand) Run(args []string) int {
ServiceFilter: service,
TagFilter: tag,
}
opts := &consulapi.WriteOptions{
Datacenter: datacenter,
Token: token,
}
// Fire the event
id, _, err := event.Fire(params, opts)
id, _, err := event.Fire(params, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error firing event: %s", err))
return 1

View File

@ -1,13 +1,14 @@
package command
import (
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
"strings"
"testing"
)
func TestEventCommand_implements(t *testing.T) {
var _ cli.Command = &WatchCommand{}
var _ cli.Command = &EventCommand{}
}
func TestEventCommandRun(t *testing.T) {
@ -15,7 +16,12 @@ func TestEventCommandRun(t *testing.T) {
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &EventCommand{Ui: ui}
c := &EventCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}
args := []string{"-http-addr=" + a1.httpAddr, "-name=cmd"}
code := c.Run(args)

View File

@ -3,7 +3,6 @@ package command
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
@ -15,6 +14,7 @@ import (
"unicode"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
@ -53,9 +53,7 @@ const (
// rExecConf is used to pass around configuration
type rExecConf struct {
datacenter string
prefix string
token string
prefix string
foreignDC bool
localDC string
@ -118,8 +116,9 @@ type rExecExit struct {
// ExecCommand is a Command implementation that is used to
// do remote execution of commands
type ExecCommand struct {
base.Command
ShutdownCh <-chan struct{}
Ui cli.Ui
conf rExecConf
client *consulapi.Client
sessionID string
@ -127,24 +126,29 @@ type ExecCommand struct {
}
func (c *ExecCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("exec", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.StringVar(&c.conf.datacenter, "datacenter", "", "")
cmdFlags.StringVar(&c.conf.node, "node", "", "")
cmdFlags.StringVar(&c.conf.service, "service", "", "")
cmdFlags.StringVar(&c.conf.tag, "tag", "", "")
cmdFlags.StringVar(&c.conf.prefix, "prefix", rExecPrefix, "")
cmdFlags.DurationVar(&c.conf.replWait, "wait-repl", rExecReplicationWait, "")
cmdFlags.DurationVar(&c.conf.wait, "wait", rExecQuietWait, "")
cmdFlags.BoolVar(&c.conf.verbose, "verbose", false, "")
cmdFlags.StringVar(&c.conf.token, "token", "", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
f.StringVar(&c.conf.node, "node", "",
"Regular expression to filter on node names.")
f.StringVar(&c.conf.service, "service", "",
"Regular expression to filter on service instances.")
f.StringVar(&c.conf.tag, "tag", "",
"Regular expression to filter on service tags. Must be used with -service.")
f.StringVar(&c.conf.prefix, "prefix", rExecPrefix,
"Prefix in the KV store to use for request data.")
f.DurationVar(&c.conf.wait, "wait", rExecQuietWait,
"Period to wait with no responses before terminating execution.")
f.DurationVar(&c.conf.replWait, "wait-repl", rExecReplicationWait,
"Period to wait for replication before firing event. This is an "+
"optimization to allow stale reads to be performed.")
f.BoolVar(&c.conf.verbose, "verbose", false,
"Enables verbose output.")
if err := c.Command.Parse(args); err != nil {
return 1
}
// Join the commands to execute
c.conf.cmd = strings.Join(cmdFlags.Args(), " ")
c.conf.cmd = strings.Join(f.Args(), " ")
// If there is no command, read stdin for a script input
if c.conf.cmd == "-" {
@ -175,11 +179,7 @@ func (c *ExecCommand) Run(args []string) int {
}
// Create and test the HTTP client
client, err := HTTPClientConfig(func(clientConf *consulapi.Config) {
clientConf.Address = *httpAddr
clientConf.Datacenter = c.conf.datacenter
clientConf.Token = c.conf.token
})
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
@ -192,7 +192,7 @@ func (c *ExecCommand) Run(args []string) int {
c.client = client
// Check if this is a foreign datacenter
if c.conf.datacenter != "" && c.conf.datacenter != info["Config"]["Datacenter"] {
if c.Command.HTTPDatacenter() != "" && c.Command.HTTPDatacenter() != info["Config"]["Datacenter"] {
if c.conf.verbose {
c.Ui.Info("Remote exec in foreign datacenter, using Session TTL")
}
@ -489,7 +489,7 @@ func (c *ExecCommand) createSessionForeign() (string, error) {
node := services[0].Node.Node
if c.conf.verbose {
c.Ui.Info(fmt.Sprintf("Binding session to remote node %s@%s",
node, c.conf.datacenter))
node, c.Command.HTTPDatacenter()))
}
session := c.client.Session()
@ -618,22 +618,8 @@ Usage: consul exec [options] [-|command...]
definitions. If a command is '-', stdin will be read until EOF
and used as a script input.
Options:
` + c.Command.Help()
-http-addr=127.0.0.1:8500 HTTP address of the Consul agent.
-datacenter="" Datacenter to dispatch in. Defaults to that of agent.
-prefix="_rexec" Prefix in the KV store to use for request data
-node="" Regular expression to filter on node names
-service="" Regular expression to filter on service instances
-tag="" Regular expression to filter on service tags. Must be used
with -service.
-wait=2s Period to wait with no responses before terminating execution.
-wait-repl=200ms Period to wait for replication before firing event. This is an
optimization to allow stale reads to be performed.
-verbose Enables verbose output
-token="" ACL token to use during requests. Defaults to that
of the agent.
`
return strings.TrimSpace(helpText)
}

View File

@ -8,10 +8,21 @@ import (
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/command/base"
"github.com/hashicorp/consul/testutil"
"github.com/mitchellh/cli"
)
func testExecCommand(t *testing.T) (*cli.MockUi, *ExecCommand) {
ui := new(cli.MockUi)
return ui, &ExecCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
}
func TestExecCommand_implements(t *testing.T) {
var _ cli.Command = &ExecCommand{}
}
@ -21,8 +32,7 @@ func TestExecCommandRun(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &ExecCommand{Ui: ui}
ui, c := testExecCommand(t)
args := []string{"-http-addr=" + a1.httpAddr, "-wait=10s", "uptime"}
code := c.Run(args)
@ -57,8 +67,7 @@ func TestExecCommandRun_CrossDC(t *testing.T) {
waitForLeader(t, a1.httpAddr)
waitForLeader(t, a2.httpAddr)
ui := new(cli.MockUi)
c := &ExecCommand{Ui: ui}
ui, c := testExecCommand(t)
args := []string{"-http-addr=" + a1.httpAddr,
"-wait=400ms", "-datacenter=dc2", "uptime"}
@ -130,11 +139,8 @@ func TestExecCommand_Sessions(t *testing.T) {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &ExecCommand{
Ui: ui,
client: client,
}
_, c := testExecCommand(t)
c.client = client
id, err := c.createSession()
if err != nil {
@ -174,11 +180,8 @@ func TestExecCommand_Sessions_Foreign(t *testing.T) {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &ExecCommand{
Ui: ui,
client: client,
}
_, c := testExecCommand(t)
c.client = client
c.conf.foreignDC = true
c.conf.localDC = "dc1"
@ -228,11 +231,8 @@ func TestExecCommand_UploadDestroy(t *testing.T) {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &ExecCommand{
Ui: ui,
client: client,
}
_, c := testExecCommand(t)
c.client = client
id, err := c.createSession()
if err != nil {
@ -288,11 +288,8 @@ func TestExecCommand_StreamResults(t *testing.T) {
t.Fatalf("err: %v", err)
}
ui := new(cli.MockUi)
c := &ExecCommand{
Ui: ui,
client: client,
}
_, c := testExecCommand(t)
c.client = client
c.conf.prefix = "_rexec"
id, err := c.createSession()

View File

@ -1,9 +1,8 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
"sort"
"strings"
)
@ -11,7 +10,7 @@ import (
// InfoCommand is a Command implementation that queries a running
// Consul agent for various debugging statistics for operators
type InfoCommand struct {
Ui cli.Ui
base.Command
}
func (i *InfoCommand) Help() string {
@ -20,33 +19,34 @@ Usage: consul info [options]
Provides debugging information for operators
Options:
` + i.Command.Help()
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}
func (i *InfoCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("info", flag.ContinueOnError)
cmdFlags.Usage = func() { i.Ui.Output(i.Help()) }
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
i.Command.NewFlagSet(i)
if err := i.Command.Parse(args); err != nil {
return 1
}
client, err := RPCClient(*rpcAddr)
client, err := i.Command.HTTPClient()
if err != nil {
i.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
stats, err := client.Stats()
self, err := client.Agent().Self()
if err != nil {
i.Ui.Error(fmt.Sprintf("Error querying agent: %s", err))
return 1
}
stats, ok := self["Stats"]
if !ok {
i.Ui.Error(fmt.Sprintf("Agent response did not contain 'Stats' key: %v", self))
return 1
}
// Get the keys in sorted order
keys := make([]string, 0, len(stats))
@ -60,7 +60,11 @@ func (i *InfoCommand) Run(args []string) int {
i.Ui.Output(key + ":")
// Sort the sub-keys
subvals := stats[key]
subvals, ok := stats[key].(map[string]interface{})
if !ok {
i.Ui.Error(fmt.Sprintf("Got invalid subkey in stats: %v", subvals))
return 1
}
subkeys := make([]string, 0, len(subvals))
for k := range subvals {
subkeys = append(subkeys, k)
@ -77,5 +81,5 @@ func (i *InfoCommand) Run(args []string) int {
}
func (i *InfoCommand) Synopsis() string {
return "Provides debugging information for operators"
return "Provides debugging information for operators."
}

View File

@ -1,6 +1,7 @@
package command
import (
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
"strings"
"testing"
@ -15,8 +16,13 @@ func TestInfoCommandRun(t *testing.T) {
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &InfoCommand{Ui: ui}
args := []string{"-rpc-addr=" + a1.addr}
c := &InfoCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}
args := []string{"-http-addr=" + a1.httpAddr}
code := c.Run(args)
if code != 0 {

View File

@ -1,16 +1,15 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
"strings"
)
// JoinCommand is a Command implementation that tells a running Consul
// agent to join another.
type JoinCommand struct {
Ui cli.Ui
base.Command
}
func (c *JoinCommand) Help() string {
@ -20,26 +19,21 @@ Usage: consul join [options] address ...
Tells a running Consul agent (with "consul agent") to join the cluster
by specifying at least one existing member.
Options:
` + c.Command.Help()
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
-wan Joins a server to another server in the WAN pool
`
return strings.TrimSpace(helpText)
}
func (c *JoinCommand) Run(args []string) int {
var wan bool
cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.BoolVar(&wan, "wan", false, "wan")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
f.BoolVar(&wan, "wan", false, "Joins a server to another server in the WAN pool.")
if err := c.Command.Parse(args); err != nil {
return 1
}
addrs := cmdFlags.Args()
addrs := f.Args()
if len(addrs) == 0 {
c.Ui.Error("At least one address to join must be specified.")
c.Ui.Error("")
@ -47,21 +41,29 @@ func (c *JoinCommand) Run(args []string) int {
return 1
}
client, err := RPCClient(*rpcAddr)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
n, err := client.Join(addrs, wan)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error joining the cluster: %s", err))
joins := 0
for _, addr := range addrs {
err := client.Agent().Join(addr, wan)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error joining address '%s': %s", addr, err))
} else {
joins++
}
}
if joins == 0 {
c.Ui.Error("Failed to join any nodes.")
return 1
}
c.Ui.Output(fmt.Sprintf(
"Successfully joined cluster by contacting %d nodes.", n))
"Successfully joined cluster by contacting %d nodes.", joins))
return 0
}

View File

@ -2,11 +2,22 @@ package command
import (
"fmt"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
"strings"
"testing"
)
func testJoinCommand(t *testing.T) (*cli.MockUi, *JoinCommand) {
ui := new(cli.MockUi)
return ui, &JoinCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}
}
func TestJoinCommand_implements(t *testing.T) {
var _ cli.Command = &JoinCommand{}
}
@ -17,10 +28,9 @@ func TestJoinCommandRun(t *testing.T) {
defer a1.Shutdown()
defer a2.Shutdown()
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
ui, c := testJoinCommand(t)
args := []string{
"-rpc-addr=" + a1.addr,
"-http-addr=" + a1.httpAddr,
fmt.Sprintf("127.0.0.1:%d", a2.config.Ports.SerfLan),
}
@ -40,10 +50,9 @@ func TestJoinCommandRun_wan(t *testing.T) {
defer a1.Shutdown()
defer a2.Shutdown()
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
ui, c := testJoinCommand(t)
args := []string{
"-rpc-addr=" + a1.addr,
"-http-addr=" + a1.httpAddr,
"-wan",
fmt.Sprintf("127.0.0.1:%d", a2.config.Ports.SerfWan),
}
@ -59,9 +68,8 @@ func TestJoinCommandRun_wan(t *testing.T) {
}
func TestJoinCommandRun_noAddrs(t *testing.T) {
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
args := []string{"-rpc-addr=foo"}
ui, c := testJoinCommand(t)
args := []string{"-http-addr=foo"}
code := c.Run(args)
if code != 1 {

View File

@ -6,16 +6,21 @@ import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KeygenCommand is a Command implementation that generates an encryption
// key for use in `consul agent`.
type KeygenCommand struct {
Ui cli.Ui
base.Command
}
func (c *KeygenCommand) Run(_ []string) int {
func (c *KeygenCommand) Run(args []string) int {
c.Command.NewFlagSet(c)
if err := c.Command.Parse(args); err != nil {
return 1
}
key := make([]byte, 16)
n, err := rand.Reader.Read(key)
if err != nil {
@ -42,6 +47,8 @@ Usage: consul keygen
Generates a new encryption key that can be used to configure the
agent to encrypt traffic. The output of this command is already
in the proper format that the agent expects.
`
` + c.Command.Help()
return strings.TrimSpace(helpText)
}

View File

@ -2,6 +2,7 @@ package command
import (
"encoding/base64"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
"testing"
)
@ -12,7 +13,12 @@ func TestKeygenCommand_implements(t *testing.T) {
func TestKeygenCommand(t *testing.T) {
ui := new(cli.MockUi)
c := &KeygenCommand{Ui: ui}
c := &KeygenCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetNone,
},
}
code := c.Run(nil)
if code != 0 {
t.Fatalf("bad: %d", code)

View File

@ -1,37 +1,46 @@
package command
import (
"flag"
"fmt"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
// KeyringCommand is a Command implementation that handles querying, installing,
// and removing gossip encryption keys from a keyring.
type KeyringCommand struct {
Ui cli.Ui
base.Command
}
func (c *KeyringCommand) Run(args []string) int {
var installKey, useKey, removeKey, token string
var installKey, useKey, removeKey string
var listKeys bool
var relay int
cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
f := c.Command.NewFlagSet(c)
cmdFlags.StringVar(&installKey, "install", "", "install key")
cmdFlags.StringVar(&useKey, "use", "", "use key")
cmdFlags.StringVar(&removeKey, "remove", "", "remove key")
cmdFlags.BoolVar(&listKeys, "list", false, "list keys")
cmdFlags.StringVar(&token, "token", "", "acl token")
cmdFlags.IntVar(&relay, "relay-factor", 0, "relay factor")
f.StringVar(&installKey, "install", "",
"Install a new encryption key. This will broadcast the new key to "+
"all members in the cluster.")
f.StringVar(&useKey, "use", "",
"Change the primary encryption key, which is used to encrypt "+
"messages. The key must already be installed before this operation "+
"can succeed.")
f.StringVar(&removeKey, "remove", "",
"Remove the given key from the cluster. This operation may only be "+
"performed on keys which are not currently the primary key.")
f.BoolVar(&listKeys, "list", false,
"List all keys currently in use within the cluster.")
f.IntVar(&relay, "relay-factor", 0,
"Setting this to a non-zero value will cause nodes to relay their response "+
"to the operation through this many randomly-chosen other nodes in the "+
"cluster. The maximum allowed value is 5.")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
if err := c.Command.Parse(args); err != nil {
return 1
}
@ -66,124 +75,69 @@ func (c *KeyringCommand) Run(args []string) int {
}
// All other operations will require a client connection
client, err := RPCClient(*rpcAddr)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
if listKeys {
c.Ui.Info("Gathering installed encryption keys...")
r, err := client.ListKeys(token, relayFactor)
responses, err := client.Operator().KeyringList(&consulapi.QueryOptions{RelayFactor: relayFactor})
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
if rval := c.handleResponse(r.Info, r.Messages); rval != 0 {
return rval
}
c.handleList(r.Info, r.Keys)
c.handleList(responses)
return 0
}
opts := &consulapi.WriteOptions{RelayFactor: relayFactor}
if installKey != "" {
c.Ui.Info("Installing new gossip encryption key...")
r, err := client.InstallKey(installKey, token, relayFactor)
err := client.Operator().KeyringInstall(installKey, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
return 0
}
if useKey != "" {
c.Ui.Info("Changing primary gossip encryption key...")
r, err := client.UseKey(useKey, token, relayFactor)
err := client.Operator().KeyringUse(useKey, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
return 0
}
if removeKey != "" {
c.Ui.Info("Removing gossip encryption key...")
r, err := client.RemoveKey(removeKey, token, relayFactor)
err := client.Operator().KeyringRemove(removeKey, opts)
if err != nil {
c.Ui.Error(fmt.Sprintf("error: %s", err))
return 1
}
return c.handleResponse(r.Info, r.Messages)
return 0
}
// Should never make it here
return 0
}
func (c *KeyringCommand) handleResponse(
info []agent.KeyringInfo,
messages []agent.KeyringMessage) int {
var rval int
for _, i := range info {
if i.Error != "" {
pool := i.Pool
if pool != "WAN" {
pool = i.Datacenter + " (LAN)"
}
c.Ui.Error("")
c.Ui.Error(fmt.Sprintf("%s error: %s", pool, i.Error))
for _, msg := range messages {
if msg.Datacenter != i.Datacenter || msg.Pool != i.Pool {
continue
}
c.Ui.Error(fmt.Sprintf(" %s: %s", msg.Node, msg.Message))
}
rval = 1
}
}
if rval == 0 {
c.Ui.Info("Done!")
}
return rval
}
func (c *KeyringCommand) handleList(
info []agent.KeyringInfo,
keys []agent.KeyringEntry) {
installed := make(map[string]map[string][]int)
for _, key := range keys {
var nodes int
for _, i := range info {
if i.Datacenter == key.Datacenter && i.Pool == key.Pool {
nodes = i.NumNodes
}
func (c *KeyringCommand) handleList(responses []*consulapi.KeyringResponse) {
for _, response := range responses {
pool := response.Datacenter + " (LAN)"
if response.WAN {
pool = "WAN"
}
pool := key.Pool
if pool != "WAN" {
pool = key.Datacenter + " (LAN)"
}
if _, ok := installed[pool]; !ok {
installed[pool] = map[string][]int{key.Key: []int{key.Count, nodes}}
} else {
installed[pool][key.Key] = []int{key.Count, nodes}
}
}
for pool, keys := range installed {
c.Ui.Output("")
c.Ui.Output(pool + ":")
for key, num := range keys {
c.Ui.Output(fmt.Sprintf(" %s [%d/%d]", key, num[0], num[1]))
for key, num := range response.Keys {
c.Ui.Output(fmt.Sprintf(" %s [%d/%d]", key, num, response.NumNodes))
}
}
}
@ -205,26 +159,8 @@ Usage: consul keyring [options]
are no errors. If any node fails to reply or reports failure, the exit code
will be 1.
Options:
` + c.Command.Help()
-install=<key> Install a new encryption key. This will broadcast
the new key to all members in the cluster.
-list List all keys currently in use within the cluster.
-remove=<key> Remove the given key from the cluster. This
operation may only be performed on keys which are
not currently the primary key.
-token="" ACL token to use during requests. Defaults to that
of the agent.
-relay-factor Added in Consul 0.7.4, setting this to a non-zero
value will cause nodes to relay their response to
the operation through this many randomly-chosen
other nodes in the cluster. The maximum allowed
value is 5.
-use=<key> Change the primary encryption key, which is used to
encrypt messages. The key must already be installed
before this operation can succeed.
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}

View File

@ -5,9 +5,20 @@ import (
"testing"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testKeyringCommand(t *testing.T) (*cli.MockUi, *KeyringCommand) {
ui := new(cli.MockUi)
return ui, &KeyringCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}
}
func TestKeyringCommand_implements(t *testing.T) {
var _ cli.Command = &KeyringCommand{}
}
@ -23,7 +34,7 @@ func TestKeyringCommandRun(t *testing.T) {
defer a1.Shutdown()
// The LAN and WAN keyrings were initialized with key1
out := listKeys(t, a1.addr)
out := listKeys(t, a1.httpAddr)
if !strings.Contains(out, "dc1 (LAN):\n "+key1) {
t.Fatalf("bad: %#v", out)
}
@ -35,10 +46,10 @@ func TestKeyringCommandRun(t *testing.T) {
}
// Install the second key onto the keyring
installKey(t, a1.addr, key2)
installKey(t, a1.httpAddr, key2)
// Both keys should be present
out = listKeys(t, a1.addr)
out = listKeys(t, a1.httpAddr)
for _, key := range []string{key1, key2} {
if !strings.Contains(out, key) {
t.Fatalf("bad: %#v", out)
@ -46,11 +57,11 @@ func TestKeyringCommandRun(t *testing.T) {
}
// Rotate to key2, remove key1
useKey(t, a1.addr, key2)
removeKey(t, a1.addr, key1)
useKey(t, a1.httpAddr, key2)
removeKey(t, a1.httpAddr, key1)
// Only key2 is present now
out = listKeys(t, a1.addr)
out = listKeys(t, a1.httpAddr)
if !strings.Contains(out, "dc1 (LAN):\n "+key2) {
t.Fatalf("bad: %#v", out)
}
@ -63,8 +74,7 @@ func TestKeyringCommandRun(t *testing.T) {
}
func TestKeyringCommandRun_help(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
code := c.Run(nil)
if code != 1 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
@ -77,9 +87,8 @@ func TestKeyringCommandRun_help(t *testing.T) {
}
func TestKeyringCommandRun_failedConnection(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
args := []string{"-list", "-rpc-addr=127.0.0.1:0"}
ui, c := testKeyringCommand(t)
args := []string{"-list", "-http-addr=127.0.0.1:0"}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d, %#v", code, ui.ErrorWriter.String())
@ -90,8 +99,7 @@ func TestKeyringCommandRun_failedConnection(t *testing.T) {
}
func TestKeyringCommandRun_invalidRelayFactor(t *testing.T) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
args := []string{"-list", "-relay-factor=6"}
code := c.Run(args)
@ -101,10 +109,9 @@ func TestKeyringCommandRun_invalidRelayFactor(t *testing.T) {
}
func listKeys(t *testing.T, addr string) string {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
args := []string{"-list", "-rpc-addr=" + addr}
args := []string{"-list", "-http-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
@ -114,10 +121,9 @@ func listKeys(t *testing.T, addr string) string {
}
func installKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
args := []string{"-install=" + key, "-rpc-addr=" + addr}
args := []string{"-install=" + key, "-http-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
@ -125,10 +131,9 @@ func installKey(t *testing.T, addr string, key string) {
}
func useKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
args := []string{"-use=" + key, "-rpc-addr=" + addr}
args := []string{"-use=" + key, "-http-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
@ -136,10 +141,9 @@ func useKey(t *testing.T, addr string, key string) {
}
func removeKey(t *testing.T, addr string, key string) {
ui := new(cli.MockUi)
c := &KeyringCommand{Ui: ui}
ui, c := testKeyringCommand(t)
args := []string{"-remove=" + key, "-rpc-addr=" + addr}
args := []string{"-remove=" + key, "-http-addr=" + addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())

View File

@ -3,13 +3,14 @@ package command
import (
"strings"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
// KVCommand is a Command implementation that just shows help for
// the subcommands nested below it.
type KVCommand struct {
Ui cli.Ui
base.Command
}
func (c *KVCommand) Run(args []string) int {

View File

@ -1,18 +1,17 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KVDeleteCommand is a Command implementation that is used to delete a key or
// prefix of keys from the key-value store.
type KVDeleteCommand struct {
Ui cli.Ui
base.Command
}
func (c *KVDeleteCommand) Help() string {
@ -33,40 +32,30 @@ Usage: consul kv delete [options] KEY_OR_PREFIX
This will delete the keys named "foo", "food", and "foo/bar/zip" if they
existed.
` + apiOptsText + `
` + c.Command.Help()
KV Delete Options:
-cas Perform a Check-And-Set operation. Specifying this
value also requires the -modify-index flag to be set.
The default value is false.
-modify-index=<int> Unsigned integer representing the ModifyIndex of the
key. This is used in combination with the -cas flag.
-recurse Recursively delete all keys with the path. The default
value is false.
`
return strings.TrimSpace(helpText)
}
func (c *KVDeleteCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
cas := cmdFlags.Bool("cas", false, "")
modifyIndex := cmdFlags.Uint64("modify-index", 0, "")
recurse := cmdFlags.Bool("recurse", false, "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
cas := f.Bool("cas", false,
"Perform a Check-And-Set operation. Specifying this value also requires "+
"the -modify-index flag to be set. The default value is false.")
modifyIndex := f.Uint64("modify-index", 0,
"Unsigned integer representing the ModifyIndex of the key. This is "+
"used in combination with the -cas flag.")
recurse := f.Bool("recurse", false,
"Recursively delete all keys with the path. The default value is false.")
if err := c.Command.Parse(args); err != nil {
return 1
}
key := ""
// Check for arg validation
args = cmdFlags.Args()
args = f.Args()
switch len(args) {
case 0:
key = ""
@ -109,22 +98,15 @@ func (c *KVDeleteCommand) Run(args []string) int {
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
wo := &api.WriteOptions{
Datacenter: *datacenter,
}
switch {
case *recurse:
if _, err := client.KV().DeleteTree(key, wo); err != nil {
if _, err := client.KV().DeleteTree(key, nil); err != nil {
c.Ui.Error(fmt.Sprintf("Error! Did not delete prefix %s: %s", key, err))
return 1
}
@ -137,7 +119,7 @@ func (c *KVDeleteCommand) Run(args []string) int {
ModifyIndex: *modifyIndex,
}
success, _, err := client.KV().DeleteCAS(pair, wo)
success, _, err := client.KV().DeleteCAS(pair, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! Did not delete key %s: %s", key, err))
return 1
@ -150,7 +132,7 @@ func (c *KVDeleteCommand) Run(args []string) int {
c.Ui.Info(fmt.Sprintf("Success! Deleted key: %s", key))
return 0
default:
if _, err := client.KV().Delete(key, wo); err != nil {
if _, err := client.KV().Delete(key, nil); err != nil {
c.Ui.Error(fmt.Sprintf("Error deleting key %s: %s", key, err))
return 1
}

View File

@ -6,9 +6,20 @@ import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testKVDeleteCommand(t *testing.T) (*cli.MockUi, *KVDeleteCommand) {
ui := new(cli.MockUi)
return ui, &KVDeleteCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
}
func TestKVDeleteCommand_implements(t *testing.T) {
var _ cli.Command = &KVDeleteCommand{}
}
@ -18,8 +29,7 @@ func TestKVDeleteCommand_noTabs(t *testing.T) {
}
func TestKVDeleteCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &KVDeleteCommand{Ui: ui}
ui, c := testKVDeleteCommand(t)
cases := map[string]struct {
args []string
@ -73,8 +83,7 @@ func TestKVDeleteCommand_Run(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVDeleteCommand{Ui: ui}
ui, c := testKVDeleteCommand(t)
pair := &api.KVPair{
Key: "foo",
@ -109,8 +118,7 @@ func TestKVDeleteCommand_Recurse(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVDeleteCommand{Ui: ui}
ui, c := testKVDeleteCommand(t)
keys := []string{"foo/a", "foo/b", "food"}
@ -152,8 +160,7 @@ func TestKVDeleteCommand_CAS(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVDeleteCommand{Ui: ui}
ui, c := testKVDeleteCommand(t)
pair := &api.KVPair{
Key: "foo",

View File

@ -3,18 +3,17 @@ package command
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KVExportCommand is a Command implementation that is used to export
// a KV tree as JSON
type KVExportCommand struct {
Ui cli.Ui
base.Command
}
func (c *KVExportCommand) Synopsis() string {
@ -33,29 +32,20 @@ Usage: consul kv export [KEY_OR_PREFIX]
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText + `
` + c.Command.Help()
KV Export Options:
None.
`
return strings.TrimSpace(helpText)
}
func (c *KVExportCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("export", flag.ContinueOnError)
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
stale := cmdFlags.Bool("stale", false, "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
if err := c.Command.Parse(args); err != nil {
return 1
}
key := ""
// Check for arg validation
args = cmdFlags.Args()
args = f.Args()
switch len(args) {
case 0:
key = ""
@ -74,18 +64,14 @@ func (c *KVExportCommand) Run(args []string) int {
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
pairs, _, err := client.KV().List(key, &api.QueryOptions{
Datacenter: *datacenter,
AllowStale: *stale,
AllowStale: c.Command.HTTPStale(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err))

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
@ -15,7 +16,12 @@ func TestKVExportCommand_Run(t *testing.T) {
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVExportCommand{Ui: ui}
c := KVExportCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
keys := map[string]string{
"foo/a": "a",

View File

@ -3,20 +3,19 @@ package command
import (
"bytes"
"encoding/base64"
"flag"
"fmt"
"io"
"strings"
"text/tabwriter"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KVGetCommand is a Command implementation that is used to fetch the value of
// a key from the key-value store.
type KVGetCommand struct {
Ui cli.Ui
base.Command
}
func (c *KVGetCommand) Help() string {
@ -51,54 +50,39 @@ Usage: consul kv get [options] [KEY_OR_PREFIX]
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText + `
` + c.Command.Help()
KV Get Options:
-base64 Base64 encode the value. The default value is false.
-detailed Provide additional metadata about the key in addition
to the value such as the ModifyIndex and any flags
that may have been set on the key. The default value
is false.
-keys List keys which start with the given prefix, but not
their values. This is especially useful if you only
need the key names themselves. This option is commonly
combined with the -separator option. The default value
is false.
-recurse Recursively look at all keys prefixed with the given
path. The default value is false.
-separator=<string> String to use as a separator between keys. The default
value is "/", but this option is only taken into
account when paired with the -keys flag.
`
return strings.TrimSpace(helpText)
}
func (c *KVGetCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
stale := cmdFlags.Bool("stale", false, "")
detailed := cmdFlags.Bool("detailed", false, "")
keys := cmdFlags.Bool("keys", false, "")
base64encode := cmdFlags.Bool("base64", false, "")
recurse := cmdFlags.Bool("recurse", false, "")
separator := cmdFlags.String("separator", "/", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
base64encode := f.Bool("base64", false,
"Base64 encode the value. The default value is false.")
detailed := f.Bool("detailed", false,
"Provide additional metadata about the key in addition to the value such "+
"as the ModifyIndex and any flags that may have been set on the key. "+
"The default value is false.")
keys := f.Bool("keys", false,
"List keys which start with the given prefix, but not their values. "+
"This is especially useful if you only need the key names themselves. "+
"This option is commonly combined with the -separator option. The default "+
"value is false.")
recurse := f.Bool("recurse", false,
"Recursively look at all keys prefixed with the given path. The default "+
"value is false.")
separator := f.String("separator", "/",
"String to use as a separator between keys. The default value is \"/\", "+
"but this option is only taken into account when paired with the -keys flag.")
if err := c.Command.Parse(args); err != nil {
return 1
}
key := ""
// Check for arg validation
args = cmdFlags.Args()
args = f.Args()
switch len(args) {
case 0:
key = ""
@ -124,10 +108,7 @@ func (c *KVGetCommand) Run(args []string) int {
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
@ -136,8 +117,7 @@ func (c *KVGetCommand) Run(args []string) int {
switch {
case *keys:
keys, _, err := client.KV().Keys(key, *separator, &api.QueryOptions{
Datacenter: *datacenter,
AllowStale: *stale,
AllowStale: c.Command.HTTPStale(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
@ -151,8 +131,7 @@ func (c *KVGetCommand) Run(args []string) int {
return 0
case *recurse:
pairs, _, err := client.KV().List(key, &api.QueryOptions{
Datacenter: *datacenter,
AllowStale: *stale,
AllowStale: c.Command.HTTPStale(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err))
@ -184,8 +163,7 @@ func (c *KVGetCommand) Run(args []string) int {
return 0
default:
pair, _, err := client.KV().Get(key, &api.QueryOptions{
Datacenter: *datacenter,
AllowStale: *stale,
AllowStale: c.Command.HTTPStale(),
})
if err != nil {
c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err))

View File

@ -6,9 +6,20 @@ import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testKVGetCommand(t *testing.T) (*cli.MockUi, *KVGetCommand) {
ui := new(cli.MockUi)
return ui, &KVGetCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
}
func TestKVGetCommand_implements(t *testing.T) {
var _ cli.Command = &KVGetCommand{}
}
@ -18,8 +29,7 @@ func TestKVGetCommand_noTabs(t *testing.T) {
}
func TestKVGetCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
cases := map[string]struct {
args []string
@ -61,8 +71,7 @@ func TestKVGetCommand_Run(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
pair := &api.KVPair{
Key: "foo",
@ -94,8 +103,7 @@ func TestKVGetCommand_Missing(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
_, c := testKVGetCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,
@ -113,8 +121,7 @@ func TestKVGetCommand_Empty(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
pair := &api.KVPair{
Key: "empty",
@ -141,8 +148,7 @@ func TestKVGetCommand_Detailed(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
pair := &api.KVPair{
Key: "foo",
@ -184,8 +190,7 @@ func TestKVGetCommand_Keys(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
keys := []string{"foo/bar", "foo/baz", "foo/zip"}
for _, key := range keys {
@ -218,8 +223,7 @@ func TestKVGetCommand_Recurse(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
keys := map[string]string{
"foo/a": "a",
@ -257,8 +261,7 @@ func TestKVGetCommand_RecurseBase64(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
keys := map[string]string{
"foo/a": "Hello World 1",
@ -297,8 +300,7 @@ func TestKVGetCommand_DetailedBase64(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVGetCommand{Ui: ui}
ui, c := testKVGetCommand(t)
pair := &api.KVPair{
Key: "foo",

View File

@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
@ -13,13 +12,13 @@ import (
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KVImportCommand is a Command implementation that is used to import
// a KV tree stored as JSON
type KVImportCommand struct {
Ui cli.Ui
base.Command
// testStdin is the input for testing.
testStdin io.Reader
@ -50,27 +49,20 @@ Usage: consul kv import [DATA]
For a full list of options and examples, please see the Consul documentation.
` + apiOptsText + `
` + c.Command.Help()
KV Import Options:
None.
`
return strings.TrimSpace(helpText)
}
func (c *KVImportCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("import", flag.ContinueOnError)
f := c.Command.NewFlagSet(c)
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
if err := c.Command.Parse(args); err != nil {
return 1
}
// Check for arg validation
args = cmdFlags.Args()
args = f.Args()
data, err := c.dataFromArgs(args)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! %s", err))
@ -78,10 +70,7 @@ func (c *KVImportCommand) Run(args []string) int {
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
@ -106,12 +95,7 @@ func (c *KVImportCommand) Run(args []string) int {
Value: value,
}
wo := &api.WriteOptions{
Datacenter: *datacenter,
Token: *token,
}
if _, err := client.KV().Put(pair, wo); err != nil {
if _, err := client.KV().Put(pair, nil); err != nil {
c.Ui.Error(fmt.Sprintf("Error! Failed writing data for key %s: %s", pair.Key, err))
return 1
}

View File

@ -4,6 +4,7 @@ import (
"strings"
"testing"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
@ -27,7 +28,10 @@ func TestKVImportCommand_Run(t *testing.T) {
ui := new(cli.MockUi)
c := &KVImportCommand{
Ui: ui,
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
testStdin: strings.NewReader(json),
}

View File

@ -3,7 +3,6 @@ package command
import (
"bytes"
"encoding/base64"
"flag"
"fmt"
"io"
"io/ioutil"
@ -11,13 +10,13 @@ import (
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// KVPutCommand is a Command implementation that is used to write data to the
// key-value store.
type KVPutCommand struct {
Ui cli.Ui
base.Command
// testStdin is the input for testing.
testStdin io.Reader
@ -57,62 +56,45 @@ Usage: consul kv put [options] KEY [DATA]
Additional flags and more advanced use cases are detailed below.
` + apiOptsText + `
` + c.Command.Help()
KV Put Options:
-acquire Obtain a lock on the key. If the key does not exist,
this operation will create the key and obtain the
lock. The session must already exist and be specified
via the -session flag. The default value is false.
-base64 Treat the data as base 64 encoded. The default value
is false.
-cas Perform a Check-And-Set operation. Specifying this
value also requires the -modify-index flag to be set.
The default value is false.
-flags=<int> Unsigned integer value to assign to this key-value
pair. This value is not read by Consul, so clients can
use this value however makes sense for their use case.
The default value is 0 (no flags).
-modify-index=<int> Unsigned integer representing the ModifyIndex of the
key. This is used in combination with the -cas flag.
-release Forfeit the lock on the key at the given path. This
requires the -session flag to be set. The key must be
held by the session in order to be unlocked. The
default value is false.
-session=<string> User-defined identifer for this session as a string.
This is commonly used with the -acquire and -release
operations to build robust locking, but it can be set
on any key. The default value is empty (no session).
`
return strings.TrimSpace(helpText)
}
func (c *KVPutCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
httpAddr := HTTPAddrFlag(cmdFlags)
datacenter := cmdFlags.String("datacenter", "", "")
token := cmdFlags.String("token", "", "")
cas := cmdFlags.Bool("cas", false, "")
flags := cmdFlags.Uint64("flags", 0, "")
base64encoded := cmdFlags.Bool("base64", false, "")
modifyIndex := cmdFlags.Uint64("modify-index", 0, "")
session := cmdFlags.String("session", "", "")
acquire := cmdFlags.Bool("acquire", false, "")
release := cmdFlags.Bool("release", false, "")
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
cas := f.Bool("cas", false,
"Perform a Check-And-Set operation. Specifying this value also "+
"requires the -modify-index flag to be set. The default value "+
"is false.")
flags := f.Uint64("flags", 0,
"Unsigned integer value to assign to this key-value pair. This "+
"value is not read by Consul, so clients can use this value however "+
"makes sense for their use case. The default value is 0 (no flags).")
base64encoded := f.Bool("base64", false,
"Treat the data as base 64 encoded. The default value is false.")
modifyIndex := f.Uint64("modify-index", 0,
"Unsigned integer representing the ModifyIndex of the key. This is "+
"used in combination with the -cas flag.")
session := f.String("session", "",
"User-defined identifer for this session as a string. This is commonly "+
"used with the -acquire and -release operations to build robust locking, "+
"but it can be set on any key. The default value is empty (no session).")
acquire := f.Bool("acquire", false,
"Obtain a lock on the key. If the key does not exist, this operation "+
"will create the key and obtain the lock. The session must already "+
"exist and be specified via the -session flag. The default value is false.")
release := f.Bool("release", false,
"Forfeit the lock on the key at the given path. This requires the "+
"-session flag to be set. The key must be held by the session in order to "+
"be unlocked. The default value is false.")
if err := c.Command.Parse(args); err != nil {
return 1
}
// Check for arg validation
args = cmdFlags.Args()
args = f.Args()
key, data, err := c.dataFromArgs(args)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! %s", err))
@ -140,10 +122,7 @@ func (c *KVPutCommand) Run(args []string) int {
}
// Create and test the HTTP client
conf := api.DefaultConfig()
conf.Address = *httpAddr
conf.Token = *token
client, err := api.NewClient(conf)
client, err := c.Command.HTTPClient()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
@ -157,14 +136,9 @@ func (c *KVPutCommand) Run(args []string) int {
Session: *session,
}
wo := &api.WriteOptions{
Datacenter: *datacenter,
Token: *token,
}
switch {
case *cas:
ok, _, err := client.KV().CAS(pair, wo)
ok, _, err := client.KV().CAS(pair, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! Did not write to %s: %s", key, err))
return 1
@ -177,7 +151,7 @@ func (c *KVPutCommand) Run(args []string) int {
c.Ui.Info(fmt.Sprintf("Success! Data written to: %s", key))
return 0
case *acquire:
ok, _, err := client.KV().Acquire(pair, wo)
ok, _, err := client.KV().Acquire(pair, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", err))
return 1
@ -190,7 +164,7 @@ func (c *KVPutCommand) Run(args []string) int {
c.Ui.Info(fmt.Sprintf("Success! Lock acquired on: %s", key))
return 0
case *release:
ok, _, err := client.KV().Release(pair, wo)
ok, _, err := client.KV().Release(pair, nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", key))
return 1
@ -203,7 +177,7 @@ func (c *KVPutCommand) Run(args []string) int {
c.Ui.Info(fmt.Sprintf("Success! Lock released on: %s", key))
return 0
default:
if _, err := client.KV().Put(pair, wo); err != nil {
if _, err := client.KV().Put(pair, nil); err != nil {
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", err))
return 1
}

View File

@ -11,20 +11,30 @@ import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testKVPutCommand(t *testing.T) (*cli.MockUi, *KVPutCommand) {
ui := new(cli.MockUi)
return ui, &KVPutCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
}
func TestKVPutCommand_implements(t *testing.T) {
var _ cli.Command = &KVPutCommand{}
}
func TestKVPutCommand_noTabs(t *testing.T) {
assertNoTabs(t, new(KVPutCommand))
assertNoTabs(t, new(KVDeleteCommand))
}
func TestKVPutCommand_Validation(t *testing.T) {
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
cases := map[string]struct {
args []string
@ -78,8 +88,7 @@ func TestKVPutCommand_Run(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,
@ -106,8 +115,7 @@ func TestKVPutCommand_RunEmptyDataQuoted(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,
@ -134,8 +142,7 @@ func TestKVPutCommand_RunBase64(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
const encodedString = "aGVsbG8gd29ybGQK"
@ -170,8 +177,7 @@ func TestKVPutCommand_File(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
f, err := ioutil.TempFile("", "kv-put-command-file")
if err != nil {
@ -203,8 +209,7 @@ func TestKVPutCommand_File(t *testing.T) {
}
func TestKVPutCommand_FileNoExist(t *testing.T) {
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"foo", "@/nope/definitely/not-a-real-file.txt",
@ -228,11 +233,8 @@ func TestKVPutCommand_Stdin(t *testing.T) {
stdinR, stdinW := io.Pipe()
ui := new(cli.MockUi)
c := &KVPutCommand{
Ui: ui,
testStdin: stdinR,
}
ui, c := testKVPutCommand(t)
c.testStdin = stdinR
go func() {
stdinW.Write([]byte("bar"))
@ -264,8 +266,7 @@ func TestKVPutCommand_NegativeVal(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,
@ -292,8 +293,7 @@ func TestKVPutCommand_Flags(t *testing.T) {
defer srv.Shutdown()
waitForLeader(t, srv.httpAddr)
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,
@ -330,8 +330,7 @@ func TestKVPutCommand_CAS(t *testing.T) {
t.Fatalf("err: %#v", err)
}
ui := new(cli.MockUi)
c := &KVPutCommand{Ui: ui}
ui, c := testKVPutCommand(t)
args := []string{
"-http-addr=" + srv.httpAddr,

View File

@ -41,14 +41,20 @@ func init() {
"event": func() (cli.Command, error) {
return &command.EventCommand{
Ui: ui,
Command: base.Command{
Flags: base.FlagSetHTTP,
Ui: ui,
},
}, nil
},
"exec": func() (cli.Command, error) {
return &command.ExecCommand{
ShutdownCh: makeShutdownCh(),
Ui: ui,
Command: base.Command{
Flags: base.FlagSetHTTP,
Ui: ui,
},
}, nil
},
@ -61,57 +67,93 @@ func init() {
}, nil
},
"kv": func() (cli.Command, error) {
return &command.KVCommand{
Ui: ui,
}, nil
},
"kv delete": func() (cli.Command, error) {
return &command.KVDeleteCommand{
Ui: ui,
}, nil
},
"kv get": func() (cli.Command, error) {
return &command.KVGetCommand{
Ui: ui,
}, nil
},
"kv put": func() (cli.Command, error) {
return &command.KVPutCommand{
Ui: ui,
}, nil
},
"kv export": func() (cli.Command, error) {
return &command.KVExportCommand{
Ui: ui,
}, nil
},
"kv import": func() (cli.Command, error) {
return &command.KVImportCommand{
Ui: ui,
"info": func() (cli.Command, error) {
return &command.InfoCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}, nil
},
"join": func() (cli.Command, error) {
return &command.JoinCommand{
Ui: ui,
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}, nil
},
"keygen": func() (cli.Command, error) {
return &command.KeygenCommand{
Ui: ui,
Command: base.Command{
Ui: ui,
Flags: base.FlagSetNone,
},
}, nil
},
"keyring": func() (cli.Command, error) {
return &command.KeyringCommand{
Ui: ui,
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}, nil
},
"kv": func() (cli.Command, error) {
return &command.KVCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetNone,
},
}, nil
},
"kv delete": func() (cli.Command, error) {
return &command.KVDeleteCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}, nil
},
"kv get": func() (cli.Command, error) {
return &command.KVGetCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}, nil
},
"kv put": func() (cli.Command, error) {
return &command.KVPutCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}, nil
},
"kv export": func() (cli.Command, error) {
return &command.KVExportCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}, nil
},
"kv import": func() (cli.Command, error) {
return &command.KVImportCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}, nil
},
@ -156,12 +198,6 @@ func init() {
}, nil
},
"info": func() (cli.Command, error) {
return &command.InfoCommand{
Ui: ui,
}, nil
},
"reload": func() (cli.Command, error) {
return &command.ReloadCommand{
Ui: ui,

View File

@ -39,13 +39,12 @@ Usage: `consul event [options] [payload]`
The only required option is `-name` which specifies the event name. An optional
payload can be provided as the final argument.
The list of available flags are:
#### API Options
* `-http-addr` - Address to the HTTP server of the agent you want to contact
to send this command. If this isn't specified, the command will contact
`127.0.0.1:8500` which is the default HTTP address of a Consul agent.
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
* `-datacenter` - Datacenter to query. Defaults to that of agent.
#### Command Options
* `-name` - The name of the event.
@ -57,5 +56,3 @@ The list of available flags are:
a matching tag. This must be used with `-service`. As an example, you may
do `-service mysql -tag secondary`.
* `-token` - The ACL token to use when firing the event. This token must have
write-level privileges for the event specified. Defaults to that of the agent.

View File

@ -37,14 +37,12 @@ The only required option is a command to execute. This is either given
as trailing arguments, or by specifying `-`; STDIN will be read to
completion as a script to evaluate.
The list of available flags are:
#### API Options
* `-http-addr` - Address to the HTTP server of the agent you want to contact
to send this command. If this isn't specified, the command will contact
`127.0.0.1:8500` which is the default HTTP address of a Consul agent.
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
* `-datacenter` - Datacenter to query. Defaults to that of agent. In version
0.4, that is the only supported value.
#### Command Options
* `-prefix` - Key prefix in the KV store to use for storing request data.
Defaults to `_rexec`.
@ -67,7 +65,3 @@ The list of available flags are:
to 200 msec.
* `-verbose` - Enables verbose output.
* `-token` - The ACL token to use during requests. This token must have access
to the prefix in the KV store as well as exec "write" access for the `_rexec`
event. Defaults to that of the agent.

View File

@ -72,10 +72,6 @@ serf_wan:
Usage: `consul info`
The command-line flags are all optional. The list of available flags are:
* `-rpc-addr` - Address to the RPC server of the agent you want to contact
to send this command. If this isn't specified, the command checks the
CONSUL_RPC_ADDR env variable. If this isn't set, the default RPC
address will be set to "127.0.0.1:8400".
#### API Options
<%= partial "docs/commands/http_api_options_client" %>

View File

@ -31,14 +31,14 @@ You may call join with multiple addresses if you want to try to join
multiple clusters. Consul will attempt to join all addresses, and the join
command will fail only if Consul was unable to join with any.
The command-line flags are all optional. The list of available flags are:
#### API Options
<%= partial "docs/commands/http_api_options_client" %>
#### Command Options
* `-wan` - For agents running in server mode, the agent will attempt to join
other servers gossiping in a WAN cluster. This is used to form a bridge between
multiple datacenters.
* `-rpc-addr` - Address to the RPC server of the agent you want to contact
to send this command. If this isn't specified, the command checks the
CONSUL_RPC_ADDR env variable. If this isn't set, the default RPC
address will be set to "127.0.0.1:8400".

View File

@ -33,7 +33,11 @@ Usage: `consul keyring [options]`
Only one actionable argument may be specified per run, including `-list`,
`-install`, `-remove`, and `-use`.
The list of available flags are:
#### API Options
<%= partial "docs/commands/http_api_options_client" %>
#### Command Options
* `-list` - List all keys currently in use within the cluster.
@ -46,16 +50,10 @@ The list of available flags are:
* `-remove` - Remove the given key from the cluster. This operation may only be
performed on keys which are not currently the primary key.
* `-token=""` - ACL token to use during requests. Defaults to that of the agent.
* `-relay-factor` - Added in Consul 0.7.4, setting this to a non-zero value will
cause nodes to relay their response to the operation through this many
randomly-chosen other nodes in the cluster. The maximum allowed value is 5.
* `-rpc-addr` - Address to the RPC server of the agent you want to contact
to send this command. If this isn't specified, the command will contact
"127.0.0.1:8400" which is the default RPC address of a Consul agent.
## Output
The output of the `consul keyring -list` command consolidates information from

View File

@ -87,6 +87,10 @@
<a href="/docs/commands/force-leave.html">force-leave</a>
</li>
<li<%= sidebar_current("docs-commands-info") %>>
<a href="/docs/commands/info.html">info</a>
</li>
<li<%= sidebar_current("docs-commands-join") %>>
<a href="/docs/commands/join.html">join</a>
</li>
@ -143,10 +147,6 @@
<a href="/docs/commands/operator.html">operator</a>
</li>
<li<%= sidebar_current("docs-commands-info") %>>
<a href="/docs/commands/info.html">info</a>
</li>
<li<%= sidebar_current("docs-commands-reload") %>>
<a href="/docs/commands/reload.html">reload</a>
</li>