NestingSingle blocks can be null

NestingSingle blocks removed from from the config were causing a plan to
error out with "... planned for existence but config wants absence".
Terraform core was proposing an incorrect value in this case, taking the
prior instead as a fallback because a null value was not expected.

Unlike other collection nesting modes, a NestingSingle block not present
in the configuration is a null value, and should be allowed when
planning a new value rather than building an empty object or falling
back to the prior value.
This commit is contained in:
James Bardin 2023-01-04 16:29:45 -05:00
parent 6e2a7496c4
commit d5d6d61c4c
2 changed files with 89 additions and 3 deletions

View File

@ -69,10 +69,15 @@ func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty
func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value {
if config.IsNull() || !config.IsKnown() {
// This is a weird situation, but we'll allow it anyway to free
// callers from needing to specifically check for these cases.
// A block config should never be null at this point. The only nullable
// block type is NestingSingle, which will return early before coming
// back here. We'll allow the null here anyway to free callers from
// needing to specifically check for these cases, and any mismatch will
// be caught in validation, so just take the prior value rather than
// the invalid null.
return prior
}
if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) {
panic("ProposedNew only supports object-typed values")
}
@ -106,7 +111,17 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.
switch schema.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
case configschema.NestingSingle:
// A NestingSingle configuration block value can be null, and since it
// cannot be computed we can always take the configuration value.
if config.IsNull() {
newV = config
break
}
// Otherwise use the same assignment rules as NestingGroup
fallthrough
case configschema.NestingGroup:
newV = ProposedNew(&schema.Block, prior, config)
case configschema.NestingList:

View File

@ -353,6 +353,77 @@ func TestProposedNew(t *testing.T) {
}),
}),
},
"prior nested single to null": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{
"foo": {
Nesting: configschema.NestingSingle,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"bar": {
Type: cty.String,
Optional: true,
Computed: true,
},
"baz": {
Type: cty.String,
Optional: true,
Computed: true,
},
},
},
},
},
Attributes: map[string]*configschema.Attribute{
"bloop": {
NestedType: &configschema.Object{
Nesting: configschema.NestingSingle,
Attributes: map[string]*configschema.Attribute{
"blop": {
Type: cty.String,
Required: true,
},
"bleep": {
Type: cty.String,
Optional: true,
},
},
},
Optional: true,
},
},
},
cty.ObjectVal(map[string]cty.Value{
"foo": cty.ObjectVal(map[string]cty.Value{
"bar": cty.StringVal("beep"),
"baz": cty.StringVal("boop"),
}),
"bloop": cty.ObjectVal(map[string]cty.Value{
"blop": cty.StringVal("glub"),
"bleep": cty.NullVal(cty.String),
}),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.String,
})),
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
})),
}),
cty.ObjectVal(map[string]cty.Value{
"foo": cty.NullVal(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.String,
})),
"bloop": cty.NullVal(cty.Object(map[string]cty.Type{
"blop": cty.String,
"bleep": cty.String,
})),
}),
},
"prior nested list": {
&configschema.Block{
BlockTypes: map[string]*configschema.NestedBlock{