Skip to content

Commit

Permalink
Attribute plan modification (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmoe authored Sep 1, 2021
1 parent 5158520 commit c14d3d7
Show file tree
Hide file tree
Showing 10 changed files with 1,579 additions and 29 deletions.
10 changes: 5 additions & 5 deletions docs/design/plan-modification.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,29 +391,29 @@ func RequiresReplace() AttributePlanModifier {

type RequiresReplaceModifier struct{}

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

func (r RequiresReplace) Description(ctx context.Context) string {
func (r RequiresReplaceModifier) Description(ctx context.Context) string {
// ...
}

func (r RequiresReplace) MarkdownDescription(ctx context.Context) string {
func (r RequiresReplaceModifier) MarkdownDescription(ctx context.Context) string {
// ...
}
```

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

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

type RequiresReplaceIfModifier struct {
f RequiresReplaceIfFunc
Expand Down
49 changes: 49 additions & 0 deletions tfsdk/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ type Attribute struct {

// Validators defines validation functionality for the attribute.
Validators []AttributeValidator

// PlanModifiers defines a sequence of modifiers for this attribute at
// plan time.
// Please note that plan modification only applies to resources, not
// data sources. Setting PlanModifiers on a data source attribute will
// have no effect.
PlanModifiers AttributePlanModifiers
}

// ApplyTerraform5AttributePathStep transparently calls
Expand Down Expand Up @@ -369,3 +376,45 @@ 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
if diagnostics.DiagsHasErrors(resp.Diagnostics) {
return
}
}
}
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,
})
}
2 changes: 1 addition & 1 deletion tfsdk/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ type ModifyResourcePlanResponse struct {

// 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
// indicates a successful plan modification with no warnings or errors
// generated.
Diagnostics []*tfprotov6.Diagnostic
}
Expand Down
Loading

0 comments on commit c14d3d7

Please sign in to comment.