Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utility to call Read() from the end of Create()/Update() #277

Closed
magodo opened this issue Mar 15, 2022 · 3 comments
Closed

Utility to call Read() from the end of Create()/Update() #277

magodo opened this issue Mar 15, 2022 · 3 comments
Labels
enhancement New feature or request

Comments

@magodo
Copy link

magodo commented Mar 15, 2022

Module version

v0.6.0

Use-cases

In plugin SDK, it is prevalent to call Read() at the end of Create()/Update() in the resource implementation. This is avoid code duplication for APIs that doesn't return the full model in its "create" response.

I'd like to continue this pattern in the framework.

Attempted Solutions

I manually convert to and from the tfsdk.ReadResource{Request|Response}, as below:

func (r resourceFoo) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
        //...
	rreq := tfsdk.ReadResourceRequest{
		State:        resp.State,
		ProviderMeta: req.ProviderMeta,
	}
	rresp := tfsdk.ReadResourceResponse{
		State:       resp.State,
		Diagnostics: resp.Diagnostics,
	}
	r.Read(ctx, rreq, &rresp)

	*resp = tfsdk.CreateResourceResponse{
		State:       rresp.State,
		Diagnostics: rresp.Diagnostics,
	}
}

Full example can be found here: https://github.com/magodo/terraform-provider-demo/blob/41121592396989284faad906d3943fcd084b4272/demo/resource_foo.go#L124-L137

Proposal

Apparently, above solution is not ideal. Propose there can be some builtin utilities to allow users to do that.

@magodo magodo added the enhancement New feature or request label Mar 15, 2022
@magodo magodo changed the title Utility to call Read() from Create()/Update() Utility to call Read() from the end of Create()/Update() Mar 15, 2022
@bflad
Copy link
Contributor

bflad commented Jul 8, 2022

Hi @magodo 👋 Thank you for raising this feature request and apologies it took so long to respond! It certainly follows some of the conventional details of how some providers worked with terraform-plugin-sdk resource implementations, where they would call the read functions at the end of their create or update functions since they matched in function signatures.

While the framework could theoretically provide some functionality to allow provider developers to easily cross these logical boundaries, there are actually some specific reasons not to do so:

  • As you may be able to tell in this new framework, there is a much stronger emphasis on correlating the provider protocol details of each request and response through the usage of very specific Go types, such as tfsdk.CreateResourceRequest/tfsdk.CreateResourceResponse versus tfsdk.ReadResourceRequest/tfsdk.ReadResourceResponse. Each of those operation-specific types implements operation-specific details of the underlying protocol details, such as the ReadResource RPC only providing the prior state information and not hiding the fact that configuration information is not available. These distinctions feel important enough for provider developers to understand the consequences of trying to work outside their original intent.
  • terraform-plugin-sdk was allowed to return differing state data than what was planned, while this framework (or more generally, any implementation outside terraform-plugin-sdk) is not. If the read call returns unexpectedly different data than the original creation plan, then Terraform will throw a generic provider produced inconsistent result after apply type error with the framework rather than warning logs about the issue. Introducing a remote system read call that overwrites all state information adds to the chances this may occur. Instead, it is generally preferable that any read-after-create or read-after-update call would only set unknown attribute values or known-consistent attribute values in the response state.
  • If the read functionality calls RemoveResource() when the resource is not found (similar to d.SetId("") in terraform-plugin-sdk) during create/update, then terraform will throw a confusing version of the provider produced inconsistent result after apply error -- saying the applied resource was unexpectedly null. This was the source of many bugs in large providers, such as the AWS provider.
  • Providers are free to separately implement the underlying "read" logic already, if really desired (see below for a contrived example), however, doing something like this makes it harder to capture the nuance associated with proper error messaging and state handling mentioned above.
// Contrived example of using Go functions to share resource reading logic.
// This example, however, is not a recommended practice.
type exampleResourceData struct {/*...*/}

func readExample(ctx context.Context, client examplesdk.Client, id string) (exampleResourceData, diag.Diagnostics) {/*...*/}

func (r exampleResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
  // logic to create resource and get id string
  data, diags := readExample(ctx, r.provider.client, id)

  resp.Diagnostics.Append(diags...)

  if resp.Diagnostics.HasError() {
    return
  }

  if data == nil {
    // this should return an error or retry the read call,
    // rather than calling resp.State.RemoveResource()
  }

  resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}

func (r exampleResource) Read(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
  var id string

  diags := req.State.GetAttribute(ctx, path.Root("id") &id)

  resp.Diagnostics.Append(diags...)

  if resp.Diagnostics.HasError() {
    return
  }

  data, diags := readExample(ctx, r.provider.client, id)

  resp.Diagnostics.Append(diags...)

  if resp.Diagnostics.HasError() {
    return
  }

  if data == nil {
    resp.State.RemoveResource()
    return
  }

  resp.Diagnostics.Append(resp.State.Set(ctx, data)...)
}

Some additional details about proper resource disappearance handling between Create/Read/Update/Delete are captured in this Discuss post, which hopefully will make its way into the terraform.io documentation at some point.

Therefore given the above, I think we would be very hesitant to introduce anything in this space. Thanks again for suggesting it though and reach out if you have any further questions about this.

@bflad bflad closed this as not planned Won't fix, can't repro, duplicate, stale Jul 8, 2022
@bflad
Copy link
Contributor

bflad commented Jul 8, 2022

As an aside from writing the above response, if there is not already a feature request issue for having the framework return a very specific "oh no you called RemoveResource() when you shouldn't have" error diagnostic after the Create and Update methods, I'll add one and cross-reference this issue and the discuss post.

@github-actions
Copy link

github-actions bot commented Aug 7, 2022

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants