Skip to content

Consider SDK ResourceData HasChange / HasChanges Equivalent #526

Open
@bflad

Description

@bflad

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 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:

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.Paths 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsdkv2-parityIssues tracking feature parity with terraform-plugin-sdk v2 and PRs working towards it.thinking

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions