Terraform
- Plugin Framework
- v1.16.x (latest)
- No versions of this document exist before v1.15.x. Click below to redirect to the version homepage.
- v1.15.x
- v1.14.x
- v1.13.x
- v1.12.x
- v1.11.x
- v1.10.x
- v1.9.x
- v1.8.x
- v1.7.x
- v1.6.x
- v1.5.x
- v1.4.x
- v1.3.x
- v1.2.x
- v1.1.x
- v1.0.x
- v0.16.x
- v0.15.x
- v0.14.x
- v0.13.x
- v0.12.x
- v0.11.x
- v0.10.x
- v0.9.x
- v0.8.x
- v0.7.x
Plan Modification
Note: The Plugin Framework is in beta.
Your provider can modify the Terraform plan to match the expected end state. This can include replacing unknown values with expected known values or marking a resource that must be replaced. Refer to Plan Modification in the Framework documentation for details.
This page explains how to migrate resource CustomizeDiff functions in SDKv2 to PlanModifiers in the plugin
Framework.
SDKv2
In SDKv2, plan modification is implemented with the CustomizeDiff field on the schema.Resource struct. The following
code shows a basic implementation of plan modification with SDKv2.
func resourceExample() *schema.Resource {
    return &schema.Resource{
        CustomizeDiff: CustomizeDiffFunc,
        /* ... */
Framework
In the Framework, you implement plan modification either by implementing the  ResourceWithModifyPlan interface on your
resource type, or by implementing PlanModifiers on individual attributes. This page demonstrates how to implement the
plan modifiers on individual attributes. Refer to
Attributes - Default Values and
Attributes - Force New in this guide for further information on how
to implement a plan modifier on an attribute.
The ResourceWithModifyPlan interface requires a ModifyPlan function.
The following code shows how you can implement the ModifyPlan function on your resource.Resource type.
func (r *resourceExample) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
    /* ... */
}
Migration Notes
Remember the following differences between SDKv2 and the Framework when completing the migration.
- In SDKv2, you implement plan modification with the CustomizeDifffield on theschema.Resourcestruct. In the Framework, you can either implement plan modification for the entire resource by implementing theResourceWithModifyPlaninterface, or on individual attributes by addingPlanModifiersto your resource attributes.
- Many existing CustomizeDiff implementations may be better suited to implementation as attribute plan modifiers in the Framework.
Example
The following examples show how to migrate portions of the random provider.
For a complete example, clone the
terraform-provider-random repository and compare the resource_password.go file in
v3.3.2
with v3.4.1.
SDKv2
In SDKv2, the CustomizeDiff field on the schema.Resource struct refers to a function or set of functions that
implement plan modification.
The following example from the resource_password.go files shows the use of CustomizeDiff to keep two attributes
synchronized (i.e., ensure that they contain the same value) with SDKv2.
func resourcePassword() *schema.Resource {
    /* ... */
    customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("number", "numeric"))
    customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("numeric", "number"))
    return &schema.Resource{
        /* ... */
        CustomizeDiff: customdiff.All(
            customizeDiffFuncs...,
        ),
    }
}
The following example shows the implementation of the planSyncIfChange function.
func planSyncIfChange(key, keyToSync string) func(context.Context, *schema.ResourceDiff, interface{}) error {
    return customdiff.IfValueChange(
        key,
        func(ctx context.Context, oldValue, newValue, meta interface{}) bool {
            return oldValue != newValue
        },
        func(_ context.Context, d *schema.ResourceDiff, _ interface{}) error {
            return d.SetNew(keyToSync, d.Get(key))
        },
    )
}
Framework
Many existing CustomizeDiff implementations would be better suited to migration to attribute plan modifiers in the
Framework. This code shows the implementation using attribute plan modifiers with the Framework.
func passwordSchemaV2() schema.Schema {
    return schema.Schema{
        /* ... */
        Attributes: map[string]schema.Attribute{
            /* ... */
            "number": schema.BoolAttribute{
                /* ... */
                PlanModifiers: []planmodifier.Bool{
                    planmodifiers.NumberNumericAttributePlanModifier(),
                    /* ... */
                },
            },
            "numeric": schema.BoolAttribute{
                /* ... */
                PlanModifiers: []planmodifier.Bool{
                    planmodifiers.NumberNumericAttributePlanModifier(),
                    /* ... */
                },
            },
The following shows an implementation of NumberNumericAttributePlanModifier in the Framework.
func NumberNumericAttributePlanModifier() planmodifier.Bool {
    return &numberNumericAttributePlanModifier{}
}
type numberNumericAttributePlanModifier struct {
}
func (d *numberNumericAttributePlanModifier) Description(ctx context.Context) string {
    return "Ensures that number and numeric attributes are kept synchronised."
}
func (d *numberNumericAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
    return d.Description(ctx)
}
func (d *numberNumericAttributePlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
    var numberConfig types.Bool
    diags := req.Config.GetAttribute(ctx, path.Root("number"), &numberConfig)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    var numericConfig types.Bool
    req.Config.GetAttribute(ctx, path.Root("numeric"), &numericConfig)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    if !numberConfig.IsNull() && !numericConfig.IsNull() && (numberConfig.ValueBool() != numericConfig.ValueBool()) {
        resp.Diagnostics.AddError(
            "Number and numeric are both configured with different values",
            "Number is deprecated, use numeric instead",
        )
        return
    }
    // Default to true for both number and numeric when both are null.
    if numberConfig.IsNull() && numericConfig.IsNull() {
        resp.PlanValue = types.BoolValue(true)
        return
    }
    // Default to using value for numeric if number is null
    if numberConfig.IsNull() && !numericConfig.IsNull() {
        resp.PlanValue = numericConfig
        return
    }
    // Default to using value for number if numeric is null
    if !numberConfig.IsNull() && numericConfig.IsNull() {
        resp.PlanValue = numberConfig
        return
    }
}