Skip to content

Commit

Permalink
implement attribute plan modification
Browse files Browse the repository at this point in the history
  • Loading branch information
kmoe committed Aug 17, 2021
1 parent d9268bf commit b44c515
Show file tree
Hide file tree
Showing 11 changed files with 1,152 additions and 248 deletions.
130 changes: 39 additions & 91 deletions tfsdk/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,97 +82,6 @@ type Attribute struct {
PlanModifiers AttributePlanModifiers
}

// AttributePlanModifier represents a modifier for an attribute at plan time.
// An AttributePlanModifier can only modify the planned value for the attribute
// on which it is defined. For plan-time modifications that modify the values of
// several attributes at once, please instead use the ResourceWithModifyPlan
// interface by defining a ModifyPlan function on the resource.
type AttributePlanModifier interface {
// Description is used in various tooling, like the language server, to
// give practitioners more information about what this modifier is,
// what it's for, and how it should be used. It should be written as
// plain text, with no special formatting.
Description(context.Context) string

// MarkdownDescription is used in various tooling, like the
// documentation generator, to give practitioners more information
// about what this modifier is, what it's for, and how it should be
// used. It should be formatted using Markdown.
MarkdownDescription(context.Context) string

// Modify is called when the provider has an opportunity to modify
// the plan: once during the plan phase when Terraform is determining
// the diff that should be shown to the user for approval, and once
// during the apply phase with any unknown values from configuration
// filled in with their final values.
// The Modify function has access to the config, state, and plan for
// both the attribute in question and the entire resource, but it can
// only modify the value of the one attribute.
//
// Please see the documentation for ResourceWithModifyPlan#ModifyPlan
// for further details.
Modify(context.Context, ModifyAttributePlanRequest, *ModifyAttributePlanResponse)
}

// AttributePlanModifiers represents a sequence of AttributePlanModifiers, in
// order.
type AttributePlanModifiers []AttributePlanModifier

// RequiresReplace returns AttributePlanModifiers specifying the attribute as
// requiring replacement. This behaviour is identical to the ForceNew behaviour
// in terraform-plugin-sdk.
func RequiresReplace() AttributePlanModifiers {
return []AttributePlanModifier{RequiresReplaceModifier{}}
}

// RequiresReplaceModifier is an AttributePlanModifier that sets RequiresReplace
// on the attribute.
type RequiresReplaceModifier struct{}

func (r RequiresReplaceModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
resp.RequiresReplace = true
}

func (r RequiresReplaceModifier) Description(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}

func (r RequiresReplaceModifier) MarkdownDescription(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}

func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) AttributePlanModifier {
return RequiresReplaceIfModifier{
f: f,
description: description,
markdownDescription: markdownDescription,
}
}

type RequiresReplaceIfFunc func(ctx context.Context, state, config attr.Value) (bool, error)

type RequiresReplaceIfModifier struct {
f RequiresReplaceIfFunc
description string
markdownDescription string
}

func (r RequiresReplaceIfModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
res, err := r.f(ctx, req.State, req.Config)
if err != nil {
resp.AddError("Error running RequiresReplaceIf func for attribute", err.Error())
}
resp.RequiresReplace = res
}

func (r RequiresReplaceIfModifier) Description(ctx context.Context) string {
return r.description
}

func (r RequiresReplaceIfModifier) MarkdownDescription(ctx context.Context) string {
return r.markdownDescription
}

// ApplyTerraform5AttributePathStep transparently calls
// ApplyTerraform5AttributePathStep on a.Type or a.Attributes, whichever is
// non-nil. It allows Attributes to be walked using tftypes.Walk and
Expand Down Expand Up @@ -467,3 +376,42 @@ func (a Attribute) validate(ctx context.Context, req ValidateAttributeRequest, r
}
}
}

// modifyPlan runs all AttributePlanModifiers
func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
attrConfig, diags := req.Config.GetAttribute(ctx, req.AttributePath)
resp.Diagnostics = append(resp.Diagnostics, diags...)
if diagnostics.DiagsHasErrors(diags) {
return
}
req.AttributeConfig = attrConfig

attrState, diags := req.State.GetAttribute(ctx, req.AttributePath)
resp.Diagnostics = append(resp.Diagnostics, diags...)
if diagnostics.DiagsHasErrors(diags) {
return
}
req.AttributeState = attrState

attrPlan, diags := req.Plan.GetAttribute(ctx, req.AttributePath)
resp.Diagnostics = append(resp.Diagnostics, diags...)
if diagnostics.DiagsHasErrors(diags) {
return
}
req.AttributePlan = attrPlan

modifyReq := ModifyAttributePlanRequest{
AttributePath: req.AttributePath,
Config: req.Config,
State: req.State,
Plan: req.Plan,
AttributeConfig: req.AttributeConfig,
AttributeState: req.AttributeState,
AttributePlan: req.AttributePlan,
ProviderMeta: req.ProviderMeta,
}
for _, planModifier := range a.PlanModifiers {
planModifier.Modify(ctx, modifyReq, resp)
modifyReq.AttributePlan = resp.AttributePlan
}
}
204 changes: 204 additions & 0 deletions tfsdk/attribute_plan_modification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package tfsdk

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// AttributePlanModifier represents a modifier for an attribute at plan time.
// An AttributePlanModifier can only modify the planned value for the attribute
// on which it is defined. For plan-time modifications that modify the values of
// several attributes at once, please instead use the ResourceWithModifyPlan
// interface by defining a ModifyPlan function on the resource.
type AttributePlanModifier interface {
// Description is used in various tooling, like the language server, to
// give practitioners more information about what this modifier is,
// what it's for, and how it should be used. It should be written as
// plain text, with no special formatting.
Description(context.Context) string

// MarkdownDescription is used in various tooling, like the
// documentation generator, to give practitioners more information
// about what this modifier is, what it's for, and how it should be
// used. It should be formatted using Markdown.
MarkdownDescription(context.Context) string

// Modify is called when the provider has an opportunity to modify
// the plan: once during the plan phase when Terraform is determining
// the diff that should be shown to the user for approval, and once
// during the apply phase with any unknown values from configuration
// filled in with their final values.
// The Modify function has access to the config, state, and plan for
// both the attribute in question and the entire resource, but it can
// only modify the value of the one attribute.
//
// Please see the documentation for ResourceWithModifyPlan#ModifyPlan
// for further details.
Modify(context.Context, ModifyAttributePlanRequest, *ModifyAttributePlanResponse)
}

// AttributePlanModifiers represents a sequence of AttributePlanModifiers, in
// order.
type AttributePlanModifiers []AttributePlanModifier

// RequiresReplace returns an AttributePlanModifier specifying the attribute as
// requiring replacement. This behaviour is identical to the ForceNew behaviour
// in terraform-plugin-sdk.
func RequiresReplace() AttributePlanModifier {
return RequiresReplaceModifier{}
}

// RequiresReplaceModifier is an AttributePlanModifier that sets RequiresReplace
// on the attribute.
type RequiresReplaceModifier struct{}

// Modify sets RequiresReplace on the response to true.
func (r RequiresReplaceModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
resp.RequiresReplace = true
}

// Description returns a human-readable description of the plan modifier.
func (r RequiresReplaceModifier) Description(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (r RequiresReplaceModifier) MarkdownDescription(ctx context.Context) string {
return "If the value of this attribute changes, Terraform will destroy and recreate the resource."
}

// RequiresReplaceIf returns an AttributePlanModifier that runs the conditional
// function f: if it returns true, it specifies the attribute as requiring
// replacement.
func RequiresReplaceIf(f RequiresReplaceIfFunc, description, markdownDescription string) AttributePlanModifier {
return RequiresReplaceIfModifier{
f: f,
description: description,
markdownDescription: markdownDescription,
}
}

// RequiresReplaceIfFunc is a conditional function used in the RequiresReplaceIf
// plan modifier to determine whether the attribute requires replacement.
type RequiresReplaceIfFunc func(ctx context.Context, state, config attr.Value) (bool, error)

// RequiresReplaceIfModifier is an AttributePlanModifier that sets RequiresReplace
// on the attribute if the conditional function returns true.
type RequiresReplaceIfModifier struct {
f RequiresReplaceIfFunc
description string
markdownDescription string
}

// Modify sets RequiresReplace on the response to true if the conditional
// RequiresReplaceIfFunc returns true.
func (r RequiresReplaceIfModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
res, err := r.f(ctx, req.AttributeState, req.AttributeConfig)
if err != nil {
resp.AddError("Error running RequiresReplaceIf func for attribute", err.Error())
}
resp.RequiresReplace = res
}

// Description returns a human-readable description of the plan modifier.
func (r RequiresReplaceIfModifier) Description(ctx context.Context) string {
return r.description
}

// MarkdownDescription returns a markdown description of the plan modifier.
func (r RequiresReplaceIfModifier) MarkdownDescription(ctx context.Context) string {
return r.markdownDescription
}

// ModifyAttributePlanRequest represents a request for the provider to modify an
// attribute value, or mark it as requiring replacement, at plan time. An
// instance of this request struct is supplied as an argument to the Modify
// function of an attribute's plan modifier(s).
type ModifyAttributePlanRequest struct {
// AttributePath is the path of the attribute.
AttributePath *tftypes.AttributePath

// Config is the configuration the user supplied for the resource.
Config Config

// State is the current state of the resource.
State State

// Plan is the planned new state for the resource.
Plan Plan

// AttributeConfig is the configuration the user supplied for the attribute.
AttributeConfig attr.Value

// AttributeState is the current state of the attribute.
AttributeState attr.Value

// AttributePlan is the planned new state for the attribute.
AttributePlan attr.Value

// ProviderMeta is metadata from the provider_meta block of the module.
ProviderMeta Config
}

// ModifyAttributePlanResponse represents a response to a
// ModifyAttributePlanRequest. An instance of this response struct is supplied
// as an argument to the Modify function of an attribute's plan modifier(s).
type ModifyAttributePlanResponse struct {
// AttributePlan is the planned new state for the attribute.
AttributePlan attr.Value

// RequiresReplace indicates whether a change in the attribute
// requires replacement of the whole resource.
RequiresReplace bool

// Diagnostics report errors or warnings related to determining the
// planned state of the requested resource. Returning an empty slice
// indicates a successful validation with no warnings or errors
// generated.
Diagnostics []*tfprotov6.Diagnostic
}

// AddWarning appends a warning diagnostic to the response. If the warning
// concerns a particular attribute, AddAttributeWarning should be used instead.
func (r *ModifyAttributePlanResponse) AddWarning(summary, detail string) {
r.Diagnostics = append(r.Diagnostics, &tfprotov6.Diagnostic{
Summary: summary,
Detail: detail,
Severity: tfprotov6.DiagnosticSeverityWarning,
})
}

// AddAttributeWarning appends a warning diagnostic to the response and labels
// it with a specific attribute.
func (r *ModifyAttributePlanResponse) AddAttributeWarning(attributePath *tftypes.AttributePath, summary, detail string) {
r.Diagnostics = append(r.Diagnostics, &tfprotov6.Diagnostic{
Attribute: attributePath,
Summary: summary,
Detail: detail,
Severity: tfprotov6.DiagnosticSeverityWarning,
})
}

// AddError appends an error diagnostic to the response. If the error concerns a
// particular attribute, AddAttributeError should be used instead.
func (r *ModifyAttributePlanResponse) AddError(summary, detail string) {
r.Diagnostics = append(r.Diagnostics, &tfprotov6.Diagnostic{
Summary: summary,
Detail: detail,
Severity: tfprotov6.DiagnosticSeverityError,
})
}

// AddAttributeError appends an error diagnostic to the response and labels it
// with a specific attribute.
func (r *ModifyAttributePlanResponse) AddAttributeError(attributePath *tftypes.AttributePath, summary, detail string) {
r.Diagnostics = append(r.Diagnostics, &tfprotov6.Diagnostic{
Attribute: attributePath,
Summary: summary,
Detail: detail,
Severity: tfprotov6.DiagnosticSeverityError,
})
}
31 changes: 0 additions & 31 deletions tfsdk/request.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package tfsdk

import (
"github.com/hashicorp/terraform-plugin-framework/attr"
)

// ConfigureProviderRequest represents a request containing the values the user
// specified for the provider configuration block, along with other runtime
// information from Terraform or the Plugin SDK. An instance of this request
Expand Down Expand Up @@ -122,30 +118,3 @@ type ReadDataSourceRequest struct {
// ProviderMeta is metadata from the provider_meta block of the module.
ProviderMeta Config
}

// ModifyAttributePlanRequest represents a request for the provider to modify an
// attribute value, or mark it as requiring replacement, at plan time. An
// instance of this request struct is supplied as an argument to the Modify
// function of an attribute's plan modifier(s).
type ModifyAttributePlanRequest struct {
// ResourceConfig is the configuration the user supplied for the resource.
ResourceConfig Config

// ResourceState is the current state of the resource.
ResourceState State

// ResourcePlan is the planned new state for the resource.
ResourcePlan Plan

// Config is the configuration the user supplied for the attribute.
Config attr.Value

// State is the current state of the attribute.
State attr.Value

// Plan is the planned new state for the attribute.
Plan attr.Value

// ProviderMeta is metadata from the provider_meta block of the module.
ProviderMeta Config
}
Loading

0 comments on commit b44c515

Please sign in to comment.