mirror of https://github.com/status-im/consul.git
Merge pull request #2717 from hashicorp/f-cli-rework
Begin centralizing command-line parsing
This commit is contained in:
commit
a4cb414e58
|
@ -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")
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,17 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/command/agent"
|
"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
|
// ConfigTestCommand is a Command implementation that is used to
|
||||||
// verify config files
|
// verify config files
|
||||||
type ConfigTestCommand struct {
|
type ConfigTestCommand struct {
|
||||||
Ui cli.Ui
|
base.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigTestCommand) Help() string {
|
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.
|
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)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConfigTestCommand) Run(args []string) int {
|
func (c *ConfigTestCommand) Run(args []string) int {
|
||||||
var configFiles []string
|
var configFiles []string
|
||||||
cmdFlags := flag.NewFlagSet("configtest", flag.ContinueOnError)
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
|
f := c.Command.NewFlagSet(c)
|
||||||
cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-file", "json file to read config from")
|
f.Var((*agent.AppendSliceValue)(&configFiles), "config-file",
|
||||||
cmdFlags.Var((*agent.AppendSliceValue)(&configFiles), "config-dir", "directory of json files to read")
|
"Path to a JSON file to read configuration from. This can be specified multiple times.")
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,20 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/command/base"
|
||||||
"github.com/mitchellh/cli"
|
"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) {
|
func TestConfigTestCommand_implements(t *testing.T) {
|
||||||
var _ cli.Command = &ConfigTestCommand{}
|
var _ cli.Command = &ConfigTestCommand{}
|
||||||
}
|
}
|
||||||
|
@ -20,9 +31,7 @@ func TestConfigTestCommandFailOnEmptyFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpFile.Name())
|
defer os.RemoveAll(tmpFile.Name())
|
||||||
|
|
||||||
cmd := &ConfigTestCommand{
|
_, cmd := testConfigTestCommand(t)
|
||||||
Ui: new(cli.MockUi),
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-config-file", tmpFile.Name(),
|
"-config-file", tmpFile.Name(),
|
||||||
|
@ -40,16 +49,14 @@ func TestConfigTestCommandSucceedOnEmptyDir(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(td)
|
defer os.RemoveAll(td)
|
||||||
|
|
||||||
cmd := &ConfigTestCommand{
|
ui, cmd := testConfigTestCommand(t)
|
||||||
Ui: new(cli.MockUi),
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-config-dir", td,
|
"-config-dir", td,
|
||||||
}
|
}
|
||||||
|
|
||||||
if code := cmd.Run(args); code != 0 {
|
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)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &ConfigTestCommand{
|
_, cmd := testConfigTestCommand(t)
|
||||||
Ui: new(cli.MockUi),
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-config-file", fp,
|
"-config-file", fp,
|
||||||
|
@ -91,9 +96,7 @@ func TestConfigTestCommandSucceedOnMinimalConfigDir(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &ConfigTestCommand{
|
_, cmd := testConfigTestCommand(t)
|
||||||
Ui: new(cli.MockUi),
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-config-dir", td,
|
"-config-dir", td,
|
||||||
|
@ -116,9 +119,7 @@ func TestConfigTestCommandSucceedOnConfigDirWithEmptyFile(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := &ConfigTestCommand{
|
_, cmd := testConfigTestCommand(t)
|
||||||
Ui: new(cli.MockUi),
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-config-dir", td,
|
"-config-dir", td,
|
||||||
|
|
|
@ -1,42 +1,38 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/hashicorp/consul/command/base"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ForceLeaveCommand is a Command implementation that tells a running Consul
|
// ForceLeaveCommand is a Command implementation that tells a running Consul
|
||||||
// to force a member to enter the "left" state.
|
// to force a member to enter the "left" state.
|
||||||
type ForceLeaveCommand struct {
|
type ForceLeaveCommand struct {
|
||||||
Ui cli.Ui
|
base.Command
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ForceLeaveCommand) Run(args []string) int {
|
func (c *ForceLeaveCommand) Run(args []string) int {
|
||||||
cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError)
|
f := c.Command.NewFlagSet(c)
|
||||||
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
|
if err := c.Command.Parse(args); err != nil {
|
||||||
rpcAddr := RPCAddrFlag(cmdFlags)
|
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes := cmdFlags.Args()
|
nodes := f.Args()
|
||||||
if len(nodes) != 1 {
|
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.Ui.Error(c.Help())
|
c.Ui.Error(c.Help())
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := RPCClient(*rpcAddr)
|
client, err := c.Command.HTTPClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
err = client.ForceLeave(nodes[0])
|
err = client.Agent().ForceLeave(nodes[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err))
|
||||||
return 1
|
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
|
Consul will attempt to reconnect to those failed nodes for some period of
|
||||||
time before eventually reaping them.
|
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)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package command
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/hashicorp/consul/command/base"
|
||||||
"github.com/hashicorp/consul/testutil"
|
"github.com/hashicorp/consul/testutil"
|
||||||
"github.com/hashicorp/serf/serf"
|
"github.com/hashicorp/serf/serf"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
|
@ -10,6 +11,16 @@ import (
|
||||||
"testing"
|
"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) {
|
func TestForceLeaveCommand_implements(t *testing.T) {
|
||||||
var _ cli.Command = &ForceLeaveCommand{}
|
var _ cli.Command = &ForceLeaveCommand{}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +40,9 @@ func TestForceLeaveCommandRun(t *testing.T) {
|
||||||
// Forcibly shutdown a2 so that it appears "failed" in a1
|
// Forcibly shutdown a2 so that it appears "failed" in a1
|
||||||
a2.Shutdown()
|
a2.Shutdown()
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testForceLeaveCommand(t)
|
||||||
c := &ForceLeaveCommand{Ui: ui}
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-rpc-addr=" + a1.addr,
|
"-http-addr=" + a1.httpAddr,
|
||||||
a2.config.NodeName,
|
a2.config.NodeName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,8 +67,8 @@ func TestForceLeaveCommandRun(t *testing.T) {
|
||||||
|
|
||||||
func TestForceLeaveCommandRun_noAddrs(t *testing.T) {
|
func TestForceLeaveCommandRun_noAddrs(t *testing.T) {
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &ForceLeaveCommand{Ui: ui}
|
ui, c := testForceLeaveCommand(t)
|
||||||
args := []string{"-rpc-addr=foo"}
|
args := []string{"-http-addr=foo"}
|
||||||
|
|
||||||
code := c.Run(args)
|
code := c.Run(args)
|
||||||
if code != 1 {
|
if code != 1 {
|
||||||
|
|
136
command/lock.go
136
command/lock.go
|
@ -1,7 +1,6 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -12,7 +11,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/command/agent"
|
"github.com/hashicorp/consul/command/agent"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/hashicorp/consul/command/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -37,54 +36,37 @@ const (
|
||||||
// LockCommand is a Command implementation that is used to setup
|
// LockCommand is a Command implementation that is used to setup
|
||||||
// a "lock" which manages lock acquisition and invokes a sub-process
|
// a "lock" which manages lock acquisition and invokes a sub-process
|
||||||
type LockCommand struct {
|
type LockCommand struct {
|
||||||
|
base.Command
|
||||||
|
|
||||||
ShutdownCh <-chan struct{}
|
ShutdownCh <-chan struct{}
|
||||||
Ui cli.Ui
|
|
||||||
|
|
||||||
child *os.Process
|
child *os.Process
|
||||||
childLock sync.Mutex
|
childLock sync.Mutex
|
||||||
verbose bool
|
|
||||||
|
verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LockCommand) Help() string {
|
func (c *LockCommand) Help() string {
|
||||||
helpText := `
|
helpText := `
|
||||||
Usage: consul lock [options] prefix child...
|
Usage: consul lock [options] prefix child...
|
||||||
|
|
||||||
Acquires a lock or semaphore at a given path, and invokes a child
|
Acquires a lock or semaphore at a given path, and invokes a child process
|
||||||
process when successful. The child process can assume the lock is
|
when successful. The child process can assume the lock is held while it
|
||||||
held while it executes. If the lock is lost or communication is
|
executes. If the lock is lost or communication is disrupted the child
|
||||||
disrupted the child process will be sent a SIGTERM signal and given
|
process will be sent a SIGTERM signal and given time to gracefully exit.
|
||||||
time to gracefully exit. After the grace period expires the process
|
After the grace period expires the process will be hard terminated.
|
||||||
will be hard terminated.
|
|
||||||
|
|
||||||
For Consul agents on Windows, the child process is always hard
|
For Consul agents on Windows, the child process is always hard terminated
|
||||||
terminated with a SIGKILL, since Windows has no POSIX compatible
|
with a SIGKILL, since Windows has no POSIX compatible notion for SIGTERM.
|
||||||
notion for SIGTERM.
|
|
||||||
|
|
||||||
When -n=1, only a single lock holder or leader exists providing
|
When -n=1, only a single lock holder or leader exists providing mutual
|
||||||
mutual exclusion. Setting a higher value switches to a semaphore
|
exclusion. Setting a higher value switches to a semaphore allowing multiple
|
||||||
allowing multiple holders to coordinate.
|
holders to coordinate.
|
||||||
|
|
||||||
The prefix provided must have write privileges.
|
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)
|
return strings.TrimSpace(helpText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,25 +75,41 @@ func (c *LockCommand) Run(args []string) int {
|
||||||
return c.run(args, &lu)
|
return c.run(args, &lu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// run exposes the underlying lock for testing.
|
|
||||||
func (c *LockCommand) run(args []string, lu **LockUnlock) int {
|
func (c *LockCommand) run(args []string, lu **LockUnlock) int {
|
||||||
var childDone chan struct{}
|
var childDone chan struct{}
|
||||||
var name, token string
|
|
||||||
var limit int
|
var limit int
|
||||||
|
var monitorRetry int
|
||||||
|
var name string
|
||||||
var passStdin bool
|
var passStdin bool
|
||||||
var try string
|
var timeout time.Duration
|
||||||
var retry int
|
|
||||||
cmdFlags := flag.NewFlagSet("watch", flag.ContinueOnError)
|
f := c.Command.NewFlagSet(c)
|
||||||
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
|
f.IntVar(&limit, "n", 1,
|
||||||
cmdFlags.IntVar(&limit, "n", 1, "")
|
"Optional limit on the number of concurrent lock holders. The underlying "+
|
||||||
cmdFlags.StringVar(&name, "name", "", "")
|
"implementation switches from a lock to a semaphore when the value is "+
|
||||||
cmdFlags.StringVar(&token, "token", "", "")
|
"greater than 1. The default value is 1.")
|
||||||
cmdFlags.BoolVar(&passStdin, "pass-stdin", false, "")
|
f.IntVar(&monitorRetry, "monitor-retry", defaultMonitorRetry,
|
||||||
cmdFlags.StringVar(&try, "try", "", "")
|
"Number of times to retry if Consul returns a 500 error while monitoring "+
|
||||||
cmdFlags.IntVar(&retry, "monitor-retry", defaultMonitorRetry, "")
|
"the lock. This allows riding out brief periods of unavailability "+
|
||||||
cmdFlags.BoolVar(&c.verbose, "verbose", false, "")
|
"without causing leader elections, but increases the amount of time "+
|
||||||
httpAddr := HTTPAddrFlag(cmdFlags)
|
"required to detect a lost lock in some cases. The default value is 3, "+
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
"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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,52 +120,36 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the prefix and child are provided
|
// Verify the prefix and child are provided
|
||||||
extra := cmdFlags.Args()
|
extra := f.Args()
|
||||||
if len(extra) < 2 {
|
if len(extra) < 2 {
|
||||||
c.Ui.Error("Key prefix and child command must be specified")
|
c.Ui.Error("Key prefix and child command must be specified")
|
||||||
c.Ui.Error("")
|
|
||||||
c.Ui.Error(c.Help())
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
prefix := extra[0]
|
prefix := extra[0]
|
||||||
prefix = strings.TrimPrefix(prefix, "/")
|
prefix = strings.TrimPrefix(prefix, "/")
|
||||||
script := strings.Join(extra[1:], " ")
|
script := strings.Join(extra[1:], " ")
|
||||||
|
|
||||||
|
if timeout < 0 {
|
||||||
|
c.Ui.Error("Timeout must be positive")
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate a session name if none provided
|
// Calculate a session name if none provided
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = fmt.Sprintf("Consul lock for '%s' at '%s'", script, prefix)
|
name = fmt.Sprintf("Consul lock for '%s' at '%s'", script, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the duration if given.
|
// Calculate oneshot
|
||||||
oneshot := false
|
oneshot := timeout > 0
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the retry parameter
|
// Check the retry parameter
|
||||||
if retry < 0 {
|
if monitorRetry < 0 {
|
||||||
c.Ui.Error("Number for 'monitor-retry' must be >= 0")
|
c.Ui.Error("Number for 'monitor-retry' must be >= 0")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and test the HTTP client
|
// Create and test the HTTP client
|
||||||
conf := api.DefaultConfig()
|
client, err := c.Command.HTTPClient()
|
||||||
conf.Address = *httpAddr
|
|
||||||
conf.Token = token
|
|
||||||
client, err := api.NewClient(conf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -180,9 +162,9 @@ func (c *LockCommand) run(args []string, lu **LockUnlock) int {
|
||||||
|
|
||||||
// Setup the lock or semaphore
|
// Setup the lock or semaphore
|
||||||
if limit == 1 {
|
if limit == 1 {
|
||||||
*lu, err = c.setupLock(client, prefix, name, oneshot, wait, retry)
|
*lu, err = c.setupLock(client, prefix, name, oneshot, timeout, monitorRetry)
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Lock setup failed: %s", err))
|
c.Ui.Error(fmt.Sprintf("Lock setup failed: %s", err))
|
||||||
|
|
|
@ -9,16 +9,26 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
|
"github.com/hashicorp/consul/command/base"
|
||||||
"github.com/mitchellh/cli"
|
"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) {
|
func TestLockCommand_implements(t *testing.T) {
|
||||||
var _ cli.Command = &LockCommand{}
|
var _ cli.Command = &LockCommand{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func argFail(t *testing.T, args []string, expected string) {
|
func argFail(t *testing.T, args []string, expected string) {
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
if code := c.Run(args); code != 1 {
|
if code := c.Run(args); code != 1 {
|
||||||
t.Fatalf("expected return code 1, got %d", code)
|
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) {
|
func TestLockCommand_BadArgs(t *testing.T) {
|
||||||
argFail(t, []string{"-try=blah", "test/prefix", "date"}, "parsing try timeout")
|
argFail(t, []string{"-try=blah", "test/prefix", "date"}, "invalid duration")
|
||||||
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=-10s", "test/prefix", "date"}, "timeout must be positive")
|
|
||||||
argFail(t, []string{"-monitor-retry=-5", "test/prefix", "date"}, "must be >= 0")
|
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()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
|
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
|
||||||
|
@ -63,8 +71,7 @@ func TestLockCommand_Try_Lock(t *testing.T) {
|
||||||
defer a1.Shutdown()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "-try=10s", "test/prefix", touchCmd}
|
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()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "-try=10s", "test/prefix", touchCmd}
|
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()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
|
args := []string{"-http-addr=" + a1.httpAddr, "test/prefix", touchCmd}
|
||||||
|
@ -160,8 +165,7 @@ func TestLockCommand_MonitorRetry_Semaphore_Default(t *testing.T) {
|
||||||
defer a1.Shutdown()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "test/prefix", touchCmd}
|
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()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "-monitor-retry=9", "test/prefix", touchCmd}
|
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()
|
defer a1.Shutdown()
|
||||||
waitForLeader(t, a1.httpAddr)
|
waitForLeader(t, a1.httpAddr)
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui, c := testLockCommand(t)
|
||||||
c := &LockCommand{Ui: ui}
|
|
||||||
filePath := filepath.Join(a1.dir, "test_touch")
|
filePath := filepath.Join(a1.dir, "test_touch")
|
||||||
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
touchCmd := fmt.Sprintf("touch '%s'", filePath)
|
||||||
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "-monitor-retry=9", "test/prefix", touchCmd}
|
args := []string{"-http-addr=" + a1.httpAddr, "-n=3", "-monitor-retry=9", "test/prefix", touchCmd}
|
||||||
|
|
16
commands.go
16
commands.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/consul/command"
|
"github.com/hashicorp/consul/command"
|
||||||
"github.com/hashicorp/consul/command/agent"
|
"github.com/hashicorp/consul/command/agent"
|
||||||
|
"github.com/hashicorp/consul/command/base"
|
||||||
"github.com/hashicorp/consul/version"
|
"github.com/hashicorp/consul/version"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -31,7 +32,10 @@ func init() {
|
||||||
|
|
||||||
"configtest": func() (cli.Command, error) {
|
"configtest": func() (cli.Command, error) {
|
||||||
return &command.ConfigTestCommand{
|
return &command.ConfigTestCommand{
|
||||||
Ui: ui,
|
Command: base.Command{
|
||||||
|
Flags: base.FlagSetNone,
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -50,7 +54,10 @@ func init() {
|
||||||
|
|
||||||
"force-leave": func() (cli.Command, error) {
|
"force-leave": func() (cli.Command, error) {
|
||||||
return &command.ForceLeaveCommand{
|
return &command.ForceLeaveCommand{
|
||||||
Ui: ui,
|
Command: base.Command{
|
||||||
|
Flags: base.FlagSetClientHTTP,
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -117,7 +124,10 @@ func init() {
|
||||||
"lock": func() (cli.Command, error) {
|
"lock": func() (cli.Command, error) {
|
||||||
return &command.LockCommand{
|
return &command.LockCommand{
|
||||||
ShutdownCh: makeShutdownCh(),
|
ShutdownCh: makeShutdownCh(),
|
||||||
Ui: ui,
|
Command: base.Command{
|
||||||
|
Flags: base.FlagSetHTTP,
|
||||||
|
Ui: ui,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"snapshot_agent": {
|
||||||
|
"snapshot": {
|
||||||
|
"interval": "1h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"snapshot_agent": {
|
||||||
|
"snapshot": {
|
||||||
|
"interval": "2h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"not_related": true
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"snapshot_agent": {
|
||||||
|
"snapshot": {
|
||||||
|
"interval": "3h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"snapshot_agent": {
|
||||||
|
"snapshot": {
|
||||||
|
"interval": "5h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
||||||
|
// Package text provides rudimentary functions for manipulating text in
|
||||||
|
// paragraphs.
|
||||||
|
package text
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -740,6 +740,12 @@
|
||||||
"revision": "bb4de0191aa41b5507caa14b0650cdbddcd9280b",
|
"revision": "bb4de0191aa41b5507caa14b0650cdbddcd9280b",
|
||||||
"revisionTime": "2016-09-30T03:27:40Z"
|
"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=",
|
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
|
||||||
"path": "golang.org/x/net/context",
|
"path": "golang.org/x/net/context",
|
||||||
|
|
|
@ -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.
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -28,11 +28,7 @@ as it will be removed from the Raft quorum.
|
||||||
|
|
||||||
Usage: `consul force-leave [options] node`
|
Usage: `consul force-leave [options] node`
|
||||||
|
|
||||||
The following command-line options are available for this command.
|
#### API Options
|
||||||
Every option is optional:
|
|
||||||
|
|
||||||
* `-rpc-addr` - Address to the RPC server of the agent you want to contact
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
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`.
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ Usage: `consul kv delete [options] KEY_OR_PREFIX`
|
||||||
|
|
||||||
#### API Options
|
#### 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
|
#### KV Delete Options
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ Usage: `consul kv export [PREFIX]`
|
||||||
|
|
||||||
#### API Options
|
#### API Options
|
||||||
|
|
||||||
<%= partial "docs/commands/http_api_options" %>
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
<%= partial "docs/commands/http_api_options_server" %>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,8 @@ Usage: `consul kv get [options] [KEY_OR_PREFIX]`
|
||||||
|
|
||||||
#### API Options
|
#### 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
|
#### KV Get Options
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ Usage: `consul kv import [DATA]`
|
||||||
|
|
||||||
#### API Options
|
#### API Options
|
||||||
|
|
||||||
<%= partial "docs/commands/http_api_options" %>
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
<%= partial "docs/commands/http_api_options_server" %>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,8 @@ Usage: `consul kv put [options] KEY [DATA]`
|
||||||
|
|
||||||
#### API Options
|
#### 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
|
#### KV Put Options
|
||||||
|
|
||||||
|
|
|
@ -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
|
on Windows, the child process is always terminated with a `SIGKILL`, since
|
||||||
Windows has no POSIX compatible notion for `SIGTERM`.
|
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
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
to send this command. If this isn't specified, the command will contact
|
<%= partial "docs/commands/http_api_options_server" %>
|
||||||
"127.0.0.1:8500" which is the default HTTP address of a Consul agent.
|
|
||||||
|
#### 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
|
* `-n` - Optional, limit of lock holders. Defaults to 1. The underlying
|
||||||
implementation switches from a lock to a semaphore when increased past
|
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.
|
* `-name` - Optional name to associate with the underlying session.
|
||||||
If not provided, one is generated based on the child command.
|
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.
|
* `-pass-stdin` - Pass stdin to child process.
|
||||||
|
|
||||||
* `-try` - Attempt to acquire the lock up to the given timeout. The timeout is a
|
* `-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
|
positive decimal number, with unit suffix, such as "500ms". Valid time units
|
||||||
are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
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.
|
* `-verbose` - Enables verbose output.
|
||||||
|
|
||||||
## SHELL
|
## SHELL
|
|
@ -60,7 +60,7 @@ Usage: `consul snapshot agent [options]`
|
||||||
|
|
||||||
#### API Options
|
#### API Options
|
||||||
|
|
||||||
<%= partial "docs/commands/http_api_options" %>
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
|
||||||
#### Config File Options:
|
#### Config File Options:
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ Usage: `consul snapshot restore [options] FILE`
|
||||||
|
|
||||||
#### API Options
|
#### API Options
|
||||||
|
|
||||||
<%= partial "docs/commands/http_api_options" %>
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Usage: `consul snapshot save [options] FILE`
|
||||||
|
|
||||||
#### API Options
|
#### API Options
|
||||||
|
|
||||||
<%= partial "docs/commands/http_api_options" %>
|
<%= partial "docs/commands/http_api_options_client" %>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue