Merge pull request #2717 from hashicorp/f-cli-rework

Begin centralizing command-line parsing
This commit is contained in:
Kyle Havlovitz 2017-02-07 21:15:06 -05:00 committed by GitHub
commit a4cb414e58
35 changed files with 1094 additions and 194 deletions

238
command/base/command.go Normal file
View File

@ -0,0 +1,238 @@
package base
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"strings"
"github.com/hashicorp/consul/api"
"github.com/mitchellh/cli"
text "github.com/tonnerre/golang-text"
)
// maxLineLength is the maximum width of any line.
const maxLineLength int = 72
// FlagSetFlags is an enum to define what flags are present in the
// default FlagSet returned.
type FlagSetFlags uint
const (
FlagSetNone FlagSetFlags = 1 << iota
FlagSetClientHTTP FlagSetFlags = 1 << iota
FlagSetServerHTTP FlagSetFlags = 1 << iota
FlagSetHTTP = FlagSetClientHTTP | FlagSetServerHTTP
)
type Command struct {
Ui cli.Ui
Flags FlagSetFlags
flagSet *flag.FlagSet
// These are the options which correspond to the HTTP API options
httpAddr stringValue
token stringValue
datacenter stringValue
stale boolValue
}
// HTTPClient returns a client with the parsed flags. It panics if the command
// does not accept HTTP flags or if the flags have not been parsed.
func (c *Command) HTTPClient() (*api.Client, error) {
if !c.hasClientHTTP() && !c.hasServerHTTP() {
panic("no http flags defined")
}
if !c.flagSet.Parsed() {
panic("flags have not been parsed")
}
config := api.DefaultConfig()
c.httpAddr.Merge(&config.Address)
c.token.Merge(&config.Token)
c.datacenter.Merge(&config.Datacenter)
return api.NewClient(config)
}
// httpFlagsClient is the list of flags that apply to HTTP connections.
func (c *Command) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
if f == nil {
f = flag.NewFlagSet("", flag.ContinueOnError)
}
f.Var(&c.httpAddr, "http-addr",
"Address and port to 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.")
f.Var(&c.token, "token",
"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.")
return f
}
// httpFlagsServer is the list of flags that apply to HTTP connections.
func (c *Command) httpFlagsServer(f *flag.FlagSet) *flag.FlagSet {
if f == nil {
f = flag.NewFlagSet("", flag.ContinueOnError)
}
f.Var(&c.datacenter, "datacenter",
"Name of the datacenter to query. If unspecified, this will default to "+
"the datacenter of the queried agent.")
f.Var(&c.stale, "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.")
return f
}
// NewFlagSet creates a new flag set for the given command. It automatically
// generates help output and adds the appropriate API flags.
func (c *Command) NewFlagSet(command cli.Command) *flag.FlagSet {
f := flag.NewFlagSet("", flag.ContinueOnError)
f.Usage = func() { c.Ui.Error(command.Help()) }
if c.hasClientHTTP() {
c.httpFlagsClient(f)
}
if c.hasServerHTTP() {
c.httpFlagsServer(f)
}
errR, errW := io.Pipe()
errScanner := bufio.NewScanner(errR)
go func() {
for errScanner.Scan() {
c.Ui.Error(errScanner.Text())
}
}()
f.SetOutput(errW)
c.flagSet = f
return f
}
// Parse is used to parse the underlying flag set.
func (c *Command) Parse(args []string) error {
return c.flagSet.Parse(args)
}
// Help returns the help for this flagSet.
func (c *Command) Help() string {
return c.helpFlagsFor(c.flagSet)
}
// hasClientHTTP returns true if this meta command contains client HTTP flags.
func (c *Command) hasClientHTTP() bool {
return c.Flags&FlagSetClientHTTP != 0
}
// hasServerHTTP returns true if this meta command contains server HTTP flags.
func (c *Command) hasServerHTTP() bool {
return c.Flags&FlagSetServerHTTP != 0
}
// helpFlagsFor visits all flags in the given flag set and prints formatted
// help output. This function is sad because there's no "merging" of command
// line flags. We explicitly pull out our "common" options into another section
// by doing string comparisons :(.
func (c *Command) helpFlagsFor(f *flag.FlagSet) string {
httpFlagsClient := c.httpFlagsClient(nil)
httpFlagsServer := c.httpFlagsServer(nil)
var out bytes.Buffer
firstHTTP := true
if c.hasClientHTTP() {
if firstHTTP {
printTitle(&out, "HTTP API Options")
firstHTTP = false
}
httpFlagsClient.VisitAll(func(f *flag.Flag) {
printFlag(&out, f)
})
}
if c.hasServerHTTP() {
if firstHTTP {
printTitle(&out, "HTTP API Options")
firstHTTP = false
}
httpFlagsServer.VisitAll(func(f *flag.Flag) {
printFlag(&out, f)
})
}
firstCommand := true
f.VisitAll(func(f *flag.Flag) {
// Skip HTTP flags as they will be grouped separately
if flagContains(httpFlagsClient, f) || flagContains(httpFlagsServer, f) {
return
}
if firstCommand {
printTitle(&out, "Command Options")
firstCommand = false
}
printFlag(&out, f)
})
return strings.TrimRight(out.String(), "\n")
}
// printTitle prints a consistently-formatted title to the given writer.
func printTitle(w io.Writer, s string) {
fmt.Fprintf(w, "%s\n\n", s)
}
// printFlag prints a single flag to the given writer.
func printFlag(w io.Writer, f *flag.Flag) {
example, _ := flag.UnquoteUsage(f)
if example != "" {
fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example)
} else {
fmt.Fprintf(w, " -%s\n", f.Name)
}
indented := wrapAtLength(f.Usage, 5)
fmt.Fprintf(w, "%s\n\n", indented)
}
// flagContains returns true if the given flag is contained in the given flag
// set or false otherwise.
func flagContains(fs *flag.FlagSet, f *flag.Flag) bool {
var skip bool
fs.VisitAll(func(hf *flag.Flag) {
if skip {
return
}
if f.Name == hf.Name && f.Usage == hf.Usage {
skip = true
return
}
})
return skip
}
// wrapAtLength wraps the given text at the maxLineLength, taking into account
// any provided left padding.
func wrapAtLength(s string, pad int) string {
wrapped := text.Wrap(s, maxLineLength-pad)
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
lines[i] = strings.Repeat(" ", pad) + line
}
return strings.Join(lines, "\n")
}

319
command/base/config_util.go Normal file
View File

@ -0,0 +1,319 @@
package base
import (
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"time"
"github.com/mitchellh/mapstructure"
)
// TODO (slackpad) - Trying out a different pattern here for config handling.
// These classes support the flag.Value interface but work in a manner where
// we can tell if they have been set. This lets us work with an all-pointer
// config structure and merge it in a clean-ish way. If this ends up being a
// good pattern we should pull this out into a reusable library.
// configDecodeHook should be passed to mapstructure in order to decode into
// the *Value objects here.
var configDecodeHook = mapstructure.ComposeDecodeHookFunc(
boolToBoolValueFunc(),
stringToDurationValueFunc(),
stringToStringValueFunc(),
float64ToUintValueFunc(),
)
// boolValue provides a flag value that's aware if it has been set.
type boolValue struct {
v *bool
}
// See flag.Value.
func (b *boolValue) IsBoolFlag() bool {
return true
}
// Merge will overlay this value if it has been set.
func (b *boolValue) Merge(onto *bool) {
if b.v != nil {
*onto = *(b.v)
}
}
// See flag.Value.
func (b *boolValue) Set(v string) error {
if b.v == nil {
b.v = new(bool)
}
var err error
*(b.v), err = strconv.ParseBool(v)
return err
}
// See flag.Value.
func (b *boolValue) String() string {
var current bool
if b.v != nil {
current = *(b.v)
}
return fmt.Sprintf("%v", current)
}
// boolToBoolValueFunc is a mapstructure hook that looks for an incoming bool
// mapped to a boolValue and does the translation.
func boolToBoolValueFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.Bool {
return data, nil
}
val := boolValue{}
if t != reflect.TypeOf(val) {
return data, nil
}
val.v = new(bool)
*(val.v) = data.(bool)
return val, nil
}
}
// durationValue provides a flag value that's aware if it has been set.
type durationValue struct {
v *time.Duration
}
// Merge will overlay this value if it has been set.
func (d *durationValue) Merge(onto *time.Duration) {
if d.v != nil {
*onto = *(d.v)
}
}
// See flag.Value.
func (d *durationValue) Set(v string) error {
if d.v == nil {
d.v = new(time.Duration)
}
var err error
*(d.v), err = time.ParseDuration(v)
return err
}
// See flag.Value.
func (d *durationValue) String() string {
var current time.Duration
if d.v != nil {
current = *(d.v)
}
return current.String()
}
// stringToDurationValueFunc is a mapstructure hook that looks for an incoming
// string mapped to a durationValue and does the translation.
func stringToDurationValueFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
val := durationValue{}
if t != reflect.TypeOf(val) {
return data, nil
}
if err := val.Set(data.(string)); err != nil {
return nil, err
}
return val, nil
}
}
// stringValue provides a flag value that's aware if it has been set.
type stringValue struct {
v *string
}
// Merge will overlay this value if it has been set.
func (s *stringValue) Merge(onto *string) {
if s.v != nil {
*onto = *(s.v)
}
}
// See flag.Value.
func (s *stringValue) Set(v string) error {
if s.v == nil {
s.v = new(string)
}
*(s.v) = v
return nil
}
// See flag.Value.
func (s *stringValue) String() string {
var current string
if s.v != nil {
current = *(s.v)
}
return current
}
// stringToStringValueFunc is a mapstructure hook that looks for an incoming
// string mapped to a stringValue and does the translation.
func stringToStringValueFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.String {
return data, nil
}
val := stringValue{}
if t != reflect.TypeOf(val) {
return data, nil
}
val.v = new(string)
*(val.v) = data.(string)
return val, nil
}
}
// uintValue provides a flag value that's aware if it has been set.
type uintValue struct {
v *uint
}
// Merge will overlay this value if it has been set.
func (u *uintValue) Merge(onto *uint) {
if u.v != nil {
*onto = *(u.v)
}
}
// See flag.Value.
func (u *uintValue) Set(v string) error {
if u.v == nil {
u.v = new(uint)
}
parsed, err := strconv.ParseUint(v, 0, 64)
*(u.v) = (uint)(parsed)
return err
}
// See flag.Value.
func (u *uintValue) String() string {
var current uint
if u.v != nil {
current = *(u.v)
}
return fmt.Sprintf("%v", current)
}
// float64ToUintValueFunc is a mapstructure hook that looks for an incoming
// float64 mapped to a uintValue and does the translation.
func float64ToUintValueFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data interface{}) (interface{}, error) {
if f.Kind() != reflect.Float64 {
return data, nil
}
val := uintValue{}
if t != reflect.TypeOf(val) {
return data, nil
}
fv := data.(float64)
if fv < 0 {
return nil, fmt.Errorf("value cannot be negative")
}
// The standard guarantees at least this, and this is fine for
// values we expect to use in configs vs. being fancy with the
// machine's size for uint.
if fv > (1<<32 - 1) {
return nil, fmt.Errorf("value is too large")
}
val.v = new(uint)
*(val.v) = (uint)(fv)
return val, nil
}
}
// visitFn is a callback that gets a chance to visit each file found during a
// traversal with visit().
type visitFn func(path string) error
// visit will call the visitor function on the path if it's a file, or for each
// file in the path if it's a directory. Directories will not be recursed into,
// and files in the directory will be visited in alphabetical order.
func visit(path string, visitor visitFn) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("error reading %q: %v", path, err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return fmt.Errorf("error checking %q: %v", path, err)
}
if !fi.IsDir() {
if err := visitor(path); err != nil {
return fmt.Errorf("error in %q: %v", path, err)
}
return nil
}
contents, err := f.Readdir(-1)
if err != nil {
return fmt.Errorf("error listing %q: %v", path, err)
}
sort.Sort(dirEnts(contents))
for _, fi := range contents {
if fi.IsDir() {
continue
}
fullPath := filepath.Join(path, fi.Name())
if err := visitor(fullPath); err != nil {
return fmt.Errorf("error in %q: %v", fullPath, err)
}
}
return nil
}
// dirEnts applies sort.Interface to directory entries for sorting by name.
type dirEnts []os.FileInfo
// See sort.Interface.
func (d dirEnts) Len() int {
return len(d)
}
// See sort.Interface.
func (d dirEnts) Less(i, j int) bool {
return d[i].Name() < d[j].Name()
}
// See sort.Interface.
func (d dirEnts) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}

View File

@ -0,0 +1,128 @@
package base
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/mitchellh/mapstructure"
"path"
"reflect"
)
func TestConfigUtil_Values(t *testing.T) {
type config struct {
B boolValue `mapstructure:"bool"`
D durationValue `mapstructure:"duration"`
S stringValue `mapstructure:"string"`
U uintValue `mapstructure:"uint"`
}
cases := []struct {
in string
success string
failure string
}{
{
`{ }`,
`"false" "0s" "" "0"`,
"",
},
{
`{ "bool": true, "duration": "2h", "string": "hello", "uint": 23 }`,
`"true" "2h0m0s" "hello" "23"`,
"",
},
{
`{ "bool": "nope" }`,
"",
"got 'string'",
},
{
`{ "duration": "nope" }`,
"",
"invalid duration nope",
},
{
`{ "string": 123 }`,
"",
"got 'float64'",
},
{
`{ "uint": -1 }`,
"",
"value cannot be negative",
},
{
`{ "uint": 4294967296 }`,
"",
"value is too large",
},
}
for i, c := range cases {
var raw interface{}
dec := json.NewDecoder(bytes.NewBufferString(c.in))
if err := dec.Decode(&raw); err != nil {
t.Fatalf("(case %d) err: %v", i, err)
}
var r config
msdec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: configDecodeHook,
Result: &r,
ErrorUnused: true,
})
if err != nil {
t.Fatalf("(case %d) err: %v", i, err)
}
err = msdec.Decode(raw)
if c.failure != "" {
if err == nil || !strings.Contains(err.Error(), c.failure) {
t.Fatalf("(case %d) err: %v", i, err)
}
continue
}
if err != nil {
t.Fatalf("(case %d) err: %v", i, err)
}
actual := fmt.Sprintf("%q %q %q %q",
r.B.String(),
r.D.String(),
r.S.String(),
r.U.String())
if actual != c.success {
t.Fatalf("(case %d) bad: %s", i, actual)
}
}
}
func TestConfigUtil_Visit(t *testing.T) {
var trail []string
visitor := func(path string) error {
trail = append(trail, path)
return nil
}
basePath := "../../test/command/merge"
if err := visit(basePath, visitor); err != nil {
t.Fatalf("err: %v", err)
}
if err := visit(path.Join(basePath, "subdir", "c.json"), visitor); err != nil {
t.Fatalf("err: %v", err)
}
expected := []string{
path.Join(basePath, "a.json"),
path.Join(basePath, "b.json"),
path.Join(basePath, "nope"),
path.Join(basePath, "zero.json"),
path.Join(basePath, "subdir", "c.json"),
}
if !reflect.DeepEqual(trail, expected) {
t.Fatalf("bad: %#v", trail)
}
}

View File

@ -1,18 +1,17 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/consul/command/agent"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
// ConfigTestCommand is a Command implementation that is used to
// verify config files
type ConfigTestCommand struct {
Ui cli.Ui
base.Command
}
func (c *ConfigTestCommand) Help() string {
@ -27,25 +26,22 @@ Usage: consul configtest [options]
Returns 0 if the configuration is valid, or 1 if there are problems.
Options:
` + c.Command.Help()
-config-file=foo Path to a JSON file to read configuration from.
This can be specified multiple times.
-config-dir=foo Path to a directory to read configuration files
from. This will read every file ending in ".json"
as configuration in this directory in alphabetical
order.
`
return strings.TrimSpace(helpText)
}
func (c *ConfigTestCommand) Run(args []string) int {
var configFiles []string
cmdFlags := flag.NewFlagSet("configtest", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-file", "json file to read config from")
cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-dir", "directory of json files to read")
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
f.Var((*agent.AppendSliceValue)(&configFiles), "config-file",
"Path to a JSON file to read configuration from. This can be specified multiple times.")
f.Var((*agent.AppendSliceValue)(&configFiles), "config-dir",
"Path to a directory to read configuration files from. This will read every file ending in "+
".json as configuration in this directory in alphabetical order.")
if err := c.Command.Parse(args); err != nil {
return 1
}

View File

@ -6,9 +6,20 @@ import (
"path/filepath"
"testing"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testConfigTestCommand(t *testing.T) (*cli.MockUi, *ConfigTestCommand) {
ui := new(cli.MockUi)
return ui, &ConfigTestCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetNone,
},
}
}
func TestConfigTestCommand_implements(t *testing.T) {
var _ cli.Command = &ConfigTestCommand{}
}
@ -20,9 +31,7 @@ func TestConfigTestCommandFailOnEmptyFile(t *testing.T) {
}
defer os.RemoveAll(tmpFile.Name())
cmd := &ConfigTestCommand{
Ui: new(cli.MockUi),
}
_, cmd := testConfigTestCommand(t)
args := []string{
"-config-file", tmpFile.Name(),
@ -40,16 +49,14 @@ func TestConfigTestCommandSucceedOnEmptyDir(t *testing.T) {
}
defer os.RemoveAll(td)
cmd := &ConfigTestCommand{
Ui: new(cli.MockUi),
}
ui, cmd := testConfigTestCommand(t)
args := []string{
"-config-dir", td,
}
if code := cmd.Run(args); code != 0 {
t.Fatalf("bad: %d", code)
t.Fatalf("bad: %d, %s", code, ui.ErrorWriter.String())
}
}
@ -66,9 +73,7 @@ func TestConfigTestCommandSucceedOnMinimalConfigFile(t *testing.T) {
t.Fatalf("err: %s", err)
}
cmd := &ConfigTestCommand{
Ui: new(cli.MockUi),
}
_, cmd := testConfigTestCommand(t)
args := []string{
"-config-file", fp,
@ -91,9 +96,7 @@ func TestConfigTestCommandSucceedOnMinimalConfigDir(t *testing.T) {
t.Fatalf("err: %s", err)
}
cmd := &ConfigTestCommand{
Ui: new(cli.MockUi),
}
_, cmd := testConfigTestCommand(t)
args := []string{
"-config-dir", td,
@ -116,9 +119,7 @@ func TestConfigTestCommandSucceedOnConfigDirWithEmptyFile(t *testing.T) {
t.Fatalf("err: %s", err)
}
cmd := &ConfigTestCommand{
Ui: new(cli.MockUi),
}
_, cmd := testConfigTestCommand(t)
args := []string{
"-config-dir", td,

View File

@ -1,42 +1,38 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
"strings"
)
// ForceLeaveCommand is a Command implementation that tells a running Consul
// to force a member to enter the "left" state.
type ForceLeaveCommand struct {
Ui cli.Ui
base.Command
}
func (c *ForceLeaveCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
f := c.Command.NewFlagSet(c)
if err := c.Command.Parse(args); err != nil {
return 1
}
nodes := cmdFlags.Args()
nodes := f.Args()
if len(nodes) != 1 {
c.Ui.Error("A node name must be specified to force leave.")
c.Ui.Error("A single node name must be specified to force leave.")
c.Ui.Error("")
c.Ui.Error(c.Help())
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()
err = client.ForceLeave(nodes[0])
err = client.Agent().ForceLeave(nodes[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err))
return 1
@ -60,10 +56,7 @@ Usage: consul force-leave [options] name
Consul will attempt to reconnect to those failed nodes for some period of
time before eventually reaping them.
Options:
` + c.Command.Help()
-rpc-addr=127.0.0.1:8400 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}

View File

@ -3,6 +3,7 @@ package command
import (
"errors"
"fmt"
"github.com/hashicorp/consul/command/base"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/serf/serf"
"github.com/mitchellh/cli"
@ -10,6 +11,16 @@ import (
"testing"
)
func testForceLeaveCommand(t *testing.T) (*cli.MockUi, *ForceLeaveCommand) {
ui := new(cli.MockUi)
return ui, &ForceLeaveCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetClientHTTP,
},
}
}
func TestForceLeaveCommand_implements(t *testing.T) {
var _ cli.Command = &ForceLeaveCommand{}
}
@ -29,10 +40,9 @@ func TestForceLeaveCommandRun(t *testing.T) {
// Forcibly shutdown a2 so that it appears "failed" in a1
a2.Shutdown()
ui := new(cli.MockUi)
c := &ForceLeaveCommand{Ui: ui}
ui, c := testForceLeaveCommand(t)
args := []string{
"-rpc-addr=" + a1.addr,
"-http-addr=" + a1.httpAddr,
a2.config.NodeName,
}
@ -57,8 +67,8 @@ func TestForceLeaveCommandRun(t *testing.T) {
func TestForceLeaveCommandRun_noAddrs(t *testing.T) {
ui := new(cli.MockUi)
c := &ForceLeaveCommand{Ui: ui}
args := []string{"-rpc-addr=foo"}
ui, c := testForceLeaveCommand(t)
args := []string{"-http-addr=foo"}
code := c.Run(args)
if code != 1 {

View File

@ -1,7 +1,6 @@
package command
import (
"flag"
"fmt"
"os"
"path"
@ -12,7 +11,7 @@ import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/agent"
"github.com/mitchellh/cli"
"github.com/hashicorp/consul/command/base"
)
const (
@ -37,54 +36,37 @@ const (
// LockCommand is a Command implementation that is used to setup
// a "lock" which manages lock acquisition and invokes a sub-process
type LockCommand struct {
base.Command
ShutdownCh <-chan struct{}
Ui cli.Ui
child *os.Process
childLock sync.Mutex
verbose bool
verbose bool
}
func (c *LockCommand) Help() string {
helpText := `
Usage: consul lock [options] prefix child...
Acquires a lock or semaphore at a given path, and invokes a child
process when successful. The child process can assume the lock is
held while it executes. If the lock is lost or communication is
disrupted the child process will be sent a SIGTERM signal and given
time to gracefully exit. After the grace period expires the process
will be hard terminated.
Acquires a lock or semaphore at a given path, and invokes a child process
when successful. The child process can assume the lock is held while it
executes. If the lock is lost or communication is disrupted the child
process will be sent a SIGTERM signal and given time to gracefully exit.
After the grace period expires the process will be hard terminated.
For Consul agents on Windows, the child process is always hard
terminated with a SIGKILL, since Windows has no POSIX compatible
notion for SIGTERM.
For Consul agents on Windows, the child process is always hard terminated
with a SIGKILL, since Windows has no POSIX compatible notion for SIGTERM.
When -n=1, only a single lock holder or leader exists providing
mutual exclusion. Setting a higher value switches to a semaphore
allowing multiple holders to coordinate.
When -n=1, only a single lock holder or leader exists providing mutual
exclusion. Setting a higher value switches to a semaphore allowing multiple
holders to coordinate.
The prefix provided must have write privileges.
Options:
` + c.Command.Help()
-http-addr=127.0.0.1:8500 HTTP address of the Consul agent.
-n=1 Maximum number of allowed lock holders. If this
value is one, it operates as a lock, otherwise
a semaphore is used.
-name="" Optional name to associate with lock session.
-token="" ACL token to use. Defaults to that of agent.
-pass-stdin Pass stdin to child process.
-try=timeout Attempt to acquire the lock up to the given
timeout (eg. "15s").
-monitor-retry=n Retry up to n times if Consul returns a 500 error
while monitoring the lock. This allows riding out brief
periods of unavailability without causing leader
elections, but increases the amount of time required
to detect a lost lock in some cases. Defaults to 3,
with a 1s wait between retries. Set to 0 to disable.
-verbose Enables verbose output
`
return strings.TrimSpace(helpText)
}
@ -93,25 +75,41 @@ func (c *LockCommand) Run(args []string) int {
return c.run(args, &lu)
}
// run exposes the underlying lock for testing.
func (c *LockCommand) run(args []string, lu **LockUnlock) int {
var childDone chan struct{}
var name, token string
var limit int
var monitorRetry int
var name string
var passStdin bool
var try string
var retry int
cmdFlags := flag.NewFlagSet("watch", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.IntVar(&limit, "n", 1, "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.StringVar(&token, "token", "", "")
cmdFlags.BoolVar(&passStdin, "pass-stdin", false, "")
cmdFlags.StringVar(&try, "try", "", "")
cmdFlags.IntVar(&retry, "monitor-retry", defaultMonitorRetry, "")
cmdFlags.BoolVar(&c.verbose, "verbose", false, "")
httpAddr := HTTPAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
var timeout time.Duration
f := c.Command.NewFlagSet(c)
f.IntVar(&limit, "n", 1,
"Optional limit on the number of concurrent lock holders. The underlying "+
"implementation switches from a lock to a semaphore when the value is "+
"greater than 1. The default value is 1.")
f.IntVar(&monitorRetry, "monitor-retry", defaultMonitorRetry,
"Number of times to retry if Consul returns a 500 error while monitoring "+
"the lock. This allows riding out brief periods of unavailability "+
"without causing leader elections, but increases the amount of time "+
"required to detect a lost lock in some cases. The default value is 3, "+
"with a 1s wait between retries. Set this value to 0 to disable retires.")
f.StringVar(&name, "name", "",
"Optional name to associate with the lock session. It not provided, one "+
"is generated based on the provided child command.")
f.BoolVar(&passStdin, "pass-stdin", false,
"Pass stdin to the child process.")
f.DurationVar(&timeout, "timeout", 0,
"Maximum amount of time to wait to acquire the lock, specified as a "+
"timestamp like \"1s\" or \"3h\". The default value is 0.")
f.BoolVar(&c.verbose, "verbose", false,
"Enable verbose (debugging) output.")
// Deprecations
f.DurationVar(&timeout, "try", 0,
"DEPRECATED. Use -timeout instead.")
if err := c.Command.Parse(args); err != nil {
return 1
}
@ -122,52 +120,36 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
}
// Verify the prefix and child are provided
extra := cmdFlags.Args()
extra := f.Args()
if len(extra) < 2 {
c.Ui.Error("Key prefix and child command must be specified")
c.Ui.Error("")
c.Ui.Error(c.Help())
return 1
}
prefix := extra[0]
prefix = strings.TrimPrefix(prefix, "/")
script := strings.Join(extra[1:], " ")
if timeout < 0 {
c.Ui.Error("Timeout must be positive")
return 1
}
// Calculate a session name if none provided
if name == "" {
name = fmt.Sprintf("Consul lock for '%s' at '%s'", script, prefix)
}
// Verify the duration if given.
oneshot := false
var wait time.Duration
if try != "" {
var err error
wait, err = time.ParseDuration(try)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing try timeout: %s", err))
return 1
}
if wait <= 0 {
c.Ui.Error("Try timeout must be positive")
return 1
}
oneshot = true
}
// Calculate oneshot
oneshot := timeout > 0
// Check the retry parameter
if retry < 0 {
if monitorRetry < 0 {
c.Ui.Error("Number for 'monitor-retry' must be >= 0")
return 1
}
// 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
@ -180,9 +162,9 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
// Setup the lock or semaphore
if limit == 1 {
*lu, err = c.setupLock(client, prefix, name, oneshot, wait, retry)
*lu, err = c.setupLock(client, prefix, name, oneshot, timeout, monitorRetry)
} else {
*lu, err = c.setupSemaphore(client, limit, prefix, name, oneshot, wait, retry)
*lu, err = c.setupSemaphore(client, limit, prefix, name, oneshot, timeout, monitorRetry)
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Lock setup failed: %s", err))

View File

@ -9,16 +9,26 @@ import (
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/base"
"github.com/mitchellh/cli"
)
func testLockCommand(t *testing.T) (*cli.MockUi, *LockCommand) {
ui := new(cli.MockUi)
return ui, &LockCommand{
Command: base.Command{
Ui: ui,
Flags: base.FlagSetHTTP,
},
}
}
func TestLockCommand_implements(t *testing.T) {
var _ cli.Command = &LockCommand{}
}
func argFail(t *testing.T, args []string, expected string) {
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
if code := c.Run(args); code != 1 {
t.Fatalf("expected return code 1, got %d", code)
}
@ -29,9 +39,8 @@ func argFail(t *testing.T, args []string, expected string) {
}
func TestLockCommand_BadArgs(t *testing.T) {
argFail(t, []string{"-try=blah", "test/prefix", "date"}, "parsing try timeout")
argFail(t, []string{"-try=0s", "test/prefix", "date"}, "timeout must be positive")
argFail(t, []string{"-try=-10s", "test/prefix", "date"}, "timeout must be positive")
argFail(t, []string{"-try=blah", "test/prefix", "date"}, "invalid duration")
argFail(t, []string{"-try=-10s", "test/prefix", "date"}, "Timeout must be positive")
argFail(t, []string{"-monitor-retry=-5", "test/prefix", "date"}, "must be >= 0")
}
@ -40,8 +49,7 @@ func TestLockCommand_Run(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
@ -63,8 +71,7 @@ func TestLockCommand_Try_Lock(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "-try=10s", "test/prefix", touchCmd}
@ -95,8 +102,7 @@ func TestLockCommand_Try_Semaphore(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "-try=10s", "test/prefix", touchCmd}
@ -127,8 +133,7 @@ func TestLockCommand_MonitorRetry_Lock_Default(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
@ -160,8 +165,7 @@ func TestLockCommand_MonitorRetry_Semaphore_Default(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "test/prefix", touchCmd}
@ -193,8 +197,7 @@ func TestLockCommand_MonitorRetry_Lock_Arg(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "-monitor-retry=9", "test/prefix", touchCmd}
@ -226,8 +229,7 @@ func TestLockCommand_MonitorRetry_Semaphore_Arg(t *testing.T) {
defer a1.Shutdown()
waitForLeader(t, a1.httpAddr)
ui := new(cli.MockUi)
c := &LockCommand{Ui: ui}
ui, c := testLockCommand(t)
filePath := filepath.Join(a1.dir, "test_touch")
touchCmd := fmt.Sprintf("touch '%s'", filePath)
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "-monitor-retry=9", "test/prefix", touchCmd}

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/consul/command"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/command/base"
"github.com/hashicorp/consul/version"
"github.com/mitchellh/cli"
)
@ -31,7 +32,10 @@ func init() {
"configtest": func() (cli.Command, error) {
return &command.ConfigTestCommand{
Ui: ui,
Command: base.Command{
Flags: base.FlagSetNone,
Ui: ui,
},
}, nil
},
@ -50,7 +54,10 @@ func init() {
"force-leave": func() (cli.Command, error) {
return &command.ForceLeaveCommand{
Ui: ui,
Command: base.Command{
Flags: base.FlagSetClientHTTP,
Ui: ui,
},
}, nil
},
@ -117,7 +124,10 @@ func init() {
"lock": func() (cli.Command, error) {
return &command.LockCommand{
ShutdownCh: makeShutdownCh(),
Ui: ui,
Command: base.Command{
Flags: base.FlagSetHTTP,
Ui: ui,
},
}, nil
},

View File

@ -0,0 +1,7 @@
{
"snapshot_agent": {
"snapshot": {
"interval": "1h"
}
}
}

View File

@ -0,0 +1,7 @@
{
"snapshot_agent": {
"snapshot": {
"interval": "2h"
}
}
}

View File

@ -0,0 +1,3 @@
{
"not_related": true
}

7
test/command/merge/nope Normal file
View File

@ -0,0 +1,7 @@
{
"snapshot_agent": {
"snapshot": {
"interval": "3h"
}
}
}

View File

@ -0,0 +1,7 @@
{
"snapshot_agent": {
"snapshot": {
"interval": "5h"
}
}
}

View File

19
vendor/github.com/tonnerre/golang-text/License generated vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright 2012 Keith Rarick
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

3
vendor/github.com/tonnerre/golang-text/Readme generated vendored Normal file
View File

@ -0,0 +1,3 @@
This is a Go package for manipulating paragraphs of text.
See http://go.pkgdoc.org/github.com/kr/text for full documentation.

3
vendor/github.com/tonnerre/golang-text/doc.go generated vendored Normal file
View File

@ -0,0 +1,3 @@
// Package text provides rudimentary functions for manipulating text in
// paragraphs.
package text

74
vendor/github.com/tonnerre/golang-text/indent.go generated vendored Normal file
View File

@ -0,0 +1,74 @@
package text
import (
"io"
)
// Indent inserts prefix at the beginning of each non-empty line of s. The
// end-of-line marker is NL.
func Indent(s, prefix string) string {
return string(IndentBytes([]byte(s), []byte(prefix)))
}
// IndentBytes inserts prefix at the beginning of each non-empty line of b.
// The end-of-line marker is NL.
func IndentBytes(b, prefix []byte) []byte {
var res []byte
bol := true
for _, c := range b {
if bol && c != '\n' {
res = append(res, prefix...)
}
res = append(res, c)
bol = c == '\n'
}
return res
}
// Writer indents each line of its input.
type indentWriter struct {
w io.Writer
bol bool
pre [][]byte
sel int
off int
}
// NewIndentWriter makes a new write filter that indents the input
// lines. Each line is prefixed in order with the corresponding
// element of pre. If there are more lines than elements, the last
// element of pre is repeated for each subsequent line.
func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer {
return &indentWriter{
w: w,
pre: pre,
bol: true,
}
}
// The only errors returned are from the underlying indentWriter.
func (w *indentWriter) Write(p []byte) (n int, err error) {
for _, c := range p {
if w.bol {
var i int
i, err = w.w.Write(w.pre[w.sel][w.off:])
w.off += i
if err != nil {
return n, err
}
}
_, err = w.w.Write([]byte{c})
if err != nil {
return n, err
}
n++
w.bol = c == '\n'
if w.bol {
w.off = 0
if w.sel < len(w.pre)-1 {
w.sel++
}
}
}
return n, nil
}

86
vendor/github.com/tonnerre/golang-text/wrap.go generated vendored Executable file
View File

@ -0,0 +1,86 @@
package text
import (
"bytes"
"math"
)
var (
nl = []byte{'\n'}
sp = []byte{' '}
)
const defaultPenalty = 1e5
// Wrap wraps s into a paragraph of lines of length lim, with minimal
// raggedness.
func Wrap(s string, lim int) string {
return string(WrapBytes([]byte(s), lim))
}
// WrapBytes wraps b into a paragraph of lines of length lim, with minimal
// raggedness.
func WrapBytes(b []byte, lim int) []byte {
words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp)
var lines [][]byte
for _, line := range WrapWords(words, 1, lim, defaultPenalty) {
lines = append(lines, bytes.Join(line, sp))
}
return bytes.Join(lines, nl)
}
// WrapWords is the low-level line-breaking algorithm, useful if you need more
// control over the details of the text wrapping process. For most uses, either
// Wrap or WrapBytes will be sufficient and more convenient.
//
// WrapWords splits a list of words into lines with minimal "raggedness",
// treating each byte as one unit, accounting for spc units between adjacent
// words on each line, and attempting to limit lines to lim units. Raggedness
// is the total error over all lines, where error is the square of the
// difference of the length of the line and lim. Too-long lines (which only
// happen when a single word is longer than lim units) have pen penalty units
// added to the error.
func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte {
n := len(words)
length := make([][]int, n)
for i := 0; i < n; i++ {
length[i] = make([]int, n)
length[i][i] = len(words[i])
for j := i + 1; j < n; j++ {
length[i][j] = length[i][j-1] + spc + len(words[j])
}
}
nbrk := make([]int, n)
cost := make([]int, n)
for i := range cost {
cost[i] = math.MaxInt32
}
for i := n - 1; i >= 0; i-- {
if length[i][n-1] <= lim {
cost[i] = 0
nbrk[i] = n
} else {
for j := i + 1; j < n; j++ {
d := lim - length[i][j-1]
c := d*d + cost[j]
if length[i][j-1] > lim {
c += pen // too-long lines get a worse penalty
}
if c < cost[i] {
cost[i] = c
nbrk[i] = j
}
}
}
}
var lines [][][]byte
i := 0
for i < n {
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]
}
return lines
}

6
vendor/vendor.json vendored
View File

@ -740,6 +740,12 @@
"revision": "bb4de0191aa41b5507caa14b0650cdbddcd9280b",
"revisionTime": "2016-09-30T03:27:40Z"
},
{
"checksumSHA1": "t24KnvC9jRxiANVhpw2pqFpmEu8=",
"path": "github.com/tonnerre/golang-text",
"revision": "048ed3d792f7104850acbc8cfc01e5a6070f4c04",
"revisionTime": "2013-09-25T19:58:46Z"
},
{
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
"path": "golang.org/x/net/context",

View File

@ -1,16 +0,0 @@
* `-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,8 @@
* `-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.
* `-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.

View File

@ -0,0 +1,7 @@
* `-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.
* `-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

@ -28,11 +28,7 @@ as it will be removed from the Raft quorum.
Usage: `consul force-leave [options] node`
The following command-line options are available for this command.
Every option is optional:
#### API Options
* `-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`.
<%= partial "docs/commands/http_api_options_client" %>

View File

@ -17,7 +17,8 @@ Usage: `consul kv delete [options] KEY_OR_PREFIX`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
#### KV Delete Options

View File

@ -19,7 +19,8 @@ Usage: `consul kv export [PREFIX]`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
## Examples

View File

@ -20,7 +20,8 @@ Usage: `consul kv get [options] [KEY_OR_PREFIX]`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
#### KV Get Options

View File

@ -17,7 +17,8 @@ Usage: `consul kv import [DATA]`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
## Examples

View File

@ -16,7 +16,8 @@ Usage: `consul kv put [options] KEY [DATA]`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
<%= partial "docs/commands/http_api_options_server" %>
#### KV Put Options

View File

@ -45,11 +45,18 @@ of 5 seconds, a `SIGKILL` will be used to force termination. For Consul agents
on Windows, the child process is always terminated with a `SIGKILL`, since
Windows has no POSIX compatible notion for `SIGTERM`.
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" %>
#### Command Options
* `-monitor-retry` - Retry up to this number of times if Consul returns a 500 error
while monitoring the lock. This allows riding out brief periods of unavailability
without causing leader elections, but increases the amount of time required
to detect a lost lock in some cases. Defaults to 3, with a 1s wait between retries.
Set to 0 to disable.
* `-n` - Optional, limit of lock holders. Defaults to 1. The underlying
implementation switches from a lock to a semaphore when increased past
@ -58,20 +65,12 @@ The list of available flags are:
* `-name` - Optional name to associate with the underlying session.
If not provided, one is generated based on the child command.
* `-token` - ACL token to use. Defaults to that of agent.
* `-pass-stdin` - Pass stdin to child process.
* `-try` - Attempt to acquire the lock up to the given timeout. The timeout is a
positive decimal number, with unit suffix, such as "500ms". Valid time units
are "ns", "us" (or "µs"), "ms", "s", "m", "h".
* `-monitor-retry` - Retry up to this number of times if Consul returns a 500 error
while monitoring the lock. This allows riding out brief periods of unavailability
without causing leader elections, but increases the amount of time required
to detect a lost lock in some cases. Defaults to 3, with a 1s wait between retries.
Set to 0 to disable.
* `-verbose` - Enables verbose output.
## SHELL

View File

@ -60,7 +60,7 @@ Usage: `consul snapshot agent [options]`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
#### Config File Options:

View File

@ -27,7 +27,7 @@ Usage: `consul snapshot restore [options] FILE`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
## Examples

View File

@ -22,7 +22,7 @@ Usage: `consul snapshot save [options] FILE`
#### API Options
<%= partial "docs/commands/http_api_options" %>
<%= partial "docs/commands/http_api_options_client" %>
## Examples