github.com/kevinklinger/open_terraform@v1.3.6/noninternal/terraform/eval_variable_test.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/kevinklinger/open_terraform/noninternal/addrs" 12 "github.com/kevinklinger/open_terraform/noninternal/lang" 13 "github.com/kevinklinger/open_terraform/noninternal/lang/marks" 14 "github.com/kevinklinger/open_terraform/noninternal/tfdiags" 15 ) 16 17 func TestPrepareFinalInputVariableValue(t *testing.T) { 18 // This is just a concise way to define a bunch of *configs.Variable 19 // objects to use in our tests below. We're only going to decode this 20 // config, not fully evaluate it. 21 cfgSrc := ` 22 variable "nullable_required" { 23 } 24 variable "nullable_optional_default_string" { 25 default = "hello" 26 } 27 variable "nullable_optional_default_null" { 28 default = null 29 } 30 variable "constrained_string_nullable_required" { 31 type = string 32 } 33 variable "constrained_string_nullable_optional_default_string" { 34 type = string 35 default = "hello" 36 } 37 variable "constrained_string_nullable_optional_default_bool" { 38 type = string 39 default = true 40 } 41 variable "constrained_string_nullable_optional_default_null" { 42 type = string 43 default = null 44 } 45 variable "required" { 46 nullable = false 47 } 48 variable "optional_default_string" { 49 nullable = false 50 default = "hello" 51 } 52 variable "constrained_string_required" { 53 nullable = false 54 type = string 55 } 56 variable "constrained_string_optional_default_string" { 57 nullable = false 58 type = string 59 default = "hello" 60 } 61 variable "constrained_string_optional_default_bool" { 62 nullable = false 63 type = string 64 default = true 65 } 66 variable "constrained_string_sensitive_required" { 67 sensitive = true 68 nullable = false 69 type = string 70 } 71 variable "complex_type_with_nested_default_optional" { 72 type = set(object({ 73 name = string 74 schedules = set(object({ 75 name = string 76 cold_storage_after = optional(number, 10) 77 })) 78 })) 79 } 80 variable "complex_type_with_nested_complex_types" { 81 type = object({ 82 name = string 83 nested_object = object({ 84 name = string 85 value = optional(string, "foo") 86 }) 87 nested_object_with_default = optional(object({ 88 name = string 89 value = optional(string, "bar") 90 }), { 91 name = "nested_object_with_default" 92 }) 93 }) 94 } 95 // https://github.com/hashicorp/terraform/issues/32152 96 // This variable was originally added to test that optional attribute 97 // metadata is stripped from empty default collections. Essentially, you 98 // should be able to mix and match custom and default values for the 99 // optional_list attribute. 100 variable "complex_type_with_empty_default_and_nested_optional" { 101 type = list(object({ 102 name = string 103 optional_list = optional(list(object({ 104 string = string 105 optional_string = optional(string) 106 })), []) 107 })) 108 } 109 // https://github.com/hashicorp/terraform/issues/32160#issuecomment-1302783910 110 // These variables were added to test the specific use case from this 111 // GitHub comment. 112 variable "empty_object_with_optional_nested_object_with_optional_bool" { 113 type = object({ 114 thing = optional(object({ 115 flag = optional(bool, false) 116 })) 117 }) 118 default = {} 119 } 120 variable "populated_object_with_optional_nested_object_with_optional_bool" { 121 type = object({ 122 thing = optional(object({ 123 flag = optional(bool, false) 124 })) 125 }) 126 default = { 127 thing = {} 128 } 129 } 130 variable "empty_object_with_default_nested_object_with_optional_bool" { 131 type = object({ 132 thing = optional(object({ 133 flag = optional(bool, false) 134 }), {}) 135 }) 136 default = {} 137 } 138 // https://github.com/hashicorp/terraform/issues/32160 139 // This variable was originally added to test that optional objects do 140 // get created containing only their defaults. Instead they should be 141 // left empty. We do not expect nested_object to be created just because 142 // optional_string has a default value. 143 variable "object_with_nested_object_with_required_and_optional_attributes" { 144 type = object({ 145 nested_object = optional(object({ 146 string = string 147 optional_string = optional(string, "optional") 148 })) 149 }) 150 } 151 // https://github.com/hashicorp/terraform/issues/32157 152 // Similar to above, we want to see that merging combinations of the 153 // nested_object into a single collection doesn't crash because of 154 // inconsistent elements. 155 variable "list_with_nested_object_with_required_and_optional_attributes" { 156 type = list(object({ 157 nested_object = optional(object({ 158 string = string 159 optional_string = optional(string, "optional") 160 })) 161 })) 162 } 163 // https://github.com/hashicorp/terraform/issues/32109 164 // This variable was originally introduced to test the behaviour of 165 // the dynamic type constraint. You should be able to use the 'any' 166 // constraint and introduce empty, null, and populated values into the 167 // list. 168 variable "list_with_nested_list_of_any" { 169 type = list(object({ 170 a = string 171 b = optional(list(any)) 172 })) 173 default = [ 174 { 175 a = "a" 176 }, 177 { 178 a = "b" 179 b = [1] 180 } 181 ] 182 } 183 ` 184 cfg := testModuleInline(t, map[string]string{ 185 "main.tf": cfgSrc, 186 }) 187 variableConfigs := cfg.Module.Variables 188 189 // Because we loaded our pseudo-module from a temporary file, the 190 // declaration source ranges will have unpredictable filenames. We'll 191 // fix that here just to make things easier below. 192 for _, vc := range variableConfigs { 193 vc.DeclRange.Filename = "main.tf" 194 } 195 196 tests := []struct { 197 varName string 198 given cty.Value 199 want cty.Value 200 wantErr string 201 }{ 202 // nullable_required 203 { 204 "nullable_required", 205 cty.NilVal, 206 cty.UnknownVal(cty.DynamicPseudoType), 207 `Required variable not set: The variable "nullable_required" is required, but is not set.`, 208 }, 209 { 210 "nullable_required", 211 cty.NullVal(cty.DynamicPseudoType), 212 cty.NullVal(cty.DynamicPseudoType), 213 ``, // "required" for a nullable variable means only that it must be set, even if it's set to null 214 }, 215 { 216 "nullable_required", 217 cty.StringVal("ahoy"), 218 cty.StringVal("ahoy"), 219 ``, 220 }, 221 { 222 "nullable_required", 223 cty.UnknownVal(cty.String), 224 cty.UnknownVal(cty.String), 225 ``, 226 }, 227 228 // nullable_optional_default_string 229 { 230 "nullable_optional_default_string", 231 cty.NilVal, 232 cty.StringVal("hello"), // the declared default value 233 ``, 234 }, 235 { 236 "nullable_optional_default_string", 237 cty.NullVal(cty.DynamicPseudoType), 238 cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default 239 ``, 240 }, 241 { 242 "nullable_optional_default_string", 243 cty.StringVal("ahoy"), 244 cty.StringVal("ahoy"), 245 ``, 246 }, 247 { 248 "nullable_optional_default_string", 249 cty.UnknownVal(cty.String), 250 cty.UnknownVal(cty.String), 251 ``, 252 }, 253 254 // nullable_optional_default_null 255 { 256 "nullable_optional_default_null", 257 cty.NilVal, 258 cty.NullVal(cty.DynamicPseudoType), // the declared default value 259 ``, 260 }, 261 { 262 "nullable_optional_default_null", 263 cty.NullVal(cty.String), 264 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 265 ``, 266 }, 267 { 268 "nullable_optional_default_null", 269 cty.StringVal("ahoy"), 270 cty.StringVal("ahoy"), 271 ``, 272 }, 273 { 274 "nullable_optional_default_null", 275 cty.UnknownVal(cty.String), 276 cty.UnknownVal(cty.String), 277 ``, 278 }, 279 280 // constrained_string_nullable_required 281 { 282 "constrained_string_nullable_required", 283 cty.NilVal, 284 cty.UnknownVal(cty.String), 285 `Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`, 286 }, 287 { 288 "constrained_string_nullable_required", 289 cty.NullVal(cty.DynamicPseudoType), 290 cty.NullVal(cty.String), // the null value still gets converted to match the type constraint 291 ``, // "required" for a nullable variable means only that it must be set, even if it's set to null 292 }, 293 { 294 "constrained_string_nullable_required", 295 cty.StringVal("ahoy"), 296 cty.StringVal("ahoy"), 297 ``, 298 }, 299 { 300 "constrained_string_nullable_required", 301 cty.UnknownVal(cty.String), 302 cty.UnknownVal(cty.String), 303 ``, 304 }, 305 306 // constrained_string_nullable_optional_default_string 307 { 308 "constrained_string_nullable_optional_default_string", 309 cty.NilVal, 310 cty.StringVal("hello"), // the declared default value 311 ``, 312 }, 313 { 314 "constrained_string_nullable_optional_default_string", 315 cty.NullVal(cty.DynamicPseudoType), 316 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 317 ``, 318 }, 319 { 320 "constrained_string_nullable_optional_default_string", 321 cty.StringVal("ahoy"), 322 cty.StringVal("ahoy"), 323 ``, 324 }, 325 { 326 "constrained_string_nullable_optional_default_string", 327 cty.UnknownVal(cty.String), 328 cty.UnknownVal(cty.String), 329 ``, 330 }, 331 332 // constrained_string_nullable_optional_default_bool 333 { 334 "constrained_string_nullable_optional_default_bool", 335 cty.NilVal, 336 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 337 ``, 338 }, 339 { 340 "constrained_string_nullable_optional_default_bool", 341 cty.NullVal(cty.DynamicPseudoType), 342 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 343 ``, 344 }, 345 { 346 "constrained_string_nullable_optional_default_bool", 347 cty.StringVal("ahoy"), 348 cty.StringVal("ahoy"), 349 ``, 350 }, 351 { 352 "constrained_string_nullable_optional_default_bool", 353 cty.UnknownVal(cty.String), 354 cty.UnknownVal(cty.String), 355 ``, 356 }, 357 358 // constrained_string_nullable_optional_default_null 359 { 360 "constrained_string_nullable_optional_default_null", 361 cty.NilVal, 362 cty.NullVal(cty.String), 363 ``, 364 }, 365 { 366 "constrained_string_nullable_optional_default_null", 367 cty.NullVal(cty.DynamicPseudoType), 368 cty.NullVal(cty.String), 369 ``, 370 }, 371 { 372 "constrained_string_nullable_optional_default_null", 373 cty.StringVal("ahoy"), 374 cty.StringVal("ahoy"), 375 ``, 376 }, 377 { 378 "constrained_string_nullable_optional_default_null", 379 cty.UnknownVal(cty.String), 380 cty.UnknownVal(cty.String), 381 ``, 382 }, 383 384 // required 385 { 386 "required", 387 cty.NilVal, 388 cty.UnknownVal(cty.DynamicPseudoType), 389 `Required variable not set: The variable "required" is required, but is not set.`, 390 }, 391 { 392 "required", 393 cty.NullVal(cty.DynamicPseudoType), 394 cty.UnknownVal(cty.DynamicPseudoType), 395 `Required variable not set: Unsuitable value for var.required set from outside of the configuration: required variable may not be set to null.`, 396 }, 397 { 398 "required", 399 cty.StringVal("ahoy"), 400 cty.StringVal("ahoy"), 401 ``, 402 }, 403 { 404 "required", 405 cty.UnknownVal(cty.String), 406 cty.UnknownVal(cty.String), 407 ``, 408 }, 409 410 // optional_default_string 411 { 412 "optional_default_string", 413 cty.NilVal, 414 cty.StringVal("hello"), // the declared default value 415 ``, 416 }, 417 { 418 "optional_default_string", 419 cty.NullVal(cty.DynamicPseudoType), 420 cty.StringVal("hello"), // the declared default value 421 ``, 422 }, 423 { 424 "optional_default_string", 425 cty.StringVal("ahoy"), 426 cty.StringVal("ahoy"), 427 ``, 428 }, 429 { 430 "optional_default_string", 431 cty.UnknownVal(cty.String), 432 cty.UnknownVal(cty.String), 433 ``, 434 }, 435 436 // constrained_string_required 437 { 438 "constrained_string_required", 439 cty.NilVal, 440 cty.UnknownVal(cty.String), 441 `Required variable not set: The variable "constrained_string_required" is required, but is not set.`, 442 }, 443 { 444 "constrained_string_required", 445 cty.NullVal(cty.DynamicPseudoType), 446 cty.UnknownVal(cty.String), 447 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 448 }, 449 { 450 "constrained_string_required", 451 cty.StringVal("ahoy"), 452 cty.StringVal("ahoy"), 453 ``, 454 }, 455 { 456 "constrained_string_required", 457 cty.UnknownVal(cty.String), 458 cty.UnknownVal(cty.String), 459 ``, 460 }, 461 462 // constrained_string_optional_default_string 463 { 464 "constrained_string_optional_default_string", 465 cty.NilVal, 466 cty.StringVal("hello"), // the declared default value 467 ``, 468 }, 469 { 470 "constrained_string_optional_default_string", 471 cty.NullVal(cty.DynamicPseudoType), 472 cty.StringVal("hello"), // the declared default value 473 ``, 474 }, 475 { 476 "constrained_string_optional_default_string", 477 cty.StringVal("ahoy"), 478 cty.StringVal("ahoy"), 479 ``, 480 }, 481 { 482 "constrained_string_optional_default_string", 483 cty.UnknownVal(cty.String), 484 cty.UnknownVal(cty.String), 485 ``, 486 }, 487 488 // constrained_string_optional_default_bool 489 { 490 "constrained_string_optional_default_bool", 491 cty.NilVal, 492 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 493 ``, 494 }, 495 { 496 "constrained_string_optional_default_bool", 497 cty.NullVal(cty.DynamicPseudoType), 498 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 499 ``, 500 }, 501 { 502 "constrained_string_optional_default_bool", 503 cty.StringVal("ahoy"), 504 cty.StringVal("ahoy"), 505 ``, 506 }, 507 { 508 "constrained_string_optional_default_bool", 509 cty.UnknownVal(cty.String), 510 cty.UnknownVal(cty.String), 511 ``, 512 }, 513 514 // complex types 515 516 { 517 "complex_type_with_nested_default_optional", 518 cty.SetVal([]cty.Value{ 519 cty.ObjectVal(map[string]cty.Value{ 520 "name": cty.StringVal("test1"), 521 "schedules": cty.SetVal([]cty.Value{ 522 cty.MapVal(map[string]cty.Value{ 523 "name": cty.StringVal("daily"), 524 }), 525 }), 526 }), 527 cty.ObjectVal(map[string]cty.Value{ 528 "name": cty.StringVal("test2"), 529 "schedules": cty.SetVal([]cty.Value{ 530 cty.MapVal(map[string]cty.Value{ 531 "name": cty.StringVal("daily"), 532 }), 533 cty.MapVal(map[string]cty.Value{ 534 "name": cty.StringVal("weekly"), 535 "cold_storage_after": cty.StringVal("0"), 536 }), 537 }), 538 }), 539 }), 540 cty.SetVal([]cty.Value{ 541 cty.ObjectVal(map[string]cty.Value{ 542 "name": cty.StringVal("test1"), 543 "schedules": cty.SetVal([]cty.Value{ 544 cty.ObjectVal(map[string]cty.Value{ 545 "name": cty.StringVal("daily"), 546 "cold_storage_after": cty.NumberIntVal(10), 547 }), 548 }), 549 }), 550 cty.ObjectVal(map[string]cty.Value{ 551 "name": cty.StringVal("test2"), 552 "schedules": cty.SetVal([]cty.Value{ 553 cty.ObjectVal(map[string]cty.Value{ 554 "name": cty.StringVal("daily"), 555 "cold_storage_after": cty.NumberIntVal(10), 556 }), 557 cty.ObjectVal(map[string]cty.Value{ 558 "name": cty.StringVal("weekly"), 559 "cold_storage_after": cty.NumberIntVal(0), 560 }), 561 }), 562 }), 563 }), 564 ``, 565 }, 566 { 567 "complex_type_with_nested_complex_types", 568 cty.ObjectVal(map[string]cty.Value{ 569 "name": cty.StringVal("object"), 570 "nested_object": cty.ObjectVal(map[string]cty.Value{ 571 "name": cty.StringVal("nested_object"), 572 }), 573 }), 574 cty.ObjectVal(map[string]cty.Value{ 575 "name": cty.StringVal("object"), 576 "nested_object": cty.ObjectVal(map[string]cty.Value{ 577 "name": cty.StringVal("nested_object"), 578 "value": cty.StringVal("foo"), 579 }), 580 "nested_object_with_default": cty.ObjectVal(map[string]cty.Value{ 581 "name": cty.StringVal("nested_object_with_default"), 582 "value": cty.StringVal("bar"), 583 }), 584 }), 585 ``, 586 }, 587 { 588 "complex_type_with_empty_default_and_nested_optional", 589 cty.ListVal([]cty.Value{ 590 cty.ObjectVal(map[string]cty.Value{ 591 "name": cty.StringVal("abc"), 592 "optional_list": cty.ListVal([]cty.Value{ 593 cty.ObjectVal(map[string]cty.Value{ 594 "string": cty.StringVal("child"), 595 "optional_string": cty.NullVal(cty.String), 596 }), 597 }), 598 }), 599 cty.ObjectVal(map[string]cty.Value{ 600 "name": cty.StringVal("def"), 601 "optional_list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 602 "string": cty.String, 603 "optional_string": cty.String, 604 }))), 605 }), 606 }), 607 cty.ListVal([]cty.Value{ 608 cty.ObjectVal(map[string]cty.Value{ 609 "name": cty.StringVal("abc"), 610 "optional_list": cty.ListVal([]cty.Value{ 611 cty.ObjectVal(map[string]cty.Value{ 612 "string": cty.StringVal("child"), 613 "optional_string": cty.NullVal(cty.String), 614 }), 615 }), 616 }), 617 cty.ObjectVal(map[string]cty.Value{ 618 "name": cty.StringVal("def"), 619 "optional_list": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 620 "string": cty.String, 621 "optional_string": cty.String, 622 })), 623 }), 624 }), 625 ``, 626 }, 627 { 628 "object_with_nested_object_with_required_and_optional_attributes", 629 cty.EmptyObjectVal, 630 cty.ObjectVal(map[string]cty.Value{ 631 "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ 632 "string": cty.String, 633 "optional_string": cty.String, 634 })), 635 }), 636 ``, 637 }, 638 { 639 "empty_object_with_optional_nested_object_with_optional_bool", 640 cty.NilVal, 641 cty.ObjectVal(map[string]cty.Value{ 642 "thing": cty.NullVal(cty.Object(map[string]cty.Type{ 643 "flag": cty.Bool, 644 })), 645 }), 646 ``, 647 }, 648 { 649 "populated_object_with_optional_nested_object_with_optional_bool", 650 cty.NilVal, 651 cty.ObjectVal(map[string]cty.Value{ 652 "thing": cty.ObjectVal(map[string]cty.Value{ 653 "flag": cty.False, 654 }), 655 }), 656 ``, 657 }, 658 { 659 "empty_object_with_default_nested_object_with_optional_bool", 660 cty.NilVal, 661 cty.ObjectVal(map[string]cty.Value{ 662 "thing": cty.ObjectVal(map[string]cty.Value{ 663 "flag": cty.False, 664 }), 665 }), 666 ``, 667 }, 668 { 669 "list_with_nested_object_with_required_and_optional_attributes", 670 cty.ListVal([]cty.Value{ 671 cty.ObjectVal(map[string]cty.Value{ 672 "nested_object": cty.ObjectVal(map[string]cty.Value{ 673 "string": cty.StringVal("string"), 674 "optional_string": cty.NullVal(cty.String), 675 }), 676 }), 677 cty.ObjectVal(map[string]cty.Value{ 678 "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ 679 "string": cty.String, 680 "optional_string": cty.String, 681 })), 682 }), 683 }), 684 cty.ListVal([]cty.Value{ 685 cty.ObjectVal(map[string]cty.Value{ 686 "nested_object": cty.ObjectVal(map[string]cty.Value{ 687 "string": cty.StringVal("string"), 688 "optional_string": cty.StringVal("optional"), 689 }), 690 }), 691 cty.ObjectVal(map[string]cty.Value{ 692 "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ 693 "string": cty.String, 694 "optional_string": cty.String, 695 })), 696 }), 697 }), 698 ``, 699 }, 700 { 701 "list_with_nested_list_of_any", 702 cty.NilVal, 703 cty.ListVal([]cty.Value{ 704 cty.ObjectVal(map[string]cty.Value{ 705 "a": cty.StringVal("a"), 706 "b": cty.NullVal(cty.List(cty.Number)), 707 }), 708 cty.ObjectVal(map[string]cty.Value{ 709 "a": cty.StringVal("b"), 710 "b": cty.ListVal([]cty.Value{ 711 cty.NumberIntVal(1), 712 }), 713 }), 714 }), 715 ``, 716 }, 717 718 // sensitive 719 { 720 "constrained_string_sensitive_required", 721 cty.UnknownVal(cty.String), 722 cty.UnknownVal(cty.String), 723 ``, 724 }, 725 } 726 727 for _, test := range tests { 728 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 729 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 730 varCfg := variableConfigs[test.varName] 731 if varCfg == nil { 732 t.Fatalf("invalid variable name %q", test.varName) 733 } 734 735 t.Logf( 736 "test case\nvariable: %s\nconstraint: %#v\ndefault: %#v\nnullable: %#v\ngiven value: %#v", 737 varAddr, 738 varCfg.Type, 739 varCfg.Default, 740 varCfg.Nullable, 741 test.given, 742 ) 743 744 rawVal := &InputValue{ 745 Value: test.given, 746 SourceType: ValueFromCaller, 747 } 748 749 got, diags := prepareFinalInputVariableValue( 750 varAddr, rawVal, varCfg, 751 ) 752 753 if test.wantErr != "" { 754 if !diags.HasErrors() { 755 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 756 } else if got, want := diags.Err().Error(), test.wantErr; got != want { 757 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 758 } 759 } else { 760 if diags.HasErrors() { 761 t.Errorf("unexpected error\ngot: %s", diags.Err().Error()) 762 } 763 } 764 765 // NOTE: should still have returned some reasonable value even if there was an error 766 if !test.want.RawEquals(got) { 767 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 768 } 769 }) 770 } 771 772 t.Run("SourceType error message variants", func(t *testing.T) { 773 tests := []struct { 774 SourceType ValueSourceType 775 SourceRange tfdiags.SourceRange 776 WantTypeErr string 777 WantNullErr string 778 }{ 779 { 780 ValueFromUnknown, 781 tfdiags.SourceRange{}, 782 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 783 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 784 }, 785 { 786 ValueFromConfig, 787 tfdiags.SourceRange{ 788 Filename: "example.tf", 789 Start: tfdiags.SourcePos(hcl.InitialPos), 790 End: tfdiags.SourcePos(hcl.InitialPos), 791 }, 792 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 793 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 794 }, 795 { 796 ValueFromAutoFile, 797 tfdiags.SourceRange{ 798 Filename: "example.auto.tfvars", 799 Start: tfdiags.SourcePos(hcl.InitialPos), 800 End: tfdiags.SourcePos(hcl.InitialPos), 801 }, 802 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 803 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 804 }, 805 { 806 ValueFromNamedFile, 807 tfdiags.SourceRange{ 808 Filename: "example.tfvars", 809 Start: tfdiags.SourcePos(hcl.InitialPos), 810 End: tfdiags.SourcePos(hcl.InitialPos), 811 }, 812 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 813 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 814 }, 815 { 816 ValueFromCLIArg, 817 tfdiags.SourceRange{}, 818 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": string required.`, 819 `Required variable not set: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": required variable may not be set to null.`, 820 }, 821 { 822 ValueFromEnvVar, 823 tfdiags.SourceRange{}, 824 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: string required.`, 825 `Required variable not set: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: required variable may not be set to null.`, 826 }, 827 { 828 ValueFromInput, 829 tfdiags.SourceRange{}, 830 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using an interactive prompt: string required.`, 831 `Required variable not set: Unsuitable value for var.constrained_string_required set using an interactive prompt: required variable may not be set to null.`, 832 }, 833 { 834 // NOTE: This isn't actually a realistic case for this particular 835 // function, because if we have a value coming from a plan then 836 // we must be in the apply step, and we shouldn't be able to 837 // get past the plan step if we have invalid variable values, 838 // and during planning we'll always have other source types. 839 ValueFromPlan, 840 tfdiags.SourceRange{}, 841 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 842 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 843 }, 844 { 845 ValueFromCaller, 846 tfdiags.SourceRange{}, 847 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 848 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 849 }, 850 } 851 852 for _, test := range tests { 853 t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { 854 varAddr := addrs.InputVariable{Name: "constrained_string_required"}.Absolute(addrs.RootModuleInstance) 855 varCfg := variableConfigs[varAddr.Variable.Name] 856 t.Run("type error", func(t *testing.T) { 857 rawVal := &InputValue{ 858 Value: cty.EmptyObjectVal, 859 SourceType: test.SourceType, 860 SourceRange: test.SourceRange, 861 } 862 863 _, diags := prepareFinalInputVariableValue( 864 varAddr, rawVal, varCfg, 865 ) 866 if !diags.HasErrors() { 867 t.Fatalf("unexpected success; want error") 868 } 869 870 if got, want := diags.Err().Error(), test.WantTypeErr; got != want { 871 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 872 } 873 }) 874 t.Run("null error", func(t *testing.T) { 875 rawVal := &InputValue{ 876 Value: cty.NullVal(cty.DynamicPseudoType), 877 SourceType: test.SourceType, 878 SourceRange: test.SourceRange, 879 } 880 881 _, diags := prepareFinalInputVariableValue( 882 varAddr, rawVal, varCfg, 883 ) 884 if !diags.HasErrors() { 885 t.Fatalf("unexpected success; want error") 886 } 887 888 if got, want := diags.Err().Error(), test.WantNullErr; got != want { 889 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 890 } 891 }) 892 }) 893 } 894 }) 895 896 t.Run("SensitiveVariable error message variants, with source variants", func(t *testing.T) { 897 tests := []struct { 898 SourceType ValueSourceType 899 SourceRange tfdiags.SourceRange 900 WantTypeErr string 901 HideSubject bool 902 }{ 903 { 904 ValueFromUnknown, 905 tfdiags.SourceRange{}, 906 "Invalid value for input variable: Unsuitable value for var.constrained_string_sensitive_required set from outside of the configuration: string required.", 907 false, 908 }, 909 { 910 ValueFromConfig, 911 tfdiags.SourceRange{ 912 Filename: "example.tfvars", 913 Start: tfdiags.SourcePos(hcl.InitialPos), 914 End: tfdiags.SourcePos(hcl.InitialPos), 915 }, 916 `Invalid value for input variable: The given value is not suitable for var.constrained_string_sensitive_required, which is sensitive: string required. Invalid value defined at example.tfvars:1,1-1.`, 917 true, 918 }, 919 } 920 921 for _, test := range tests { 922 t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { 923 varAddr := addrs.InputVariable{Name: "constrained_string_sensitive_required"}.Absolute(addrs.RootModuleInstance) 924 varCfg := variableConfigs[varAddr.Variable.Name] 925 t.Run("type error", func(t *testing.T) { 926 rawVal := &InputValue{ 927 Value: cty.EmptyObjectVal, 928 SourceType: test.SourceType, 929 SourceRange: test.SourceRange, 930 } 931 932 _, diags := prepareFinalInputVariableValue( 933 varAddr, rawVal, varCfg, 934 ) 935 if !diags.HasErrors() { 936 t.Fatalf("unexpected success; want error") 937 } 938 939 if got, want := diags.Err().Error(), test.WantTypeErr; got != want { 940 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 941 } 942 943 if test.HideSubject { 944 if got, want := diags[0].Source().Subject.StartString(), test.SourceRange.StartString(); got == want { 945 t.Errorf("Subject start should have been hidden, but was %s", got) 946 } 947 } 948 }) 949 }) 950 } 951 }) 952 } 953 954 // These tests cover the JSON syntax configuration edge case handling, 955 // the background of which is described in detail in comments in the 956 // evalVariableValidations function. Future versions of Terraform may 957 // be able to remove this behaviour altogether. 958 func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { 959 cfgSrc := `{ 960 "variable": { 961 "valid": { 962 "type": "string", 963 "validation": { 964 "condition": "${var.valid != \"bar\"}", 965 "error_message": "Valid template string ${var.valid}" 966 } 967 }, 968 "invalid": { 969 "type": "string", 970 "validation": { 971 "condition": "${var.invalid != \"bar\"}", 972 "error_message": "Invalid template string ${" 973 } 974 } 975 } 976 } 977 ` 978 cfg := testModuleInline(t, map[string]string{ 979 "main.tf.json": cfgSrc, 980 }) 981 variableConfigs := cfg.Module.Variables 982 983 // Because we loaded our pseudo-module from a temporary file, the 984 // declaration source ranges will have unpredictable filenames. We'll 985 // fix that here just to make things easier below. 986 for _, vc := range variableConfigs { 987 vc.DeclRange.Filename = "main.tf.json" 988 for _, v := range vc.Validations { 989 v.DeclRange.Filename = "main.tf.json" 990 } 991 } 992 993 tests := []struct { 994 varName string 995 given cty.Value 996 wantErr []string 997 wantWarn []string 998 }{ 999 // Valid variable validation declaration, assigned value which passes 1000 // the condition generates no diagnostics. 1001 { 1002 varName: "valid", 1003 given: cty.StringVal("foo"), 1004 }, 1005 // Assigning a value which fails the condition generates an error 1006 // message with the expression successfully evaluated. 1007 { 1008 varName: "valid", 1009 given: cty.StringVal("bar"), 1010 wantErr: []string{ 1011 "Invalid value for variable", 1012 "Valid template string bar", 1013 }, 1014 }, 1015 // Invalid variable validation declaration due to an unparseable 1016 // template string. Assigning a value which passes the condition 1017 // results in a warning about the error message. 1018 { 1019 varName: "invalid", 1020 given: cty.StringVal("foo"), 1021 wantWarn: []string{ 1022 "Validation error message expression is invalid", 1023 "Missing expression; Expected the start of an expression, but found the end of the file.", 1024 }, 1025 }, 1026 // Assigning a value which fails the condition generates an error 1027 // message including the configured string interpreted as a literal 1028 // value, and the same warning diagnostic as above. 1029 { 1030 varName: "invalid", 1031 given: cty.StringVal("bar"), 1032 wantErr: []string{ 1033 "Invalid value for variable", 1034 "Invalid template string ${", 1035 }, 1036 wantWarn: []string{ 1037 "Validation error message expression is invalid", 1038 "Missing expression; Expected the start of an expression, but found the end of the file.", 1039 }, 1040 }, 1041 } 1042 1043 for _, test := range tests { 1044 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 1045 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 1046 varCfg := variableConfigs[test.varName] 1047 if varCfg == nil { 1048 t.Fatalf("invalid variable name %q", test.varName) 1049 } 1050 1051 // Build a mock context to allow the function under test to 1052 // retrieve the variable value and evaluate the expressions 1053 ctx := &MockEvalContext{} 1054 1055 // We need a minimal scope to allow basic functions to be passed to 1056 // the HCL scope 1057 ctx.EvaluationScopeScope = &lang.Scope{} 1058 ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { 1059 if got, want := addr.String(), varAddr.String(); got != want { 1060 t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) 1061 } 1062 return test.given 1063 } 1064 1065 gotDiags := evalVariableValidations( 1066 varAddr, varCfg, nil, ctx, 1067 ) 1068 1069 if len(test.wantErr) == 0 && len(test.wantWarn) == 0 { 1070 if len(gotDiags) > 0 { 1071 t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) 1072 } 1073 } else { 1074 wantErrs: 1075 for _, want := range test.wantErr { 1076 for _, diag := range gotDiags { 1077 if diag.Severity() != tfdiags.Error { 1078 continue 1079 } 1080 desc := diag.Description() 1081 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 1082 continue wantErrs 1083 } 1084 } 1085 t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 1086 } 1087 1088 wantWarns: 1089 for _, want := range test.wantWarn { 1090 for _, diag := range gotDiags { 1091 if diag.Severity() != tfdiags.Warning { 1092 continue 1093 } 1094 desc := diag.Description() 1095 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 1096 continue wantWarns 1097 } 1098 } 1099 t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 1100 } 1101 } 1102 }) 1103 } 1104 } 1105 1106 func TestEvalVariableValidations_sensitiveValues(t *testing.T) { 1107 cfgSrc := ` 1108 variable "foo" { 1109 type = string 1110 sensitive = true 1111 default = "boop" 1112 1113 validation { 1114 condition = length(var.foo) == 4 1115 error_message = "Foo must be 4 characters, not ${length(var.foo)}" 1116 } 1117 } 1118 1119 variable "bar" { 1120 type = string 1121 sensitive = true 1122 default = "boop" 1123 1124 validation { 1125 condition = length(var.bar) == 4 1126 error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}." 1127 } 1128 } 1129 ` 1130 cfg := testModuleInline(t, map[string]string{ 1131 "main.tf": cfgSrc, 1132 }) 1133 variableConfigs := cfg.Module.Variables 1134 1135 // Because we loaded our pseudo-module from a temporary file, the 1136 // declaration source ranges will have unpredictable filenames. We'll 1137 // fix that here just to make things easier below. 1138 for _, vc := range variableConfigs { 1139 vc.DeclRange.Filename = "main.tf" 1140 for _, v := range vc.Validations { 1141 v.DeclRange.Filename = "main.tf" 1142 } 1143 } 1144 1145 tests := []struct { 1146 varName string 1147 given cty.Value 1148 wantErr []string 1149 }{ 1150 // Validations pass on a sensitive variable with an error message which 1151 // would generate a sensitive value 1152 { 1153 varName: "foo", 1154 given: cty.StringVal("boop"), 1155 }, 1156 // Assigning a value which fails the condition generates a sensitive 1157 // error message, which is elided and generates another error 1158 { 1159 varName: "foo", 1160 given: cty.StringVal("bap"), 1161 wantErr: []string{ 1162 "Invalid value for variable", 1163 "The error message included a sensitive value, so it will not be displayed.", 1164 "Error message refers to sensitive values", 1165 }, 1166 }, 1167 // Validations pass on a sensitive variable with a correctly defined 1168 // error message 1169 { 1170 varName: "bar", 1171 given: cty.StringVal("boop"), 1172 }, 1173 // Assigning a value which fails the condition generates a nonsensitive 1174 // error message, which is displayed 1175 { 1176 varName: "bar", 1177 given: cty.StringVal("bap"), 1178 wantErr: []string{ 1179 "Invalid value for variable", 1180 "Bar must be 4 characters, not 3.", 1181 }, 1182 }, 1183 } 1184 1185 for _, test := range tests { 1186 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 1187 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 1188 varCfg := variableConfigs[test.varName] 1189 if varCfg == nil { 1190 t.Fatalf("invalid variable name %q", test.varName) 1191 } 1192 1193 // Build a mock context to allow the function under test to 1194 // retrieve the variable value and evaluate the expressions 1195 ctx := &MockEvalContext{} 1196 1197 // We need a minimal scope to allow basic functions to be passed to 1198 // the HCL scope 1199 ctx.EvaluationScopeScope = &lang.Scope{} 1200 ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { 1201 if got, want := addr.String(), varAddr.String(); got != want { 1202 t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) 1203 } 1204 if varCfg.Sensitive { 1205 return test.given.Mark(marks.Sensitive) 1206 } else { 1207 return test.given 1208 } 1209 } 1210 1211 gotDiags := evalVariableValidations( 1212 varAddr, varCfg, nil, ctx, 1213 ) 1214 1215 if len(test.wantErr) == 0 { 1216 if len(gotDiags) > 0 { 1217 t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) 1218 } 1219 } else { 1220 wantErrs: 1221 for _, want := range test.wantErr { 1222 for _, diag := range gotDiags { 1223 if diag.Severity() != tfdiags.Error { 1224 continue 1225 } 1226 desc := diag.Description() 1227 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 1228 continue wantErrs 1229 } 1230 } 1231 t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 1232 } 1233 } 1234 }) 1235 } 1236 }