diff --git a/tfsdk/attribute.go b/tfsdk/attribute.go index cf4872079..227dc30bc 100644 --- a/tfsdk/attribute.go +++ b/tfsdk/attribute.go @@ -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 @@ -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 + } +} diff --git a/tfsdk/attribute_plan_modification.go b/tfsdk/attribute_plan_modification.go new file mode 100644 index 000000000..8eac8aa37 --- /dev/null +++ b/tfsdk/attribute_plan_modification.go @@ -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, + }) +} diff --git a/tfsdk/request.go b/tfsdk/request.go index 50594d460..9407d506d 100644 --- a/tfsdk/request.go +++ b/tfsdk/request.go @@ -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 @@ -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 -} diff --git a/tfsdk/response.go b/tfsdk/response.go index 99c9a45b9..b62e0373d 100644 --- a/tfsdk/response.go +++ b/tfsdk/response.go @@ -1,7 +1,6 @@ package tfsdk import ( - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -307,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 } @@ -410,63 +409,3 @@ func (r *ReadDataSourceResponse) AddAttributeError(attributePath *tftypes.Attrib Severity: tfprotov6.DiagnosticSeverityError, }) } - -// 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 { - // Plan is the planned new state for the attribute. - Plan 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, - }) -} diff --git a/tfsdk/schema.go b/tfsdk/schema.go index bff76a332..5cecf752b 100644 --- a/tfsdk/schema.go +++ b/tfsdk/schema.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/diagnostics" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -187,6 +188,7 @@ func (s Schema) tfprotov6Schema(ctx context.Context) (*tfprotov6.Schema, error) // validate performs all Attribute validation. func (s Schema) validate(ctx context.Context, req ValidateSchemaRequest, resp *ValidateSchemaResponse) { for name, attribute := range s.Attributes { + attributeReq := ValidateAttributeRequest{ AttributePath: tftypes.NewAttributePath().WithAttributeName(name), Config: req.Config, @@ -208,3 +210,189 @@ func (s Schema) validate(ctx context.Context, req ValidateSchemaRequest, resp *V }) } } + +// modifyAttributePlans runs all AttributePlanModifiers in all schema attributes +func (s Schema) modifyAttributePlans(ctx context.Context, req ModifySchemaPlanRequest, resp *ModifySchemaPlanResponse) { + for name, a := range s.Attributes { + attrPath := tftypes.NewAttributePath().WithAttributeName(name) + + attrPlan, diags := req.Plan.GetAttribute(ctx, attrPath) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diagnostics.DiagsHasErrors(diags) { + return + } + + attributeReq := ModifyAttributePlanRequest{ + AttributePath: attrPath, + Config: req.Config, + State: req.State, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + } + + attributeResp := &ModifyAttributePlanResponse{ + AttributePlan: attrPlan, + } + + a.modifyPlan(ctx, attributeReq, attributeResp) + + if attributeResp.RequiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, attrPath) + } + + resp.Diagnostics = append(resp.Diagnostics, attributeResp.Diagnostics...) + + setAttrDiags := resp.Plan.SetAttribute(ctx, attrPath, attributeResp.AttributePlan) + resp.Diagnostics = append(resp.Diagnostics, setAttrDiags...) + if diagnostics.DiagsHasErrors(setAttrDiags) { + return + } + + if a.Attributes != nil { + nm := a.Attributes.GetNestingMode() + switch nm { + case NestingModeList: + l, ok := attrPlan.(types.List) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", attrPlan, nm, attrPath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Plan Modification Error", + Detail: "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: attrPath, + }) + + return + } + + for idx := range l.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrPath := attrPath.WithElementKeyInt(int64(idx)).WithAttributeName(nestedName) + nestedAttrPlan, diags := req.Plan.GetAttribute(ctx, nestedAttrPath) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diagnostics.DiagsHasErrors(diags) { + return + } + nestedAttrReq := ModifyAttributePlanRequest{ + AttributePath: nestedAttrPath, + Config: req.Config, + State: req.State, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + } + nestedAttrResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedAttrPlan, + Diagnostics: resp.Diagnostics, + } + + nestedAttr.modifyPlan(ctx, nestedAttrReq, nestedAttrResp) + if nestedAttrResp.RequiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, nestedAttrPath) + } + + setAttrDiags := resp.Plan.SetAttribute(ctx, nestedAttrPath, nestedAttrResp.AttributePlan) + resp.Diagnostics = append(resp.Diagnostics, setAttrDiags...) + if diagnostics.DiagsHasErrors(setAttrDiags) { + return + } + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSet: + // TODO: Set implementation + // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/53 + case NestingModeMap: + m, ok := attrPlan.(types.Map) + + if !ok { + err := fmt.Errorf("unknown attribute value type (%T) for nesting mode (%T) at path: %s", attrPlan, nm, attrPath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Plan Modification Error", + Detail: "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: attrPath, + }) + + return + } + + for key := range m.Elems { + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrPath := attrPath.WithElementKeyString(key).WithAttributeName(nestedName) + nestedAttrPlan, diags := req.Plan.GetAttribute(ctx, nestedAttrPath) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diagnostics.DiagsHasErrors(diags) { + return + } + nestedAttrReq := ModifyAttributePlanRequest{ + AttributePath: nestedAttrPath, + Config: req.Config, + State: req.State, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + } + nestedAttrResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedAttrPlan, + Diagnostics: resp.Diagnostics, + } + + nestedAttr.modifyPlan(ctx, nestedAttrReq, nestedAttrResp) + + if nestedAttrResp.RequiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, nestedAttrPath) + } + setAttrDiags := resp.Plan.SetAttribute(ctx, nestedAttrPath, nestedAttrResp.AttributePlan) + resp.Diagnostics = append(resp.Diagnostics, setAttrDiags...) + if diagnostics.DiagsHasErrors(setAttrDiags) { + return + } + resp.Diagnostics = nestedAttrResp.Diagnostics + } + } + case NestingModeSingle: + for nestedName, nestedAttr := range a.Attributes.GetAttributes() { + nestedAttrPath := attrPath.WithAttributeName(nestedName) + nestedAttrPlan, diags := req.Plan.GetAttribute(ctx, nestedAttrPath) + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diagnostics.DiagsHasErrors(diags) { + return + } + nestedAttrReq := ModifyAttributePlanRequest{ + AttributePath: nestedAttrPath, + Config: req.Config, + State: req.State, + Plan: req.Plan, + ProviderMeta: req.ProviderMeta, + } + nestedAttrResp := &ModifyAttributePlanResponse{ + AttributePlan: nestedAttrPlan, + Diagnostics: resp.Diagnostics, + } + + nestedAttr.modifyPlan(ctx, nestedAttrReq, nestedAttrResp) + + if nestedAttrResp.RequiresReplace { + resp.RequiresReplace = append(resp.RequiresReplace, nestedAttrPath) + } + setAttrDiags := resp.Plan.SetAttribute(ctx, nestedAttrPath, nestedAttrResp.AttributePlan) + resp.Diagnostics = append(resp.Diagnostics, setAttrDiags...) + if diagnostics.DiagsHasErrors(setAttrDiags) { + return + } + resp.Diagnostics = nestedAttrResp.Diagnostics + } + default: + err := fmt.Errorf("unknown attribute nesting mode (%T: %v) at path: %s", nm, nm, attrPath) + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Attribute Plan Modification Error", + Detail: "Attribute plan modifier cannot walk schema. Report this to the provider developer:\n\n" + err.Error(), + Attribute: attrPath, + }) + + return + } + } + } +} diff --git a/tfsdk/schema_plan_modification.go b/tfsdk/schema_plan_modification.go new file mode 100644 index 000000000..d92f86c25 --- /dev/null +++ b/tfsdk/schema_plan_modification.go @@ -0,0 +1,39 @@ +package tfsdk + +import ( + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ModifySchemaPlanRequest represents a request for a schema to run all +// attribute plan modification functions. +type ModifySchemaPlanRequest struct { + // 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 + + // ProviderMeta is metadata from the provider_meta block of the module. + ProviderMeta Config +} + +// ModifySchemaPlanResponse represents a response to a ModifySchemaPlanRequest. +type ModifySchemaPlanResponse struct { + // Plan is the planned new state for the resource. + Plan Plan + + // RequiresReplace is a list of tftypes.AttributePaths that require the + // resource to be replaced. They should point to the specific field + // that changed that requires the resource to be destroyed and + // recreated. + RequiresReplace []*tftypes.AttributePath + + // Diagnostics report errors or warnings related to running all attribute + // plan modifiers. Returning an empty slice indicates a successful + // plan modification with no warnings or errors generated. + Diagnostics []*tfprotov6.Diagnostic +} diff --git a/tfsdk/serve.go b/tfsdk/serve.go index d16e1b4fc..d65d2080a 100644 --- a/tfsdk/serve.go +++ b/tfsdk/serve.go @@ -584,9 +584,10 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanReso return resp, nil } + resp.PlannedState = req.ProposedNewState + if plan.IsNull() || !plan.IsKnown() { // on null or unknown plans, just bail, we can't do anything - resp.PlannedState = req.ProposedNewState return resp, nil } @@ -598,6 +599,65 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanReso return resp, nil } + // first, execute any AttributePlanModifiers + modifySchemaPlanReq := ModifySchemaPlanRequest{ + Config: Config{ + Schema: resourceSchema, + Raw: config, + }, + State: State{ + Schema: resourceSchema, + Raw: state, + }, + Plan: Plan{ + Schema: resourceSchema, + Raw: plan, + }, + } + if pm, ok := s.p.(ProviderWithProviderMeta); ok { + pmSchema, diags := pm.GetMetaSchema(ctx) + if diags != nil { + resp.Diagnostics = append(resp.Diagnostics, diags...) + if diagnostics.DiagsHasErrors(resp.Diagnostics) { + return resp, nil + } + } + modifySchemaPlanReq.ProviderMeta = Config{ + Schema: pmSchema, + Raw: tftypes.NewValue(pmSchema.TerraformType(ctx), nil), + } + + if req.ProviderMeta != nil { + pmValue, err := req.ProviderMeta.Unmarshal(pmSchema.TerraformType(ctx)) + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error parsing provider_meta", + Detail: "There was an error parsing the provider_meta block. Please report this to the provider developer:\n\n" + err.Error(), + }) + return resp, nil + } + modifySchemaPlanReq.ProviderMeta.Raw = pmValue + } + } + + modifySchemaPlanResp := ModifySchemaPlanResponse{ + Plan: Plan{ + Schema: resourceSchema, + Raw: plan, + }, + Diagnostics: resp.Diagnostics, + } + + resourceSchema.modifyAttributePlans(ctx, modifySchemaPlanReq, &modifySchemaPlanResp) + resp.RequiresReplace = modifySchemaPlanResp.RequiresReplace + plan = modifySchemaPlanResp.Plan.Raw + resp.Diagnostics = modifySchemaPlanResp.Diagnostics + if diagnostics.DiagsHasErrors(resp.Diagnostics) { + return resp, nil + } + + // second, execute any ModifyPlan func var modifyPlanResp ModifyResourcePlanResponse if resource, ok := resource.(ResourceWithModifyPlan); ok { modifyPlanReq := ModifyResourcePlanRequest{ @@ -650,7 +710,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanReso Diagnostics: resp.Diagnostics, } resource.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) - resp.Diagnostics = append(resp.Diagnostics, modifyPlanResp.Diagnostics...) + resp.Diagnostics = modifyPlanResp.Diagnostics plan = modifyPlanResp.Plan.Raw } @@ -674,7 +734,7 @@ func (s *server) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanReso return resp, nil } resp.PlannedState = &plannedState - resp.RequiresReplace = modifyPlanResp.RequiresReplace + resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) return resp, nil } diff --git a/tfsdk/serve_provider_test.go b/tfsdk/serve_provider_test.go index 2c56ad9de..90fddef05 100644 --- a/tfsdk/serve_provider_test.go +++ b/tfsdk/serve_provider_test.go @@ -431,6 +431,7 @@ func (t *testServeProvider) GetResources(_ context.Context) (map[string]Resource return map[string]ResourceType{ "test_one": testServeResourceTypeOne{}, "test_two": testServeResourceTypeTwo{}, + "test_three": testServeResourceTypeThree{}, "test_config_validators": testServeResourceTypeConfigValidators{}, "test_validate_config": testServeResourceTypeValidateConfig{}, }, nil diff --git a/tfsdk/serve_resource_one_test.go b/tfsdk/serve_resource_one_test.go index 07089b0ae..967a3c542 100644 --- a/tfsdk/serve_resource_one_test.go +++ b/tfsdk/serve_resource_one_test.go @@ -20,9 +20,8 @@ func (rt testServeResourceTypeOne) GetSchema(_ context.Context) (Schema, []*tfpr Type: types.StringType, }, "favorite_colors": { - Optional: true, - Type: types.ListType{ElemType: types.StringType}, - PlanModifiers: RequiresReplace(), + Optional: true, + Type: types.ListType{ElemType: types.StringType}, }, "created_timestamp": { Computed: true, diff --git a/tfsdk/serve_resource_three_test.go b/tfsdk/serve_resource_three_test.go new file mode 100644 index 000000000..5bb8615bd --- /dev/null +++ b/tfsdk/serve_resource_three_test.go @@ -0,0 +1,284 @@ +package tfsdk + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func (rt testServeResourceTypeThree) GetSchema(_ context.Context) (Schema, []*tfprotov6.Diagnostic) { + return Schema{ + Version: 1, + Attributes: map[string]Attribute{ + "name": { + Required: true, + Type: types.StringType, + // For the purposes of testing, these plan modifiers behave + // differently for certain values of the attribute. + PlanModifiers: []AttributePlanModifier{ + testWarningDiagModifier{}, + testErrorDiagModifier{}, + testAttrPlanValueModifierOne{}, + testAttrPlanValueModifierTwo{}, + }, + }, + "size": { + Required: true, + Type: types.NumberType, + PlanModifiers: []AttributePlanModifier{RequiresReplaceIf(func(ctx context.Context, state, config attr.Value) (bool, error) { + if state == nil && config == nil { + return false, nil + } + if (state == nil && config != nil) || (state != nil && config == nil) { + return true, nil + } + stateVal := state.(types.Number) + configVal := config.(types.Number) + + if !stateVal.Unknown && !stateVal.Null && !configVal.Unknown && !configVal.Null { + if configVal.Value.Cmp(stateVal.Value) > 0 { + return true, nil + } + } + return false, nil + }, "If the new size is greater than the old size, Terraform will destroy and recreate the resource", "If the new size is greater than the old size, Terraform will destroy and recreate the resource"), + }}, + "scratch_disk": { + Optional: true, + Attributes: SingleNestedAttributes(map[string]Attribute{ + "id": { + Required: true, + Type: types.StringType, + PlanModifiers: []AttributePlanModifier{ + testAttrPlanValueModifierTwo{}, + }, + }, + "interface": { + Required: true, + Type: types.StringType, + PlanModifiers: []AttributePlanModifier{RequiresReplace()}, + }, + }), + }, + }, + }, nil +} + +func (rt testServeResourceTypeThree) NewResource(_ context.Context, p Provider) (Resource, []*tfprotov6.Diagnostic) { + provider, ok := p.(*testServeProvider) + if !ok { + prov, ok := p.(*testServeProviderWithMetaSchema) + if !ok { + panic(fmt.Sprintf("unexpected provider type %T", p)) + } + provider = prov.testServeProvider + } + return testServeResourceThree{ + provider: provider, + }, nil +} + +var testServeResourceTypeThreeSchema = &tfprotov6.Schema{ + Version: 1, + Block: &tfprotov6.SchemaBlock{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "name", + Required: true, + Type: tftypes.String, + }, + { + Name: "scratch_disk", + Optional: true, + NestedType: &tfprotov6.SchemaObject{ + Attributes: []*tfprotov6.SchemaAttribute{ + { + Name: "id", + Required: true, + Type: tftypes.String, + }, + { + Name: "interface", + Required: true, + Type: tftypes.String, + }, + }, + Nesting: tfprotov6.SchemaObjectNestingModeSingle, + }, + }, + { + Name: "size", + Required: true, + Type: tftypes.Number, + }, + }, + }, +} + +var testServeResourceTypeThreeType = tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + "size": tftypes.Number, + "scratch_disk": tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }, + }, + }, +} + +type testServeResourceThree struct { + provider *testServeProvider +} + +type testServeResourceTypeThree struct{} + +type testWarningDiagModifier struct{} + +func (t testWarningDiagModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + attrVal, ok := req.AttributePlan.(types.String) + if !ok { + return + } + + if attrVal.Value == "TESTDIAG" { + resp.Diagnostics = append(resp.Diagnostics, + &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "Warning diag", + Detail: "This is a warning", + }, + ) + } +} + +func (t testWarningDiagModifier) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testWarningDiagModifier) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +type testErrorDiagModifier struct{} + +func (t testErrorDiagModifier) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + attrVal, ok := req.AttributePlan.(types.String) + if !ok { + return + } + + if attrVal.Value == "TESTDIAG" { + resp.Diagnostics = append(resp.Diagnostics, + &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error diag", + Detail: "This is an error", + }, + ) + } +} + +func (t testErrorDiagModifier) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testErrorDiagModifier) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +type testAttrPlanValueModifierOne struct{} + +func (t testAttrPlanValueModifierOne) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + attrVal, ok := req.AttributePlan.(types.String) + if !ok { + return + } + + if attrVal.Value == "TESTATTRONE" { + resp.AttributePlan = types.String{ + Value: "TESTATTRTWO", + } + } +} + +func (t testAttrPlanValueModifierOne) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testAttrPlanValueModifierOne) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +type testAttrPlanValueModifierTwo struct{} + +func (t testAttrPlanValueModifierTwo) Modify(ctx context.Context, req ModifyAttributePlanRequest, resp *ModifyAttributePlanResponse) { + attrVal, ok := req.AttributePlan.(types.String) + if !ok { + return + } + + if attrVal.Value == "TESTATTRTWO" { + resp.AttributePlan = types.String{ + Value: "MODIFIED_TWO", + } + } +} + +func (t testAttrPlanValueModifierTwo) Description(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (t testAttrPlanValueModifierTwo) MarkdownDescription(ctx context.Context) string { + return "This plan modifier is for use during testing only" +} + +func (r testServeResourceThree) Create(ctx context.Context, req CreateResourceRequest, resp *CreateResourceResponse) { + r.provider.applyResourceChangePlannedStateValue = req.Plan.Raw + r.provider.applyResourceChangePlannedStateSchema = req.Plan.Schema + r.provider.applyResourceChangeConfigValue = req.Config.Raw + r.provider.applyResourceChangeConfigSchema = req.Config.Schema + r.provider.applyResourceChangeProviderMetaValue = req.ProviderMeta.Raw + r.provider.applyResourceChangeProviderMetaSchema = req.ProviderMeta.Schema + r.provider.applyResourceChangeCalledResourceType = "test_three" + r.provider.applyResourceChangeCalledAction = "create" + r.provider.createFunc(ctx, req, resp) +} + +func (r testServeResourceThree) Read(ctx context.Context, req ReadResourceRequest, resp *ReadResourceResponse) { + r.provider.readResourceCurrentStateValue = req.State.Raw + r.provider.readResourceCurrentStateSchema = req.State.Schema + r.provider.readResourceProviderMetaValue = req.ProviderMeta.Raw + r.provider.readResourceProviderMetaSchema = req.ProviderMeta.Schema + r.provider.readResourceCalledResourceType = "test_three" + r.provider.readResourceImpl(ctx, req, resp) +} + +func (r testServeResourceThree) Update(ctx context.Context, req UpdateResourceRequest, resp *UpdateResourceResponse) { + r.provider.applyResourceChangePriorStateValue = req.State.Raw + r.provider.applyResourceChangePriorStateSchema = req.State.Schema + r.provider.applyResourceChangePlannedStateValue = req.Plan.Raw + r.provider.applyResourceChangePlannedStateSchema = req.Plan.Schema + r.provider.applyResourceChangeConfigValue = req.Config.Raw + r.provider.applyResourceChangeConfigSchema = req.Config.Schema + r.provider.applyResourceChangeProviderMetaValue = req.ProviderMeta.Raw + r.provider.applyResourceChangeProviderMetaSchema = req.ProviderMeta.Schema + r.provider.applyResourceChangeCalledResourceType = "test_three" + r.provider.applyResourceChangeCalledAction = "update" + r.provider.updateFunc(ctx, req, resp) +} + +func (r testServeResourceThree) Delete(ctx context.Context, req DeleteResourceRequest, resp *DeleteResourceResponse) { + r.provider.applyResourceChangePriorStateValue = req.State.Raw + r.provider.applyResourceChangePriorStateSchema = req.State.Schema + r.provider.applyResourceChangeProviderMetaValue = req.ProviderMeta.Raw + r.provider.applyResourceChangeProviderMetaSchema = req.ProviderMeta.Schema + r.provider.applyResourceChangeCalledResourceType = "test_three" + r.provider.applyResourceChangeCalledAction = "delete" + r.provider.deleteFunc(ctx, req, resp) +} diff --git a/tfsdk/serve_test.go b/tfsdk/serve_test.go index a0c14d0ce..b1f01ef75 100644 --- a/tfsdk/serve_test.go +++ b/tfsdk/serve_test.go @@ -216,8 +216,10 @@ func TestServerGetProviderSchema(t *testing.T) { expected := &tfprotov6.GetProviderSchemaResponse{ Provider: testServeProviderProviderSchema, ResourceSchemas: map[string]*tfprotov6.Schema{ - "test_one": testServeResourceTypeOneSchema, - "test_two": testServeResourceTypeTwoSchema, + "test_one": testServeResourceTypeOneSchema, + "test_two": testServeResourceTypeTwoSchema, + "test_three": testServeResourceTypeThreeSchema, + "test_config_validators": testServeResourceTypeConfigValidatorsSchema, "test_validate_config": testServeResourceTypeValidateConfigSchema, }, @@ -250,6 +252,7 @@ func TestServerGetProviderSchemaWithProviderMeta(t *testing.T) { ResourceSchemas: map[string]*tfprotov6.Schema{ "test_one": testServeResourceTypeOneSchema, "test_two": testServeResourceTypeTwoSchema, + "test_three": testServeResourceTypeThreeSchema, "test_config_validators": testServeResourceTypeConfigValidatorsSchema, "test_validate_config": testServeResourceTypeValidateConfigSchema, }, @@ -1468,8 +1471,6 @@ func TestServerReadResource(t *testing.T) { } func TestServerPlanResourceChange(t *testing.T) { - t.Parallel() - type testCase struct { // request input priorState tftypes.Value @@ -1536,12 +1537,20 @@ func TestServerPlanResourceChange(t *testing.T) { expectedPlannedState: tftypes.NewValue(testServeResourceTypeOneType, nil), }, "two_nil_state_and_config": { - priorState: tftypes.NewValue(testServeResourceTypeOneType, nil), - proposedNewState: tftypes.NewValue(testServeResourceTypeOneType, nil), - config: tftypes.NewValue(testServeResourceTypeOneType, nil), + priorState: tftypes.NewValue(testServeResourceTypeTwoType, nil), + proposedNewState: tftypes.NewValue(testServeResourceTypeTwoType, nil), + config: tftypes.NewValue(testServeResourceTypeTwoType, nil), resource: "test_two", - resourceType: testServeResourceTypeOneType, - expectedPlannedState: tftypes.NewValue(testServeResourceTypeOneType, nil), + resourceType: testServeResourceTypeTwoType, + expectedPlannedState: tftypes.NewValue(testServeResourceTypeTwoType, nil), + }, + "three_nil_state_and_config": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, nil), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, nil), + config: tftypes.NewValue(testServeResourceTypeThreeType, nil), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, nil), }, "two_delete": { priorState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ @@ -1588,47 +1597,7 @@ func TestServerPlanResourceChange(t *testing.T) { "created_timestamp": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), }), }, - "one_attr_requiresreplace": { - priorState: tftypes.NewValue(testServeResourceTypeOneType, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "hello, world"), - "favorite_colors": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "orange"), - }), - "created_timestamp": tftypes.NewValue(tftypes.String, "when the earth was young"), - }), - proposedNewState: tftypes.NewValue(testServeResourceTypeOneType, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "hello, world"), - "favorite_colors": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "orange"), - tftypes.NewValue(tftypes.String, "yellow"), - }), - "created_timestamp": tftypes.NewValue(tftypes.String, "when the earth was young"), - }), - config: tftypes.NewValue(testServeResourceTypeOneType, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "hello, world"), - "favorite_colors": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "orange"), - tftypes.NewValue(tftypes.String, "yellow"), - }), - "created_timestamp": tftypes.NewValue(tftypes.String, nil), - }), - resource: "test_one", - resourceType: testServeResourceTypeOneType, - expectedPlannedState: tftypes.NewValue(testServeResourceTypeOneType, map[string]tftypes.Value{ - "name": tftypes.NewValue(tftypes.String, "hello, world"), - "favorite_colors": tftypes.NewValue(tftypes.List{ElementType: tftypes.String}, []tftypes.Value{ - tftypes.NewValue(tftypes.String, "red"), - tftypes.NewValue(tftypes.String, "orange"), - tftypes.NewValue(tftypes.String, "yellow"), - }), - "created_timestamp": tftypes.NewValue(tftypes.String, "when the earth was young"), - }), - expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("favorite_colors")}, - }, - "two_modify_add_list_elem": { + "two_modifyplan_add_list_elem": { priorState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), "disks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ @@ -1725,7 +1694,7 @@ func TestServerPlanResourceChange(t *testing.T) { }) }, }, - "two_modify_requires_replace": { + "two_modifyplan_requires_replace": { priorState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), "disks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ @@ -1788,7 +1757,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("id")}, }, - "two_modify_diags_warning": { + "two_modifyplan_diags_warning": { priorState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), "disks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ @@ -1859,7 +1828,7 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, }, - "two_modify_diags_error": { + "two_modifyplan_diags_error": { priorState: tftypes.NewValue(testServeResourceTypeTwoType, map[string]tftypes.Value{ "id": tftypes.NewValue(tftypes.String, "123456"), "disks": tftypes.NewValue(tftypes.List{ElementType: tftypes.Object{AttributeTypes: map[string]tftypes.Type{ @@ -1930,14 +1899,318 @@ func TestServerPlanResourceChange(t *testing.T) { }, }, }, + "three_attrplanmodifiers_requiresreplace": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "something-else"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "something-else"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "something-else"), + }), + }), + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, + "three_attrplanmodifiers_requiresreplaceif_true": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 999), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 999), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 999), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("size"), tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, + "three_attrplanmodifies_requiresreplaceif_false": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 1), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 1), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 1), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, + "three_attrplanmodifiers_diags": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTDIAG"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTDIAG"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTDIAG"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTDIAG"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedDiags: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "Warning diag", + Detail: "This is a warning", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error diag", + Detail: "This is an error", + }, + }, + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, + "three_attrplanmodifiers_chained_modifiers": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "TESTATTRONE"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "MODIFIED_TWO"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, + "three_attrplanmodifiers_nested_modifier": { + priorState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "my-scr-disk"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + proposedNewState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "TESTATTRTWO"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + config: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "TESTATTRTWO"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + expectedPlannedState: tftypes.NewValue(testServeResourceTypeThreeType, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "name1"), + "size": tftypes.NewValue(tftypes.Number, 3), + "scratch_disk": tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{ + "id": tftypes.String, + "interface": tftypes.String, + }}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "MODIFIED_TWO"), + "interface": tftypes.NewValue(tftypes.String, "scsi"), + }), + }), + resource: "test_three", + resourceType: testServeResourceTypeThreeType, + expectedRequiresReplace: []*tftypes.AttributePath{tftypes.NewAttributePath().WithAttributeName("scratch_disk").WithAttributeName("interface")}, + }, } for name, tc := range tests { name, tc := name, tc t.Run(name, func(t *testing.T) { - t.Parallel() - s := &testServeProvider{ modifyPlanFunc: tc.modifyPlanFunc, } @@ -1996,7 +2269,9 @@ func TestServerPlanResourceChange(t *testing.T) { t.Errorf("Expected planned private to be %q, got %q", tc.expectedPlannedPrivate, got.PlannedPrivate) return } - if diff := cmp.Diff(got.RequiresReplace, tc.expectedRequiresReplace, cmpopts.EquateEmpty()); diff != "" { + if diff := cmp.Diff(got.RequiresReplace, tc.expectedRequiresReplace, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(x, y *tftypes.AttributePath) bool { + return x.String() < y.String() + })); diff != "" { t.Errorf("Unexpected diff in requires replace (+wanted, -got): %s", diff) return } @@ -3144,8 +3419,6 @@ func TestServerApplyResourceChange(t *testing.T) { } if tc.config.Type() != nil { if diff := cmp.Diff(s.applyResourceChangeConfigValue, tc.config); diff != "" { - t.Errorf("Unexpected diff in config (+wanted, -got): %s", diff) - return } if diff := cmp.Diff(s.applyResourceChangeConfigSchema, schema); diff != "" { t.Errorf("Unexpected diff in config schema (+wanted, -got): %s", diff)