Description
Module version
v1.15.0
Use-cases
Support "declarative" and slightly more easily reusable resource-level plan modification logic, separate from imperative resource-level plan modification logic. In environments where code generation is being used, it improves developer experience to be able to separate concerns between known-injectable logic versus wholly custom logic. Essentially, this can ensure that the method definition of ModifyPlan
can be fully omitted unless necessary, therefore allowing manually written code to be implemented in a separate file and method outside code generation. This means that no "in method logic" code generation customization needs to be supported for this use case, which is more difficult to properly support.
One particular use case for this style of resource-level plan modification that needs to get applied across many managed resources to enable provider-level configuration to set or override resource-level configuration. In hashicorp/aws
, this might be akin to the region
argument. In other providers, it might be something like an "organization identifier", that is required for managed resource API operations.
provider "examplecloud" {
organization_id = "org-123"
}
resource "examplecloud_thing" "example" {
# organization_id is "org-123" without explicit configuration, or can be overridden
}
Additional Context
When we were initially developing terraform-plugin-framework, we wanted to enable resource-level configuration validation as both declarative (resource.ResourceWithConfigValidators
) and imperative (resource.ResourceWithValidateConfig
) since it was determined there were enough use cases where there could be either style preferred. I don't exactly remember from the HashiCorp-internal RFCs if we had an explicit reason for not doing the same with resource-level plan modification, but I feel like it was only because at the time we were not sure how much benefit the declarative interface would provide versus spending the time to implement the feature.
Attempted Solutions
For now, it is technically possible to create a shared function that gets called in any necessary resource ModifyPlan
method definitions, e.g.
// shared interface
type ResourceWithProviderOrganizationId interface {
ProviderOrganizationId() types.String
}
// shared plan modification function
func ResourceOrganizationIdPlanModifier(ctx context.Context, r resource.Resource, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
resourceWithOrganizationId, ok := r.(ResourceWithProviderOrganizationId)
if !ok {
return
}
providerOrganizationId := resourceWithOrganizationId.ProviderOrganizationId()
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, path.Root("organization_id"), providerOrganizationId)...)
}
// in each managed resource
type ThingResource struct {
// ... other fields, e.g. API client
OrganizationId types.String
}
func (*r ThingResource) ProviderOrganizationId() types.String {
return r.OrganizationId
}
func (*r ThingResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := req.ProviderData.(*ProviderData)
if !ok { /* error diagnostic and return */ }
// ...
r.OrganizationId = providerData.OrganizationId
}
func (*r ThingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
ResourceOrganizationIdPlanModifer(ctx, r, req, resp)
if resp.Diagnostics.HasError() {
return
}
// ... custom logic ...
}
Proposal
Ideally this sort of code setup would be enabled:
- Automatically inject re-usable logic that is applicable to multiple managed resources as declarative-style code (new
resource.ResourceWithPlanModifiers
interface) - Support custom logic as imperative-style code (existing
resource.ResourceWithModifyPlan
interface)
A potential definition of this code may be:
/* This is design sketch for a proposed feature and not currently available. */
package resource
type ResourceWithPlanModifiers interface {
Resource
// PlanModifiers returns a list of functions which will all be performed during plan modification.
PlanModifiers(context.Context) []PlanModifier()
}
// PlanModifier describes reusable Resource plan modification functionality.
type PlanModifier interface {
// Description describes the plan modification in plain text formatting.
//
// This information may be automatically added to resource plain text
// descriptions by external tooling.
Description(context.Context) string
// MarkdownDescription describes the plan modification in Markdown formatting.
//
// This information may be automatically added to resource Markdown
// descriptions by external tooling.
MarkdownDescription(context.Context) string
// PlanModifyResource performs the plan modification on a resource.
PlanModifyResource(context.Context, Resource, ModifyPlanRequest, *ModifyPlanResponse)
}
This attempts to re-use the existing ModifyPlanRequest
and ModifyPlanResponse
types as interface method parameters, but a slightly modified request type could be introduced that directly provide access to the resource.Resource
. Type assertions against that interface, ideally against a separate provider-defined interface that gives access to provider-specific concerns such as API client and/or data that is defined on the concrete resource type, is expected in this situation.
The ordering of plan modification could follow same ordering as configuration validation, which ensures that custom logic can always override any prior declarative logic:
- Attribute-defined plan modifiers
- Resource-defined plan modifiers (new
ResourceWithPlanModifiers
) - Resource-defined modify plan (existing
ResourceWithModifyPlan
)
This feature would be unit tested similar to resource-level configuration validators and documented on the Resource Plan Modification page.