forked from hashicorp/consul
/
base.go
298 lines (252 loc) · 8.48 KB
/
base.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
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
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)
})
}
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")
}