How should Read() signal that a resource has vanished from the API server?

I’m working on creating a new provider using the plugin framework SDK, and need some clarity on how a resource’s Read() and Update() methods should behave when a resource has vanished from the API server.

Should Read() just flag all resource elements with Unknown: true (or Null: true?) in that case, or is there a different way to signal to the state that the resource no longer exists?

Related to that, how should the resource Update() method handle these situations?

The client library I’m using has functions which look roughly like:

func (c client) CreateResource(p resourceParams{}) (resourceId, error) {}
and
func (c client) UpdateResource(id resourceId, p resourceParams{}) error {}

I’m guessing that the resource Update() method in my provider should be prepared to call either one of those functions depending on how Read() has set the value of Id.Unknown.

My first question about Read() is probably somewhat revealing: I think I’m projecting this API’s behavioral model (if the object Id exists, then the object exists and can be manipulated) onto terraform state, which has no concept of a “key” value for any resource.

Am I on the right track?

Any clarity about Unknown vs. Null would be helpful here, I think.

Thanks!

Hi @bumble :wave: Welcome to the HashiCorp Discuss community and thank you for posting this question.

Properly signaling resource existence across the resource lifecycle can be summarized as:

  • Create: Return error diagnostics for any “resource not found” errors (e.g. if there are reads for additional information). Returning an error during creation will automatically taint a resource in Terraform, which will ensure its recreated next apply operation so that configured provisioners can be run.
  • Delete: Ignore “resource not found” errors. Since this operation was intended to remove the resource already, there is generally no need to return an error.
  • Read: Remove the resource from the state on “resource not found” errors. Terraform will automatically take this provider feedback to recreate a new resource on the next apply operation.
  • Update: Always return error diagnostics for any “resource not found” errors. This will ensure the practitioner exactly knows about any potentially unexpected issue during the update process. The next time that Terraform runs, the Read functionality will catch the missing resource as mentioned above.

In terraform-plugin-framework, you can remove a resource from state by calling the response type State field RemoveResource() method. So given the following resource code:

func (r exampleResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) {
  // ... other logic to call API/client ...

  if /* "resource not found" error */ {
    resp.State.RemoveResource()
    return
  }

  // ... other logic to update resp.State from API/client ...
}

Does this help answer your question?

1 Like

resp.State.RemoveResource()

This is what I was looking for!

You made several references to “resource not found” errors. I think you’re talking about errors coming from the client library / the external API. Is that right?

My new understanding includes the following features:

  • Read() is always called before Update()
  • If Read() invokes RemoveResource() to indicate a resource that has gone missing, terraform core will cause it to be re-created via Create(). Update() is not next in line.
  • Update() does not need to be prepared to handle the missing resource condition, it should blindly call the external API’s update method and return errors via diagnostic in case of 404 from the API server.

Thank you!

That’s correct. Although this handling is only for those specifically signaling the resource no longer existing (e.g. a HTTP status code of 404 or similar). You wouldn’t want another error, such a transient network error, to remove the resource. :+1:

This is mostly correct from a high level, although I think it may be worth clarifying a few additional details just in case you might be interested or might not know.

Terraform has a few underlying operations that happen in relation to how the provider logic is called. Terraform planning is separate from applying. Read is an operation that is called during planning to refresh existing resources in the Terraform state before the plan is generated, while Update is called during applying a generated plan. These underlying operations can be split across multiple Terraform commands terraform plan -out=example.tfplan then at some point later (could be any amount of time) terraform apply example.tfplan or both can happen fairly together with just terraform apply. Practitioners can also opt out of the “refresh” part of planning with terraform plan -refresh=false, however that use case is generally discouraged with Terraform and not something providers should generally worry about.

While these things generally shouldn’t affect the logic or invalidate your understanding here, I thought it may be useful to call them out for completeness.

Cheers!

Got it, thanks.

My personal workflows tend to only include plan when I’m curious, and interactive apply (without saved plan) when making changes.

This has likely led to some blurring of lines in my understanding of the flow.

Your explicitly pointing out that Read() is called during plan, and Update() is called during during apply is definitely appreciated.