hashicorp / hashicorp/terraform-plugin-framework

Consider SDK ResourceData HasChange / HasChanges Equivalent

Open
#526 1 comment 4 reactions 0 assignees View on GitHub
enhancement sdkv2-parity thinking
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.