Skip to content

Commit

Permalink
tfsdk: Support block plan modification
Browse files Browse the repository at this point in the history
Reference: #222

Adds `PlanModifiers` within `Block`s and ensures any children attribute and block plan modifiers of a `Block` are executed.

The dual response parameter pattern is twofold necessary, or rather a simpler implementation, due to the current details:

*  `AttributePlanModifier`s return a `RequiresReplace` boolean, which the caller is responsible for setting the proper attribute path for the overall schema response.
* `AttributePlanModifier`s return `AttributePlan` as an `attr.Value`, which cannot be directly set into a parent value without reimplementing all the `SetAttribute()` logic for handling nil/null/unknown values, so it is left to the caller to properly call `(Plan).SetAttribute()` in the schema response.
  • Loading branch information
bflad committed Nov 11, 2021
1 parent 085ea63 commit 4d843b5
Show file tree
Hide file tree
Showing 10 changed files with 2,298 additions and 176 deletions.
178 changes: 175 additions & 3 deletions tfsdk/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ type Attribute struct {
// resource-level plan modifications.
//
// Any errors will prevent further execution of this sequence
// of modifiers and modifiers associated with any nested Attribute, but will not
// prevent execution of PlanModifiers on any other Attribute in the Schema.
// of modifiers and modifiers associated with any nested Attribute, but
// will not prevent execution of PlanModifiers on any other Attribute or
// Block in the Schema.
//
// Plan modification only applies to resources, not data sources or
// providers. Setting PlanModifiers on a data source or provider attribute
Expand Down Expand Up @@ -477,9 +478,10 @@ func (a Attribute) validateAttributes(ctx context.Context, req ValidateAttribute
}

// modifyPlan runs all AttributePlanModifiers
func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) {
func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse, schemaResp *ModifySchemaPlanResponse) {
attrConfig, diags := req.Config.getAttributeValue(ctx, req.AttributePath)
resp.Diagnostics.Append(diags...)
schemaResp.Diagnostics = resp.Diagnostics
// Only on new errors.
if diags.HasError() {
return
Expand All @@ -488,6 +490,7 @@ func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanReques

attrState, diags := req.State.getAttributeValue(ctx, req.AttributePath)
resp.Diagnostics.Append(diags...)
schemaResp.Diagnostics = resp.Diagnostics
// Only on new errors.
if diags.HasError() {
return
Expand All @@ -496,11 +499,13 @@ func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanReques

attrPlan, diags := req.Plan.getAttributeValue(ctx, req.AttributePath)
resp.Diagnostics.Append(diags...)
schemaResp.Diagnostics = resp.Diagnostics
// Only on new errors.
if diags.HasError() {
return
}
req.AttributePlan = attrPlan
resp.AttributePlan = attrPlan

for _, planModifier := range a.PlanModifiers {
modifyResp := &ModifyAttributePlanResponse{
Expand All @@ -514,10 +519,177 @@ func (a Attribute) modifyPlan(ctx context.Context, req ModifyAttributePlanReques
resp.AttributePlan = modifyResp.AttributePlan
resp.Diagnostics.Append(modifyResp.Diagnostics...)
resp.RequiresReplace = modifyResp.RequiresReplace
schemaResp.Diagnostics = resp.Diagnostics

// Only on new errors.
if modifyResp.Diagnostics.HasError() {
return
}
}

if resp.RequiresReplace {
schemaResp.RequiresReplace = append(schemaResp.RequiresReplace, req.AttributePath)
}

setAttrDiags := schemaResp.Plan.SetAttribute(ctx, req.AttributePath, resp.AttributePlan)
resp.Diagnostics.Append(setAttrDiags...)
schemaResp.Diagnostics = resp.Diagnostics

if setAttrDiags.HasError() {
return
}

if !a.definesAttributes() {
return
}

nm := a.Attributes.GetNestingMode()
switch nm {
case NestingModeList:
l, ok := req.AttributePlan.(types.List)

if !ok {
err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributePlan, nm, req.AttributePath)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(),
)

return
}

for idx := range l.Elems {
for name, attr := range a.Attributes.GetAttributes() {
attrReq := ModifyAttributePlanRequest{
AttributePath: req.AttributePath.WithElementKeyInt(idx).WithAttributeName(name),
Config: req.Config,
Plan: schemaResp.Plan,
ProviderMeta: req.ProviderMeta,
State: req.State,
}
attrResp := &ModifyAttributePlanResponse{
Diagnostics: resp.Diagnostics,
}

attr.modifyPlan(ctx, attrReq, attrResp, schemaResp)
}
}
case NestingModeSet:
s, ok := req.AttributePlan.(types.Set)

if !ok {
err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributePlan, nm, req.AttributePath)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(),
)

return
}

for _, value := range s.Elems {
tfValueRaw, err := value.ToTerraformValue(ctx)

if err != nil {
err := fmt.Errorf("error running ToTerraformValue on element value: %v", value)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modification cannot convert element into a Terraform value. Report this to the provider developer:\n\n"+err.Error(),
)

return
}

tfValue := tftypes.NewValue(s.ElemType.TerraformType(ctx), tfValueRaw)

for name, attr := range a.Attributes.GetAttributes() {
attrReq := ModifyAttributePlanRequest{
AttributePath: req.AttributePath.WithElementKeyValue(tfValue).WithAttributeName(name),
Config: req.Config,
Plan: schemaResp.Plan,
ProviderMeta: req.ProviderMeta,
State: req.State,
}
attrResp := &ModifyAttributePlanResponse{
Diagnostics: resp.Diagnostics,
}

attr.modifyPlan(ctx, attrReq, attrResp, schemaResp)
}
}
case NestingModeMap:
m, ok := req.AttributePlan.(types.Map)

if !ok {
err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributePlan, nm, req.AttributePath)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(),
)

return
}

for key := range m.Elems {
for name, attr := range a.Attributes.GetAttributes() {
attrReq := ModifyAttributePlanRequest{
AttributePath: req.AttributePath.WithElementKeyString(key).WithAttributeName(name),
Config: req.Config,
Plan: schemaResp.Plan,
ProviderMeta: req.ProviderMeta,
State: req.State,
}
attrResp := &ModifyAttributePlanResponse{
Diagnostics: resp.Diagnostics,
}

attr.modifyPlan(ctx, attrReq, attrResp, schemaResp)
}
}
case NestingModeSingle:
o, ok := req.AttributePlan.(types.Object)

if !ok {
err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", req.AttributePlan, nm, req.AttributePath)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(),
)

return
}

if len(o.Attrs) == 0 {
return
}

for name, attr := range a.Attributes.GetAttributes() {
attrReq := ModifyAttributePlanRequest{
AttributePath: req.AttributePath.WithAttributeName(name),
Config: req.Config,
Plan: schemaResp.Plan,
ProviderMeta: req.ProviderMeta,
State: req.State,
}
attrResp := &ModifyAttributePlanResponse{
Diagnostics: resp.Diagnostics,
}

attr.modifyPlan(ctx, attrReq, attrResp, schemaResp)
}
default:
err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, req.AttributePath)
resp.Diagnostics.AddAttributeError(
req.AttributePath,
"Attribute Plan Modification Error",
"Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n"+err.Error(),
)

return
}
}
11 changes: 9 additions & 2 deletions tfsdk/attribute_plan_modification.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tfsdk

import (
"context"
"errors"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
Expand Down Expand Up @@ -127,7 +128,10 @@ func (r RequiresReplaceModifier) Modify(ctx context.Context, req ModifyAttribute
}

attrSchema, err := req.State.Schema.AttributeAtPath(req.AttributePath)
if err != nil {

// Path may lead to block instead of attribute. Blocks cannot be Computed.
// If ErrPathIsBlock, attrSchema.Computed will still be false later.
if err != nil && !errors.Is(err, ErrPathIsBlock) {
resp.Diagnostics.AddAttributeError(req.AttributePath,
"Error finding attribute schema",
fmt.Sprintf("An unexpected error was encountered retrieving the schema for this attribute. This is always a bug in the provider.\n\nError: %s", err),
Expand Down Expand Up @@ -264,7 +268,10 @@ func (r RequiresReplaceIfModifier) Modify(ctx context.Context, req ModifyAttribu
}

attrSchema, err := req.State.Schema.AttributeAtPath(req.AttributePath)
if err != nil {

// Path may lead to block instead of attribute. Blocks cannot be Computed.
// If ErrPathIsBlock, attrSchema.Computed will still be false later.
if err != nil && !errors.Is(err, ErrPathIsBlock) {
resp.Diagnostics.AddAttributeError(req.AttributePath,
"Error finding attribute schema",
fmt.Sprintf("An unexpected error was encountered retrieving the schema for this attribute. This is always a bug in the provider.\n\nError: %s", err),
Expand Down
Loading

0 comments on commit 4d843b5

Please sign in to comment.