diff --git a/.changes/unreleased/BUG FIXES-20240325-101108.yaml b/.changes/unreleased/BUG FIXES-20240325-101108.yaml new file mode 100644 index 000000000..74cafbb89 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20240325-101108.yaml @@ -0,0 +1,6 @@ +kind: BUG FIXES +body: 'resource: Ensured computed-only dynamic attributes will not cause `wrong final + value type` errors during planning' +time: 2024-03-25T10:11:08.813527-04:00 +custom: + Issue: "969" diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index c0df76647..f769eea3f 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) // PlanResourceChangeRequest is the framework server request for the @@ -406,9 +407,19 @@ func MarkComputedNilsAsUnknown(ctx context.Context, config tftypes.Value, resour } } + // Value type from planned state to create unknown with + newValueType := val.Type() + + // If the attribute is dynamic then we can't use the planned state value to create an unknown, as it may be a concrete type. + // This logic explicitly sets the unknown value type to dynamic so the type can be determined during apply. + _, isDynamic := attribute.GetType().(basetypes.DynamicTypable) + if isDynamic { + newValueType = tftypes.DynamicPseudoType + } + logging.FrameworkDebug(ctx, "marking computed attribute that is null in the config as unknown") - return tftypes.NewValue(val.Type(), tftypes.UnknownValue), nil + return tftypes.NewValue(newValueType, tftypes.UnknownValue), nil } } diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index b511face5..e8bd95c0c 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -78,7 +78,7 @@ func TestMarkComputedNilsAsUnknown(t *testing.T) { "dynamic-nil-computed": schema.DynamicAttribute{ Computed: true, }, - // underlying nil computed values should be turned into unknown, preserving the original type + // underlying nil computed values should be turned into unknown, this should not preserve the original type "dynamic-underlying-string-nil-computed": schema.DynamicAttribute{ Computed: true, }, @@ -274,7 +274,7 @@ func TestMarkComputedNilsAsUnknown(t *testing.T) { "dynamic-value": tftypes.NewValue(tftypes.String, "hello, world"), "dynamic-nil": tftypes.NewValue(tftypes.DynamicPseudoType, nil), "dynamic-nil-computed": tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue), - "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), // Preserves the underlying string type + "dynamic-underlying-string-nil-computed": tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue), // doesn't preserve original type "dynamic-nil-optional-computed": tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue), "dynamic-value-optional-computed": tftypes.NewValue(tftypes.String, "hello, world"), "dynamic-value-with-underlying-list-optional-computed": tftypes.NewValue(