diff --git a/internal/plans/objchange/objchange.go b/internal/plans/objchange/objchange.go index 534b14dfe9ab..b1ef714943ee 100644 --- a/internal/plans/objchange/objchange.go +++ b/internal/plans/objchange/objchange.go @@ -301,11 +301,7 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf // For non-computed NestedType attributes, we need to descend // into the individual nested attributes to build the final // value, unless the entire nested attribute is unknown. - if !configV.IsKnown() { - newV = configV - } else { - newV = proposedNewNestedType(attr.NestedType, priorV, configV) - } + newV = proposedNewNestedType(attr.NestedType, priorV, configV) default: // For non-computed attributes, we always take the config value, // even if it is null. If it's _required_ then null values @@ -319,12 +315,19 @@ func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, conf } func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { + // if the config isn't known at all, then we must use that value + if !config.IsNull() && !config.IsKnown() { + return config + } + // If the config is null or empty, we will be using this default value. newV := config switch schema.Nesting { case configschema.NestingSingle: - // if the config is null, we already have our value + // If the config is null, we already have our value. If the attribute + // is optional+computed, we won't reach this branch with a null value + // since the computed case would have been taken. if config.IsNull() { break } @@ -334,7 +337,7 @@ func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) case configschema.NestingList: // Nested blocks are correlated by index. configVLen := 0 - if config.IsKnown() && !config.IsNull() { + if !config.IsNull() { configVLen = config.LengthInt() } diff --git a/internal/plans/objchange/objchange_test.go b/internal/plans/objchange/objchange_test.go index 277e5e5dec3f..a2ccf2bd2523 100644 --- a/internal/plans/objchange/objchange_test.go +++ b/internal/plans/objchange/objchange_test.go @@ -424,6 +424,49 @@ func TestProposedNew(t *testing.T) { })), }), }, + + "prior optional computed nested single to null": { + &configschema.Block{ + 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, + Computed: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), + }, + "prior nested list": { &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{