Do not apply type defaults to null values

Applying object type defaults to null values can convert null to an
object with partial attributes. This means that even a specified default
value of null will not remain null after variable evaluation.

In turn, the result can then be invalid, if not all attributes in an
object type have defaults specified.

This commit skips the type default application step during config load
and variable evaluation if the default or given value is null of any
type. We still perform type conversion.
pull/31797/head
Alisdair McDiarmid 4 years ago
parent b9eb263744
commit 551f6316fa

@ -140,8 +140,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
if v.ConstraintType != cty.NilType {
var err error
// If the type constraint has defaults, we must apply those
// defaults to the variable default value before type conversion.
if v.TypeDefaults != nil {
// defaults to the variable default value before type conversion,
// unless the default value is null. Null is excluded from the
// type default application process as a special case, to allow
// nullable variables to have a null default value.
if v.TypeDefaults != nil && !val.IsNull() {
val = v.TypeDefaults.Apply(val)
}
val, err = convert.Convert(val, v.ConstraintType)

@ -0,0 +1,38 @@
variable "a" {
type = object({
foo = optional(string)
bar = optional(bool, true)
})
}
variable "b" {
type = list(
object({
foo = optional(string)
})
)
}
variable "c" {
type = set(
object({
foo = optional(string)
})
)
}
variable "d" {
type = map(
object({
foo = optional(string)
})
)
}
variable "e" {
type = object({
foo = string
bar = optional(bool, true)
})
default = null
}

@ -12119,6 +12119,52 @@ output "out" {
}
}
func TestContext2Apply_moduleVariableOptionalAttributesDefaultNull(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
variable "in" {
type = object({
required = string
optional = optional(string)
default = optional(bool, true)
})
default = null
}
# Wrap the input variable in a tuple because a null output value is elided from
# the plan, which prevents us from testing its type.
output "out" {
value = [var.in]
}
`})
ctx := testContext2(t, &ContextOpts{})
// We don't specify a value for the variable here, relying on its defined
// default.
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
state, diags := ctx.Apply(plan, m)
if diags.HasErrors() {
t.Fatal(diags.ErrWithWarnings())
}
got := state.RootModule().OutputValues["out"].Value
// The null default value should be bound, after type converting to the
// full object type
want := cty.TupleVal([]cty.Value{cty.NullVal(cty.Object(map[string]cty.Type{
"required": cty.String,
"optional": cty.String,
"default": cty.Bool,
}))})
if !want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want)
}
}
func TestContext2Apply_provisionerSensitive(t *testing.T) {
m := testModule(t, "apply-provisioner-sensitive")
p := testProvider("aws")

@ -90,8 +90,11 @@ func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *In
given = defaultVal // must be set, because we checked above that the variable isn't required
}
// Apply defaults from the variable's type constraint to the given value
if cfg.TypeDefaults != nil {
// Apply defaults from the variable's type constraint to the given value,
// unless the given value is null. We do not apply defaults to top-level
// null values, as doing so could prevent assigning null to a nullable
// variable.
if cfg.TypeDefaults != nil && !given.IsNull() {
given = cfg.TypeDefaults.Apply(given)
}

Loading…
Cancel
Save