github.com/Ilhicas/nomad@v1.0.4-0.20210304152020-e86851182bc3/jobspec2/types.variables.go (about) 1 package jobspec2 2 3 // This file is copied verbatim from Packer: https://github.com/hashicorp/packer/blob/7a1680df97e028c4a75622effe08f6610d0ee5b4/hcl2template/types.variables.go 4 // with few changes. Packer references in comments are preserved to reduce the diff between files. 5 6 import ( 7 "fmt" 8 "strings" 9 "unicode" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/ext/typeexpr" 13 "github.com/hashicorp/hcl/v2/gohcl" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 "github.com/hashicorp/nomad/jobspec2/addrs" 16 "github.com/zclconf/go-cty/cty" 17 "github.com/zclconf/go-cty/cty/convert" 18 ) 19 20 // A consistent detail message for all "not a valid identifier" diagnostics. 21 const badIdentifierDetail = "A name must start with a letter or underscore and may contain only letters, digits, underscores, and dashes." 22 23 // Local represents a single entry from a "locals" block in a file. 24 // The "locals" block itself is not represented, because it serves only to 25 // provide context for us to interpret its contents. 26 type LocalBlock struct { 27 Name string 28 Expr hcl.Expression 29 } 30 31 // VariableAssignment represents a way a variable was set: the expression 32 // setting it and the value of that expression. It helps pinpoint were 33 // something was set in diagnostics. 34 type VariableAssignment struct { 35 // From tells were it was taken from, command/varfile/env/default 36 From string 37 Value cty.Value 38 Expr hcl.Expression 39 } 40 41 type Variable struct { 42 // Values contains possible values for the variable; The last value set 43 // from these will be the one used. If none is set; an error will be 44 // returned by Value(). 45 Values []VariableAssignment 46 47 // Validations contains all variables validation rules to be applied to the 48 // used value. Only the used value - the last value from Values - is 49 // validated. 50 Validations []*VariableValidation 51 52 // Cty Type of the variable. If the default value or a collected value is 53 // not of this type nor can be converted to this type an error diagnostic 54 // will show up. This allows us to assume that values are valid later in 55 // code. 56 // 57 // When a default value - and no type - is passed in the variable 58 // declaration, the type of the default variable will be used. This will 59 // allow to ensure that users set this variable correctly. 60 Type cty.Type 61 // Common name of the variable 62 Name string 63 // Description of the variable 64 Description string 65 66 Range hcl.Range 67 } 68 69 func (v *Variable) GoString() string { 70 b := &strings.Builder{} 71 fmt.Fprintf(b, "{type:%s", v.Type.GoString()) 72 for _, vv := range v.Values { 73 fmt.Fprintf(b, ",%s:%s", vv.From, vv.Value) 74 } 75 fmt.Fprintf(b, "}") 76 return b.String() 77 } 78 79 // validateValue ensures that all of the configured custom validations for a 80 // variable value are passing. 81 // 82 func (v *Variable) validateValue(val VariableAssignment) (diags hcl.Diagnostics) { 83 if len(v.Validations) == 0 { 84 return nil 85 } 86 87 hclCtx := &hcl.EvalContext{ 88 Variables: map[string]cty.Value{ 89 "var": cty.ObjectVal(map[string]cty.Value{ 90 v.Name: val.Value, 91 }), 92 }, 93 Functions: Functions("", false), 94 } 95 96 for _, validation := range v.Validations { 97 const errInvalidCondition = "Invalid variable validation result" 98 99 result, moreDiags := validation.Condition.Value(hclCtx) 100 diags = append(diags, moreDiags...) 101 if !result.IsKnown() { 102 continue // We'll wait until we've learned more, then. 103 } 104 if result.IsNull() { 105 diags = append(diags, &hcl.Diagnostic{ 106 Severity: hcl.DiagError, 107 Summary: errInvalidCondition, 108 Detail: "Validation condition expression must return either true or false, not null.", 109 Subject: validation.Condition.Range().Ptr(), 110 Expression: validation.Condition, 111 EvalContext: hclCtx, 112 }) 113 continue 114 } 115 var err error 116 result, err = convert.Convert(result, cty.Bool) 117 if err != nil { 118 diags = append(diags, &hcl.Diagnostic{ 119 Severity: hcl.DiagError, 120 Summary: errInvalidCondition, 121 Detail: fmt.Sprintf("Invalid validation condition result value: %s.", err), 122 Subject: validation.Condition.Range().Ptr(), 123 Expression: validation.Condition, 124 EvalContext: hclCtx, 125 }) 126 continue 127 } 128 129 if result.False() { 130 subj := validation.DeclRange.Ptr() 131 if val.Expr != nil { 132 subj = val.Expr.Range().Ptr() 133 } 134 diags = append(diags, &hcl.Diagnostic{ 135 Severity: hcl.DiagError, 136 Summary: fmt.Sprintf("Invalid value for %s variable", val.From), 137 Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", validation.ErrorMessage, validation.DeclRange.String()), 138 Subject: subj, 139 }) 140 } 141 } 142 143 return diags 144 } 145 146 // Value returns the last found value from the list of variable settings. 147 func (v *Variable) Value() (cty.Value, hcl.Diagnostics) { 148 if len(v.Values) == 0 { 149 return cty.UnknownVal(v.Type), hcl.Diagnostics{&hcl.Diagnostic{ 150 Severity: hcl.DiagError, 151 Summary: fmt.Sprintf("Unset variable %q", v.Name), 152 Detail: "A used variable must be set or have a default value; see " + 153 "https://packer.io/docs/configuration/from-1.5/syntax for " + 154 "details.", 155 Context: v.Range.Ptr(), 156 }} 157 } 158 val := v.Values[len(v.Values)-1] 159 return val.Value, v.validateValue(v.Values[len(v.Values)-1]) 160 } 161 162 type Variables map[string]*Variable 163 164 func (variables Variables) Keys() []string { 165 keys := make([]string, 0, len(variables)) 166 for key := range variables { 167 keys = append(keys, key) 168 } 169 return keys 170 } 171 172 func (variables Variables) Values() (map[string]cty.Value, hcl.Diagnostics) { 173 res := map[string]cty.Value{} 174 var diags hcl.Diagnostics 175 for k, v := range variables { 176 value, moreDiags := v.Value() 177 diags = append(diags, moreDiags...) 178 res[k] = value 179 } 180 return res, diags 181 } 182 183 // decodeVariable decodes a variable key and value into Variables 184 func (variables *Variables) decodeVariable(key string, attr *hcl.Attribute, ectx *hcl.EvalContext) hcl.Diagnostics { 185 var diags hcl.Diagnostics 186 187 if (*variables) == nil { 188 (*variables) = Variables{} 189 } 190 191 if _, found := (*variables)[key]; found { 192 diags = append(diags, &hcl.Diagnostic{ 193 Severity: hcl.DiagError, 194 Summary: "Duplicate variable", 195 Detail: "Duplicate " + key + " variable definition found.", 196 Subject: attr.NameRange.Ptr(), 197 }) 198 return diags 199 } 200 201 value, moreDiags := attr.Expr.Value(ectx) 202 diags = append(diags, moreDiags...) 203 if moreDiags.HasErrors() { 204 return diags 205 } 206 207 (*variables)[key] = &Variable{ 208 Name: key, 209 Values: []VariableAssignment{{ 210 From: "default", 211 Value: value, 212 Expr: attr.Expr, 213 }}, 214 Type: value.Type(), 215 Range: attr.Range, 216 } 217 218 return diags 219 } 220 221 var variableBlockSchema = &hcl.BodySchema{ 222 Attributes: []hcl.AttributeSchema{ 223 { 224 Name: "description", 225 }, 226 { 227 Name: "default", 228 }, 229 { 230 Name: "type", 231 }, 232 }, 233 Blocks: []hcl.BlockHeaderSchema{ 234 { 235 Type: "validation", 236 }, 237 }, 238 } 239 240 // decodeVariableBlock decodes a "variables" section the way packer 1 used to 241 func (variables *Variables) decodeVariableBlock(block *hcl.Block, ectx *hcl.EvalContext) hcl.Diagnostics { 242 if (*variables) == nil { 243 (*variables) = Variables{} 244 } 245 246 if _, found := (*variables)[block.Labels[0]]; found { 247 248 return []*hcl.Diagnostic{{ 249 Severity: hcl.DiagError, 250 Summary: "Duplicate variable", 251 Detail: "Duplicate " + block.Labels[0] + " variable definition found.", 252 Context: block.DefRange.Ptr(), 253 }} 254 } 255 256 name := block.Labels[0] 257 258 content, diags := block.Body.Content(variableBlockSchema) 259 if !hclsyntax.ValidIdentifier(name) { 260 diags = append(diags, &hcl.Diagnostic{ 261 Severity: hcl.DiagError, 262 Summary: "Invalid variable name", 263 Detail: badIdentifierDetail, 264 Subject: &block.LabelRanges[0], 265 }) 266 } 267 268 v := &Variable{ 269 Name: name, 270 Range: block.DefRange, 271 } 272 273 if attr, exists := content.Attributes["description"]; exists { 274 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) 275 diags = append(diags, valDiags...) 276 } 277 278 if t, ok := content.Attributes["type"]; ok { 279 tp, moreDiags := typeexpr.Type(t.Expr) 280 diags = append(diags, moreDiags...) 281 if moreDiags.HasErrors() { 282 return diags 283 } 284 285 v.Type = tp 286 } 287 288 if def, ok := content.Attributes["default"]; ok { 289 defaultValue, moreDiags := def.Expr.Value(ectx) 290 diags = append(diags, moreDiags...) 291 if moreDiags.HasErrors() { 292 return diags 293 } 294 295 if v.Type != cty.NilType { 296 var err error 297 defaultValue, err = convert.Convert(defaultValue, v.Type) 298 if err != nil { 299 diags = append(diags, &hcl.Diagnostic{ 300 Severity: hcl.DiagError, 301 Summary: "Invalid default value for variable", 302 Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), 303 Subject: def.Expr.Range().Ptr(), 304 }) 305 defaultValue = cty.DynamicVal 306 } 307 } 308 309 v.Values = append(v.Values, VariableAssignment{ 310 From: "default", 311 Value: defaultValue, 312 Expr: def.Expr, 313 }) 314 315 // It's possible no type attribute was assigned so lets make sure we 316 // have a valid type otherwise there could be issues parsing the value. 317 if v.Type == cty.NilType { 318 v.Type = defaultValue.Type() 319 } 320 } 321 322 for _, block := range content.Blocks { 323 switch block.Type { 324 case "validation": 325 vv, moreDiags := decodeVariableValidationBlock(v.Name, block) 326 diags = append(diags, moreDiags...) 327 v.Validations = append(v.Validations, vv) 328 } 329 } 330 331 (*variables)[name] = v 332 333 return diags 334 } 335 336 var variableValidationBlockSchema = &hcl.BodySchema{ 337 Attributes: []hcl.AttributeSchema{ 338 { 339 Name: "condition", 340 Required: true, 341 }, 342 { 343 Name: "error_message", 344 Required: true, 345 }, 346 }, 347 } 348 349 // VariableValidation represents a configuration-defined validation rule 350 // for a particular input variable, given as a "validation" block inside 351 // a "variable" block. 352 type VariableValidation struct { 353 // Condition is an expression that refers to the variable being tested and 354 // contains no other references. The expression must return true to 355 // indicate that the value is valid or false to indicate that it is 356 // invalid. If the expression produces an error, that's considered a bug in 357 // the block defining the validation rule, not an error in the caller. 358 Condition hcl.Expression 359 360 // ErrorMessage is one or more full sentences, which _should_ be in English 361 // for consistency with the rest of the error message output but can in 362 // practice be in any language as long as it ends with a period. The 363 // message should describe what is required for the condition to return 364 // true in a way that would make sense to a caller of the module. 365 ErrorMessage string 366 367 DeclRange hcl.Range 368 } 369 370 func decodeVariableValidationBlock(varName string, block *hcl.Block) (*VariableValidation, hcl.Diagnostics) { 371 var diags hcl.Diagnostics 372 vv := &VariableValidation{ 373 DeclRange: block.DefRange, 374 } 375 376 content, moreDiags := block.Body.Content(variableValidationBlockSchema) 377 diags = append(diags, moreDiags...) 378 379 if attr, exists := content.Attributes["condition"]; exists { 380 vv.Condition = attr.Expr 381 382 // The validation condition must refer to the variable itself and 383 // nothing else; to ensure that the variable declaration can't create 384 // additional edges in the dependency graph. 385 goodRefs := 0 386 for _, traversal := range vv.Condition.Variables() { 387 388 ref, moreDiags := addrs.ParseRef(traversal) 389 if !moreDiags.HasErrors() { 390 if addr, ok := ref.Subject.(addrs.InputVariable); ok { 391 if addr.Name == varName { 392 goodRefs++ 393 continue // Reference is valid 394 } 395 } 396 } 397 398 // If we fall out here then the reference is invalid. 399 diags = diags.Append(&hcl.Diagnostic{ 400 Severity: hcl.DiagError, 401 Summary: "Invalid reference in variable validation", 402 Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName), 403 Subject: traversal.SourceRange().Ptr(), 404 }) 405 } 406 if goodRefs < 1 { 407 diags = diags.Append(&hcl.Diagnostic{ 408 Severity: hcl.DiagError, 409 Summary: "Invalid variable validation condition", 410 Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), 411 Subject: attr.Expr.Range().Ptr(), 412 }) 413 } 414 } 415 416 if attr, exists := content.Attributes["error_message"]; exists { 417 moreDiags := gohcl.DecodeExpression(attr.Expr, nil, &vv.ErrorMessage) 418 diags = append(diags, moreDiags...) 419 if !moreDiags.HasErrors() { 420 const errSummary = "Invalid validation error message" 421 switch { 422 case vv.ErrorMessage == "": 423 diags = diags.Append(&hcl.Diagnostic{ 424 Severity: hcl.DiagError, 425 Summary: errSummary, 426 Detail: "An empty string is not a valid nor useful error message.", 427 Subject: attr.Expr.Range().Ptr(), 428 }) 429 case !looksLikeSentences(vv.ErrorMessage): 430 // Because we're going to include this string verbatim as part 431 // of a bigger error message written in our usual style, we'll 432 // require the given error message to conform to that. We might 433 // relax this in future if e.g. we start presenting these error 434 // messages in a different way, or if Packer starts supporting 435 // producing error messages in other human languages, etc. For 436 // pragmatism we also allow sentences ending with exclamation 437 // points, but we don't mention it explicitly here because 438 // that's not really consistent with the Packer UI writing 439 // style. 440 diags = diags.Append(&hcl.Diagnostic{ 441 Severity: hcl.DiagError, 442 Summary: errSummary, 443 Detail: "Validation error message must be at least one full sentence starting with an uppercase letter ( if the alphabet permits it ) and ending with a period or question mark.", 444 Subject: attr.Expr.Range().Ptr(), 445 }) 446 } 447 } 448 } 449 450 return vv, diags 451 } 452 453 // looksLikeSentence is a simple heuristic that encourages writing error 454 // messages that will be presentable when included as part of a larger error 455 // diagnostic whose other text is written in the UI writing style. 456 // 457 // This is intentionally not a very strong validation since we're assuming that 458 // authors want to write good messages and might just need a nudge about 459 // Packer's specific style, rather than that they are going to try to work 460 // around these rules to write a lower-quality message. 461 func looksLikeSentences(s string) bool { 462 if len(s) < 1 { 463 return false 464 } 465 runes := []rune(s) // HCL guarantees that all strings are valid UTF-8 466 first := runes[0] 467 last := runes[len(runes)-1] 468 469 // If the first rune is a letter then it must be an uppercase letter. To 470 // sorts of nudge people into writing sentences. For alphabets that don't 471 // have the notion of 'upper', this does nothing. 472 if unicode.IsLetter(first) && !unicode.IsUpper(first) { 473 return false 474 } 475 476 // The string must be at least one full sentence, which implies having 477 // sentence-ending punctuation. 478 return last == '.' || last == '?' || last == '!' 479 } 480 481 // Prefix your environment variables with VarEnvPrefix so that Packer can see 482 // them. 483 const VarEnvPrefix = "NOMAD_VAR_" 484 485 func (c *jobConfig) collectInputVariableValues(env []string, files []*hcl.File, argv map[string]string) hcl.Diagnostics { 486 var diags hcl.Diagnostics 487 variables := c.InputVariables 488 489 for _, raw := range env { 490 if !strings.HasPrefix(raw, VarEnvPrefix) { 491 continue 492 } 493 raw = raw[len(VarEnvPrefix):] // trim the prefix 494 495 eq := strings.Index(raw, "=") 496 if eq == -1 { 497 // Seems invalid, so we'll ignore it. 498 continue 499 } 500 501 name := raw[:eq] 502 value := raw[eq+1:] 503 504 variable, found := variables[name] 505 if !found { 506 // this variable was not defined in the hcl files, let's skip it ! 507 continue 508 } 509 510 fakeFilename := fmt.Sprintf("<value for var.%s from env>", name) 511 expr, moreDiags := expressionFromVariableDefinition(fakeFilename, value, variable.Type) 512 diags = append(diags, moreDiags...) 513 if moreDiags.HasErrors() { 514 continue 515 } 516 517 val, valDiags := expr.Value(nil) 518 diags = append(diags, valDiags...) 519 if variable.Type != cty.NilType { 520 var err error 521 val, err = convert.Convert(val, variable.Type) 522 if err != nil { 523 diags = append(diags, &hcl.Diagnostic{ 524 Severity: hcl.DiagError, 525 Summary: "Invalid value for variable", 526 Detail: fmt.Sprintf("The value for %s is not compatible with the variable's type constraint: %s.", name, err), 527 Subject: expr.Range().Ptr(), 528 }) 529 val = cty.DynamicVal 530 } 531 } 532 variable.Values = append(variable.Values, VariableAssignment{ 533 From: "env", 534 Value: val, 535 Expr: expr, 536 }) 537 } 538 539 // files will contain files found in the folder then files passed as 540 // arguments. 541 for _, file := range files { 542 // Before we do our real decode, we'll probe to see if there are any 543 // blocks of type "variable" in this body, since it's a common mistake 544 // for new users to put variable declarations in pkrvars rather than 545 // variable value definitions, and otherwise our error message for that 546 // case is not so helpful. 547 { 548 content, _, _ := file.Body.PartialContent(&hcl.BodySchema{ 549 Blocks: []hcl.BlockHeaderSchema{ 550 { 551 Type: "variable", 552 LabelNames: []string{"name"}, 553 }, 554 }, 555 }) 556 for _, block := range content.Blocks { 557 name := block.Labels[0] 558 diags = append(diags, &hcl.Diagnostic{ 559 Severity: hcl.DiagError, 560 Summary: "Variable declaration in a .var file", 561 Detail: fmt.Sprintf("A .var file is used to assign "+ 562 "values to variables that have already been declared "+ 563 "in job files, not to declare new variables. To "+ 564 "declare variable %q, place this block in one of your"+ 565 " job files\n\nTo set a "+ 566 "value for this variable in %s, use the definition "+ 567 "syntax instead:\n %s = <value>", 568 name, block.TypeRange.Filename, name), 569 Subject: &block.TypeRange, 570 }) 571 } 572 if diags.HasErrors() { 573 // If we already found problems then JustAttributes below will find 574 // the same problems with less-helpful messages, so we'll bail for 575 // now to let the user focus on the immediate problem. 576 return diags 577 } 578 } 579 580 attrs, moreDiags := file.Body.JustAttributes() 581 diags = append(diags, moreDiags...) 582 583 for name, attr := range attrs { 584 variable, found := variables[name] 585 if !found { 586 sev := hcl.DiagWarning 587 if c.ParseConfig.Strict { 588 sev = hcl.DiagError 589 } 590 diags = append(diags, &hcl.Diagnostic{ 591 Severity: sev, 592 Summary: "Undefined variable", 593 Detail: fmt.Sprintf("A %q variable was set but was "+ 594 "not found in known variables. To declare "+ 595 "variable %q, place this block in your "+ 596 "job files", 597 name, name), 598 Context: attr.Range.Ptr(), 599 }) 600 continue 601 } 602 603 val, moreDiags := attr.Expr.Value(nil) 604 diags = append(diags, moreDiags...) 605 606 if variable.Type != cty.NilType { 607 var err error 608 val, err = convert.Convert(val, variable.Type) 609 if err != nil { 610 diags = append(diags, &hcl.Diagnostic{ 611 Severity: hcl.DiagError, 612 Summary: "Invalid value for variable", 613 Detail: fmt.Sprintf("The value for %s is not compatible with the variable's type constraint: %s.", name, err), 614 Subject: attr.Expr.Range().Ptr(), 615 }) 616 val = cty.DynamicVal 617 } 618 } 619 620 variable.Values = append(variable.Values, VariableAssignment{ 621 From: "varfile", 622 Value: val, 623 Expr: attr.Expr, 624 }) 625 } 626 } 627 628 // Finally we process values given explicitly on the command line. 629 for name, value := range argv { 630 variable, found := variables[name] 631 if !found { 632 diags = append(diags, &hcl.Diagnostic{ 633 Severity: hcl.DiagError, 634 Summary: "Undefined -var variable", 635 Detail: fmt.Sprintf("A %q variable was passed in the command "+ 636 "line but was not found in known variables. "+ 637 "To declare variable %q, place this block in your"+ 638 " job file", 639 name, name), 640 }) 641 continue 642 } 643 644 fakeFilename := fmt.Sprintf("<value for var.%s from arguments>", name) 645 expr, moreDiags := expressionFromVariableDefinition(fakeFilename, value, variable.Type) 646 diags = append(diags, moreDiags...) 647 if moreDiags.HasErrors() { 648 continue 649 } 650 651 val, valDiags := expr.Value(nil) 652 diags = append(diags, valDiags...) 653 654 if variable.Type != cty.NilType { 655 var err error 656 val, err = convert.Convert(val, variable.Type) 657 if err != nil { 658 diags = append(diags, &hcl.Diagnostic{ 659 Severity: hcl.DiagError, 660 Summary: "Invalid argument value for -var variable", 661 Detail: fmt.Sprintf("The received arg value for %s is not compatible with the variable's type constraint: %s.", name, err), 662 Subject: expr.Range().Ptr(), 663 }) 664 val = cty.DynamicVal 665 } 666 } 667 668 variable.Values = append(variable.Values, VariableAssignment{ 669 From: "cmd", 670 Value: val, 671 Expr: expr, 672 }) 673 } 674 675 return diags 676 } 677 678 // expressionFromVariableDefinition creates an hclsyntax.Expression that is capable of evaluating the specified value for a given cty.Type. 679 // The specified filename is to identify the source of where value originated from in the diagnostics report, if there is an error. 680 func expressionFromVariableDefinition(filename string, value string, variableType cty.Type) (hclsyntax.Expression, hcl.Diagnostics) { 681 switch variableType { 682 case cty.String, cty.Number, cty.NilType: 683 // when the type is nil (not set in a variable block) we default to 684 // interpreting everything as a string literal. 685 return &hclsyntax.LiteralValueExpr{Val: cty.StringVal(value)}, nil 686 default: 687 return hclsyntax.ParseExpression([]byte(value), filename, hcl.Pos{Line: 1, Column: 1}) 688 } 689 }