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  }