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