fix(ske): read only attributes produces noise (#1081)

* fix(ske): read-only attributes produces noise

* feat: add new planmodifier `UseStateForUnknownIf`
This commit is contained in:
Marcel Jacek 2025-12-04 13:32:18 +01:00 committed by GitHub
parent 06747751ca
commit 8a609d4ab8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 249 additions and 0 deletions

View file

@ -0,0 +1,69 @@
package utils
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
)
type UseStateForUnknownFuncResponse struct {
UseStateForUnknown bool
Diagnostics diag.Diagnostics
}
// UseStateForUnknownIfFunc is a conditional function used in UseStateForUnknownIf
type UseStateForUnknownIfFunc func(context.Context, planmodifier.StringRequest, *UseStateForUnknownFuncResponse)
type useStateForUnknownIf struct {
ifFunc UseStateForUnknownIfFunc
description string
}
// UseStateForUnknownIf returns a plan modifier similar to UseStateForUnknown with a conditional
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description string) planmodifier.String {
return useStateForUnknownIf{
ifFunc: f,
description: description,
}
}
func (m useStateForUnknownIf) Description(context.Context) string {
return m.description
}
func (m useStateForUnknownIf) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m useStateForUnknownIf) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform
// Do nothing if there is no state value.
if req.StateValue.IsNull() {
return
}
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}
// The above checks are taken from the UseStateForUnknown plan modifier implementation
// (https://github.com/hashicorp/terraform-plugin-framework/blob/44348af3923c82a93c64ae7dca906d9850ba956b/resource/schema/stringplanmodifier/use_state_for_unknown.go#L38)
funcResponse := &UseStateForUnknownFuncResponse{}
m.ifFunc(ctx, req, funcResponse)
resp.Diagnostics.Append(funcResponse.Diagnostics...)
if resp.Diagnostics.HasError() {
return
}
if funcResponse.UseStateForUnknown {
resp.PlanValue = req.StateValue
}
}

View file

@ -0,0 +1,128 @@
package utils
import (
"context"
"testing"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
func TestUseStateForUnknownIf_PlanModifyString(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
stateValue types.String
planValue types.String
configValue types.String
ifFunc UseStateForUnknownIfFunc
expectedPlanValue types.String
expectedError bool
}{
{
name: "State is Null (Creation)",
stateValue: types.StringNull(),
planValue: types.StringUnknown(),
configValue: types.StringValue("some-config"),
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
// This should not be reached because the state is null
resp.UseStateForUnknown = true
},
expectedPlanValue: types.StringUnknown(),
},
{
name: "Plan is already known - (User updated the value)",
stateValue: types.StringValue("old-state"),
planValue: types.StringValue("new-plan"),
configValue: types.StringValue("new-plan"),
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
// This should not be reached because the plan is known
resp.UseStateForUnknown = true
},
expectedPlanValue: types.StringValue("new-plan"),
},
{
name: "Config is Unknown (Interpolation)",
stateValue: types.StringValue("old-state"),
planValue: types.StringUnknown(),
configValue: types.StringUnknown(),
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
// This should not be reached
resp.UseStateForUnknown = true
},
expectedPlanValue: types.StringUnknown(),
},
{
name: "Condition returns False (Do not use state)",
stateValue: types.StringValue("old-state"),
planValue: types.StringUnknown(),
configValue: types.StringNull(), // Simulating computed only
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
resp.UseStateForUnknown = false
},
expectedPlanValue: types.StringUnknown(),
},
{
name: "Condition returns True (Use state)",
stateValue: types.StringValue("old-state"),
planValue: types.StringUnknown(),
configValue: types.StringNull(),
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
resp.UseStateForUnknown = true
},
expectedPlanValue: types.StringValue("old-state"),
},
{
name: "Func returns Error",
stateValue: types.StringValue("old-state"),
planValue: types.StringUnknown(),
configValue: types.StringNull(),
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
resp.Diagnostics.AddError("Test Error", "Something went wrong")
},
expectedPlanValue: types.StringUnknown(),
expectedError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize the modifier
modifier := UseStateForUnknownIf(tt.ifFunc, "test description")
// Construct request
req := planmodifier.StringRequest{
StateValue: tt.stateValue,
PlanValue: tt.planValue,
ConfigValue: tt.configValue,
}
// Construct response
// Note: In the framework, resp.PlanValue is initialized to req.PlanValue
// before the modifier is called. We must simulate this.
resp := &planmodifier.StringResponse{
PlanValue: tt.planValue,
}
// Run the modifier
modifier.PlanModifyString(ctx, req, resp)
// Check Errors
if tt.expectedError {
if !resp.Diagnostics.HasError() {
t.Error("Expected error, got none")
}
} else {
if resp.Diagnostics.HasError() {
t.Errorf("Unexpected error: %s", resp.Diagnostics)
}
}
// Check Plan Value
if !resp.PlanValue.Equal(tt.expectedPlanValue) {
t.Errorf("PlanValue mismatch.\nExpected: %s\nGot: %s", tt.expectedPlanValue, resp.PlanValue)
}
})
}
}