Diagnostics AddError halt without storing state

Hello community, I’m trying to implement a custom provider resource for an api where the call to create will return an ID and then I loop and read the resource info until the resource ready and its working when there is no exception from the api

Here is a snippet of the implementation:

// Create creates the resource and sets the initial Terraform state.
func (r *aviResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {

	var model AviModel
	diags := req.Plan.Get(ctx, &model)

        // Call the API
	respCre, err := r.client.CreateResource(payload)

	if err != nil {
		resp.Diagnostics.AddError(
			"Error creating avi",
			"Could not create avi, unexpected error: "+err.Error(),
		)
		return
	}

	respCreateAvi := make(map[string]interface{})
	err = json.Unmarshal(respCre, &respCreateAvi)

	fmt.Printf("Payload received  :\t %v \n", respCreateAvi)

	model.Id = types.StringValue(respCreateAvi["id"].(string))

        // Store ID of the resource 
	diags = resp.State.Set(ctx, model)
	resp.Diagnostics.Append(diags...)

	// wait for resource to be ready
	for {
		time.Sleep(60 * time.Second)

		if err != nil {
			resp.Diagnostics.AddError(
				"Error while waiting to for avi",
				"Could not wait for avi to be ready, unexpected error: "+err.Error(),
			)
			return
		}

                // Check If resource ready
		if aviReadDetails["state"] == "ACTIVE" && aviReadDetails["status"] == "deployed" {

                        // Set model values from API read response
			break
		} else if aviReadDetails["state"] == "TERMINATED" {

			resp.Diagnostics.AddError(
				"Error while creating avi",
				"Could not create avi, received status 'failed to deploy' and state 'TERMINATED', unexpected error.... ",
			)
			return
		}
	}

	// Set state to fully populated data
	diags = resp.State.Set(ctx, model)
	resp.Diagnostics.Append(diags...)
	if resp.Diagnostics.HasError() {
		return
	}

}

But if something happen while waiting for the resource to be ready (like client abort) this code doesn’t store the ID of the resource and the next terraform apply create another new resource

Reading from terraform docs
Returning an error diagnostic does not stop the state from being updated

I was expecting the State to be stored but its not the case

Hi @khaounen :wave:,

I’m wondering whether the initial call to resp.State.Set() is returning a diagnostic and is not successfully saving the state?

Are you seeing any errors when you run terraform apply?

Perhaps the following code is producing a diagnostic that is reported as an error?

	// Store ID of the resource 
	diags = resp.State.Set(ctx, model)
	resp.Diagnostics.Append(diags...)

Hi @bendbennett and thank you for your reply,

terraform apply works fine and my resource is created when there is no error from the api I’m trying to call

I was calling resp.State.Set() directly without assigning it to diags variable but it wasn’t storing the state in case of failure in the for loop so I tried to store the result in a diags and append it in the response, but doesn’t seems to work either,

Is what I’m trying to do is possible? I mean to store the state before the creation process finish and then return an error at the end ?

IIUC, any error returned during creation is going to record the resource as tainted, meaning that subsequent Terraform runs will delete and recreate it.

Hi @maxb thanks for your answer,

I’m not sure I understand, does that mean the implementation I’m looking for is not possible?

I’m not aware of any way to override this behaviour, so even if everything else worked as intended, once you’d returned that error, Terraform would still want to delete and create your API object by running your provider’s delete and create methods again on the next run.

That sounds like not what you want, so I guess, yes, it means its not possible. Perhaps you could investigate whether downgrading the error to a warning would be sufficient?

Using the following example, the state is stored but as @maxb points out it will mark the resource as tainted which will result in a destroy-create cycle on the next terraform apply.

func (e *exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data exampleResourceData

	diags := req.Plan.Get(ctx, &data)
	resp.Diagnostics.Append(diags...)

	if resp.Diagnostics.HasError() {
		return
	}

	data.Id = types.StringValue("example-id")

	tflog.Trace(ctx, "created a resource")

	diags = resp.State.Set(ctx, &data)
	resp.Diagnostics.Append(diags...)

	// Triggering an error after storing ID
	resp.Diagnostics.AddError("triggering error", "")
}

Ok I understand better now what @maxb mean, but I’m still missing something because using this code my terraform.tfstate was empty even after the first terraform apply

thank you both for your help :slight_smile:

As previously mentioned, it sounds like your resp.State.Set call is failing but you’re leaking the returned diagnostics so this never gets reported.

I’ll complement @bendbennett 's example with an example of an even more trivial Create method that demonstrates what happens on error, and the surrounding framework for the rest of the resource:

package main

import (
	"context"

	"github.com/hashicorp/go-secure-stdlib/base62"
	"github.com/hashicorp/terraform-plugin-framework/resource"
	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)

func init() {
	registerResource(func() resource.Resource {
		return new(errorOnCreateResource)
	})
}

type errorOnCreateResource struct{}

type errorOnCreateResourceModel struct {
	ID string `tfsdk:"id"`
}

func (e errorOnCreateResource) Metadata(ctx context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) {
	response.TypeName = request.ProviderTypeName + "_error_on_create"
}

func (e errorOnCreateResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
	response.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				Computed: true,
			},
		},
	}
}

func (e errorOnCreateResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
	model := errorOnCreateResourceModel{ID: base62.MustRandom(5)}
	diags := response.State.Set(ctx, model)
	response.Diagnostics.Append(diags...)
	response.Diagnostics.AddError("deliberate error", "")
}

func (e errorOnCreateResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
}

func (e errorOnCreateResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
}

func (e errorOnCreateResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
}
1 Like

Indeed using your example I see now in the state the resource created with the tainted status, will test my implementation again

{
“status”: “tainted”,
“schema_version”: 0,
“attributes”: {
“id”: “8zO0V”
},
“sensitive_attributes”:
}

As a workaround I implemented the resource just to call the api without waiting for it to be ready, and I implemented a datasource that wait for the status ready to read product infos

Thanks again :slight_smile: