| package terraform |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/lang" |
| "github.com/hashicorp/terraform/internal/lang/marks" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| func TestPrepareFinalInputVariableValue(t *testing.T) { |
| // This is just a concise way to define a bunch of *configs.Variable |
| // objects to use in our tests below. We're only going to decode this |
| // config, not fully evaluate it. |
| cfgSrc := ` |
| variable "nullable_required" { |
| } |
| variable "nullable_optional_default_string" { |
| default = "hello" |
| } |
| variable "nullable_optional_default_null" { |
| default = null |
| } |
| variable "constrained_string_nullable_required" { |
| type = string |
| } |
| variable "constrained_string_nullable_optional_default_string" { |
| type = string |
| default = "hello" |
| } |
| variable "constrained_string_nullable_optional_default_bool" { |
| type = string |
| default = true |
| } |
| variable "constrained_string_nullable_optional_default_null" { |
| type = string |
| default = null |
| } |
| variable "required" { |
| nullable = false |
| } |
| variable "optional_default_string" { |
| nullable = false |
| default = "hello" |
| } |
| variable "constrained_string_required" { |
| nullable = false |
| type = string |
| } |
| variable "constrained_string_optional_default_string" { |
| nullable = false |
| type = string |
| default = "hello" |
| } |
| variable "constrained_string_optional_default_bool" { |
| nullable = false |
| type = string |
| default = true |
| } |
| variable "constrained_string_sensitive_required" { |
| sensitive = true |
| nullable = false |
| type = string |
| } |
| variable "complex_type_with_nested_default_optional" { |
| type = set(object({ |
| name = string |
| schedules = set(object({ |
| name = string |
| cold_storage_after = optional(number, 10) |
| })) |
| })) |
| } |
| variable "complex_type_with_nested_complex_types" { |
| type = object({ |
| name = string |
| nested_object = object({ |
| name = string |
| value = optional(string, "foo") |
| }) |
| nested_object_with_default = optional(object({ |
| name = string |
| value = optional(string, "bar") |
| }), { |
| name = "nested_object_with_default" |
| }) |
| }) |
| } |
| // https://github.com/hashicorp/terraform/issues/32152 |
| // This variable was originally added to test that optional attribute |
| // metadata is stripped from empty default collections. Essentially, you |
| // should be able to mix and match custom and default values for the |
| // optional_list attribute. |
| variable "complex_type_with_empty_default_and_nested_optional" { |
| type = list(object({ |
| name = string |
| optional_list = optional(list(object({ |
| string = string |
| optional_string = optional(string) |
| })), []) |
| })) |
| } |
| // https://github.com/hashicorp/terraform/issues/32160#issuecomment-1302783910 |
| // These variables were added to test the specific use case from this |
| // GitHub comment. |
| variable "empty_object_with_optional_nested_object_with_optional_bool" { |
| type = object({ |
| thing = optional(object({ |
| flag = optional(bool, false) |
| })) |
| }) |
| default = {} |
| } |
| variable "populated_object_with_optional_nested_object_with_optional_bool" { |
| type = object({ |
| thing = optional(object({ |
| flag = optional(bool, false) |
| })) |
| }) |
| default = { |
| thing = {} |
| } |
| } |
| variable "empty_object_with_default_nested_object_with_optional_bool" { |
| type = object({ |
| thing = optional(object({ |
| flag = optional(bool, false) |
| }), {}) |
| }) |
| default = {} |
| } |
| // https://github.com/hashicorp/terraform/issues/32160 |
| // This variable was originally added to test that optional objects do |
| // get created containing only their defaults. Instead they should be |
| // left empty. We do not expect nested_object to be created just because |
| // optional_string has a default value. |
| variable "object_with_nested_object_with_required_and_optional_attributes" { |
| type = object({ |
| nested_object = optional(object({ |
| string = string |
| optional_string = optional(string, "optional") |
| })) |
| }) |
| } |
| // https://github.com/hashicorp/terraform/issues/32157 |
| // Similar to above, we want to see that merging combinations of the |
| // nested_object into a single collection doesn't crash because of |
| // inconsistent elements. |
| variable "list_with_nested_object_with_required_and_optional_attributes" { |
| type = list(object({ |
| nested_object = optional(object({ |
| string = string |
| optional_string = optional(string, "optional") |
| })) |
| })) |
| } |
| // https://github.com/hashicorp/terraform/issues/32109 |
| // This variable was originally introduced to test the behaviour of |
| // the dynamic type constraint. You should be able to use the 'any' |
| // constraint and introduce empty, null, and populated values into the |
| // list. |
| variable "list_with_nested_list_of_any" { |
| type = list(object({ |
| a = string |
| b = optional(list(any)) |
| })) |
| default = [ |
| { |
| a = "a" |
| }, |
| { |
| a = "b" |
| b = [1] |
| } |
| ] |
| } |
| // https://github.com/hashicorp/terraform/issues/32396 |
| // This variable was originally introduced to test the behaviour of the |
| // dynamic type constraint. You should be able to set primitive types in |
| // the list consistently. |
| variable "list_with_nested_collections_dynamic_with_default" { |
| type = list( |
| object({ |
| name = optional(string, "default") |
| taints = optional(list(map(any)), []) |
| }) |
| ) |
| } |
| // https://github.com/hashicorp/terraform/issues/32752 |
| // This variable was introduced to make sure the evaluation doesn't |
| // crash even when the types are wrong. |
| variable "invalid_nested_type" { |
| type = map( |
| object({ |
| rules = map( |
| object({ |
| destination_addresses = optional(list(string), []) |
| }) |
| ) |
| }) |
| ) |
| default = {} |
| } |
| ` |
| cfg := testModuleInline(t, map[string]string{ |
| "main.tf": cfgSrc, |
| }) |
| variableConfigs := cfg.Module.Variables |
| |
| // Because we loaded our pseudo-module from a temporary file, the |
| // declaration source ranges will have unpredictable filenames. We'll |
| // fix that here just to make things easier below. |
| for _, vc := range variableConfigs { |
| vc.DeclRange.Filename = "main.tf" |
| } |
| |
| tests := []struct { |
| varName string |
| given cty.Value |
| want cty.Value |
| wantErr string |
| }{ |
| // nullable_required |
| { |
| "nullable_required", |
| cty.NilVal, |
| cty.UnknownVal(cty.DynamicPseudoType), |
| `Required variable not set: The variable "nullable_required" is required, but is not set.`, |
| }, |
| { |
| "nullable_required", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.DynamicPseudoType), |
| ``, // "required" for a nullable variable means only that it must be set, even if it's set to null |
| }, |
| { |
| "nullable_required", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "nullable_required", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // nullable_optional_default_string |
| { |
| "nullable_optional_default_string", |
| cty.NilVal, |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "nullable_optional_default_string", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default |
| ``, |
| }, |
| { |
| "nullable_optional_default_string", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "nullable_optional_default_string", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // nullable_optional_default_null |
| { |
| "nullable_optional_default_null", |
| cty.NilVal, |
| cty.NullVal(cty.DynamicPseudoType), // the declared default value |
| ``, |
| }, |
| { |
| "nullable_optional_default_null", |
| cty.NullVal(cty.String), |
| cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default |
| ``, |
| }, |
| { |
| "nullable_optional_default_null", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "nullable_optional_default_null", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_nullable_required |
| { |
| "constrained_string_nullable_required", |
| cty.NilVal, |
| cty.UnknownVal(cty.String), |
| `Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`, |
| }, |
| { |
| "constrained_string_nullable_required", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.String), // the null value still gets converted to match the type constraint |
| ``, // "required" for a nullable variable means only that it must be set, even if it's set to null |
| }, |
| { |
| "constrained_string_nullable_required", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_required", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_nullable_optional_default_string |
| { |
| "constrained_string_nullable_optional_default_string", |
| cty.NilVal, |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_string", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_string", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_string", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_nullable_optional_default_bool |
| { |
| "constrained_string_nullable_optional_default_bool", |
| cty.NilVal, |
| cty.StringVal("true"), // the declared default value, automatically converted to match type constraint |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_bool", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_bool", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_bool", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_nullable_optional_default_null |
| { |
| "constrained_string_nullable_optional_default_null", |
| cty.NilVal, |
| cty.NullVal(cty.String), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_null", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.NullVal(cty.String), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_null", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_nullable_optional_default_null", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // required |
| { |
| "required", |
| cty.NilVal, |
| cty.UnknownVal(cty.DynamicPseudoType), |
| `Required variable not set: The variable "required" is required, but is not set.`, |
| }, |
| { |
| "required", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.UnknownVal(cty.DynamicPseudoType), |
| `Required variable not set: Unsuitable value for var.required set from outside of the configuration: required variable may not be set to null.`, |
| }, |
| { |
| "required", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "required", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // optional_default_string |
| { |
| "optional_default_string", |
| cty.NilVal, |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "optional_default_string", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "optional_default_string", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "optional_default_string", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_required |
| { |
| "constrained_string_required", |
| cty.NilVal, |
| cty.UnknownVal(cty.String), |
| `Required variable not set: The variable "constrained_string_required" is required, but is not set.`, |
| }, |
| { |
| "constrained_string_required", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.UnknownVal(cty.String), |
| `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.`, |
| }, |
| { |
| "constrained_string_required", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_required", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_optional_default_string |
| { |
| "constrained_string_optional_default_string", |
| cty.NilVal, |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_string", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.StringVal("hello"), // the declared default value |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_string", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_string", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| |
| // constrained_string_optional_default_bool |
| { |
| "constrained_string_optional_default_bool", |
| cty.NilVal, |
| cty.StringVal("true"), // the declared default value, automatically converted to match type constraint |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_bool", |
| cty.NullVal(cty.DynamicPseudoType), |
| cty.StringVal("true"), // the declared default value, automatically converted to match type constraint |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_bool", |
| cty.StringVal("ahoy"), |
| cty.StringVal("ahoy"), |
| ``, |
| }, |
| { |
| "constrained_string_optional_default_bool", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| { |
| "list_with_nested_collections_dynamic_with_default", |
| cty.TupleVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("default"), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("complex"), |
| "taints": cty.ListVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("my_key"), |
| "value": cty.StringVal("my_value"), |
| }), |
| }), |
| }), |
| }), |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("default"), |
| "taints": cty.ListValEmpty(cty.Map(cty.String)), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("complex"), |
| "taints": cty.ListVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("my_key"), |
| "value": cty.StringVal("my_value"), |
| }), |
| }), |
| }), |
| }), |
| ``, |
| }, |
| |
| // complex types |
| |
| { |
| "complex_type_with_nested_default_optional", |
| cty.SetVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("test1"), |
| "schedules": cty.SetVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "name": cty.StringVal("daily"), |
| }), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("test2"), |
| "schedules": cty.SetVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "name": cty.StringVal("daily"), |
| }), |
| cty.MapVal(map[string]cty.Value{ |
| "name": cty.StringVal("weekly"), |
| "cold_storage_after": cty.StringVal("0"), |
| }), |
| }), |
| }), |
| }), |
| cty.SetVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("test1"), |
| "schedules": cty.SetVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("daily"), |
| "cold_storage_after": cty.NumberIntVal(10), |
| }), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("test2"), |
| "schedules": cty.SetVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("daily"), |
| "cold_storage_after": cty.NumberIntVal(10), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("weekly"), |
| "cold_storage_after": cty.NumberIntVal(0), |
| }), |
| }), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "complex_type_with_nested_complex_types", |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("object"), |
| "nested_object": cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("nested_object"), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("object"), |
| "nested_object": cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("nested_object"), |
| "value": cty.StringVal("foo"), |
| }), |
| "nested_object_with_default": cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("nested_object_with_default"), |
| "value": cty.StringVal("bar"), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "complex_type_with_empty_default_and_nested_optional", |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("abc"), |
| "optional_list": cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "string": cty.StringVal("child"), |
| "optional_string": cty.NullVal(cty.String), |
| }), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("def"), |
| "optional_list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ |
| "string": cty.String, |
| "optional_string": cty.String, |
| }))), |
| }), |
| }), |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("abc"), |
| "optional_list": cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "string": cty.StringVal("child"), |
| "optional_string": cty.NullVal(cty.String), |
| }), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("def"), |
| "optional_list": cty.ListValEmpty(cty.Object(map[string]cty.Type{ |
| "string": cty.String, |
| "optional_string": cty.String, |
| })), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "object_with_nested_object_with_required_and_optional_attributes", |
| cty.EmptyObjectVal, |
| cty.ObjectVal(map[string]cty.Value{ |
| "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ |
| "string": cty.String, |
| "optional_string": cty.String, |
| })), |
| }), |
| ``, |
| }, |
| { |
| "empty_object_with_optional_nested_object_with_optional_bool", |
| cty.NilVal, |
| cty.ObjectVal(map[string]cty.Value{ |
| "thing": cty.NullVal(cty.Object(map[string]cty.Type{ |
| "flag": cty.Bool, |
| })), |
| }), |
| ``, |
| }, |
| { |
| "populated_object_with_optional_nested_object_with_optional_bool", |
| cty.NilVal, |
| cty.ObjectVal(map[string]cty.Value{ |
| "thing": cty.ObjectVal(map[string]cty.Value{ |
| "flag": cty.False, |
| }), |
| }), |
| ``, |
| }, |
| { |
| "empty_object_with_default_nested_object_with_optional_bool", |
| cty.NilVal, |
| cty.ObjectVal(map[string]cty.Value{ |
| "thing": cty.ObjectVal(map[string]cty.Value{ |
| "flag": cty.False, |
| }), |
| }), |
| ``, |
| }, |
| { |
| "list_with_nested_object_with_required_and_optional_attributes", |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "nested_object": cty.ObjectVal(map[string]cty.Value{ |
| "string": cty.StringVal("string"), |
| "optional_string": cty.NullVal(cty.String), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ |
| "string": cty.String, |
| "optional_string": cty.String, |
| })), |
| }), |
| }), |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "nested_object": cty.ObjectVal(map[string]cty.Value{ |
| "string": cty.StringVal("string"), |
| "optional_string": cty.StringVal("optional"), |
| }), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ |
| "string": cty.String, |
| "optional_string": cty.String, |
| })), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "list_with_nested_list_of_any", |
| cty.NilVal, |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "a": cty.StringVal("a"), |
| "b": cty.NullVal(cty.List(cty.Number)), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "a": cty.StringVal("b"), |
| "b": cty.ListVal([]cty.Value{ |
| cty.NumberIntVal(1), |
| }), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "list_with_nested_collections_dynamic_with_default", |
| cty.TupleVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("default"), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("complex"), |
| "taints": cty.ListVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("my_key"), |
| "value": cty.StringVal("my_value"), |
| }), |
| }), |
| }), |
| }), |
| cty.ListVal([]cty.Value{ |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("default"), |
| "taints": cty.ListValEmpty(cty.Map(cty.String)), |
| }), |
| cty.ObjectVal(map[string]cty.Value{ |
| "name": cty.StringVal("complex"), |
| "taints": cty.ListVal([]cty.Value{ |
| cty.MapVal(map[string]cty.Value{ |
| "key": cty.StringVal("my_key"), |
| "value": cty.StringVal("my_value"), |
| }), |
| }), |
| }), |
| }), |
| ``, |
| }, |
| { |
| "invalid_nested_type", |
| cty.MapVal(map[string]cty.Value{ |
| "mysql": cty.ObjectVal(map[string]cty.Value{ |
| "rules": cty.ObjectVal(map[string]cty.Value{ |
| "destination_addresses": cty.ListVal([]cty.Value{cty.StringVal("192.168.0.1")}), |
| }), |
| }), |
| }), |
| cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ |
| "rules": cty.Map(cty.Object(map[string]cty.Type{ |
| "destination_addresses": cty.List(cty.String), |
| })), |
| }))), |
| `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.`, |
| }, |
| |
| // sensitive |
| { |
| "constrained_string_sensitive_required", |
| cty.UnknownVal(cty.String), |
| cty.UnknownVal(cty.String), |
| ``, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { |
| varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) |
| varCfg := variableConfigs[test.varName] |
| if varCfg == nil { |
| t.Fatalf("invalid variable name %q", test.varName) |
| } |
| |
| t.Logf( |
| "test case\nvariable: %s\nconstraint: %#v\ndefault: %#v\nnullable: %#v\ngiven value: %#v", |
| varAddr, |
| varCfg.Type, |
| varCfg.Default, |
| varCfg.Nullable, |
| test.given, |
| ) |
| |
| rawVal := &InputValue{ |
| Value: test.given, |
| SourceType: ValueFromCaller, |
| } |
| |
| got, diags := prepareFinalInputVariableValue( |
| varAddr, rawVal, varCfg, |
| ) |
| |
| if test.wantErr != "" { |
| if !diags.HasErrors() { |
| t.Errorf("unexpected success\nwant error: %s", test.wantErr) |
| } else if got, want := diags.Err().Error(), test.wantErr; got != want { |
| t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) |
| } |
| } else { |
| if diags.HasErrors() { |
| t.Errorf("unexpected error\ngot: %s", diags.Err().Error()) |
| } |
| } |
| |
| // NOTE: should still have returned some reasonable value even if there was an error |
| if !test.want.RawEquals(got) { |
| t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.want) |
| } |
| }) |
| } |
| |
| t.Run("SourceType error message variants", func(t *testing.T) { |
| tests := []struct { |
| SourceType ValueSourceType |
| SourceRange tfdiags.SourceRange |
| WantTypeErr string |
| WantNullErr string |
| }{ |
| { |
| ValueFromUnknown, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, |
| `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.`, |
| }, |
| { |
| ValueFromConfig, |
| tfdiags.SourceRange{ |
| Filename: "example.tf", |
| Start: tfdiags.SourcePos(hcl.InitialPos), |
| End: tfdiags.SourcePos(hcl.InitialPos), |
| }, |
| `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.`, |
| `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.`, |
| }, |
| { |
| ValueFromAutoFile, |
| tfdiags.SourceRange{ |
| Filename: "example.auto.tfvars", |
| Start: tfdiags.SourcePos(hcl.InitialPos), |
| End: tfdiags.SourcePos(hcl.InitialPos), |
| }, |
| `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.`, |
| `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.`, |
| }, |
| { |
| ValueFromNamedFile, |
| tfdiags.SourceRange{ |
| Filename: "example.tfvars", |
| Start: tfdiags.SourcePos(hcl.InitialPos), |
| End: tfdiags.SourcePos(hcl.InitialPos), |
| }, |
| `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.`, |
| `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.`, |
| }, |
| { |
| ValueFromCLIArg, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": string required.`, |
| `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.`, |
| }, |
| { |
| ValueFromEnvVar, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: string required.`, |
| `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.`, |
| }, |
| { |
| ValueFromInput, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using an interactive prompt: string required.`, |
| `Required variable not set: Unsuitable value for var.constrained_string_required set using an interactive prompt: required variable may not be set to null.`, |
| }, |
| { |
| // NOTE: This isn't actually a realistic case for this particular |
| // function, because if we have a value coming from a plan then |
| // we must be in the apply step, and we shouldn't be able to |
| // get past the plan step if we have invalid variable values, |
| // and during planning we'll always have other source types. |
| ValueFromPlan, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, |
| `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.`, |
| }, |
| { |
| ValueFromCaller, |
| tfdiags.SourceRange{}, |
| `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, |
| `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.`, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { |
| varAddr := addrs.InputVariable{Name: "constrained_string_required"}.Absolute(addrs.RootModuleInstance) |
| varCfg := variableConfigs[varAddr.Variable.Name] |
| t.Run("type error", func(t *testing.T) { |
| rawVal := &InputValue{ |
| Value: cty.EmptyObjectVal, |
| SourceType: test.SourceType, |
| SourceRange: test.SourceRange, |
| } |
| |
| _, diags := prepareFinalInputVariableValue( |
| varAddr, rawVal, varCfg, |
| ) |
| if !diags.HasErrors() { |
| t.Fatalf("unexpected success; want error") |
| } |
| |
| if got, want := diags.Err().Error(), test.WantTypeErr; got != want { |
| t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| t.Run("null error", func(t *testing.T) { |
| rawVal := &InputValue{ |
| Value: cty.NullVal(cty.DynamicPseudoType), |
| SourceType: test.SourceType, |
| SourceRange: test.SourceRange, |
| } |
| |
| _, diags := prepareFinalInputVariableValue( |
| varAddr, rawVal, varCfg, |
| ) |
| if !diags.HasErrors() { |
| t.Fatalf("unexpected success; want error") |
| } |
| |
| if got, want := diags.Err().Error(), test.WantNullErr; got != want { |
| t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) |
| } |
| }) |
| }) |
| } |
| }) |
| |
| t.Run("SensitiveVariable error message variants, with source variants", func(t *testing.T) { |
| tests := []struct { |
| SourceType ValueSourceType |
| SourceRange tfdiags.SourceRange |
| WantTypeErr string |
| HideSubject bool |
| }{ |
| { |
| ValueFromUnknown, |
| tfdiags.SourceRange{}, |
| "Invalid value for input variable: Unsuitable value for var.constrained_string_sensitive_required set from outside of the configuration: string required.", |
| false, |
| }, |
| { |
| ValueFromConfig, |
| tfdiags.SourceRange{ |
| Filename: "example.tfvars", |
| Start: tfdiags.SourcePos(hcl.InitialPos), |
| End: tfdiags.SourcePos(hcl.InitialPos), |
| }, |
| `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.`, |
| true, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { |
| varAddr := addrs.InputVariable{Name: "constrained_string_sensitive_required"}.Absolute(addrs.RootModuleInstance) |
| varCfg := variableConfigs[varAddr.Variable.Name] |
| t.Run("type error", func(t *testing.T) { |
| rawVal := &InputValue{ |
| Value: cty.EmptyObjectVal, |
| SourceType: test.SourceType, |
| SourceRange: test.SourceRange, |
| } |
| |
| _, diags := prepareFinalInputVariableValue( |
| varAddr, rawVal, varCfg, |
| ) |
| if !diags.HasErrors() { |
| t.Fatalf("unexpected success; want error") |
| } |
| |
| if got, want := diags.Err().Error(), test.WantTypeErr; got != want { |
| t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) |
| } |
| |
| if test.HideSubject { |
| if got, want := diags[0].Source().Subject.StartString(), test.SourceRange.StartString(); got == want { |
| t.Errorf("Subject start should have been hidden, but was %s", got) |
| } |
| } |
| }) |
| }) |
| } |
| }) |
| } |
| |
| // These tests cover the JSON syntax configuration edge case handling, |
| // the background of which is described in detail in comments in the |
| // evalVariableValidations function. Future versions of Terraform may |
| // be able to remove this behaviour altogether. |
| func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { |
| cfgSrc := `{ |
| "variable": { |
| "valid": { |
| "type": "string", |
| "validation": { |
| "condition": "${var.valid != \"bar\"}", |
| "error_message": "Valid template string ${var.valid}" |
| } |
| }, |
| "invalid": { |
| "type": "string", |
| "validation": { |
| "condition": "${var.invalid != \"bar\"}", |
| "error_message": "Invalid template string ${" |
| } |
| } |
| } |
| } |
| ` |
| cfg := testModuleInline(t, map[string]string{ |
| "main.tf.json": cfgSrc, |
| }) |
| variableConfigs := cfg.Module.Variables |
| |
| // Because we loaded our pseudo-module from a temporary file, the |
| // declaration source ranges will have unpredictable filenames. We'll |
| // fix that here just to make things easier below. |
| for _, vc := range variableConfigs { |
| vc.DeclRange.Filename = "main.tf.json" |
| for _, v := range vc.Validations { |
| v.DeclRange.Filename = "main.tf.json" |
| } |
| } |
| |
| tests := []struct { |
| varName string |
| given cty.Value |
| wantErr []string |
| wantWarn []string |
| }{ |
| // Valid variable validation declaration, assigned value which passes |
| // the condition generates no diagnostics. |
| { |
| varName: "valid", |
| given: cty.StringVal("foo"), |
| }, |
| // Assigning a value which fails the condition generates an error |
| // message with the expression successfully evaluated. |
| { |
| varName: "valid", |
| given: cty.StringVal("bar"), |
| wantErr: []string{ |
| "Invalid value for variable", |
| "Valid template string bar", |
| }, |
| }, |
| // Invalid variable validation declaration due to an unparseable |
| // template string. Assigning a value which passes the condition |
| // results in a warning about the error message. |
| { |
| varName: "invalid", |
| given: cty.StringVal("foo"), |
| wantWarn: []string{ |
| "Validation error message expression is invalid", |
| "Missing expression; Expected the start of an expression, but found the end of the file.", |
| }, |
| }, |
| // Assigning a value which fails the condition generates an error |
| // message including the configured string interpreted as a literal |
| // value, and the same warning diagnostic as above. |
| { |
| varName: "invalid", |
| given: cty.StringVal("bar"), |
| wantErr: []string{ |
| "Invalid value for variable", |
| "Invalid template string ${", |
| }, |
| wantWarn: []string{ |
| "Validation error message expression is invalid", |
| "Missing expression; Expected the start of an expression, but found the end of the file.", |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { |
| varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) |
| varCfg := variableConfigs[test.varName] |
| if varCfg == nil { |
| t.Fatalf("invalid variable name %q", test.varName) |
| } |
| |
| // Build a mock context to allow the function under test to |
| // retrieve the variable value and evaluate the expressions |
| ctx := &MockEvalContext{} |
| |
| // We need a minimal scope to allow basic functions to be passed to |
| // the HCL scope |
| ctx.EvaluationScopeScope = &lang.Scope{} |
| ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { |
| if got, want := addr.String(), varAddr.String(); got != want { |
| t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) |
| } |
| return test.given |
| } |
| |
| gotDiags := evalVariableValidations( |
| varAddr, varCfg, nil, ctx, |
| ) |
| |
| if len(test.wantErr) == 0 && len(test.wantWarn) == 0 { |
| if len(gotDiags) > 0 { |
| t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) |
| } |
| } else { |
| wantErrs: |
| for _, want := range test.wantErr { |
| for _, diag := range gotDiags { |
| if diag.Severity() != tfdiags.Error { |
| continue |
| } |
| desc := diag.Description() |
| if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { |
| continue wantErrs |
| } |
| } |
| t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) |
| } |
| |
| wantWarns: |
| for _, want := range test.wantWarn { |
| for _, diag := range gotDiags { |
| if diag.Severity() != tfdiags.Warning { |
| continue |
| } |
| desc := diag.Description() |
| if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { |
| continue wantWarns |
| } |
| } |
| t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestEvalVariableValidations_sensitiveValues(t *testing.T) { |
| cfgSrc := ` |
| variable "foo" { |
| type = string |
| sensitive = true |
| default = "boop" |
| |
| validation { |
| condition = length(var.foo) == 4 |
| error_message = "Foo must be 4 characters, not ${length(var.foo)}" |
| } |
| } |
| |
| variable "bar" { |
| type = string |
| sensitive = true |
| default = "boop" |
| |
| validation { |
| condition = length(var.bar) == 4 |
| error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}." |
| } |
| } |
| ` |
| cfg := testModuleInline(t, map[string]string{ |
| "main.tf": cfgSrc, |
| }) |
| variableConfigs := cfg.Module.Variables |
| |
| // Because we loaded our pseudo-module from a temporary file, the |
| // declaration source ranges will have unpredictable filenames. We'll |
| // fix that here just to make things easier below. |
| for _, vc := range variableConfigs { |
| vc.DeclRange.Filename = "main.tf" |
| for _, v := range vc.Validations { |
| v.DeclRange.Filename = "main.tf" |
| } |
| } |
| |
| tests := []struct { |
| varName string |
| given cty.Value |
| wantErr []string |
| }{ |
| // Validations pass on a sensitive variable with an error message which |
| // would generate a sensitive value |
| { |
| varName: "foo", |
| given: cty.StringVal("boop"), |
| }, |
| // Assigning a value which fails the condition generates a sensitive |
| // error message, which is elided and generates another error |
| { |
| varName: "foo", |
| given: cty.StringVal("bap"), |
| wantErr: []string{ |
| "Invalid value for variable", |
| "The error message included a sensitive value, so it will not be displayed.", |
| "Error message refers to sensitive values", |
| }, |
| }, |
| // Validations pass on a sensitive variable with a correctly defined |
| // error message |
| { |
| varName: "bar", |
| given: cty.StringVal("boop"), |
| }, |
| // Assigning a value which fails the condition generates a nonsensitive |
| // error message, which is displayed |
| { |
| varName: "bar", |
| given: cty.StringVal("bap"), |
| wantErr: []string{ |
| "Invalid value for variable", |
| "Bar must be 4 characters, not 3.", |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { |
| varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) |
| varCfg := variableConfigs[test.varName] |
| if varCfg == nil { |
| t.Fatalf("invalid variable name %q", test.varName) |
| } |
| |
| // Build a mock context to allow the function under test to |
| // retrieve the variable value and evaluate the expressions |
| ctx := &MockEvalContext{} |
| |
| // We need a minimal scope to allow basic functions to be passed to |
| // the HCL scope |
| ctx.EvaluationScopeScope = &lang.Scope{} |
| ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { |
| if got, want := addr.String(), varAddr.String(); got != want { |
| t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) |
| } |
| if varCfg.Sensitive { |
| return test.given.Mark(marks.Sensitive) |
| } else { |
| return test.given |
| } |
| } |
| |
| gotDiags := evalVariableValidations( |
| varAddr, varCfg, nil, ctx, |
| ) |
| |
| if len(test.wantErr) == 0 { |
| if len(gotDiags) > 0 { |
| t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) |
| } |
| } else { |
| wantErrs: |
| for _, want := range test.wantErr { |
| for _, diag := range gotDiags { |
| if diag.Severity() != tfdiags.Error { |
| continue |
| } |
| desc := diag.Description() |
| if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { |
| continue wantErrs |
| } |
| } |
| t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) |
| } |
| } |
| }) |
| } |
| } |