From 4adb2be21649274bc9a5a1cdff06390de48e3ecb Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Wed, 15 Sep 2021 08:38:56 -0700 Subject: [PATCH 1/8] First stab at attr.TypeWithModifyPlan. --- attr/type.go | 11 ++ attr/value.go | 12 ++ tfsdk/serve.go | 69 +++++++ tfsdk/type_plan_modification.go | 317 ++++++++++++++++++++++++++++++++ 4 files changed, 409 insertions(+) create mode 100644 tfsdk/type_plan_modification.go diff --git a/attr/type.go b/attr/type.go index 939beb442..8a38d7be2 100644 --- a/attr/type.go +++ b/attr/type.go @@ -86,6 +86,17 @@ type TypeWithValidate interface { Validate(context.Context, tftypes.Value, *tftypes.AttributePath) diag.Diagnostics } +// TypeWithModifyPlan extends the Type interface to include a ModifyPlan method, +// used to bundle consistent plan modification logic with the Type. +type TypeWithModifyPlan interface { + Type + + // ModifyPlan returns the Value that should be used in the plan. It is + // generally used to suppress diffs that do not correspond to semantic + // differences. In these cases, the `state` Value should be returned. + ModifyPlan(ctx context.Context, state, plan Value, path *tftypes.AttributePath) (Value, diag.Diagnostics) +} + // TypeWithPlaintextDescription extends the Type interface to include a // Description method, used to bundle extra information to include in attribute // descriptions with the Type. It expects the description to be written as diff --git a/attr/value.go b/attr/value.go index 3b151a4f3..4714e12af 100644 --- a/attr/value.go +++ b/attr/value.go @@ -21,3 +21,15 @@ type Value interface { // to the Value passed as an argument. Equal(Value) bool } + +func ValueToTerraform(ctx context.Context, val Value) (tftypes.Value, error) { + raw, err := val.ToTerraformValue(ctx) + if err != nil { + return tftypes.Value{}, err + } + err = tftypes.ValidateValue(val.Type(ctx).TerraformType(ctx), raw) + if err != nil { + return tftypes.Value{}, err + } + return tftypes.NewValue(val.Type(ctx).TerraformType(ctx), raw), nil +} diff --git a/tfsdk/serve.go b/tfsdk/serve.go index 28b68cf06..8104ae04b 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -7,6 +7,7 @@ import ( "sort" "sync" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/proto6" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -698,6 +699,70 @@ func (s *server) planResourceChange(ctx context.Context, req *tfprotov6.PlanReso return } + // Execute any attr.TypeWithModifyPlans + // + // We do this first because type plan modifications should be + // overridable by resource and attribute level plan modifications. As a + // rule of thumb, more specific plan modifiers should happen after more + // generic plan modifiers. + // + // We only do this if there's a plan to modify; otherwise, it + // represents a resource being deleted and there's no point. + if !plan.IsNull() { + rawStateVal := map[string]tftypes.Value{} + err = state.As(&rawStateVal) + if err != nil { + // TODO: error + } + rawPlanVal := map[string]tftypes.Value{} + err = plan.As(&rawPlanVal) + if err != nil { + // TODO: error + } + for attrName, a := range resourceSchema.Attributes { + path := tftypes.NewAttributePath().WithAttributeName(attrName) + state, ok := rawStateVal[attrName] + if !ok { + // TODO: error + } + plan, ok := rawPlanVal[attrName] + if !ok { + // TODO: error + } + + if a.Type != nil { + newPlan, diags := attributeTypeModifyPlan(ctx, a.Type, state, plan, path) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + // TODO: error + } + rawPlanVal[attrName] = rawNewPlan + } else if a.Attributes != nil { + newPlan, diags := attributeTypeModifyPlan(ctx, a.Attributes.AttributeType(), state, plan, path) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + // TODO: error + } + rawPlanVal[attrName] = rawNewPlan + } else { + // TODO: error + } + } + err = tftypes.ValidateValue(plan.Type(), rawPlanVal) + if err != nil { + // TODO: error + } + plan = tftypes.NewValue(plan.Type(), rawPlanVal) + } + // Execute any AttributePlanModifiers. // // This pass is before any Computed-only attributes are marked as unknown @@ -764,6 +829,10 @@ func (s *server) planResourceChange(ctx context.Context, req *tfprotov6.PlanReso plan = modifiedPlan } + // TODO: execute any type plan modifiers again to allow overwriting + // unknown values. Do we even want to do that? What could the use case + // possibly be? + // Execute any AttributePlanModifiers again. This allows overwriting // any unknown values. // diff --git a/tfsdk/type_plan_modification.go b/tfsdk/type_plan_modification.go new file mode 100644 index 000000000..8b926eea4 --- /dev/null +++ b/tfsdk/type_plan_modification.go @@ -0,0 +1,317 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func attributeTypeModifyPlanObject(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + wat, ok := typ.(attr.TypeWithAttributeTypes) + if !ok { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nObject at "+path.String()+" isn't of an attr.Type that implements TypeWithAttributeType. This isn't a valid type for this value.") + return plan, diags + } + stateVals := map[string]tftypes.Value{} + + // we can handle null states, though; if the state is null and the plan + // is not, the state gets set to the null value for the purposes of + // plan modification for that attribute + if state.IsNull() { + for attr, typ := range wat.AttributeTypes() { + stateVals[attr] = tftypes.NewValue(typ.TerraformType(ctx), nil) + } + } else { + err := state.As(&stateVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting state: "+err.Error()) + return plan, diags + } + } + + planVals := map[string]tftypes.Value{} + err := plan.As(&planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting plan: "+err.Error()) + return plan, diags + } + for attrName, attrPlan := range planVals { + attrType, ok := wat.AttributeTypes()[attrName] + if !ok { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe type of %s has no %q attribute. This should never happen.", path, attrName), + ) + return plan, diags + } + attrState, ok := stateVals[attrName] + if !ok { + attrState = tftypes.NewValue(attrType.TerraformType(ctx), nil) + } + newPlan, ds := attributeTypeModifyPlan(ctx, attrType, attrState, attrPlan, path.WithAttributeName(attrName)) + diags.Append(ds...) + if diags.HasError() { + return plan, diags + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithAttributeName(attrName), err), + ) + return plan, diags + } + planVals[attrName] = rawNewPlan + } + err = tftypes.ValidateValue(typ.TerraformType(ctx), planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe modified plan was no longer compatible with the Terraform type: %s", err), + ) + return plan, diags + } + return tftypes.NewValue(typ.TerraformType(ctx), planVals), diags +} + +func attributeTypeModifyPlanMap(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + wet, ok := typ.(attr.TypeWithElementType) + if !ok { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nMap at "+path.String()+" isn't of an attr.Type that implements TypeWithElementType. This isn't a valid type for this value.") + return plan, diags + } + stateVals := map[string]tftypes.Value{} + if !state.IsNull() { + err := state.As(&stateVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting state: "+err.Error()) + return plan, diags + } + } + planVals := map[string]tftypes.Value{} + err := plan.As(&planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting plan: "+err.Error()) + return plan, diags + } + for key, elemPlan := range planVals { + elemState, ok := stateVals[key] + if !ok { + elemState = tftypes.NewValue(wet.ElementType().TerraformType(ctx), nil) + } + newPlan, ds := attributeTypeModifyPlan(ctx, wet.ElementType(), elemState, elemPlan, path.WithElementKeyString(key)) + diags.Append(ds...) + if diags.HasError() { + return plan, diags + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyString(key), err), + ) + return plan, diags + } + planVals[key] = rawNewPlan + } + err = tftypes.ValidateValue(typ.TerraformType(ctx), planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe modified plan was no longer compatible with the Terraform type: %s", err), + ) + return plan, diags + } + return tftypes.NewValue(typ.TerraformType(ctx), planVals), diags +} + +func attributeTypeModifyPlanList(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + wet, ok := typ.(attr.TypeWithElementType) + if !ok { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nList at "+path.String()+" isn't of an attr.Type that implements TypeWithElementType. This isn't a valid type for this value.") + return plan, diags + } + stateVals := []tftypes.Value{} + if !state.IsNull() { + err := state.As(&stateVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting state: "+err.Error()) + return plan, diags + } + } + planVals := []tftypes.Value{} + err := plan.As(&planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting plan: "+err.Error()) + return plan, diags + } + for index, elemPlan := range planVals { + elemState := tftypes.NewValue(wet.ElementType().TerraformType(ctx), nil) + if index < len(stateVals) { + elemState = stateVals[index] + } + newPlan, ds := attributeTypeModifyPlan(ctx, wet.ElementType(), elemState, elemPlan, path.WithElementKeyInt(int64(index))) + diags.Append(ds...) + if diags.HasError() { + return plan, diags + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(int64(index)), err), + ) + return plan, diags + } + planVals[index] = rawNewPlan + } + err = tftypes.ValidateValue(typ.TerraformType(ctx), planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe modified plan was no longer compatible with the Terraform type: %s", err), + ) + return plan, diags + } + return tftypes.NewValue(typ.TerraformType(ctx), planVals), diags +} + +func attributeTypeModifyPlanTuple(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (tftypes.Value, diag.Diagnostics) { + var diags diag.Diagnostics + + wets, ok := typ.(attr.TypeWithElementTypes) + if !ok { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nTuple at "+path.String()+" isn't of an attr.Type that implements TypeWithElementTypes. This isn't a valid type for this value.") + return plan, diags + } + stateVals := []tftypes.Value{} + if !state.IsNull() { + err := state.As(&stateVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting state: "+err.Error()) + return plan, diags + } + } + planVals := []tftypes.Value{} + err := plan.As(&planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError converting plan: "+err.Error()) + return plan, diags + } + elemTypes := wets.ElementTypes() + for index, elemPlan := range planVals { + if index >= len(elemTypes) { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(int64(index)).String()+" that the tuple has no type for.") + return plan, diags + } + elemType := elemTypes[index] + if index >= len(stateVals) { + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(int64(index)).String()+" that has no state equivalent, which is not allowed in tuples.") + return plan, diags + } + elemState := tftypes.NewValue(elemType.TerraformType(ctx), nil) + if index < len(stateVals) { + elemState = stateVals[index] + } + newPlan, ds := attributeTypeModifyPlan(ctx, elemType, elemState, elemPlan, path.WithElementKeyInt(int64(index))) + diags.Append(ds...) + if diags.HasError() { + return plan, diags + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(int64(index)), err), + ) + return plan, diags + } + planVals[index] = rawNewPlan + } + err = tftypes.ValidateValue(typ.TerraformType(ctx), planVals) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe modified plan was no longer compatible with the Terraform type: %s", err), + ) + return plan, diags + } + return tftypes.NewValue(typ.TerraformType(ctx), planVals), diags +} + +func attributeTypeModifyPlan(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) { + var diags diag.Diagnostics + if plan.IsKnown() && !plan.IsNull() { + if typ.TerraformType(ctx).Is(tftypes.Object{}) { + newPlan, ds := attributeTypeModifyPlanObject(ctx, typ, state, plan, path) + diags.Append(ds...) + if diags.HasError() { + return nil, diags + } + plan = newPlan + } else if typ.TerraformType(ctx).Is(tftypes.Map{}) { + newPlan, ds := attributeTypeModifyPlanMap(ctx, typ, state, plan, path) + diags.Append(ds...) + if diags.HasError() { + return nil, diags + } + plan = newPlan + } else if typ.TerraformType(ctx).Is(tftypes.List{}) { + newPlan, ds := attributeTypeModifyPlanList(ctx, typ, state, plan, path) + diags.Append(ds...) + if diags.HasError() { + return nil, diags + } + plan = newPlan + } else if typ.TerraformType(ctx).Is(tftypes.Set{}) { + // modifying the plan for each element in a set isn't + // supported at the moment, because there's no way to + // correlate the new value in the plan with the old + // value in the state; sets can only be reliably + // compared by the identity of the elements, and the + // identity of the element changed. + // + // as such, sets must have plan modification applied at + // the set level, because anything else doesn't really + // make much sense. + // + // providers unhappy with this can always implement the + // logic to call each element's ModifyPlan inside their + // set type's ModifyPlan, but I can't see a way to do + // it that's not rife with weird behaviors. + } else if typ.TerraformType(ctx).Is(tftypes.Tuple{}) { + newPlan, ds := attributeTypeModifyPlanTuple(ctx, typ, state, plan, path) + diags.Append(ds...) + if diags.HasError() { + return nil, diags + } + plan = newPlan + } + } + planModifier, ok := typ.(attr.TypeWithModifyPlan) + if !ok { + planVal, err := typ.ValueFromTerraform(ctx, plan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nThe modified plan was no longer compatible with the Terraform type: %s", err), + ) + return nil, diags + } + return planVal, nil + } + stateVal, err := typ.ValueFromTerraform(ctx, state) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError creating state tftypes.Value from attr.Value: %s", err), + ) + return nil, diags + } + planVal, err := typ.ValueFromTerraform(ctx, plan) + if err != nil { + diags.AddAttributeError(path, "Error generating plan", + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nError creating plan tftypes.Value from attr.Value: %s", err), + ) + return nil, diags + } + return planModifier.ModifyPlan(ctx, stateVal, planVal, path) +} From c85a4a7b46ec595b72f0a71f72fb46d44b0ad469 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Wed, 15 Sep 2021 08:53:09 -0700 Subject: [PATCH 2/8] handle null states --- tfsdk/serve.go | 71 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/tfsdk/serve.go b/tfsdk/serve.go index 8104ae04b..b871fea11 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -709,28 +709,38 @@ func (s *server) planResourceChange(ctx context.Context, req *tfprotov6.PlanReso // We only do this if there's a plan to modify; otherwise, it // represents a resource being deleted and there's no point. if !plan.IsNull() { - rawStateVal := map[string]tftypes.Value{} - err = state.As(&rawStateVal) - if err != nil { - // TODO: error - } rawPlanVal := map[string]tftypes.Value{} err = plan.As(&rawPlanVal) if err != nil { - // TODO: error + resp.Diagnostics.AddError( + "Error parsing plan", + "An unexpected error was encountered trying to parse the prior state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } + rawStateVal := map[string]tftypes.Value{} + if !state.IsNull() { + err = state.As(&rawStateVal) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing prior state", + "An unexpected error was encountered trying to parse the prior state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return + } } for attrName, a := range resourceSchema.Attributes { path := tftypes.NewAttributePath().WithAttributeName(attrName) - state, ok := rawStateVal[attrName] - if !ok { - // TODO: error - } plan, ok := rawPlanVal[attrName] if !ok { // TODO: error } if a.Type != nil { + state, ok := rawStateVal[attrName] + if !ok { + state = tftypes.NewValue(a.Type.TerraformType(ctx), nil) + } newPlan, diags := attributeTypeModifyPlan(ctx, a.Type, state, plan, path) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -742,25 +752,48 @@ func (s *server) planResourceChange(ctx context.Context, req *tfprotov6.PlanReso } rawPlanVal[attrName] = rawNewPlan } else if a.Attributes != nil { + state, ok := rawStateVal[attrName] + if !ok { + state = tftypes.NewValue(a.Attributes.AttributeType().TerraformType(ctx), nil) + } newPlan, diags := attributeTypeModifyPlan(ctx, a.Attributes.AttributeType(), state, plan, path) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) - if err != nil { + + if a.Type != nil { + newPlan, diags := attributeTypeModifyPlan(ctx, a.Type, state, plan, path) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + // TODO: error + } + rawPlanVal[attrName] = rawNewPlan + } else if a.Attributes != nil { + newPlan, diags := attributeTypeModifyPlan(ctx, a.Attributes.AttributeType(), state, plan, path) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + // TODO: error + } + rawPlanVal[attrName] = rawNewPlan + } else { // TODO: error } - rawPlanVal[attrName] = rawNewPlan - } else { + } + err = tftypes.ValidateValue(plan.Type(), rawPlanVal) + if err != nil { // TODO: error } + plan = tftypes.NewValue(plan.Type(), rawPlanVal) } - err = tftypes.ValidateValue(plan.Type(), rawPlanVal) - if err != nil { - // TODO: error - } - plan = tftypes.NewValue(plan.Type(), rawPlanVal) } // Execute any AttributePlanModifiers. From 4cecdb04828770d3220a0c09d179f369cd8ece7a Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Wed, 3 Nov 2021 10:54:00 -0700 Subject: [PATCH 3/8] Update for breaking changes, extract. Move out of serve and into a helper function. Simplify some of the logic. --- tfsdk/serve.go | 91 ++------------------------------- tfsdk/type_plan_modification.go | 73 +++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 93 deletions(-) diff --git a/tfsdk/serve.go b/tfsdk/serve.go index b871fea11..b86798fc9 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -7,7 +7,6 @@ import ( "sort" "sync" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/proto6" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -708,93 +707,11 @@ func (s *server) planResourceChange(ctx context.Context, req *tfprotov6.PlanReso // // We only do this if there's a plan to modify; otherwise, it // represents a resource being deleted and there's no point. - if !plan.IsNull() { - rawPlanVal := map[string]tftypes.Value{} - err = plan.As(&rawPlanVal) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing plan", - "An unexpected error was encountered trying to parse the prior state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), - ) - return - } - rawStateVal := map[string]tftypes.Value{} - if !state.IsNull() { - err = state.As(&rawStateVal) - if err != nil { - resp.Diagnostics.AddError( - "Error parsing prior state", - "An unexpected error was encountered trying to parse the prior state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), - ) - return - } - } - for attrName, a := range resourceSchema.Attributes { - path := tftypes.NewAttributePath().WithAttributeName(attrName) - plan, ok := rawPlanVal[attrName] - if !ok { - // TODO: error - } - - if a.Type != nil { - state, ok := rawStateVal[attrName] - if !ok { - state = tftypes.NewValue(a.Type.TerraformType(ctx), nil) - } - newPlan, diags := attributeTypeModifyPlan(ctx, a.Type, state, plan, path) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) - if err != nil { - // TODO: error - } - rawPlanVal[attrName] = rawNewPlan - } else if a.Attributes != nil { - state, ok := rawStateVal[attrName] - if !ok { - state = tftypes.NewValue(a.Attributes.AttributeType().TerraformType(ctx), nil) - } - newPlan, diags := attributeTypeModifyPlan(ctx, a.Attributes.AttributeType(), state, plan, path) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - if a.Type != nil { - newPlan, diags := attributeTypeModifyPlan(ctx, a.Type, state, plan, path) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) - if err != nil { - // TODO: error - } - rawPlanVal[attrName] = rawNewPlan - } else if a.Attributes != nil { - newPlan, diags := attributeTypeModifyPlan(ctx, a.Attributes.AttributeType(), state, plan, path) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) - if err != nil { - // TODO: error - } - rawPlanVal[attrName] = rawNewPlan - } else { - // TODO: error - } - } - err = tftypes.ValidateValue(plan.Type(), rawPlanVal) - if err != nil { - // TODO: error - } - plan = tftypes.NewValue(plan.Type(), rawPlanVal) - } + newPlan, ok := runTypePlanModifiers(ctx, state, plan, resourceSchema, resp) + if !ok { + return } + plan = newPlan // Execute any AttributePlanModifiers. // diff --git a/tfsdk/type_plan_modification.go b/tfsdk/type_plan_modification.go index 8b926eea4..28c8629fd 100644 --- a/tfsdk/type_plan_modification.go +++ b/tfsdk/type_plan_modification.go @@ -9,6 +9,67 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" ) +func runTypePlanModifiers(ctx context.Context, state, plan tftypes.Value, schema Schema, resp *planResourceChangeResponse) (tftypes.Value, bool) { + if plan.IsNull() { + return plan, true + } + rawPlan := map[string]tftypes.Value{} + err := plan.As(&rawPlan) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing plan", + "An unexpected error was encountered trying to parse the plan. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return plan, false + } + rawState := map[string]tftypes.Value{} + if !state.IsNull() { + err = state.As(&rawState) + if err != nil { + resp.Diagnostics.AddError( + "Error parsing state", + "An unexpected error was encountered trying to parse the prior state. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return plan, false + } + } + for attrName, a := range schema.Attributes { + path := tftypes.NewAttributePath().WithAttributeName(attrName) + planAttr, ok := rawPlan[attrName] + if !ok { + resp.Diagnostics.AddError( + "Error modifying plan", + // TODO: this isn't ideal + // but should it be a pathed diagnostic? + // Terraform is horribly broken at this point + // there may be nothing in the config to point to + fmt.Sprintf("An attribute %s in the schema was not present in the plan. This is possibly a bug with Terraform. Please report it to the provider developer.", path), + ) + return plan, false + } + stateAttr, ok := rawState[attrName] + if !ok { + stateAttr = tftypes.NewValue(a.terraformType(ctx), nil) + } + newPlan, diags := attributeTypeModifyPlan(ctx, a.attributeType(), stateAttr, planAttr, path) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return plan, false + } + rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + if err != nil { + // TODO: error + } + rawPlan[attrName] = rawNewPlan + } + err = tftypes.ValidateValue(plan.Type(), rawPlan) + if err != nil { + // TODO: error + } + plan = tftypes.NewValue(plan.Type(), rawPlan) + return plan, true +} + func attributeTypeModifyPlanObject(ctx context.Context, typ attr.Type, state, plan tftypes.Value, path *tftypes.AttributePath) (tftypes.Value, diag.Diagnostics) { var diags diag.Diagnostics @@ -154,7 +215,7 @@ func attributeTypeModifyPlanList(ctx context.Context, typ attr.Type, state, plan if index < len(stateVals) { elemState = stateVals[index] } - newPlan, ds := attributeTypeModifyPlan(ctx, wet.ElementType(), elemState, elemPlan, path.WithElementKeyInt(int64(index))) + newPlan, ds := attributeTypeModifyPlan(ctx, wet.ElementType(), elemState, elemPlan, path.WithElementKeyInt(index)) diags.Append(ds...) if diags.HasError() { return plan, diags @@ -162,7 +223,7 @@ func attributeTypeModifyPlanList(ctx context.Context, typ attr.Type, state, plan rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) if err != nil { diags.AddAttributeError(path, "Error generating plan", - fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(int64(index)), err), + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(index), err), ) return plan, diags } @@ -203,19 +264,19 @@ func attributeTypeModifyPlanTuple(ctx context.Context, typ attr.Type, state, pla elemTypes := wets.ElementTypes() for index, elemPlan := range planVals { if index >= len(elemTypes) { - diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(int64(index)).String()+" that the tuple has no type for.") + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(index).String()+" that the tuple has no type for.") return plan, diags } elemType := elemTypes[index] if index >= len(stateVals) { - diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(int64(index)).String()+" that has no state equivalent, which is not allowed in tuples.") + diags.AddAttributeError(path, "Error generating plan", "An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nPlan has a value at "+path.WithElementKeyInt(index).String()+" that has no state equivalent, which is not allowed in tuples.") return plan, diags } elemState := tftypes.NewValue(elemType.TerraformType(ctx), nil) if index < len(stateVals) { elemState = stateVals[index] } - newPlan, ds := attributeTypeModifyPlan(ctx, elemType, elemState, elemPlan, path.WithElementKeyInt(int64(index))) + newPlan, ds := attributeTypeModifyPlan(ctx, elemType, elemState, elemPlan, path.WithElementKeyInt(index)) diags.Append(ds...) if diags.HasError() { return plan, diags @@ -223,7 +284,7 @@ func attributeTypeModifyPlanTuple(ctx context.Context, typ attr.Type, state, pla rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) if err != nil { diags.AddAttributeError(path, "Error generating plan", - fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(int64(index)), err), + fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(index), err), ) return plan, diags } From 475c0a11e603065599f9bbca15aef4c577e7be66 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Thu, 11 Nov 2021 05:03:32 -0800 Subject: [PATCH 4/8] Fill in some error handling. --- tfsdk/type_plan_modification.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tfsdk/type_plan_modification.go b/tfsdk/type_plan_modification.go index 28c8629fd..417c9fb3a 100644 --- a/tfsdk/type_plan_modification.go +++ b/tfsdk/type_plan_modification.go @@ -58,13 +58,21 @@ func runTypePlanModifiers(ctx context.Context, state, plan tftypes.Value, schema } rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) if err != nil { - // TODO: error + resp.Diagnostics.AddError( + "Error converting value", + "An unexpected error was encountered converting a value to its protocol type during plan modification. This is always a bug in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return plan, false } rawPlan[attrName] = rawNewPlan } err = tftypes.ValidateValue(plan.Type(), rawPlan) if err != nil { - // TODO: error + resp.Diagnostics.AddError( + "Error modifying plan", + "An unexpected error was encountered validating the modified plan. This is always a bug in the provider. Please report the following to the provider developer:\n\n"+err.Error(), + ) + return plan, false } plan = tftypes.NewValue(plan.Type(), rawPlan) return plan, true From 6aa1a85e197b6b7e8088a930ebb2656c2e709bf7 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Thu, 11 Nov 2021 05:43:43 -0800 Subject: [PATCH 5/8] Add first test and test harness. --- tfsdk/type_plan_modification_test.go | 147 +++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tfsdk/type_plan_modification_test.go diff --git a/tfsdk/type_plan_modification_test.go b/tfsdk/type_plan_modification_test.go new file mode 100644 index 000000000..218170e9c --- /dev/null +++ b/tfsdk/type_plan_modification_test.go @@ -0,0 +1,147 @@ +package tfsdk + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + testtypes "github.com/hashicorp/terraform-plugin-framework/internal/testing/types" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +type typeWithPlanModifier struct { + modifyPlan func(ctx context.Context, state attr.Value, plan attr.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) +} + +func (t typeWithPlanModifier) TerraformType(_ context.Context) tftypes.Type { + return tftypes.String +} + +func (t typeWithPlanModifier) ValueFromTerraform(_ context.Context, val tftypes.Value) (attr.Value, error) { + ret := testtypes.String{CreatedBy: t} + if val.IsNull() { + ret.String = types.String{Null: true} + return ret, nil + } + if !val.IsKnown() { + ret.String = types.String{Unknown: true} + return ret, nil + } + var v string + err := val.As(&v) + if err != nil { + return nil, err + } + ret.String = types.String{Value: v} + return ret, nil +} + +func (t typeWithPlanModifier) Equal(o attr.Type) bool { + _, ok := o.(typeWithPlanModifier) + if !ok { + return false + } + return true +} + +func (t typeWithPlanModifier) String() string { + return "tfsdk.typeWithPlanModifier" +} + +func (t typeWithPlanModifier) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return nil, fmt.Errorf("cannot apply AttributePathStep %T to %s", step, t.String()) +} + +func (t typeWithPlanModifier) ModifyPlan(ctx context.Context, state attr.Value, plan attr.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) { + return t.modifyPlan(ctx, state, plan, path) +} + +func TestRunTypePlanModifiers(t *testing.T) { + t.Parallel() + + type testCase struct { + state tftypes.Value + plan tftypes.Value + schema Schema + resp *planResourceChangeResponse + expectedPlan tftypes.Value + expectedDiags diag.Diagnostics + expectedRR []*tftypes.AttributePath + expectedOK bool + } + + tests := map[string]testCase{ + "case-insensitive": { + state: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hello, world"), + }), + plan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hElLo, WoRlD"), + }), + schema: Schema{ + Attributes: map[string]Attribute{ + "input": { + Type: typeWithPlanModifier{ + modifyPlan: func(ctx context.Context, state attr.Value, plan attr.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) { + st := state.(testtypes.String) + pl := plan.(testtypes.String) + if strings.ToLower(st.String.Value) == strings.ToLower(pl.String.Value) { + return state, nil + } + return plan, nil + }, + }, + Required: true, + }, + }, + }, + resp: &planResourceChangeResponse{}, + expectedPlan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hello, world"), + }), + expectedDiags: nil, + expectedRR: nil, + expectedOK: true, + }, + } + + for name, tc := range tests { + name, tc := name, tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + plan, ok := runTypePlanModifiers(context.Background(), tc.state, tc.plan, tc.schema, tc.resp) + + if ok != tc.expectedOK { + t.Fatalf("expected ok to be %v, got %v", tc.expectedOK, ok) + } + if diff := cmp.Diff(tc.resp.Diagnostics, tc.expectedDiags); diff != "" { + t.Fatalf("Unexpected diff in diagnostics (+wanted, -got): %s", diff) + } + if diff := cmp.Diff(plan, tc.expectedPlan); diff != "" { + t.Fatalf("Unexpected diff in plan result (+wanted, -got): %s", diff) + } + if diff := cmp.Diff(tc.resp.RequiresReplace, tc.expectedRR); diff != "" { + t.Fatalf("Unexpected diff in requires replace (+wanted, -got): %s", diff) + } + }) + } +} From fa6fac128a64a278dd2d09ae1ee9092bc2a97fe1 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Fri, 3 Dec 2021 06:26:41 -0800 Subject: [PATCH 6/8] Rebase. Rebased on top of paddy_toterraformvalue branch so we can get rid of our helper. --- attr/value.go | 12 ------------ tfsdk/type_plan_modification.go | 10 +++++----- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/attr/value.go b/attr/value.go index 4714e12af..3b151a4f3 100644 --- a/attr/value.go +++ b/attr/value.go @@ -21,15 +21,3 @@ type Value interface { // to the Value passed as an argument. Equal(Value) bool } - -func ValueToTerraform(ctx context.Context, val Value) (tftypes.Value, error) { - raw, err := val.ToTerraformValue(ctx) - if err != nil { - return tftypes.Value{}, err - } - err = tftypes.ValidateValue(val.Type(ctx).TerraformType(ctx), raw) - if err != nil { - return tftypes.Value{}, err - } - return tftypes.NewValue(val.Type(ctx).TerraformType(ctx), raw), nil -} diff --git a/tfsdk/type_plan_modification.go b/tfsdk/type_plan_modification.go index 417c9fb3a..2ad697f70 100644 --- a/tfsdk/type_plan_modification.go +++ b/tfsdk/type_plan_modification.go @@ -56,7 +56,7 @@ func runTypePlanModifiers(ctx context.Context, state, plan tftypes.Value, schema if resp.Diagnostics.HasError() { return plan, false } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + rawNewPlan, err := newPlan.ToTerraformValue(ctx) if err != nil { resp.Diagnostics.AddError( "Error converting value", @@ -126,7 +126,7 @@ func attributeTypeModifyPlanObject(ctx context.Context, typ attr.Type, state, pl if diags.HasError() { return plan, diags } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + rawNewPlan, err := newPlan.ToTerraformValue(ctx) if err != nil { diags.AddAttributeError(path, "Error generating plan", fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithAttributeName(attrName), err), @@ -177,7 +177,7 @@ func attributeTypeModifyPlanMap(ctx context.Context, typ attr.Type, state, plan if diags.HasError() { return plan, diags } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + rawNewPlan, err := newPlan.ToTerraformValue(ctx) if err != nil { diags.AddAttributeError(path, "Error generating plan", fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyString(key), err), @@ -228,7 +228,7 @@ func attributeTypeModifyPlanList(ctx context.Context, typ attr.Type, state, plan if diags.HasError() { return plan, diags } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + rawNewPlan, err := newPlan.ToTerraformValue(ctx) if err != nil { diags.AddAttributeError(path, "Error generating plan", fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(index), err), @@ -289,7 +289,7 @@ func attributeTypeModifyPlanTuple(ctx context.Context, typ attr.Type, state, pla if diags.HasError() { return plan, diags } - rawNewPlan, err := attr.ValueToTerraform(ctx, newPlan) + rawNewPlan, err := newPlan.ToTerraformValue(ctx) if err != nil { diags.AddAttributeError(path, "Error generating plan", fmt.Sprintf("An unexpected error was encountered while trying to generate the plan. Please report the following to the provider developer:\n\nCouldn't convert the attr.Value at %s back to a tftypes.Value: %s", path.WithElementKeyInt(index), err), From 9bcc2463ef4384a480c1b1e2a2a4d3e089ba71b7 Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Fri, 3 Dec 2021 06:50:55 -0800 Subject: [PATCH 7/8] Add more test cases. --- tfsdk/type_plan_modification_test.go | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tfsdk/type_plan_modification_test.go b/tfsdk/type_plan_modification_test.go index 218170e9c..7ad5462d8 100644 --- a/tfsdk/type_plan_modification_test.go +++ b/tfsdk/type_plan_modification_test.go @@ -120,6 +120,120 @@ func TestRunTypePlanModifiers(t *testing.T) { expectedRR: nil, expectedOK: true, }, + "preserve-existing": { + state: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hello, world"), + }), + plan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hElLo, WoRlD"), + }), + schema: Schema{ + Attributes: map[string]Attribute{ + "input": { + Type: typeWithPlanModifier{ + modifyPlan: func(ctx context.Context, state attr.Value, plan attr.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) { + st := state.(testtypes.String) + pl := plan.(testtypes.String) + if strings.ToLower(st.String.Value) == strings.ToLower(pl.String.Value) { + return state, diag.Diagnostics{ + diag.NewWarningDiagnostic( + "Diff suppressed", + "We suppressed a diff because the strings were only different in capitalization. Normally you wouldn't warn on this, but work with me here.", + ), + } + } + return plan, nil + }, + }, + Required: true, + }, + }, + }, + resp: &planResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "Other warning", + "Deprecated attribute or something", + ), + }, + RequiresReplace: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + }, + expectedPlan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hello, world"), + }), + expectedDiags: diag.Diagnostics{ + diag.NewWarningDiagnostic( + "Other warning", + "Deprecated attribute or something", + ), + diag.NewWarningDiagnostic( + "Diff suppressed", + "We suppressed a diff because the strings were only different in capitalization. Normally you wouldn't warn on this, but work with me here.", + ), + }, + expectedRR: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("foo"), + }, + expectedOK: true, + }, + "error": { + state: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hello, world"), + }), + plan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hElLo, WoRlD"), + }), + schema: Schema{ + Attributes: map[string]Attribute{ + "input": { + Type: typeWithPlanModifier{ + modifyPlan: func(ctx context.Context, state attr.Value, plan attr.Value, path *tftypes.AttributePath) (attr.Value, diag.Diagnostics) { + // something bad happened + return plan, diag.Diagnostics{ + diag.NewErrorDiagnostic("Ooops", "something bad happened"), + } + }, + }, + Required: true, + }, + }, + }, + resp: &planResourceChangeResponse{}, + expectedPlan: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "input": tftypes.String, + }, + }, map[string]tftypes.Value{ + "input": tftypes.NewValue(tftypes.String, "hElLo, WoRlD"), + }), + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic("Ooops", "something bad happened"), + }, + expectedRR: nil, + expectedOK: false, + }, } for name, tc := range tests { From 69053e4b612586ce1e94f48e017c39c65f98d39e Mon Sep 17 00:00:00 2001 From: Paddy Carver Date: Fri, 3 Dec 2021 06:53:10 -0800 Subject: [PATCH 8/8] Add changelog entry. --- .changelog/162.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/162.txt diff --git a/.changelog/162.txt b/.changelog/162.txt new file mode 100644 index 000000000..f7b4eb1d0 --- /dev/null +++ b/.changelog/162.txt @@ -0,0 +1,3 @@ +```release-note:feature +Added support for a ModifyPlan method on the `attr.Type` type, allowing custom types to include plan modification logic that will be applied to every attribute or element using that type. +```