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