consul/command/base.go

303 lines
8.5 KiB
Go

package command
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"strings"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/configutil"
"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 BaseCommand struct {
UI cli.Ui
Flags FlagSetFlags
HideNormalFlagsHelp bool
flagSet *flag.FlagSet
hidden *flag.FlagSet
// These are the options which correspond to the HTTP API options
httpAddr configutil.StringValue
token configutil.StringValue
caFile configutil.StringValue
caPath configutil.StringValue
certFile configutil.StringValue
keyFile configutil.StringValue
tlsServerName configutil.StringValue
datacenter configutil.StringValue
stale configutil.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 *BaseCommand) 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.caFile.Merge(&config.TLSConfig.CAFile)
c.caPath.Merge(&config.TLSConfig.CAPath)
c.certFile.Merge(&config.TLSConfig.CertFile)
c.keyFile.Merge(&config.TLSConfig.KeyFile)
c.tlsServerName.Merge(&config.TLSConfig.Address)
c.datacenter.Merge(&config.Datacenter)
return api.NewClient(config)
}
func (c *BaseCommand) HTTPAddr() string {
return c.httpAddr.String()
}
func (c *BaseCommand) HTTPToken() string {
return c.token.String()
}
func (c *BaseCommand) HTTPDatacenter() string {
return c.datacenter.String()
}
func (c *BaseCommand) HTTPStale() bool {
var stale bool
c.stale.Merge(&stale)
return stale
}
// httpFlagsClient is the list of flags that apply to HTTP connections.
func (c *BaseCommand) httpFlagsClient(f *flag.FlagSet) *flag.FlagSet {
if f == nil {
f = flag.NewFlagSet("", flag.ContinueOnError)
}
f.Var(&c.caFile, "ca-file",
"Path to a CA file to use for TLS when communicating with Consul. This "+
"can also be specified via the CONSUL_CACERT environment variable.")
f.Var(&c.caPath, "ca-path",
"Path to a directory of CA certificates to use for TLS when communicating "+
"with Consul. This can also be specified via the CONSUL_CAPATH environment variable.")
f.Var(&c.certFile, "client-cert",
"Path to a client cert file to use for TLS when 'verify_incoming' is enabled. This "+
"can also be specified via the CONSUL_CLIENT_CERT environment variable.")
f.Var(&c.keyFile, "client-key",
"Path to a client key file to use for TLS when 'verify_incoming' is enabled. This "+
"can also be specified via the CONSUL_CLIENT_KEY environment variable.")
f.Var(&c.httpAddr, "http-addr",
"The `address` and port of the Consul HTTP agent. The value can be an IP "+
"address or DNS address, but it must also include the port. This can "+
"also be specified via the CONSUL_HTTP_ADDR environment variable. The "+
"default value is http://127.0.0.1:8500. The scheme can also be set to "+
"HTTPS by setting the environment variable CONSUL_HTTP_SSL=true.")
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.")
f.Var(&c.tlsServerName, "tls-server-name",
"The server name to use as the SNI host when connecting via TLS. This "+
"can also be specified via the CONSUL_TLS_SERVER_NAME environment variable.")
return f
}
// httpFlagsServer is the list of flags that apply to HTTP connections.
func (c *BaseCommand) 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 *BaseCommand) 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
c.hidden = flag.NewFlagSet("", flag.ContinueOnError)
return f
}
// HideFlags is used to set hidden flags that will not be shown in help text
func (c *BaseCommand) HideFlags(flags ...string) {
for _, f := range flags {
c.hidden.String(f, "", "")
}
}
// Parse is used to parse the underlying flag set.
func (c *BaseCommand) Parse(args []string) error {
return c.flagSet.Parse(args)
}
// Help returns the help for this flagSet.
func (c *BaseCommand) Help() string {
// Some commands with subcommands (kv/snapshot) call this without initializing
// any flags first, so exit early to avoid a panic
if c.flagSet == nil {
return ""
}
return c.helpFlagsFor(c.flagSet)
}
// hasClientHTTP returns true if this meta command contains client HTTP flags.
func (c *BaseCommand) hasClientHTTP() bool {
return c.Flags&FlagSetClientHTTP != 0
}
// hasServerHTTP returns true if this meta command contains server HTTP flags.
func (c *BaseCommand) 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 *BaseCommand) 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)
})
}
if !c.HideNormalFlagsHelp {
firstCommand := true
f.VisitAll(func(f *flag.Flag) {
// Skip HTTP flags as they will be grouped separately
if flagContains(httpFlagsClient, f) || flagContains(httpFlagsServer, f) || flagContains(c.hidden, 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 {
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")
}