Skip to content

Feature: Consider resource.ResourceWithPlanModifiers interfaceΒ #1169

Open
@bflad

Description

@bflad

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions