hashicorp / hashicorp/terraform-plugin-framework
Consider SDK ResourceData HasChange / HasChanges Equivalent
- Dominant language
- Go
- Stars
- 384
- Forks
- 107
- Avg merge
- 3m
- Merged PRs (30d)
- 1
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](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#ResourceData.HasChanges), 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:
```go
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:
```go
// 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:
```go
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:
```go
// 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 #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 against `tfsdk.State`; providers may need to check `tfsdk.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:
```go
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:
```go
// 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
### References
- https://discuss.hashicorp.com/t/frameworks-alternative-to-haschange/46112
- Internal Slack thread
- #527
Contributor guide
Assessment
This issue has not been assessed yet.