Description
Module version
v0.15.0
Use-cases
Provider developers migrating from terraform-plugin-sdk may be looking for something similar to its (helper/schema.ResourceData).HasChanges(...string)
method, which checking for individual attribute changes between plan and prior state. Some APIs require only sending differences during in-place updates of resources.
Attempted Solutions
If using a schema data model type:
type ThingResourceModel struct {
Attribute1 types.String `tfsdk:"attribute1"`
Attribute2 types.String `tfsdk:"attribute2"`
// ...
}
// Update logic
var plan, state ThingResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if plan.Attribute1.Equal(state.Attribute1) {
// ...
}
if plan.Attribute2.Equal(state.Attribute2) {
// ...
}
If getting individual attributes:
// Update logic
// Reusable variables for brevity
var attributePlan, attributeState types.String
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute1"), &attributePlan)...)
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("attribute1"), &attributeState)...)
if resp.Diagnostics.HasError() {
return
}
if attributePlan.Equal(attributeState) {
// ...
}
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute2"), &attributePlan)...)
resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("attribute2"), &attributeState)...)
if resp.Diagnostics.HasError() {
return
}
if attributePlan.Equal(attributeState) {
// ...
}
Proposal
Upfront, it seems like the downsides may outweigh the benefits of introduction of something like this into the framework, although opening this issue for discussion.
Proposal 1 (Plan Path-Based HasChanges)
Consider providing a (tfsdk.Plan).HasChanges(context.Context, tfsdk.State, ...path.Path)
method.
If using a schema data model type:
type ThingResourceModel struct {
Attribute1 types.String `tfsdk:"attribute1"`
Attribute2 types.String `tfsdk:"attribute2"`
// ...
}
// Update logic
var plan ThingResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if req.Plan.HasChanges(ctx, req.State, path.Root("attribute1")) {
// ... plan.Attribute1 ...
}
if req.Plan.HasChanges(ctx, req.State, path.Root("attribute2")) {
// ... plan.Attribute2 ...
}
If getting individual attributes:
// Update logic
if req.Plan.HasChanges(ctx, req.State, path.Root("attribute1")) {
var attributePlan types.String
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute1"), &attributePlan)...)
if resp.Diagnostics.HasError() {
return
}
// ...
}
if req.Plan.HasChanges(ctx, req.State, path.Root("attribute2")) {
var attributePlan types.String
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute2"), &attributePlan)...)
if resp.Diagnostics.HasError() {
return
}
// ...
}
Upsides:
- Easier migration path for terraform-plugin-sdk logic
- Simplified provider logic
Downsides:
- Leaky abstraction (the wrapping schema data types now do more than get/set data and must stay up to date with value handling interfaces)
- How are differences such as Classifying normalization vs. drift #70 are handled? Additional methods?
- Providers are still required to fetch the actual values if needed, which means the logic to get data is required already
- This method is very specific to the schema data types (
tfsdk.Plan
checking againsttfsdk.State
; providers may need to checktfsdk.Config
too?)
Proposal 2 (Plan GetChanges)
Consider providing a (tfsdk.Plan).GetChanges(context.Context, tfsdk.State) []path.Path
method.
If using a schema data model type:
type ThingResourceModel struct {
Attribute1 types.String `tfsdk:"attribute1"`
Attribute2 types.String `tfsdk:"attribute2"`
// ...
}
// Update logic
var plan ThingResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
changedPaths := req.Plan.GetChanges(ctx, req.State)
for _, changedPath := range changedPaths {
switch {
case changedPath.Equal(path.Root("attribute1")):
// ... plan.Attribute1 ...
case changedPath.Equal(path.Root("attribute2")):
// ... plan.Attribute2 ...
}
}
If getting individual attributes:
// Update logic
changedPaths := req.Plan.GetChanges(ctx, req.State)
for _, changedPath := range changedPaths {
switch {
case changedPath.Equal(path.Root("attribute1")):
var attributePlan types.String
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute1"), &attributePlan)...)
if resp.Diagnostics.HasError() {
return
}
// ...
case changedPath.Equal(path.Root("attribute2")):
var attributePlan types.String
resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("attribute2"), &attributePlan)...)
if resp.Diagnostics.HasError() {
return
}
// ...
}
}
Upsides:
- Even simpler provider logic (lookup does not need to be done per-attribute)
Downsides:
- Same as Proposal 1
Proposal 3 (UpdateRequest Change Slices)
Consider adding new methods/fields to resource.UpdateRequest
which contain path.Path
s with changes, e.g. resource.UpdateRequest.StateToPlanDifferences() []path.Path
Upsides:
- Even simpler provider logic (the framework already did the work for you)
Downsides:
- Same as Proposal 1