-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathmeta.go
429 lines (356 loc) · 11.9 KB
/
meta.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"flag"
"fmt"
"os"
"reflect"
"strings"
"github.com/hashicorp/cli"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/helper/pointer"
colorable "github.com/mattn/go-colorable"
"github.com/mitchellh/colorstring"
"github.com/posener/complete"
"golang.org/x/crypto/ssh/terminal"
)
const (
// Constants for CLI identifier length
shortId = 8
fullId = 36
)
// FlagSetFlags is an enum to define what flags are present in the
// default FlagSet returned by Meta.FlagSet.
type FlagSetFlags uint
const (
FlagSetNone FlagSetFlags = 0
FlagSetClient FlagSetFlags = 1 << iota
FlagSetDefault = FlagSetClient
)
// Meta contains the meta-options and functionality that nearly every
// Nomad command inherits.
type Meta struct {
Ui cli.Ui
// These are set by the command line flags.
flagAddress string
// Whether to not-colorize output
noColor bool
// Whether to force colorized output
forceColor bool
// The region to send API requests
region string
// namespace to send API requests
namespace string
// token is used for ACLs to access privileged information
token string
caCert string
caPath string
clientCert string
clientKey string
tlsServerName string
insecure bool
}
// FlagSet returns a FlagSet with the common flags that every
// command implements. The exact behavior of FlagSet can be configured
// using the flags as the second parameter, for example to disable
// server settings on the commands that don't talk to a server.
func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError)
// FlagSetClient is used to enable the settings for specifying
// client connectivity options.
if fs&FlagSetClient != 0 {
f.StringVar(&m.flagAddress, "address", "", "")
f.StringVar(&m.region, "region", "", "")
f.StringVar(&m.namespace, "namespace", "", "")
f.BoolVar(&m.noColor, "no-color", false, "")
f.BoolVar(&m.forceColor, "force-color", false, "")
f.StringVar(&m.caCert, "ca-cert", "", "")
f.StringVar(&m.caPath, "ca-path", "", "")
f.StringVar(&m.clientCert, "client-cert", "", "")
f.StringVar(&m.clientKey, "client-key", "", "")
f.BoolVar(&m.insecure, "insecure", false, "")
f.StringVar(&m.tlsServerName, "tls-server-name", "", "")
f.BoolVar(&m.insecure, "tls-skip-verify", false, "")
f.StringVar(&m.token, "token", "", "")
}
f.SetOutput(&uiErrorWriter{ui: m.Ui})
return f
}
// AutocompleteFlags returns a set of flag completions for the given flag set.
func (m *Meta) AutocompleteFlags(fs FlagSetFlags) complete.Flags {
if fs&FlagSetClient == 0 {
return nil
}
return complete.Flags{
"-address": complete.PredictAnything,
"-region": complete.PredictAnything,
"-namespace": NamespacePredictor(m.Client, nil),
"-no-color": complete.PredictNothing,
"-force-color": complete.PredictNothing,
"-ca-cert": complete.PredictFiles("*"),
"-ca-path": complete.PredictDirs("*"),
"-client-cert": complete.PredictFiles("*"),
"-client-key": complete.PredictFiles("*"),
"-insecure": complete.PredictNothing,
"-tls-server-name": complete.PredictNothing,
"-tls-skip-verify": complete.PredictNothing,
"-token": complete.PredictAnything,
}
}
// ApiClientFactory is the signature of a API client factory
type ApiClientFactory func() (*api.Client, error)
// Client is used to initialize and return a new API client using
// the default command line arguments and env vars.
func (m *Meta) clientConfig() *api.Config {
config := api.DefaultConfig()
if m.flagAddress != "" {
config.Address = m.flagAddress
}
if m.region != "" {
config.Region = m.region
}
if m.namespace != "" {
config.Namespace = m.namespace
}
if m.token != "" {
config.SecretID = m.token
}
// Override TLS configuration fields we may have received from env vars with
// flag arguments from the user only if they're provided.
if m.caCert != "" {
config.TLSConfig.CACert = m.caCert
}
if m.caPath != "" {
config.TLSConfig.CAPath = m.caPath
}
if m.clientCert != "" {
config.TLSConfig.ClientCert = m.clientCert
}
if m.clientKey != "" {
config.TLSConfig.ClientKey = m.clientKey
}
if m.tlsServerName != "" {
config.TLSConfig.TLSServerName = m.tlsServerName
}
if m.insecure {
config.TLSConfig.Insecure = m.insecure
}
return config
}
func (m *Meta) Client() (*api.Client, error) {
return api.NewClient(m.clientConfig())
}
func (m *Meta) allNamespaces() bool {
return m.clientConfig().Namespace == api.AllNamespacesNamespace
}
func (m *Meta) Colorize() *colorstring.Colorize {
ui := m.Ui
coloredUi := false
// Meta.Ui may wrap other cli.Ui instances, so unwrap them until we find a
// *cli.ColoredUi or there is nothing left to unwrap.
for {
if ui == nil {
break
}
_, coloredUi = ui.(*cli.ColoredUi)
if coloredUi {
break
}
v := reflect.ValueOf(ui)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
if !v.Field(i).CanInterface() {
continue
}
ui, _ = v.Field(i).Interface().(cli.Ui)
if ui != nil {
break
}
}
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: !coloredUi,
Reset: true,
}
}
func (m *Meta) SetupUi(args []string) {
noColor := os.Getenv(EnvNomadCLINoColor) != ""
forceColor := os.Getenv(EnvNomadCLIForceColor) != ""
for _, arg := range args {
// Check if color is set
if arg == "-no-color" || arg == "--no-color" {
noColor = true
} else if arg == "-force-color" || arg == "--force-color" {
forceColor = true
}
}
m.Ui = &cli.BasicUi{
Reader: os.Stdin,
Writer: colorable.NewColorableStdout(),
ErrorWriter: colorable.NewColorableStderr(),
}
// Only use colored UI if not disabled and stdout is a tty or colors are
// forced.
isTerminal := terminal.IsTerminal(int(os.Stdout.Fd()))
useColor := !noColor && (isTerminal || forceColor)
if useColor {
m.Ui = &cli.ColoredUi{
ErrorColor: cli.UiColorRed,
WarnColor: cli.UiColorYellow,
InfoColor: cli.UiColorGreen,
Ui: m.Ui,
}
}
}
// FormatWarnings returns a string with the warnings formatted for CLI output.
func (m *Meta) FormatWarnings(header string, warnings string) string {
return m.Colorize().Color(
fmt.Sprintf("[bold][yellow]%s Warnings:\n%s[reset]\n",
header,
warnings,
))
}
// JobByPrefixFilterFunc is a function used to filter jobs when performing a
// prefix match. Only jobs that return true are included in the prefix match.
type JobByPrefixFilterFunc func(*api.JobListStub) bool
// NoJobWithPrefixError is the error returned when the job prefix doesn't
// return any matches.
type NoJobWithPrefixError struct {
Prefix string
}
func (e *NoJobWithPrefixError) Error() string {
return fmt.Sprintf("No job(s) with prefix or ID %q found", e.Prefix)
}
// JobByPrefix returns the job that best matches the given prefix. Returns an
// error if there are no matches or if there are more than one exact match
// across namespaces.
func (m *Meta) JobByPrefix(client *api.Client, prefix string, filter JobByPrefixFilterFunc) (*api.Job, error) {
jobID, namespace, err := m.JobIDByPrefix(client, prefix, filter)
if err != nil {
return nil, err
}
q := &api.QueryOptions{Namespace: namespace}
job, _, err := client.Jobs().Info(jobID, q)
if err != nil {
return nil, fmt.Errorf("Error querying job %q: %s", jobID, err)
}
job.Namespace = pointer.Of(namespace)
return job, nil
}
// JobIDByPrefix provides best effort match for the given job prefix.
// Returns the prefix itself if job prefix search is not allowed and an error
// if there are no matches or if there are more than one exact match across
// namespaces.
func (m *Meta) JobIDByPrefix(client *api.Client, prefix string, filter JobByPrefixFilterFunc) (string, string, error) {
// Search job by prefix. Return an error if there is not an exact match.
jobs, _, err := client.Jobs().PrefixList(prefix)
if err != nil {
if strings.Contains(err.Error(), api.PermissionDeniedErrorContent) {
return prefix, "", nil
}
return "", "", fmt.Errorf("Error querying job prefix %q: %s", prefix, err)
}
if filter != nil {
var filtered []*api.JobListStub
for _, j := range jobs {
if filter(j) {
filtered = append(filtered, j)
}
}
jobs = filtered
}
if len(jobs) == 0 {
return "", "", &NoJobWithPrefixError{Prefix: prefix}
}
if len(jobs) > 1 {
exactMatch := prefix == jobs[0].ID
matchInMultipleNamespaces := m.allNamespaces() && jobs[0].ID == jobs[1].ID
if !exactMatch || matchInMultipleNamespaces {
return "", "", fmt.Errorf(
"Prefix %q matched multiple jobs\n\n%s",
prefix,
createStatusListOutput(jobs, m.allNamespaces()),
)
}
}
return jobs[0].ID, jobs[0].JobSummary.Namespace, nil
}
type usageOptsFlags uint8
const (
usageOptsDefault usageOptsFlags = 0
usageOptsNoNamespace = 1 << iota
)
// generalOptionsUsage returns the help string for the global options.
func generalOptionsUsage(usageOpts usageOptsFlags) string {
helpText := `
-address=<addr>
The address of the Nomad server.
Overrides the NOMAD_ADDR environment variable if set.
Default = http://127.0.0.1:4646
-region=<region>
The region of the Nomad servers to forward commands to.
Overrides the NOMAD_REGION environment variable if set.
Defaults to the Agent's local region.
`
namespaceText := `
-namespace=<namespace>
The target namespace for queries and actions bound to a namespace.
Overrides the NOMAD_NAMESPACE environment variable if set.
If set to '*', subcommands which support this functionality query
all namespaces authorized to user.
Defaults to the "default" namespace.
`
// note: that although very few commands use color explicitly, all of them
// return red-colored text on error so we want the color flags to always be
// present in the help messages.
remainingText := `
-no-color
Disables colored command output. Alternatively, NOMAD_CLI_NO_COLOR may be
set. This option takes precedence over -force-color.
-force-color
Forces colored command output. This can be used in cases where the usual
terminal detection fails. Alternatively, NOMAD_CLI_FORCE_COLOR may be set.
This option has no effect if -no-color is also used.
-ca-cert=<path>
Path to a PEM encoded CA cert file to use to verify the
Nomad server SSL certificate. Overrides the NOMAD_CACERT
environment variable if set.
-ca-path=<path>
Path to a directory of PEM encoded CA cert files to verify
the Nomad server SSL certificate. If both -ca-cert and
-ca-path are specified, -ca-cert is used. Overrides the
NOMAD_CAPATH environment variable if set.
-client-cert=<path>
Path to a PEM encoded client certificate for TLS authentication
to the Nomad server. Must also specify -client-key. Overrides
the NOMAD_CLIENT_CERT environment variable if set.
-client-key=<path>
Path to an unencrypted PEM encoded private key matching the
client certificate from -client-cert. Overrides the
NOMAD_CLIENT_KEY environment variable if set.
-tls-server-name=<value>
The server name to use as the SNI host when connecting via
TLS. Overrides the NOMAD_TLS_SERVER_NAME environment variable if set.
-tls-skip-verify
Do not verify TLS certificate. This is highly not recommended. Verification
will also be skipped if NOMAD_SKIP_VERIFY is set.
-token
The SecretID of an ACL token to use to authenticate API requests with.
Overrides the NOMAD_TOKEN environment variable if set.
`
if usageOpts&usageOptsNoNamespace == 0 {
helpText = helpText + namespaceText
}
helpText = helpText + remainingText
return strings.TrimSpace(helpText)
}
// funcVar is a type of flag that accepts a function that is the string given
// by the user.
type funcVar func(s string) error
func (f funcVar) Set(s string) error { return f(s) }
func (f funcVar) String() string { return "" }
func (f funcVar) IsBoolFlag() bool { return false }