github.com/hernad/nomad@v1.6.112/command/var_put.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "encoding/json" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 multierror "github.com/hashicorp/go-multierror" 17 "github.com/hashicorp/go-set" 18 "github.com/hashicorp/hcl" 19 "github.com/hashicorp/hcl/hcl/ast" 20 "github.com/hernad/nomad/api" 21 "github.com/hernad/nomad/helper" 22 "github.com/mitchellh/cli" 23 "github.com/mitchellh/mapstructure" 24 "github.com/posener/complete" 25 "golang.org/x/exp/slices" 26 ) 27 28 // Detect characters that are not valid identifiers to emit a warning when they 29 // are used in as a variable key. 30 var invalidIdentifier = regexp.MustCompile(`[^_\pN\pL]`) 31 32 type VarPutCommand struct { 33 Meta 34 35 contents []byte 36 inFmt string 37 outFmt string 38 tmpl string 39 testStdin io.Reader // for tests 40 verbose func(string) 41 } 42 43 func (c *VarPutCommand) Help() string { 44 helpText := ` 45 Usage: 46 nomad var put [options] <variable spec file reference> [<key>=<value>]... 47 nomad var put [options] <path to store variable> [<variable spec file reference>] [<key>=<value>]... 48 49 The 'var put' command is used to create or update an existing variable. 50 Variable metadata and items can be supplied using a variable specification, 51 by using command arguments, or by a combination of the two techniques. 52 53 An entire variable specification can be provided to the command via standard 54 input (stdin) by setting the first argument to "-" or from a file by using an 55 @-prefixed path to a variable specification file. When providing variable 56 data via stdin, you must provide the "-in" flag with the format of the 57 specification, either "hcl" or "json" 58 59 Items to be stored in the variable can be supplied using the specification, 60 as a series of key-value pairs, or both. The value for a key-value pair can 61 be a string, an @-prefixed file reference, or a '-' to get the value from 62 stdin. Item values provided from file references or stdin are consumed as-is 63 with no additional processing and do not require the input format to be 64 specified. 65 66 Values supplied as command line arguments supersede values provided in 67 any variable specification piped into the command or loaded from file. 68 69 If ACLs are enabled, this command requires the 'variables:write' capability 70 for the destination namespace and path. 71 72 General Options: 73 74 ` + generalOptionsUsage(usageOptsDefault) + ` 75 76 Apply Options: 77 78 -check-index 79 If set, the variable is only acted upon if the server-side version's index 80 matches the provided value. When a variable specification contains 81 a modify index, that modify index is used as the check-index for the 82 check-and-set operation and can be overridden using this flag. 83 84 -force 85 Perform this operation regardless of the state or index of the variable 86 on the server-side. 87 88 -in (hcl | json) 89 Parser to use for data supplied via standard input or when the variable 90 specification's type can not be known using the file extension. Defaults 91 to "json". 92 93 -out (go-template | hcl | json | none | table) 94 Format to render created or updated variable. Defaults to "none" when 95 stdout is a terminal and "json" when the output is redirected. 96 97 -template 98 Template to render output with. Required when format is "go-template", 99 invalid for other formats. 100 101 -verbose 102 Provides additional information via standard error to preserve standard 103 output (stdout) for redirected output. 104 105 ` 106 return strings.TrimSpace(helpText) 107 } 108 109 func (c *VarPutCommand) AutocompleteFlags() complete.Flags { 110 return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), 111 complete.Flags{ 112 "-in": complete.PredictSet("hcl", "json"), 113 "-out": complete.PredictSet("none", "hcl", "json", "go-template", "table"), 114 }, 115 ) 116 } 117 118 func (c *VarPutCommand) AutocompleteArgs() complete.Predictor { 119 return VariablePathPredictor(c.Meta.Client) 120 } 121 122 func (c *VarPutCommand) Synopsis() string { 123 return "Create or update a variable" 124 } 125 126 func (c *VarPutCommand) Name() string { return "var put" } 127 128 func (c *VarPutCommand) Run(args []string) int { 129 var force, enforce, doVerbose bool 130 var path, checkIndexStr string 131 var checkIndex uint64 132 var err error 133 134 flags := c.Meta.FlagSet(c.Name(), FlagSetClient) 135 flags.Usage = func() { c.Ui.Output(c.Help()) } 136 137 flags.BoolVar(&force, "force", false, "") 138 flags.BoolVar(&doVerbose, "verbose", false, "") 139 flags.StringVar(&checkIndexStr, "check-index", "", "") 140 flags.StringVar(&c.inFmt, "in", "json", "") 141 flags.StringVar(&c.tmpl, "template", "", "") 142 143 if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 { 144 flags.StringVar(&c.outFmt, "out", "none", "") 145 } else { 146 flags.StringVar(&c.outFmt, "out", "json", "") 147 } 148 149 if err := flags.Parse(args); err != nil { 150 c.Ui.Error(commandErrorText(c)) 151 return 1 152 } 153 154 args = flags.Args() 155 156 // Manage verbose output 157 verbose := func(_ string) {} //no-op 158 if doVerbose { 159 verbose = func(msg string) { 160 c.Ui.Warn(msg) 161 } 162 } 163 c.verbose = verbose 164 165 // Parse the check-index 166 checkIndex, enforce, err = parseCheckIndex(checkIndexStr) 167 if err != nil { 168 c.Ui.Error(fmt.Sprintf("Error parsing check-index value %q: %v", checkIndexStr, err)) 169 return 1 170 } 171 172 if c.Meta.namespace == "*" { 173 c.Ui.Error(errWildcardNamespaceNotAllowed) 174 return 1 175 } 176 177 // Pull our fake stdin if needed 178 stdin := (io.Reader)(os.Stdin) 179 if c.testStdin != nil { 180 stdin = c.testStdin 181 } 182 183 switch { 184 case len(args) < 1: 185 c.Ui.Error(fmt.Sprintf("Not enough arguments (expected >1, got %d)", len(args))) 186 c.Ui.Error(commandErrorText(c)) 187 return 1 188 case len(args) == 1 && !isArgStdinRef(args[0]) && !isArgFileRef(args[0]): 189 c.Ui.Error("Must supply data") 190 c.Ui.Error(commandErrorText(c)) 191 return 1 192 } 193 194 if err = c.validateInputFlag(); err != nil { 195 c.Ui.Error(err.Error()) 196 c.Ui.Error(commandErrorText(c)) 197 return 1 198 } 199 200 if err := c.validateOutputFlag(); err != nil { 201 c.Ui.Error(err.Error()) 202 c.Ui.Error(commandErrorText(c)) 203 return 1 204 } 205 206 arg := args[0] 207 switch { 208 // Handle first argument: can be -, @file, «var path» 209 case isArgStdinRef(arg): 210 211 // read the specification into memory from stdin 212 stat, _ := os.Stdin.Stat() 213 if (stat.Mode() & os.ModeCharDevice) == 0 { 214 c.contents, err = io.ReadAll(os.Stdin) 215 if err != nil { 216 c.Ui.Error(fmt.Sprintf("Error reading from stdin: %s", err)) 217 return 1 218 } 219 } 220 verbose(fmt.Sprintf("Reading whole %s variable specification from stdin", strings.ToUpper(c.inFmt))) 221 222 case isArgFileRef(arg): 223 // ArgFileRefs start with "@" so we need to peel that off 224 // detect format based on file extension 225 specPath := arg[1:] 226 err = c.setParserForFileArg(specPath) 227 if err != nil { 228 c.Ui.Error(err.Error()) 229 return 1 230 } 231 verbose(fmt.Sprintf("Reading whole %s variable specification from %q", strings.ToUpper(c.inFmt), specPath)) 232 c.contents, err = os.ReadFile(specPath) 233 if err != nil { 234 c.Ui.Error(fmt.Sprintf("Error reading %q: %s", specPath, err)) 235 return 1 236 } 237 default: 238 path = sanitizePath(arg) 239 verbose(fmt.Sprintf("Writing to path %q", path)) 240 } 241 242 args = args[1:] 243 switch { 244 // Handle second argument: can be -, @file, or kv 245 case len(args) == 0: 246 // no-op 247 case isArgStdinRef(args[0]): 248 verbose(fmt.Sprintf("Creating variable %q using specification from stdin", path)) 249 stat, _ := os.Stdin.Stat() 250 if (stat.Mode() & os.ModeCharDevice) == 0 { 251 c.contents, err = io.ReadAll(os.Stdin) 252 if err != nil { 253 c.Ui.Error(fmt.Sprintf("Error reading from stdin: %s", err)) 254 return 1 255 } 256 } 257 args = args[1:] 258 259 case isArgFileRef(args[0]): 260 arg := args[0] 261 err = c.setParserForFileArg(arg) 262 if err != nil { 263 c.Ui.Error(err.Error()) 264 return 1 265 } 266 verbose(fmt.Sprintf("Creating variable %q from specification file %q", path, arg)) 267 fPath := arg[1:] 268 c.contents, err = os.ReadFile(fPath) 269 if err != nil { 270 c.Ui.Error(fmt.Sprintf("error reading %q: %s", fPath, err)) 271 return 1 272 } 273 args = args[1:] 274 default: 275 // no-op - should be KV arg 276 } 277 278 sv, err := c.makeVariable(path) 279 if err != nil { 280 c.Ui.Error(fmt.Sprintf("Failed to parse variable data: %s", err)) 281 return 1 282 } 283 284 var warnings *multierror.Error 285 if len(args) > 0 { 286 data, err := parseArgsData(stdin, args) 287 if err != nil { 288 c.Ui.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) 289 return 1 290 } 291 292 for k, v := range data { 293 vs := v.(string) 294 if vs == "" { 295 if _, ok := sv.Items[k]; ok { 296 verbose(fmt.Sprintf("Removed item %q", k)) 297 delete(sv.Items, k) 298 } else { 299 verbose(fmt.Sprintf("Item %q does not exist, continuing...", k)) 300 } 301 continue 302 } 303 if err := warnInvalidIdentifier(k); err != nil { 304 warnings = multierror.Append(warnings, err) 305 } 306 sv.Items[k] = vs 307 } 308 } 309 // Get the HTTP client 310 client, err := c.Meta.Client() 311 if err != nil { 312 c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) 313 return 1 314 } 315 316 if enforce { 317 sv.ModifyIndex = checkIndex 318 } 319 320 if force { 321 sv, _, err = client.Variables().Update(sv, nil) 322 } else { 323 sv, _, err = client.Variables().CheckedUpdate(sv, nil) 324 } 325 if err != nil { 326 if handled := handleCASError(err, c); handled { 327 return 1 328 } 329 c.Ui.Error(fmt.Sprintf("Error creating variable: %s", err)) 330 return 1 331 } 332 333 successMsg := fmt.Sprintf( 334 "Created variable %q with modify index %v", sv.Path, sv.ModifyIndex) 335 336 if warnings != nil { 337 c.Ui.Warn(c.FormatWarnings( 338 "Variable", 339 helper.MergeMultierrorWarnings(warnings), 340 )) 341 } 342 343 var out string 344 switch c.outFmt { 345 case "json": 346 out = sv.AsPrettyJSON() 347 case "hcl": 348 out = renderAsHCL(sv) 349 case "go-template": 350 if out, err = renderWithGoTemplate(sv, c.tmpl); err != nil { 351 c.Ui.Error(err.Error()) 352 return 1 353 } 354 case "table": 355 // the renderSVAsUiTable func writes directly to the ui and doesn't error. 356 verbose(successMsg) 357 renderSVAsUiTable(sv, c) 358 return 0 359 default: 360 c.Ui.Output(successMsg) 361 return 0 362 } 363 verbose(successMsg) 364 c.Ui.Output(out) 365 return 0 366 } 367 368 // makeVariable creates a variable based on whether or not there is data in 369 // content and the format is set. 370 func (c *VarPutCommand) makeVariable(path string) (*api.Variable, error) { 371 var err error 372 out := new(api.Variable) 373 if len(c.contents) == 0 { 374 out.Path = path 375 out.Namespace = c.Meta.namespace 376 out.Items = make(map[string]string) 377 return out, nil 378 } 379 switch c.inFmt { 380 case "json": 381 err = json.Unmarshal(c.contents, out) 382 if err != nil { 383 return nil, fmt.Errorf("error unmarshaling json: %w", err) 384 } 385 case "hcl": 386 out, err = parseVariableSpec(c.contents, c.verbose) 387 if err != nil { 388 return nil, fmt.Errorf("error parsing hcl: %w", err) 389 } 390 case "": 391 return nil, errors.New("format flag required") 392 default: 393 return nil, fmt.Errorf("unknown format flag value") 394 } 395 396 // Handle cases where values are provided by CLI flags that modify the 397 // the created variable. Typical of a "copy" operation, it is a convenience 398 // to reset the Create and Modify metadata to zero. 399 var resetIndex bool 400 401 // Step on the namespace in the object if one is provided by flag 402 if c.Meta.namespace != "" && c.Meta.namespace != out.Namespace { 403 out.Namespace = c.Meta.namespace 404 resetIndex = true 405 } 406 407 // Step on the path in the object if one is provided by argument. 408 if path != "" && path != out.Path { 409 out.Path = path 410 resetIndex = true 411 } 412 413 if resetIndex { 414 out.CreateIndex = 0 415 out.CreateTime = 0 416 out.ModifyIndex = 0 417 out.ModifyTime = 0 418 } 419 return out, nil 420 } 421 422 // parseVariableSpec is used to parse the variable specification 423 // from HCL 424 func parseVariableSpec(input []byte, verbose func(string)) (*api.Variable, error) { 425 root, err := hcl.ParseBytes(input) 426 if err != nil { 427 return nil, err 428 } 429 430 // Top-level item should be a list 431 list, ok := root.Node.(*ast.ObjectList) 432 if !ok { 433 return nil, fmt.Errorf("error parsing: root should be an object") 434 } 435 436 var out api.Variable 437 if err := parseVariableSpecImpl(&out, list); err != nil { 438 return nil, err 439 } 440 return &out, nil 441 } 442 443 // parseVariableSpecImpl parses the variable taking as input the AST tree 444 func parseVariableSpecImpl(result *api.Variable, list *ast.ObjectList) error { 445 // Decode the full thing into a map[string]interface for ease 446 var m map[string]interface{} 447 if err := hcl.DecodeObject(&m, list); err != nil { 448 return err 449 } 450 451 // Check for invalid keys 452 valid := []string{ 453 "namespace", 454 "path", 455 "create_index", 456 "modify_index", 457 "create_time", 458 "modify_time", 459 "items", 460 } 461 if err := helper.CheckHCLKeys(list, valid); err != nil { 462 return err 463 } 464 465 for _, index := range []string{"create_index", "modify_index"} { 466 if value, ok := m[index]; ok { 467 vInt, ok := value.(int) 468 if !ok { 469 return fmt.Errorf("%s must be integer; got (%T) %[2]v", index, value) 470 } 471 idx := uint64(vInt) 472 n := strings.ReplaceAll(strings.Title(strings.ReplaceAll(index, "_", " ")), " ", "") 473 m[n] = idx 474 delete(m, index) 475 } 476 } 477 478 for _, index := range []string{"create_time", "modify_time"} { 479 if value, ok := m[index]; ok { 480 vInt, ok := value.(int) 481 if !ok { 482 return fmt.Errorf("%s must be a int64; got a (%T) %[2]v", index, value) 483 } 484 n := strings.ReplaceAll(strings.Title(strings.ReplaceAll(index, "_", " ")), " ", "") 485 m[n] = vInt 486 delete(m, index) 487 } 488 } 489 490 // Decode the rest 491 if err := mapstructure.WeakDecode(m, result); err != nil { 492 return err 493 } 494 495 return nil 496 } 497 498 func isArgFileRef(a string) bool { 499 return strings.HasPrefix(a, "@") && !strings.HasPrefix(a, "\\@") 500 } 501 502 func isArgStdinRef(a string) bool { 503 return a == "-" 504 } 505 506 // sanitizePath removes any leading or trailing things from a "path". 507 func sanitizePath(s string) string { 508 return strings.Trim(strings.TrimSpace(s), "/") 509 } 510 511 // parseArgsData parses the given args in the format key=value into a map of 512 // the provided arguments. The given reader can also supply key=value pairs. 513 func parseArgsData(stdin io.Reader, args []string) (map[string]interface{}, error) { 514 builder := &KVBuilder{Stdin: stdin} 515 if err := builder.Add(args...); err != nil { 516 return nil, err 517 } 518 return builder.Map(), nil 519 } 520 521 func (c *VarPutCommand) GetConcurrentUI() cli.ConcurrentUi { 522 return cli.ConcurrentUi{Ui: c.Ui} 523 } 524 525 func (c *VarPutCommand) setParserForFileArg(arg string) error { 526 switch filepath.Ext(arg) { 527 case ".json": 528 c.inFmt = "json" 529 case ".hcl": 530 c.inFmt = "hcl" 531 default: 532 return fmt.Errorf("Unable to determine format of %s; Use the -in flag to specify it.", arg) 533 } 534 return nil 535 } 536 537 func (c *VarPutCommand) validateInputFlag() error { 538 switch c.inFmt { 539 case "hcl", "json": 540 return nil 541 default: 542 return errors.New(errInvalidInFormat) 543 } 544 } 545 546 func (c *VarPutCommand) validateOutputFlag() error { 547 if c.outFmt != "go-template" && c.tmpl != "" { 548 return errors.New(errUnexpectedTemplate) 549 } 550 switch c.outFmt { 551 case "none", "json", "hcl", "table": 552 return nil 553 case "go-template": 554 if c.tmpl == "" { 555 return errors.New(errMissingTemplate) 556 } 557 return nil 558 default: 559 return errors.New(errInvalidOutFormat) 560 } 561 } 562 563 func warnInvalidIdentifier(in string) error { 564 invalid := invalidIdentifier.FindAllString(in, -1) 565 if len(invalid) == 0 { 566 return nil 567 } 568 569 // Use %s instead of %q to avoid escaping characters. 570 return fmt.Errorf( 571 `"%s" contains characters %s that require the 'index' function for direct access in templates`, 572 in, 573 formatInvalidVarKeyChars(invalid), 574 ) 575 } 576 577 func formatInvalidVarKeyChars(invalid []string) string { 578 // Deduplicate characters 579 chars := set.From(invalid) 580 581 // Sort the characters for output 582 charList := make([]string, 0, chars.Size()) 583 for _, k := range chars.List() { 584 // Use %s instead of %q to avoid escaping characters. 585 charList = append(charList, fmt.Sprintf(`"%s"`, k)) 586 } 587 slices.Sort(charList) 588 589 // Build string 590 return fmt.Sprintf("[%s]", strings.Join(charList, ",")) 591 }