How to use Resource Identity for CRUD in Terraform Plugin Framework?

Hi, I have a resource that has an Identity which includes its id and group which is necessary for importing the object as both attributes are necessary to identify the resource on the API side. As it seemed natural, I used the identity in the Read, Update and Delete operations to get the id and group of the resource. This worked fine, except for the fact that if RequireReplace is triggered because the group was changed, the identity supplied during Delete is nil.

My question is:

  • Is this indented behavior,
  • should we always use state instead of identity to retrieve id and group.

To my knowledge, the documentation does not include any guidance on how and where identity should be used in the CRUD operations, and only mentions reading from state.

Relevant Documentation:

This is a minimal example of what the resource looks like:

var (
	_ resource.Resource               = &thingResource{}
	_ resource.ResourceWithIdentity   = &thingResource{}
)
 
type thingResource struct{}
 
type thingModel struct {
	ID    types.String `tfsdk:"id"`
	Group types.String `tfsdk:"group"`
}
 
type thingIdentityModel struct {
	UUID  types.String `tfsdk:"uuid"`
	Group types.String `tfsdk:"group"`
}
 
func (r *thingResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
	resp.TypeName = req.ProviderTypeName + "_thing"
}
 
func (r *thingResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"id": schema.StringAttribute{
				Computed:      true,
				PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
			},
			"group": schema.StringAttribute{
				Required:      true,
				PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
			},
		},
	}
}
 
func (r *thingResource) IdentitySchema(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
	resp.IdentitySchema = identityschema.Schema{
		Attributes: map[string]identityschema.Attribute{
			"uuid":  identityschema.StringAttribute{RequiredForImport: true},
			"group": identityschema.StringAttribute{RequiredForImport: true},
		},
	}
}
 
func (r *thingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var plan thingModel
	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
	if resp.Diagnostics.HasError() {
		return
	}
 
	uuid := "generated-" + plan.Group.ValueString() // stand-in for API call
	plan.ID = types.StringValue(uuid)
 
	resp.Diagnostics.Append(resp.Identity.Set(ctx, thingIdentityModel{
		UUID:  types.StringValue(uuid),
		Group: plan.Group,
	})...)
	resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
 
// Omit Read and Update
 
// Delete reads the id from IDENTITY
func (r *thingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
	// On the destroy-half of a replace, req.Identity.Raw is NULL here,
	// even though Create set it and Read received it populated
	if req.Identity == nil || req.Identity.Raw.IsNull() {
		resp.Diagnostics.AddError(
			"Missing resource identity",
			"Expected a resource identity value in Delete but none was provided.",
		)
		return
	}
 
	var identity thingIdentityModel
	resp.Diagnostics.Append(req.Identity.Get(ctx, &identity)...)
	if resp.Diagnostics.HasError() {
		return
	}
 
	// deleteThing(identity.UUID.ValueString()) -- the API call that needs the id
	_ = identity
}

Thank you in advance,
Thomas

1 Like