diff --git a/.changes/unreleased/BUG FIXES-20250612-115411.yaml b/.changes/unreleased/BUG FIXES-20250612-115411.yaml new file mode 100644 index 000000000..2e17c03d4 --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20250612-115411.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'all: Fixed bug with `UseStateForUnknown` where known null state values were not preserved during update plans.' +time: 2025-06-12T11:54:11.818923-04:00 +custom: + Issue: "1117" diff --git a/internal/fwserver/attribute_plan_modification_test.go b/internal/fwserver/attribute_plan_modification_test.go index a5c717964..c0b635b4c 100644 --- a/internal/fwserver/attribute_plan_modification_test.go +++ b/internal/fwserver/attribute_plan_modification_test.go @@ -641,6 +641,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -723,6 +761,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: testtypes.ListValueWithSemanticEquals{ ListValue: types.ListValueMust( types.ObjectType{ @@ -979,6 +1055,60 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -1537,6 +1667,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -1619,6 +1787,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: testtypes.SetValueWithSemanticEquals{ SetValue: types.SetValueMust( types.ObjectType{ @@ -1742,6 +1948,60 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -1887,6 +2147,60 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2016,6 +2330,60 @@ func TestAttributeModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2581,6 +2949,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "key1": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.MapValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2663,6 +3069,44 @@ func TestAttributeModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Map{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + map[string]tftypes.Value{ + "key1": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: testtypes.MapValueWithSemanticEquals{ MapValue: types.MapValueMust( types.ObjectType{ @@ -2977,6 +3421,31 @@ func TestAttributeModifyPlan(t *testing.T) { "nested_computed": types.StringType, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, AttributeState: types.ObjectValueMust( map[string]attr.Type{ "nested_computed": types.StringType, @@ -3035,6 +3504,31 @@ func TestAttributeModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, AttributeState: testtypes.ObjectValueWithSemanticEquals{ ObjectValue: types.ObjectValueMust( map[string]attr.Type{ diff --git a/internal/fwserver/block_plan_modification_test.go b/internal/fwserver/block_plan_modification_test.go index 53f18579f..69d9e9b6e 100644 --- a/internal/fwserver/block_plan_modification_test.go +++ b/internal/fwserver/block_plan_modification_test.go @@ -996,6 +996,44 @@ func TestBlockModifyPlan(t *testing.T) { }, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -1075,6 +1113,44 @@ func TestBlockModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: testtypes.ListValueWithSemanticEquals{ ListValue: types.ListValueMust( types.ObjectType{ @@ -1874,6 +1950,92 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "list": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "one"), + "list": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue"), + "nested_required": tftypes.NewValue(tftypes.String, "configvalue"), + }, + ), + }, + ), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -1989,7 +2151,7 @@ func TestBlockModifyPlan(t *testing.T) { NestedObject: testschema.NestedBlockObject{ Attributes: map[string]fwschema.Attribute{ "nested_computed": testschema.AttributeWithStringPlanModifiers{ - Required: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -2065,6 +2227,60 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2138,7 +2354,7 @@ func TestBlockModifyPlan(t *testing.T) { NestedObject: testschema.NestedBlockObject{ Attributes: map[string]fwschema.Attribute{ "nested_computed": testschema.AttributeWithStringPlanModifiers{ - Required: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -2194,6 +2410,60 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.List{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.ListValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2255,7 +2525,7 @@ func TestBlockModifyPlan(t *testing.T) { NestedObject: testschema.NestedBlockObject{ Attributes: map[string]fwschema.Attribute{ "nested_computed": testschema.AttributeWithStringPlanModifiers{ - Required: true, + Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -2331,6 +2601,60 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2476,6 +2800,60 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2605,6 +2983,60 @@ func TestBlockModifyPlan(t *testing.T) { ), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue1"), + }, + ), + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue2"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue2"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2689,6 +3121,44 @@ func TestBlockModifyPlan(t *testing.T) { }, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: types.SetValueMust( types.ObjectType{ AttrTypes: map[string]attr.Type{ @@ -2768,6 +3238,44 @@ func TestBlockModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Set{ + ElementType: tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + []tftypes.Value{ + tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, + ), + }, AttributeState: testtypes.SetValueWithSemanticEquals{ SetValue: types.SetValueMust( types.ObjectType{ @@ -3188,6 +3696,34 @@ func TestBlockModifyPlan(t *testing.T) { "nested_required": types.StringValue("testvalue"), }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + "nested_required": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue"), + "nested_required": tftypes.NewValue(tftypes.String, "testvalue"), + }, + ), + }, + ), + }, AttributeState: types.ObjectValueMust( map[string]attr.Type{ "nested_computed": types.StringType, @@ -3236,6 +3772,31 @@ func TestBlockModifyPlan(t *testing.T) { "nested_computed": types.StringType, }, ), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, AttributeState: types.ObjectValueMust( map[string]attr.Type{ "nested_computed": types.StringType, @@ -3291,6 +3852,31 @@ func TestBlockModifyPlan(t *testing.T) { }, ), }, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "nested_computed": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "nested_computed": tftypes.NewValue(tftypes.String, "statevalue1"), + }, + ), + }, + ), + }, AttributeState: testtypes.ObjectValueWithSemanticEquals{ ObjectValue: types.ObjectValueMust( map[string]attr.Type{ diff --git a/resource/schema/boolplanmodifier/use_state_for_unknown.go b/resource/schema/boolplanmodifier/use_state_for_unknown.go index efab19637..693303e77 100644 --- a/resource/schema/boolplanmodifier/use_state_for_unknown.go +++ b/resource/schema/boolplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyBool implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyBool(_ context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/boolplanmodifier/use_state_for_unknown_test.go b/resource/schema/boolplanmodifier/use_state_for_unknown_test.go index c385c8c75..f7d3c56ff 100644 --- a/resource/schema/boolplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/boolplanmodifier/use_state_for_unknown_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { @@ -26,6 +26,16 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.BoolRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Bool, + }, + }, + nil, + ), + }, StateValue: types.BoolNull(), PlanValue: types.BoolUnknown(), ConfigValue: types.BoolNull(), @@ -42,6 +52,18 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.BoolRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Bool, false), + }, + ), + }, StateValue: types.BoolValue(false), PlanValue: types.BoolValue(true), ConfigValue: types.BoolNull(), @@ -50,10 +72,22 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { PlanValue: types.BoolValue(true), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.BoolRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Bool, true), + }, + ), + }, StateValue: types.BoolValue(true), PlanValue: types.BoolUnknown(), ConfigValue: types.BoolNull(), @@ -62,6 +96,29 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { PlanValue: types.BoolValue(true), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.BoolRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Bool, nil), + }, + ), + }, + StateValue: types.BoolNull(), + PlanValue: types.BoolUnknown(), + ConfigValue: types.BoolNull(), + }, + expected: &planmodifier.BoolResponse{ + PlanValue: types.BoolNull(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +135,6 @@ func TestUseStateForUnknownModifierPlanModifyBool(t *testing.T) { PlanValue: types.BoolUnknown(), }, }, - "under-list": { - request: planmodifier.BoolRequest{ - ConfigValue: types.BoolNull(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.BoolUnknown(), - StateValue: types.BoolNull(), - }, - expected: &planmodifier.BoolResponse{ - PlanValue: types.BoolUnknown(), - }, - }, - "under-set": { - request: planmodifier.BoolRequest{ - ConfigValue: types.BoolNull(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.BoolType, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.BoolType, - }, - map[string]attr.Value{ - "nested_test": types.BoolUnknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.BoolUnknown(), - StateValue: types.BoolNull(), - }, - expected: &planmodifier.BoolResponse{ - PlanValue: types.BoolUnknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/dynamicplanmodifier/use_state_for_unknown.go b/resource/schema/dynamicplanmodifier/use_state_for_unknown.go index 2e3f256ec..4a11f3ffa 100644 --- a/resource/schema/dynamicplanmodifier/use_state_for_unknown.go +++ b/resource/schema/dynamicplanmodifier/use_state_for_unknown.go @@ -36,9 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyDynamic implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyDynamic(ctx context.Context, req planmodifier.DynamicRequest, resp *planmodifier.DynamicResponse) { - // Do nothing if there is no state value. - // This also requires checking if the underlying value is null. - if req.StateValue.IsNull() || req.StateValue.IsUnderlyingValueNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go b/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go index 58e346bf1..642b947f3 100644 --- a/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/dynamicplanmodifier/use_state_for_unknown_test.go @@ -10,7 +10,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { @@ -23,6 +25,16 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { "null-state": { // when we first create the resource, use the unknown value request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + nil, + ), + }, StateValue: types.DynamicNull(), PlanValue: types.DynamicUnknown(), ConfigValue: types.DynamicNull(), @@ -31,23 +43,23 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { PlanValue: types.DynamicUnknown(), }, }, - "null-underlying-state-value": { - // if the state value has a known underlying type, but a null underlying value, - // use the unknown value - request: planmodifier.DynamicRequest{ - StateValue: types.DynamicValue(types.StringNull()), - PlanValue: types.DynamicUnknown(), - ConfigValue: types.DynamicNull(), - }, - expected: &planmodifier.DynamicResponse{ - PlanValue: types.DynamicUnknown(), - }, - }, "known-plan": { // this would really only happen if we had a plan // modifier setting the value before this plan modifier // got to it. We still want to preserve that value, in this case request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, StateValue: types.DynamicValue(types.StringValue("other")), PlanValue: types.DynamicValue(types.StringValue("test")), ConfigValue: types.DynamicNull(), @@ -61,6 +73,18 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { // modifier setting the value before this plan modifier // got to it. We still want to preserve that value, in this case request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, StateValue: types.DynamicValue(types.StringValue("other")), PlanValue: types.DynamicNull(), ConfigValue: types.DynamicNull(), @@ -74,6 +98,18 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { // modifier setting the value before this plan modifier // got to it. We still want to preserve that value, in this case request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, StateValue: types.DynamicValue(types.StringValue("other")), PlanValue: types.DynamicValue(types.StringNull()), ConfigValue: types.DynamicNull(), @@ -82,9 +118,21 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { PlanValue: types.DynamicValue(types.StringNull()), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state in request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, StateValue: types.DynamicValue(types.StringValue("test")), PlanValue: types.DynamicUnknown(), ConfigValue: types.DynamicNull(), @@ -93,10 +141,22 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { PlanValue: types.DynamicValue(types.StringValue("test")), }, }, - "non-null-state-unknown-underlying-plan-value": { + "non-null-state-value-unknown-underlying-plan-value": { // if the plan value has a known underlying type, but an unknown underlying value // we want to preserve the state request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, StateValue: types.DynamicValue(types.StringValue("test")), PlanValue: types.DynamicValue(types.StringUnknown()), ConfigValue: types.DynamicNull(), @@ -105,6 +165,52 @@ func TestUseStateForUnknownModifierPlanModifyDynamic(t *testing.T) { PlanValue: types.DynamicValue(types.StringValue("test")), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.DynamicPseudoType, nil), + }, + ), + }, + StateValue: types.DynamicNull(), + PlanValue: types.DynamicUnknown(), + ConfigValue: types.DynamicNull(), + }, + expected: &planmodifier.DynamicResponse{ + PlanValue: types.DynamicNull(), + }, + }, + "null-underlying-state-value-unknown-plan": { + // if the state value has a known underlying type, but a null underlying value, we should preserve this as well. + request: planmodifier.DynamicRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.DynamicPseudoType, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + StateValue: types.DynamicValue(types.StringNull()), + PlanValue: types.DynamicUnknown(), + ConfigValue: types.DynamicNull(), + }, + expected: &planmodifier.DynamicResponse{ + PlanValue: types.DynamicValue(types.StringNull()), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still diff --git a/resource/schema/float32planmodifier/use_state_for_unknown.go b/resource/schema/float32planmodifier/use_state_for_unknown.go index fc8eadb92..81d5ac09d 100644 --- a/resource/schema/float32planmodifier/use_state_for_unknown.go +++ b/resource/schema/float32planmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyFloat32 implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyFloat32(_ context.Context, req planmodifier.Float32Request, resp *planmodifier.Float32Response) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/float32planmodifier/use_state_for_unknown_test.go b/resource/schema/float32planmodifier/use_state_for_unknown_test.go index 2151333aa..b11845cde 100644 --- a/resource/schema/float32planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/float32planmodifier/use_state_for_unknown_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float32planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { @@ -26,6 +26,16 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.Float32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + nil, + ), + }, StateValue: types.Float32Null(), PlanValue: types.Float32Unknown(), ConfigValue: types.Float32Null(), @@ -42,6 +52,18 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.Float32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 2.4), + }, + ), + }, StateValue: types.Float32Value(2.4), PlanValue: types.Float32Value(1.2), ConfigValue: types.Float32Null(), @@ -50,10 +72,22 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { PlanValue: types.Float32Value(1.2), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.Float32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, StateValue: types.Float32Value(1.2), PlanValue: types.Float32Unknown(), ConfigValue: types.Float32Null(), @@ -62,6 +96,29 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { PlanValue: types.Float32Value(1.2), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.Float32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + StateValue: types.Float32Null(), + PlanValue: types.Float32Unknown(), + ConfigValue: types.Float32Null(), + }, + expected: &planmodifier.Float32Response{ + PlanValue: types.Float32Null(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +135,6 @@ func TestUseStateForUnknownModifierPlanModifyFloat32(t *testing.T) { PlanValue: types.Float32Unknown(), }, }, - "under-list": { - request: planmodifier.Float32Request{ - ConfigValue: types.Float32Null(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.Float32Unknown(), - StateValue: types.Float32Null(), - }, - expected: &planmodifier.Float32Response{ - PlanValue: types.Float32Unknown(), - }, - }, - "under-set": { - request: planmodifier.Float32Request{ - ConfigValue: types.Float32Null(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.Float32Type, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.Float32Type, - }, - map[string]attr.Value{ - "nested_test": types.Float32Unknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.Float32Unknown(), - StateValue: types.Float32Null(), - }, - expected: &planmodifier.Float32Response{ - PlanValue: types.Float32Unknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/float64planmodifier/use_state_for_unknown.go b/resource/schema/float64planmodifier/use_state_for_unknown.go index 923bd34bd..2d2491fb1 100644 --- a/resource/schema/float64planmodifier/use_state_for_unknown.go +++ b/resource/schema/float64planmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyFloat64 implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyFloat64(_ context.Context, req planmodifier.Float64Request, resp *planmodifier.Float64Response) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/float64planmodifier/use_state_for_unknown_test.go b/resource/schema/float64planmodifier/use_state_for_unknown_test.go index 75f439847..03cae9cb0 100644 --- a/resource/schema/float64planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/float64planmodifier/use_state_for_unknown_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { @@ -26,6 +26,16 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.Float64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + nil, + ), + }, StateValue: types.Float64Null(), PlanValue: types.Float64Unknown(), ConfigValue: types.Float64Null(), @@ -42,6 +52,18 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.Float64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 2.4), + }, + ), + }, StateValue: types.Float64Value(2.4), PlanValue: types.Float64Value(1.2), ConfigValue: types.Float64Null(), @@ -50,10 +72,22 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { PlanValue: types.Float64Value(1.2), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.Float64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, StateValue: types.Float64Value(1.2), PlanValue: types.Float64Unknown(), ConfigValue: types.Float64Null(), @@ -62,6 +96,29 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { PlanValue: types.Float64Value(1.2), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.Float64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + StateValue: types.Float64Null(), + PlanValue: types.Float64Unknown(), + ConfigValue: types.Float64Null(), + }, + expected: &planmodifier.Float64Response{ + PlanValue: types.Float64Null(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +135,6 @@ func TestUseStateForUnknownModifierPlanModifyFloat64(t *testing.T) { PlanValue: types.Float64Unknown(), }, }, - "under-list": { - request: planmodifier.Float64Request{ - ConfigValue: types.Float64Null(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.Float64Unknown(), - StateValue: types.Float64Null(), - }, - expected: &planmodifier.Float64Response{ - PlanValue: types.Float64Unknown(), - }, - }, - "under-set": { - request: planmodifier.Float64Request{ - ConfigValue: types.Float64Null(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.Float64Type, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.Float64Type, - }, - map[string]attr.Value{ - "nested_test": types.Float64Unknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.Float64Unknown(), - StateValue: types.Float64Null(), - }, - expected: &planmodifier.Float64Response{ - PlanValue: types.Float64Unknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/int32planmodifier/use_state_for_unknown.go b/resource/schema/int32planmodifier/use_state_for_unknown.go index 1a09f8707..0fb28639b 100644 --- a/resource/schema/int32planmodifier/use_state_for_unknown.go +++ b/resource/schema/int32planmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyInt32 implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyInt32(_ context.Context, req planmodifier.Int32Request, resp *planmodifier.Int32Response) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/int32planmodifier/use_state_for_unknown_test.go b/resource/schema/int32planmodifier/use_state_for_unknown_test.go index 45bdc2b10..14f1bc4bb 100644 --- a/resource/schema/int32planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/int32planmodifier/use_state_for_unknown_test.go @@ -9,11 +9,11 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { @@ -27,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.Int32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + nil, + ), + }, StateValue: types.Int32Null(), PlanValue: types.Int32Unknown(), ConfigValue: types.Int32Null(), @@ -43,6 +53,18 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.Int32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 2), + }, + ), + }, StateValue: types.Int32Value(2), PlanValue: types.Int32Value(1), ConfigValue: types.Int32Null(), @@ -51,10 +73,22 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { PlanValue: types.Int32Value(1), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.Int32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 1), + }, + ), + }, StateValue: types.Int32Value(1), PlanValue: types.Int32Unknown(), ConfigValue: types.Int32Null(), @@ -63,6 +97,29 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { PlanValue: types.Int32Value(1), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.Int32Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + StateValue: types.Int32Null(), + PlanValue: types.Int32Unknown(), + ConfigValue: types.Int32Null(), + }, + expected: &planmodifier.Int32Response{ + PlanValue: types.Int32Null(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -79,46 +136,6 @@ func TestUseStateForUnknownModifierPlanModifyInt32(t *testing.T) { PlanValue: types.Int32Unknown(), }, }, - "under-list": { - request: planmodifier.Int32Request{ - ConfigValue: types.Int32Null(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.Int32Unknown(), - StateValue: types.Int32Null(), - }, - expected: &planmodifier.Int32Response{ - PlanValue: types.Int32Unknown(), - }, - }, - "under-set": { - request: planmodifier.Int32Request{ - ConfigValue: types.Int32Null(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.Int32Type, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.Int32Type, - }, - map[string]attr.Value{ - "nested_test": types.Int32Unknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.Int32Unknown(), - StateValue: types.Int32Null(), - }, - expected: &planmodifier.Int32Response{ - PlanValue: types.Int32Unknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/int64planmodifier/use_state_for_unknown.go b/resource/schema/int64planmodifier/use_state_for_unknown.go index dbee8a08a..ae94f2663 100644 --- a/resource/schema/int64planmodifier/use_state_for_unknown.go +++ b/resource/schema/int64planmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyInt64 implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyInt64(_ context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/int64planmodifier/use_state_for_unknown_test.go b/resource/schema/int64planmodifier/use_state_for_unknown_test.go index c588e5e67..40145b915 100644 --- a/resource/schema/int64planmodifier/use_state_for_unknown_test.go +++ b/resource/schema/int64planmodifier/use_state_for_unknown_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { @@ -26,6 +26,16 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.Int64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + nil, + ), + }, StateValue: types.Int64Null(), PlanValue: types.Int64Unknown(), ConfigValue: types.Int64Null(), @@ -42,6 +52,18 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.Int64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 2), + }, + ), + }, StateValue: types.Int64Value(2), PlanValue: types.Int64Value(1), ConfigValue: types.Int64Null(), @@ -50,10 +72,22 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { PlanValue: types.Int64Value(1), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.Int64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 1), + }, + ), + }, StateValue: types.Int64Value(1), PlanValue: types.Int64Unknown(), ConfigValue: types.Int64Null(), @@ -62,6 +96,29 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { PlanValue: types.Int64Value(1), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.Int64Request{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + StateValue: types.Int64Null(), + PlanValue: types.Int64Unknown(), + ConfigValue: types.Int64Null(), + }, + expected: &planmodifier.Int64Response{ + PlanValue: types.Int64Null(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +135,6 @@ func TestUseStateForUnknownModifierPlanModifyInt64(t *testing.T) { PlanValue: types.Int64Unknown(), }, }, - "under-list": { - request: planmodifier.Int64Request{ - ConfigValue: types.Int64Null(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.Int64Unknown(), - StateValue: types.Int64Null(), - }, - expected: &planmodifier.Int64Response{ - PlanValue: types.Int64Unknown(), - }, - }, - "under-set": { - request: planmodifier.Int64Request{ - ConfigValue: types.Int64Null(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.Int64Type, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.Int64Type, - }, - map[string]attr.Value{ - "nested_test": types.Int64Unknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.Int64Unknown(), - StateValue: types.Int64Null(), - }, - expected: &planmodifier.Int64Response{ - PlanValue: types.Int64Unknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/listplanmodifier/use_state_for_unknown.go b/resource/schema/listplanmodifier/use_state_for_unknown.go index c8b2f3bf5..d71b6717f 100644 --- a/resource/schema/listplanmodifier/use_state_for_unknown.go +++ b/resource/schema/listplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyList implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyList(_ context.Context, req planmodifier.ListRequest, resp *planmodifier.ListResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/listplanmodifier/use_state_for_unknown_test.go b/resource/schema/listplanmodifier/use_state_for_unknown_test.go index e41b07cac..ed4a82cc6 100644 --- a/resource/schema/listplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/listplanmodifier/use_state_for_unknown_test.go @@ -9,10 +9,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { @@ -26,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.ListRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.List{ElementType: tftypes.String}, + }, + }, + nil, + ), + }, StateValue: types.ListNull(types.StringType), PlanValue: types.ListUnknown(types.StringType), ConfigValue: types.ListNull(types.StringType), @@ -42,6 +53,23 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.ListRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, + ), + }, StateValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("other")}), PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), ConfigValue: types.ListNull(types.StringType), @@ -50,10 +78,27 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.ListRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, StateValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), PlanValue: types.ListUnknown(types.StringType), ConfigValue: types.ListNull(types.StringType), @@ -62,6 +107,32 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { PlanValue: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("test")}), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.ListRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.List{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.List{ElementType: tftypes.String}, + nil, + ), + }, + ), + }, + StateValue: types.ListNull(types.StringType), + PlanValue: types.ListUnknown(types.StringType), + ConfigValue: types.ListNull(types.StringType), + }, + expected: &planmodifier.ListResponse{ + PlanValue: types.ListNull(types.StringType), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +149,6 @@ func TestUseStateForUnknownModifierPlanModifyList(t *testing.T) { PlanValue: types.ListUnknown(types.StringType), }, }, - "under-list": { - request: planmodifier.ListRequest{ - ConfigValue: types.ListNull(types.StringType), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.ListUnknown(types.StringType), - StateValue: types.ListNull(types.StringType), - }, - expected: &planmodifier.ListResponse{ - PlanValue: types.ListUnknown(types.StringType), - }, - }, - "under-set": { - request: planmodifier.ListRequest{ - ConfigValue: types.ListNull(types.StringType), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.ListType{ElemType: types.StringType}, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.ListType{ElemType: types.StringType}, - }, - map[string]attr.Value{ - "nested_test": types.ListUnknown(types.StringType), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.ListUnknown(types.StringType), - StateValue: types.ListNull(types.StringType), - }, - expected: &planmodifier.ListResponse{ - PlanValue: types.ListUnknown(types.StringType), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/mapplanmodifier/use_state_for_unknown.go b/resource/schema/mapplanmodifier/use_state_for_unknown.go index 748353583..0933cfe98 100644 --- a/resource/schema/mapplanmodifier/use_state_for_unknown.go +++ b/resource/schema/mapplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyMap implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyMap(_ context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/mapplanmodifier/use_state_for_unknown_test.go b/resource/schema/mapplanmodifier/use_state_for_unknown_test.go index 1ea2aa940..87d3a9d49 100644 --- a/resource/schema/mapplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/mapplanmodifier/use_state_for_unknown_test.go @@ -9,10 +9,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { @@ -26,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.MapRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Map{ElementType: tftypes.String}, + }, + }, + nil, + ), + }, StateValue: types.MapNull(types.StringType), PlanValue: types.MapUnknown(types.StringType), ConfigValue: types.MapNull(types.StringType), @@ -42,6 +53,23 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.MapRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, + ), + }, StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"testkey": types.StringValue("other")}), PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"testkey": types.StringValue("test")}), ConfigValue: types.MapNull(types.StringType), @@ -50,10 +78,27 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"testkey": types.StringValue("test")}), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.MapRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + map[string]tftypes.Value{ + "testkey": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, StateValue: types.MapValueMust(types.StringType, map[string]attr.Value{"testkey": types.StringValue("test")}), PlanValue: types.MapUnknown(types.StringType), ConfigValue: types.MapNull(types.StringType), @@ -62,6 +107,32 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { PlanValue: types.MapValueMust(types.StringType, map[string]attr.Value{"testkey": types.StringValue("test")}), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.MapRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Map{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Map{ElementType: tftypes.String}, + nil, + ), + }, + ), + }, + StateValue: types.MapNull(types.StringType), + PlanValue: types.MapUnknown(types.StringType), + ConfigValue: types.MapNull(types.StringType), + }, + expected: &planmodifier.MapResponse{ + PlanValue: types.MapNull(types.StringType), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +149,6 @@ func TestUseStateForUnknownModifierPlanModifyMap(t *testing.T) { PlanValue: types.MapUnknown(types.StringType), }, }, - "under-list": { - request: planmodifier.MapRequest{ - ConfigValue: types.MapNull(types.StringType), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.MapUnknown(types.StringType), - StateValue: types.MapNull(types.StringType), - }, - expected: &planmodifier.MapResponse{ - PlanValue: types.MapUnknown(types.StringType), - }, - }, - "under-set": { - request: planmodifier.MapRequest{ - ConfigValue: types.MapNull(types.StringType), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.MapType{ElemType: types.StringType}, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.MapType{ElemType: types.StringType}, - }, - map[string]attr.Value{ - "nested_test": types.MapUnknown(types.StringType), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.MapUnknown(types.StringType), - StateValue: types.MapNull(types.StringType), - }, - expected: &planmodifier.MapResponse{ - PlanValue: types.MapUnknown(types.StringType), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/numberplanmodifier/use_state_for_unknown.go b/resource/schema/numberplanmodifier/use_state_for_unknown.go index 44a207c92..94c79be5f 100644 --- a/resource/schema/numberplanmodifier/use_state_for_unknown.go +++ b/resource/schema/numberplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyNumber implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyNumber(_ context.Context, req planmodifier.NumberRequest, resp *planmodifier.NumberResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/numberplanmodifier/use_state_for_unknown_test.go b/resource/schema/numberplanmodifier/use_state_for_unknown_test.go index b910a4335..e8cfa748d 100644 --- a/resource/schema/numberplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/numberplanmodifier/use_state_for_unknown_test.go @@ -9,11 +9,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/numberplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { @@ -27,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.NumberRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + nil, + ), + }, StateValue: types.NumberNull(), PlanValue: types.NumberUnknown(), ConfigValue: types.NumberNull(), @@ -43,6 +53,18 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.NumberRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 2.4), + }, + ), + }, StateValue: types.NumberValue(big.NewFloat(2.4)), PlanValue: types.NumberValue(big.NewFloat(1.2)), ConfigValue: types.NumberNull(), @@ -51,10 +73,22 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { PlanValue: types.NumberValue(big.NewFloat(1.2)), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.NumberRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, 1.2), + }, + ), + }, StateValue: types.NumberValue(big.NewFloat(1.2)), PlanValue: types.NumberUnknown(), ConfigValue: types.NumberNull(), @@ -63,6 +97,29 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { PlanValue: types.NumberValue(big.NewFloat(1.2)), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.NumberRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Number, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.Number, nil), + }, + ), + }, + StateValue: types.NumberNull(), + PlanValue: types.NumberUnknown(), + ConfigValue: types.NumberNull(), + }, + expected: &planmodifier.NumberResponse{ + PlanValue: types.NumberNull(), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -79,46 +136,6 @@ func TestUseStateForUnknownModifierPlanModifyNumber(t *testing.T) { PlanValue: types.NumberUnknown(), }, }, - "under-list": { - request: planmodifier.NumberRequest{ - ConfigValue: types.NumberNull(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.NumberUnknown(), - StateValue: types.NumberNull(), - }, - expected: &planmodifier.NumberResponse{ - PlanValue: types.NumberUnknown(), - }, - }, - "under-set": { - request: planmodifier.NumberRequest{ - ConfigValue: types.NumberNull(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.NumberType, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.NumberType, - }, - map[string]attr.Value{ - "nested_test": types.NumberUnknown(), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.NumberUnknown(), - StateValue: types.NumberNull(), - }, - expected: &planmodifier.NumberResponse{ - PlanValue: types.NumberUnknown(), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/objectplanmodifier/use_state_for_unknown.go b/resource/schema/objectplanmodifier/use_state_for_unknown.go index 67b13f18b..3e5b7e67f 100644 --- a/resource/schema/objectplanmodifier/use_state_for_unknown.go +++ b/resource/schema/objectplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyObject implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyObject(_ context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/objectplanmodifier/use_state_for_unknown_test.go b/resource/schema/objectplanmodifier/use_state_for_unknown_test.go index f90747529..e4b94ce16 100644 --- a/resource/schema/objectplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/objectplanmodifier/use_state_for_unknown_test.go @@ -9,10 +9,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { @@ -26,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.ObjectRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + nil, + ), + }, StateValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), @@ -42,6 +53,23 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.ObjectRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, + ), + }, StateValue: types.ObjectValueMust(map[string]attr.Type{"testattr": types.StringType}, map[string]attr.Value{"testattr": types.StringValue("other")}), PlanValue: types.ObjectValueMust(map[string]attr.Type{"testattr": types.StringType}, map[string]attr.Value{"testattr": types.StringValue("test")}), ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), @@ -50,10 +78,27 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { PlanValue: types.ObjectValueMust(map[string]attr.Type{"testattr": types.StringType}, map[string]attr.Value{"testattr": types.StringValue("test")}), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.ObjectRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + map[string]tftypes.Value{ + "testattr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, StateValue: types.ObjectValueMust(map[string]attr.Type{"testattr": types.StringType}, map[string]attr.Value{"testattr": types.StringValue("test")}), PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), @@ -62,6 +107,32 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { PlanValue: types.ObjectValueMust(map[string]attr.Type{"testattr": types.StringType}, map[string]attr.Value{"testattr": types.StringValue("test")}), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.ObjectRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Object{AttributeTypes: map[string]tftypes.Type{"testattr": tftypes.String}}, + nil, + ), + }, + ), + }, + StateValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), + PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), + ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), + }, + expected: &planmodifier.ObjectResponse{ + PlanValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +149,6 @@ func TestUseStateForUnknownModifierPlanModifyObject(t *testing.T) { PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), }, }, - "under-list": { - request: planmodifier.ObjectRequest{ - ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), - StateValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), - }, - expected: &planmodifier.ObjectResponse{ - PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), - }, - }, - "under-set": { - request: planmodifier.ObjectRequest{ - ConfigValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.ObjectType{AttrTypes: map[string]attr.Type{"testattr": types.StringType}}, - }, - map[string]attr.Value{ - "nested_test": types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), - StateValue: types.ObjectNull(map[string]attr.Type{"testattr": types.StringType}), - }, - expected: &planmodifier.ObjectResponse{ - PlanValue: types.ObjectUnknown(map[string]attr.Type{"testattr": types.StringType}), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/setplanmodifier/use_state_for_unknown.go b/resource/schema/setplanmodifier/use_state_for_unknown.go index 0bf359cd2..1bff4e27a 100644 --- a/resource/schema/setplanmodifier/use_state_for_unknown.go +++ b/resource/schema/setplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifySet implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifySet(_ context.Context, req planmodifier.SetRequest, resp *planmodifier.SetResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/setplanmodifier/use_state_for_unknown_test.go b/resource/schema/setplanmodifier/use_state_for_unknown_test.go index c55bc2034..c05e5400e 100644 --- a/resource/schema/setplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/setplanmodifier/use_state_for_unknown_test.go @@ -9,10 +9,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { @@ -26,6 +27,16 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.SetRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Set{ElementType: tftypes.String}, + }, + }, + nil, + ), + }, StateValue: types.SetNull(types.StringType), PlanValue: types.SetUnknown(types.StringType), ConfigValue: types.SetNull(types.StringType), @@ -42,6 +53,23 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.SetRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, + ), + }, StateValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("other")}), PlanValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), ConfigValue: types.SetNull(types.StringType), @@ -50,10 +78,27 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { PlanValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.SetRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + []tftypes.Value{ + tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, + ), + }, StateValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), PlanValue: types.SetUnknown(types.StringType), ConfigValue: types.SetNull(types.StringType), @@ -62,6 +107,32 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { PlanValue: types.SetValueMust(types.StringType, []attr.Value{types.StringValue("test")}), }, }, + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. + request: planmodifier.SetRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.Set{ElementType: tftypes.String}, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue( + tftypes.Set{ElementType: tftypes.String}, + nil, + ), + }, + ), + }, + StateValue: types.SetNull(types.StringType), + PlanValue: types.SetUnknown(types.StringType), + ConfigValue: types.SetNull(types.StringType), + }, + expected: &planmodifier.SetResponse{ + PlanValue: types.SetNull(types.StringType), + }, + }, "unknown-config": { // this is the situation in which a user is // interpolating into a field. We want that to still @@ -78,46 +149,6 @@ func TestUseStateForUnknownModifierPlanModifySet(t *testing.T) { PlanValue: types.SetUnknown(types.StringType), }, }, - "under-list": { - request: planmodifier.SetRequest{ - ConfigValue: types.SetNull(types.StringType), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.SetUnknown(types.StringType), - StateValue: types.SetNull(types.StringType), - }, - expected: &planmodifier.SetResponse{ - PlanValue: types.SetUnknown(types.StringType), - }, - }, - "under-set": { - request: planmodifier.SetRequest{ - ConfigValue: types.SetNull(types.StringType), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.SetType{ElemType: types.StringType}, - }, - }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.SetType{ElemType: types.StringType}, - }, - map[string]attr.Value{ - "nested_test": types.SetUnknown(types.StringType), - }, - ), - }, - ), - ).AtName("nested_test"), - PlanValue: types.SetUnknown(types.StringType), - StateValue: types.SetNull(types.StringType), - }, - expected: &planmodifier.SetResponse{ - PlanValue: types.SetUnknown(types.StringType), - }, - }, } for name, testCase := range testCases { diff --git a/resource/schema/stringplanmodifier/use_state_for_unknown.go b/resource/schema/stringplanmodifier/use_state_for_unknown.go index 983bc5cb0..a6d77962e 100644 --- a/resource/schema/stringplanmodifier/use_state_for_unknown.go +++ b/resource/schema/stringplanmodifier/use_state_for_unknown.go @@ -36,8 +36,8 @@ func (m useStateForUnknownModifier) MarkdownDescription(_ context.Context) strin // PlanModifyString implements the plan modification logic. func (m useStateForUnknownModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { - // Do nothing if there is no state value. - if req.StateValue.IsNull() { + // Do nothing if there is no state (resource is being created). + if req.State.Raw.IsNull() { return } diff --git a/resource/schema/stringplanmodifier/use_state_for_unknown_test.go b/resource/schema/stringplanmodifier/use_state_for_unknown_test.go index 6a686469c..7ed69d1d9 100644 --- a/resource/schema/stringplanmodifier/use_state_for_unknown_test.go +++ b/resource/schema/stringplanmodifier/use_state_for_unknown_test.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" ) func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { @@ -26,6 +26,16 @@ func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { // when we first create the resource, use the unknown // value request: planmodifier.StringRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.String, + }, + }, + nil, + ), + }, StateValue: types.StringNull(), PlanValue: types.StringUnknown(), ConfigValue: types.StringNull(), @@ -42,6 +52,18 @@ func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { // but we still want to preserve that value, in this // case request: planmodifier.StringRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "other"), + }, + ), + }, StateValue: types.StringValue("other"), PlanValue: types.StringValue("test"), ConfigValue: types.StringNull(), @@ -50,10 +72,22 @@ func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { PlanValue: types.StringValue("test"), }, }, - "non-null-state-unknown-plan": { + "non-null-state-value-unknown-plan": { // this is the situation we want to preserve the state // in request: planmodifier.StringRequest{ + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "test"), + }, + ), + }, StateValue: types.StringValue("test"), PlanValue: types.StringUnknown(), ConfigValue: types.StringNull(), @@ -62,57 +96,52 @@ func TestUseStateForUnknownModifierPlanModifyString(t *testing.T) { PlanValue: types.StringValue("test"), }, }, - "unknown-config": { - // this is the situation in which a user is - // interpolating into a field. We want that to still - // show up as unknown, otherwise they'll get apply-time - // errors for changing the value even though we knew it - // was legitimately possible for it to change and the - // provider can't prevent this from happening + "null-state-value-unknown-plan": { + // Null state values are still known, so we should preserve this as well. request: planmodifier.StringRequest{ - StateValue: types.StringValue("test"), + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, nil), + }, + ), + }, + StateValue: types.StringNull(), PlanValue: types.StringUnknown(), - ConfigValue: types.StringUnknown(), - }, - expected: &planmodifier.StringResponse{ - PlanValue: types.StringUnknown(), - }, - }, - "under-list": { - request: planmodifier.StringRequest{ ConfigValue: types.StringNull(), - Path: path.Root("test").AtListIndex(0).AtName("nested_test"), - PlanValue: types.StringUnknown(), - StateValue: types.StringNull(), }, expected: &planmodifier.StringResponse{ - PlanValue: types.StringUnknown(), + PlanValue: types.StringNull(), }, }, - "under-set": { + "unknown-config": { + // this is the situation in which a user is + // interpolating into a field. We want that to still + // show up as unknown, otherwise they'll get apply-time + // errors for changing the value even though we knew it + // was legitimately possible for it to change and the + // provider can't prevent this from happening request: planmodifier.StringRequest{ - ConfigValue: types.StringNull(), - Path: path.Root("test").AtSetValue( - types.SetValueMust( - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "nested_test": types.StringType, + State: tfsdk.State{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "attr": tftypes.String, }, }, - []attr.Value{ - types.ObjectValueMust( - map[string]attr.Type{ - "nested_test": types.StringType, - }, - map[string]attr.Value{ - "nested_test": types.StringUnknown(), - }, - ), + map[string]tftypes.Value{ + "attr": tftypes.NewValue(tftypes.String, "test"), }, ), - ).AtName("nested_test"), - PlanValue: types.StringUnknown(), - StateValue: types.StringNull(), + }, + StateValue: types.StringValue("test"), + PlanValue: types.StringUnknown(), + ConfigValue: types.StringUnknown(), }, expected: &planmodifier.StringResponse{ PlanValue: types.StringUnknown(),