diff --git a/command/base.go b/command/base.go index 441318e713..96a9e726a9 100644 --- a/command/base.go +++ b/command/base.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/configutil" + "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" text "github.com/tonnerre/golang-text" ) @@ -39,16 +39,16 @@ type BaseCommand struct { 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 + httpAddr flags.StringValue + token flags.StringValue + caFile flags.StringValue + caPath flags.StringValue + certFile flags.StringValue + keyFile flags.StringValue + tlsServerName flags.StringValue - datacenter configutil.StringValue - stale configutil.BoolValue + datacenter flags.StringValue + stale flags.BoolValue } // HTTPClient returns a client with the parsed flags. It panics if the command diff --git a/command/catalog_list_nodes.go b/command/catalog_list_nodes.go index c338b8ddd5..c57896673c 100644 --- a/command/catalog_list_nodes.go +++ b/command/catalog_list_nodes.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/configutil" + "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" "github.com/ryanuber/columnize" ) @@ -31,7 +31,7 @@ func (c *CatalogListNodesCommand) initFlags() { c.FlagSet.StringVar(&c.near, "near", "", "Node name to sort the node list in ascending "+ "order based on estimated round-trip time from that node. "+ "Passing \"_agent\" will use this agent's node for sorting.") - c.FlagSet.Var((*configutil.FlagMapValue)(&c.nodeMeta), "node-meta", "Metadata to "+ + c.FlagSet.Var((*flags.FlagMapValue)(&c.nodeMeta), "node-meta", "Metadata to "+ "filter nodes with the given `key=value` pairs. This flag may be "+ "specified multiple times to filter on multiple sources of metadata.") c.FlagSet.StringVar(&c.service, "service", "", "Service `id or name` to filter nodes. "+ diff --git a/command/catalog_list_services.go b/command/catalog_list_services.go index fa2e73ef38..dce02c4b85 100644 --- a/command/catalog_list_services.go +++ b/command/catalog_list_services.go @@ -8,7 +8,7 @@ import ( "text/tabwriter" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/configutil" + "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -29,7 +29,7 @@ func (c *CatalogListServicesCommand) initFlags() { c.InitFlagSet() c.FlagSet.StringVar(&c.node, "node", "", "Node `id or name` for which to list services.") - c.FlagSet.Var((*configutil.FlagMapValue)(&c.nodeMeta), "node-meta", "Metadata to "+ + c.FlagSet.Var((*flags.FlagMapValue)(&c.nodeMeta), "node-meta", "Metadata to "+ "filter nodes with the given `key=value` pairs. If specified, only "+ "services running on nodes matching the given metadata will be returned. "+ "This flag may be specified multiple times to filter on multiple sources "+ diff --git a/configutil/config.go b/command/flags/config.go similarity index 99% rename from configutil/config.go rename to command/flags/config.go index 786914aacc..be8f914364 100644 --- a/configutil/config.go +++ b/command/flags/config.go @@ -1,4 +1,4 @@ -package configutil +package flags import ( "fmt" diff --git a/configutil/config_test.go b/command/flags/config_test.go similarity index 97% rename from configutil/config_test.go rename to command/flags/config_test.go index 03870cbe1b..ba8cf7bb75 100644 --- a/configutil/config_test.go +++ b/command/flags/config_test.go @@ -1,14 +1,13 @@ -package configutil +package flags import ( "bytes" "encoding/json" "fmt" - "strings" - "testing" - "path" "reflect" + "strings" + "testing" "github.com/mitchellh/mapstructure" ) @@ -108,7 +107,7 @@ func TestConfigUtil_Visit(t *testing.T) { return nil } - basePath := "../test/command/merge" + basePath := "../../test/command/merge" if err := Visit(basePath, visitor); err != nil { t.Fatalf("err: %v", err) } diff --git a/configutil/flag_map_value.go b/command/flags/flag_map_value.go similarity index 97% rename from configutil/flag_map_value.go rename to command/flags/flag_map_value.go index 51a88eeba1..aee640b978 100644 --- a/configutil/flag_map_value.go +++ b/command/flags/flag_map_value.go @@ -1,4 +1,4 @@ -package configutil +package flags import ( "flag" diff --git a/configutil/flag_map_value_test.go b/command/flags/flag_map_value_test.go similarity index 98% rename from configutil/flag_map_value_test.go rename to command/flags/flag_map_value_test.go index cddeed7441..2f0f093719 100644 --- a/configutil/flag_map_value_test.go +++ b/command/flags/flag_map_value_test.go @@ -1,4 +1,4 @@ -package configutil +package flags import ( "fmt" diff --git a/configutil/flag_slice_value.go b/command/flags/flag_slice_value.go similarity index 95% rename from configutil/flag_slice_value.go rename to command/flags/flag_slice_value.go index 2d33c2ab1d..a40fd1838e 100644 --- a/configutil/flag_slice_value.go +++ b/command/flags/flag_slice_value.go @@ -1,4 +1,4 @@ -package configutil +package flags import "strings" diff --git a/configutil/flag_slice_value_test.go b/command/flags/flag_slice_value_test.go similarity index 96% rename from configutil/flag_slice_value_test.go rename to command/flags/flag_slice_value_test.go index 3107c565fc..82dff4c377 100644 --- a/configutil/flag_slice_value_test.go +++ b/command/flags/flag_slice_value_test.go @@ -1,4 +1,4 @@ -package configutil +package flags import ( "flag" diff --git a/command/flags/http.go b/command/flags/http.go new file mode 100644 index 0000000000..bd703ae41f --- /dev/null +++ b/command/flags/http.go @@ -0,0 +1,92 @@ +package flags + +import ( + "flag" + + "github.com/hashicorp/consul/api" +) + +type HTTPFlags struct { + // client api flags + address StringValue + token StringValue + caFile StringValue + caPath StringValue + certFile StringValue + keyFile StringValue + tlsServerName StringValue + + // server flags + datacenter StringValue + stale BoolValue +} + +func (f *HTTPFlags) ClientFlags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Var(&f.address, "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.") + fs.Var(&f.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.") + fs.Var(&f.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.") + fs.Var(&f.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.") + fs.Var(&f.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.") + fs.Var(&f.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.") + fs.Var(&f.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 fs +} + +func (f *HTTPFlags) ServerFlags() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.Var(&f.datacenter, "datacenter", + "Name of the datacenter to query. If unspecified, this will default to "+ + "the datacenter of the queried agent.") + fs.Var(&f.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 fs +} + +func (f *HTTPFlags) Datacenter() string { + return f.datacenter.String() +} + +func (f *HTTPFlags) Stale() bool { + if f.stale.v == nil { + return false + } + return *f.stale.v +} + +func (f *HTTPFlags) APIClient() (*api.Client, error) { + c := api.DefaultConfig() + + f.address.Merge(&c.Address) + f.token.Merge(&c.Token) + f.caFile.Merge(&c.TLSConfig.CAFile) + f.caPath.Merge(&c.TLSConfig.CAPath) + f.certFile.Merge(&c.TLSConfig.CertFile) + f.keyFile.Merge(&c.TLSConfig.KeyFile) + f.tlsServerName.Merge(&c.TLSConfig.Address) + f.datacenter.Merge(&c.Datacenter) + + return api.NewClient(c) +} diff --git a/command/flags/merge.go b/command/flags/merge.go new file mode 100644 index 0000000000..0409824eb9 --- /dev/null +++ b/command/flags/merge.go @@ -0,0 +1,15 @@ +package flags + +import "flag" + +func Merge(dst, src *flag.FlagSet) { + if dst == nil { + panic("dst cannot be nil") + } + if src == nil { + return + } + src.VisitAll(func(f *flag.Flag) { + dst.Var(f.Value, f.Name, f.DefValue) + }) +} diff --git a/command/flags/usage.go b/command/flags/usage.go new file mode 100644 index 0000000000..ac02f0da19 --- /dev/null +++ b/command/flags/usage.go @@ -0,0 +1,115 @@ +package flags + +import ( + "bytes" + "flag" + "fmt" + "io" + "strings" + + text "github.com/tonnerre/golang-text" +) + +func Usage(txt string, cmdFlags, clientFlags, serverFlags *flag.FlagSet) string { + u := &Usager{ + Usage: txt, + CmdFlags: cmdFlags, + HTTPClientFlags: clientFlags, + HTTPServerFlags: serverFlags, + } + return u.String() +} + +type Usager struct { + Usage string + CmdFlags *flag.FlagSet + HTTPClientFlags *flag.FlagSet + HTTPServerFlags *flag.FlagSet +} + +func (u *Usager) String() string { + out := new(bytes.Buffer) + out.WriteString(strings.TrimSpace(u.Usage)) + out.WriteString("\n") + out.WriteString("\n") + + httpFlags := u.HTTPClientFlags + if httpFlags == nil { + httpFlags = u.HTTPServerFlags + } else { + Merge(httpFlags, u.HTTPServerFlags) + } + + if httpFlags != nil { + printTitle(out, "HTTP API Options") + httpFlags.VisitAll(func(f *flag.Flag) { + printFlag(out, f) + }) + } + + if u.CmdFlags != nil { + printTitle(out, "Command Options") + u.CmdFlags.VisitAll(func(f *flag.Flag) { + if flagContains(httpFlags, f) { + return + } + 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 { + if fs == nil { + return false + } + + var skip bool + fs.VisitAll(func(hf *flag.Flag) { + if skip { + return + } + + if f.Name == hf.Name { + skip = true + return + } + }) + + return skip +} + +// maxLineLength is the maximum width of any line. +const maxLineLength int = 72 + +// 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") +} diff --git a/command/operator_autopilot_set.go b/command/operator_autopilot_set.go index 9fcecfe766..cd3a9365ae 100644 --- a/command/operator_autopilot_set.go +++ b/command/operator_autopilot_set.go @@ -6,20 +6,20 @@ import ( "time" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/configutil" + "github.com/hashicorp/consul/command/flags" ) type OperatorAutopilotSetCommand struct { BaseCommand // flags - cleanupDeadServers configutil.BoolValue - maxTrailingLogs configutil.UintValue - lastContactThreshold configutil.DurationValue - serverStabilizationTime configutil.DurationValue - redundancyZoneTag configutil.StringValue - disableUpgradeMigration configutil.BoolValue - upgradeVersionTag configutil.StringValue + cleanupDeadServers flags.BoolValue + maxTrailingLogs flags.UintValue + lastContactThreshold flags.DurationValue + serverStabilizationTime flags.DurationValue + redundancyZoneTag flags.StringValue + disableUpgradeMigration flags.BoolValue + upgradeVersionTag flags.StringValue } func (c *OperatorAutopilotSetCommand) initFlags() {