From d5d6d61c4cc225b9405bcd5011090d9069c50176 Mon Sep 17 00:00:00 2001 From: James Bardin Date: Wed, 4 Jan 2023 16:29:45 -0500 Subject: [PATCH] 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. --- internal/plans/objchange/objchange.go | 21 ++++++- internal/plans/objchange/objchange_test.go | 71 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/internal/plans/objchange/objchange.go b/internal/plans/objchange/objchange.go index 3164099918..8c5d25f51f 100644 --- a/internal/plans/objchange/objchange.go +++ b/internal/plans/objchange/objchange.go @@ -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: diff --git a/internal/plans/objchange/objchange_test.go b/internal/plans/objchange/objchange_test.go index 4c434d3e42..3a1d3661c0 100644 --- a/internal/plans/objchange/objchange_test.go +++ b/internal/plans/objchange/objchange_test.go @@ -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{