Terraform
Implement resource update
In this tutorial, you will add update capabilities to the order
resource of a provider that interacts with the API of a fictional coffee-shop application called Hashicups. To do this, you will:
- Verify your schema and model.
Verify thelast_updated
attribute is in theorder
resource schema and model. The provider will update this attribute to the current date time whenever the order resource is updated. - Implement resource update.
This update method uses the HashiCups client library to invoke aPUT
request to the/orders/{orderId}
endpoint with the updated order items in the request body. After the update is successful, it updates the resource's state. - Enhance plan output with plan modifier.
This clarifies the plan output of theid
attribute to remove its difference by keeping the existing state value on updates. - Verify update functionality.
This ensures the resource is working as expected.
Prerequisites
To follow this tutorial, you need:
- Go 1.21+ installed and configured.
- Terraform v1.8+ installed locally.
- Docker and Docker Compose to run an instance of HashiCups locally.
Navigate to your terraform-provider-hashicups
directory.
Your code should match the 05-create-read-order
directory
from the example repository.
If you're stuck at any point during this tutorial, refer to the update-order
branch to see the changes implemented in this tutorial.
Verify schema and model
Verify your Schema
method includes an attribute named last_updated
.
internal/provider/order_resource.go
func (r *orderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"last_updated": schema.StringAttribute{
Computed: true,
},
"items": schema.ListNestedAttribute{
// ...
Verify that your orderResourceModel
type includes a field to handle the last_updated
attribute.
internal/provider/order_resource.go
type orderResourceModel struct {
ID types.String `tfsdk:"id"`
Items []orderItemModel `tfsdk:"items"`
LastUpdated types.String `tfsdk:"last_updated"`
}
Implement update functionality
The provider uses the Update
method to update an existing resource based on the schema data.
The update method follows these steps:
- Retrieves values from the plan. The method will attempt to retrieve values from the plan and convert it to an
orderResourceModel
. The model includes the order'sid
attribute, which specifies which order to update. - Generates an API request body from the plan values. The method loops through each plan item and maps it to a
hashicups.OrderItem
. This is what the API client needs to update an existing order. - Updates the order. The method invokes the API client's
UpdateOrder
method with the order's ID and OrderItems. - Maps the response body to resource schema attributes. After the method updates the order, it maps the
hashicups.Order
response to[]OrderItem
so the provider can update the Terraform state. - Sets the LastUpdated attribute. The method sets the Order's LastUpdated attribute to the current system time.
- Sets Terraform's state with the updated order.
Open the internal/provider/order_resource.go
file.
Replace your Order resource's Update
method in order_resource.go
with the following.
internal/provider/order_resource.go
func (r *orderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Retrieve values from plan
var plan orderResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from plan
var hashicupsItems []hashicups.OrderItem
for _, item := range plan.Items {
hashicupsItems = append(hashicupsItems, hashicups.OrderItem{
Coffee: hashicups.Coffee{
ID: int(item.Coffee.ID.ValueInt64()),
},
Quantity: int(item.Quantity.ValueInt64()),
})
}
// Update existing order
_, err := r.client.UpdateOrder(plan.ID.ValueString(), hashicupsItems)
if err != nil {
resp.Diagnostics.AddError(
"Error Updating HashiCups Order",
"Could not update order, unexpected error: "+err.Error(),
)
return
}
// Fetch updated items from GetOrder as UpdateOrder items are not
// populated.
order, err := r.client.GetOrder(plan.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Reading HashiCups Order",
"Could not read HashiCups order ID "+plan.ID.ValueString()+": "+err.Error(),
)
return
}
// Update resource state with updated items and timestamp
plan.Items = []orderItemModel{}
for _, item := range order.Items {
plan.Items = append(plan.Items, orderItemModel{
Coffee: orderItemCoffeeModel{
ID: types.Int64Value(int64(item.Coffee.ID)),
Name: types.StringValue(item.Coffee.Name),
Teaser: types.StringValue(item.Coffee.Teaser),
Description: types.StringValue(item.Coffee.Description),
Price: types.Float64Value(item.Coffee.Price),
Image: types.StringValue(item.Coffee.Image),
},
Quantity: types.Int64Value(int64(item.Quantity)),
})
}
plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850))
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
Build and install the updated provider.
$ go install .
Verify update functionality
Navigate to the examples/order
directory. This contains a sample Terraform configuration for the Terraform HashiCups provider.
$ cd examples/order
Replace your hashicups_order.edu
resource in examples/order/main.tf
. This configuration changes the drinks and quantities in the order.
examples/order/main.tf
resource "hashicups_order" "edu" {
items = [{
coffee = {
id = 3
}
quantity = 2
},
{
coffee = {
id = 2
}
quantity = 3
}]
}
Plan the configuration.
$ terraform plan
hashicups_order.edu: Refreshing state... [id=1]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# hashicups_order.edu will be updated in-place
~ resource "hashicups_order" "edu" {
~ id = "1" -> (known after apply)
~ items = [
~ {
~ coffee = {
+ description = (known after apply)
id = 3
~ image = "/vault.png" -> (known after apply)
~ name = "Vaulatte" -> (known after apply)
~ price = 200 -> (known after apply)
~ teaser = "Nothing gives you a safe and secure feeling like a Vaulatte" -> (known after apply)
}
# (1 unchanged attribute hidden)
},
~ {
~ coffee = {
+ description = (known after apply)
~ id = 1 -> 2
~ image = "/hashicorp.png" -> (known after apply)
~ name = "HCP Aeropress" -> (known after apply)
~ price = 200 -> (known after apply)
~ teaser = "Automation in a cup" -> (known after apply)
}
~ quantity = 2 -> 3
},
]
~ last_updated = "Thursday, 09-Feb-23 11:32:05 EST" -> (known after apply)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Note that the id
attribute is showing a plan difference where the value is
going from the known value to an unknown value ((known after apply)
).
~ id = "1" -> (known after apply)
During updates, this attribute value is not expected to change. To prevent
practitioner confusion, in the next section you will update the id
attribute's
definition to keep the existing state value on update.
Enhance plan output
Terraform Plugin Framework attributes which are not configurable and that should
not show updates from the existing state value should implement the
UseStateForUnknown()
plan modifier.
Open the internal/provider/order_resource.go
file.
Replace the id
attribute definition in the Schema
method with the following.
internal/provider/order_resource.go
// Schema defines the schema for the resource.
func (r *orderResource) 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(),
},
},
Replace the import
statement with the following.
internal/provider/order_resource.go
import (
"context"
"fmt"
"strconv"
"time"
"github.com/hashicorp-demoapp/hashicups-client-go"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
In your terminal, navigate to the terraform-provider-hashicups
directory.
cd ../..
Build and install the updated provider.
go install .
Verify update functionality
Navigate back to the examples/order
directory.
cd examples/order
Run a Terraform apply to update your order. The plan will not mark the id
attribute as (known after apply)
any longer. Your provider will update your order and set a new value for the last_updated
attribute.
$ terraform apply -auto-approve
##...
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
Outputs:
edu_order = {
"id" = "1"
"items" = tolist([
{
"coffee" = {
"description" = ""
"id" = 3
"image" = "/vault.png"
"name" = "Vaulatte"
"price" = 200
"teaser" = "Nothing gives you a safe and secure feeling like a Vaulatte"
}
"quantity" = 2
},
{
"coffee" = {
"description" = ""
"id" = 2
"image" = "/packer.png"
"name" = "Packer Spiced Latte"
"price" = 350
"teaser" = "Packed with goodness to spice up your images"
}
"quantity" = 3
},
])
"last_updated" = "Thursday, 09-Feb-23 11:39:35 EST"
}
Verify that the provider updated your order by invoking the HashiCups API.
$ curl -X GET -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders/1
{"id":1,"items":[{"coffee":{"id":3,"name":"Vaulatte","teaser":"Nothing gives you a safe and secure feeling like a Vaulatte","collection":"Foundations","origin":"Spring 2015","color":"#FFD814","description":"","price":200,"image":"/vault.png","ingredients":[{"ingredient_id":1},{"ingredient_id":2}]},"quantity":2},{"coffee":{"id":2,"name":"Packer Spiced Latte","teaser":"Packed with goodness to spice up your images","collection":"Origins","origin":"Summer 2013","color":"#1FA7EE","description":"","price":350,"image":"/packer.png","ingredients":[{"ingredient_id":1},{"ingredient_id":2},{"ingredient_id":4}]},"quantity":3}]}%
This is the same API call that your Terraform provider made to update your order's state.
Navigate back to the terraform-provider-hashicups
directory.
$ cd ../..
Next steps
Congratulations! You have enhanced the order
resource with update
capabilities.
If you were stuck during this tutorial, checkout the
06-update-order
directory in the example repository to see the code implemented in this
tutorial.
- To learn more about the Terraform Plugin Framework, refer to the Terraform Plugin Framework documentation.
- For a full capability comparison between the SDKv2 and the Plugin Framework, refer to the Which SDK Should I Use? documentation.
- The example repository contains directories corresponding to each tutorial in this collection.
- Submit any Terraform Plugin Framework bug reports or feature requests to the development team in the Terraform Plugin Framework Github repository.
- Submit any Terraform Plugin Framework questions in the Terraform Plugin Framework Discuss forum.