Terraform
Build a Terraform Provider
Providers are Terraform plugins that allow users to manage their infrastructure by calling cloud service APIs to create, update, and delete supported resources. Providers are written in Go using the Terraform plugin framework to provide an interface between Terraform and those APIs.
In this hands-on lab, you will create a custom provider for a fictional coffee shop API called HashiCups. You will use the Terraform plugin framework to implement your provider, including authentication, resources, and data sources.
You can complete this lab in our online environment, or follow the tutorial below to complete the lab on your local machine.
Launch Terminal
This tutorial includes a free interactive command-line lab that lets you follow along on actual cloud infrastructure.
This lab is an abbreviated version of our provider development tutorial collection. After completing this lab, follow the collection on your local machine to learn more about provider development, including how to implement provider-defined functions, ephemeral resources, automated testing, and documentation generation.
Prerequisites
To follow this lab on your local machine, you will need:
Docker and Docker Compose, to run an instance of HashiCups locally.
Set up your development environment
When you develop your own provider, we recommend that you start with the Terraform Provider Scaffolding repository. For this lab, start with the following boilerplate code for your HashiCups provider.
$ git clone --branch provider-workshop-boilerplate https://github.com/hashicorp/terraform-provider-hashicups
Change into the provider code directory.
$ cd terraform-provider-hashicups
This code has been updated from the scaffolding by renaming it for the hashicups
provider, starting code for your provider, and a docker_compose
directory
containing Docker configuration to run an instance of the HashiCups API locally.
Open a new terminal window or tab to start the HashiCups API.
In that terminal, change into the docker_compse
directory.
$ cd docker_compose
Start the HashiCups API.
$ docker compose up
Docker will download and install the required containers for HashiCups, and stream the logs to this terminal.
Return to your other terminal to complete the rest of this lab.
Verify HashiCups API
Verify that HashiCups is running by sending a request to its health check endpoint.
$ curl localhost:19090/health
The API will respond with the string ok
, with no newline.
Create HashiCups user
HashiCups requires a username and password to generate an JSON web token (JWT) which is used to authenticate against protected endpoints. You will use this user to authenticate to the HashiCups provider to manage your orders.
Create a user on HashiCups named education
with the password test123
.
$ curl -X POST localhost:19090/signup -d '{"username":"education", "password":"test123"}'
Set the HASHICUPS_TOKEN
environment variable to the token you retrieved from
invoking the /signup
endpoint. You will use this later in the tutorial to
verify that Terraform has created your HashiCups order.
$ export HASHICUPS_TOKEN=
Now that the HashiCups API is running and configured with credentials, you are ready to start working on the Terraform provider.
Configure provider
In your text editor, open the main.go
file in the
terraform-provider-hashcups
directory.
The main
package is the entrypoint to your provider. It imports the
providerserver
package from the plugin framework to allow Terraform to
communicate with your provider over RPC, and the provider
package from the
internal/provider
directory, which implements your provider.
The main()
function configures your provider and calls the
providerserver.Serve
function from the framework to serve your provider over
RPC, allowing Terraform to connect to it to perform operations.
Open the internal/provider/provider.go
file to review the code that implements
your provider.
The New
function returns an anonymous function that returns a reference to an
instance of the provider for Terraform to interact with. Next, the
hashicupsProviderModel
struct defines the configuration options for your
provider, while the hashicupsProvider
struct and Metadata
function define
metadata about the provider, in this case the provider name and version.
Next, the provider defines a schema that Terraform will use to configure the HashiCups API client. In the framework, a schema defines the attributes available to configure a provider, resource, or data source.
In the internal/provider/provider.go
file, replace the Schema
functon with
following code.
internal/provider/provider.go
// Schema defines the provider-level schema for configuration data.
func (p *hashicupsProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Interact with HashiCups.",
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Description: "URI for HashiCups API. May also be provided via HASHICUPS_HOST environment variable.",
Optional: true,
},
"username": schema.StringAttribute{
Description: "Username for HashiCups API. May also be provided via HASHICUPS_USERNAME environment variable.",
Optional: true,
},
"password": schema.StringAttribute{
Description: "Password for HashiCups API. May also be provided via HASHICUPS_PASSWORD environment variable.",
Optional: true,
Sensitive: true,
},
},
}
}
All three values are strings, and all of them are set to be optional, meaning
they are not required in the provider's configuration block. The password
attribute is also set to be sensitive, meaning it will not be printed in the
output of Terraform commands by default.
Review the Configure function
The Configure
function configures the API client and adds it to the provider.
internal/provider/provider.go
func (p *hashicupsProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
tflog.Info(ctx, "Configuring HashiCups client")
## ...
The function takes three arguments:
ctx context.Context
is part of Go’s standard library and commonly used to make API requests.req provider.ConfigureRequest
represents the request Terraform sends to the provider during the configure step, and includes the values from the provider block in a Terraform configuration, along with other runtime information from Terraform.resp *provider.ConfigureResponse
represents the response from the provider. You can use this to return error and warning messages.
The Configure function performs the following steps:
- Retrieves the provider data from the configuration, or environment variables.
- Validates that the
username
,password
, andhost
are configured. - Creates a new API client and makes it available to resources and data sources.
Terraform will call your provider's Configure
function before performing any
operations using the provider, and passes any attributes defined in the
provider's configuration block in the request attribute. For example, the
following Terraform configuration could be used to configure your HashiCups
provider:
main.tf
provider "hashicups" {
username = "education"
password = "test123"
host = "http://localhost:19090"
}
Note
For simplicity, the examples in this lab include the API password in the
configuration. In general, we recommend that end users not include secret values
such as passwords in their configuration files. Instead, your provider should
allow these values to be configured via other means, such as environment
variables. The example Configure
function demonstrates loading the values for
the provider from environmnet variables.
Finally, the provider code includes functions to return the resources and data sources your provider supports.
Add a resource to your provider
In the internal/provider/provider.go
file, replace the Resources
functon
with following code.
internal/provider/provider.go
// Resources defines the resources implemented in the provider.
func (p *hashicupsProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewOrderResource,
}
}
A provider's Resources
function returns an anonymous function that Terraform
will call to get a list of resources supported by your provider. In the next
challenge, you will begin implementing the order resource.
Define order schema
To manage a particular resource with your provider, you will implement a corresponding Go type in your provider code using the plugin framework. The resource type will include a schema to define the attributes available to configure the resource, and functions to create, read, update, and destroy instances of the resource.
Define a schema for a resource representing a coffee order in the HashiCups API.
In your editor, open the internal/provider/order_resource.go
file. As a
general convention, Terraform providers implement each resource in its own file
named after the resource, suffixed with _resource
. This resource corresponds
to an order for coffee placed with the HashiCups API.
The orderResource
type defines a struct to represent this kind of resource,
and the code ensures it satisfies the required interfaces from the framework.
These interfaces require that your resource's type implement the required
functions to manage your infrastructure, including those required to return your
resource's schema, and implement create, read, update, and delete functionality.
Next, the NewOrderResource
function returns a new instance of the resource,
defined by the orderResource
type. Terraform will call the corresponding
functions that you define on this type to perform operations on instances of
this resource type.
Define order schema
For each resource your provider supports, you will define the resource's schema to map the values returned by your cloud service's API to attributes defined in Terraform configuration and stored in Terraform state.
Before you define the schema, review the structure returned by the HashiCups API when you create a new order.
In your terminal, create a new order in by calling the API directly with the
curl
command and piping the response to jq
to format the JSON data returned
by the API.
$ curl -X POST -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders -d '[{"coffee": { "id":1 }, "quantity":4}, {"coffee": { "id":3 }, "quantity":3}]' | jq .
Your provider will represent the data about the resources it manages as a
Schema
with the Terraform plugin framework.
In your editor, open the internal/provider/order_resource.go
file. Replace the
Schema
function with the following code. This schema maps the data returned by
the HashiCups API to a data structure Terraform will use to configure the
resource.
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{
Description: "Manages an order.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Numeric identifier of the order.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"last_updated": schema.StringAttribute{
Description: "Timestamp of the last Terraform update of the order.",
Computed: true,
},
"items": schema.ListNestedAttribute{
Description: "List of items in the order.",
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"quantity": schema.Int64Attribute{
Description: "Count of this item in the order.",
Required: true,
},
"coffee": schema.SingleNestedAttribute{
Description: "Coffee item in the order.",
Required: true,
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Description: "Numeric identifier of the coffee.",
Required: true,
},
"name": schema.StringAttribute{
Description: "Product name of the coffee.",
Computed: true,
},
"teaser": schema.StringAttribute{
Description: "Fun tagline for the coffee.",
Computed: true,
},
"description": schema.StringAttribute{
Description: "Product description of the coffee.",
Computed: true,
},
"price": schema.Float64Attribute{
Description: "Suggested cost of the coffee.",
Computed: true,
},
"image": schema.StringAttribute{
Description: "URI for an image of the coffee.",
Computed: true,
},
},
},
},
},
},
},
}
}
This schema includes attributes for the id
and last_updated
fields returned
by the HashiCups API, as well as a list of items included in the order.
Review HashiCups models
The order_resource.go
file also includes a series of Go structs that
correspond to the schema. The provider uses these models to store data about the
resource. Together the schema and the models map between the data returned by
the HashiCups API client and the the resource’s configuration, plan, and state
as represented by Terraform.
Implement create and read
Each resource type you implement in your provider is responsible for managing
resources of the given type in the cloud service API and in Terraform state. The
Create
function makes the necessary API calls to create a resource of the
given type and then persists that resource's data into Terraform state. The
Read
function loads data about an existing resource from the API.
Implement the create and read functions for your HashiCups order resource.
Implement the Configure
function
The code that implements your resource will need access to the HashiCups API client to use it to make calls to the HashiCups API.
In your editor, open the internal/provider/order_resource.go
file and replace
the Configure
function with the following.
internal/provider/order_resource.go
// Configure adds the provider configured client to the resource.
func (r *orderResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Add a nil check when handling ProviderData because Terraform
// sets that data after it calls the ConfigureProvider RPC.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*hashicups.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *hashicups.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
After ensuring that the provider has been configured, this function accesses the
HashiCups API client that your provider set up in it's own Configure
function,
and assigns it to client
on the order resource instance.
Implement the Create
function
Terraform will call your resource type's Create
function to create a new
instance of the resource by calling the cloud service's API. For example, if an
end user adds a new hashicups_order
block to their configuration and applies
the change with Terraform, Terraform will call your order resource's Create
function to create the order.
main.tf
resource "hashicups_order" "edu" {
## ...
}
Terraform will pass the attributes your end user includes in the order's
configuration block to the Create
function as part of the request.
The Create
function performs the following steps:
- Checks whether the provider and API Client are configured.
- Retrieves values from the plan.
- Generates an API request body from the plan values.
- Creates a new order by calling the appropriate function in the API client.
- Maps the response body to the resource schema attributes.
- Sets the state to the new order.
In the internal/provider/order_resource.go
file, replace the Create
function
with the following.
internal/provider/order_resource.go
// Create a new resource.
func (r *orderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// 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 items []hashicups.OrderItem
for _, item := range plan.Items {
items = append(items, hashicups.OrderItem{
Coffee: hashicups.Coffee{
ID: int(item.Coffee.ID.ValueInt64()),
},
Quantity: int(item.Quantity.ValueInt64()),
})
}
// Create new order
order, err := r.client.CreateOrder(items)
if err != nil {
resp.Diagnostics.AddError(
"Error creating order",
"Could not create order, unexpected error: "+err.Error(),
)
return
}
// Map response body to schema and populate Computed attribute values
plan.ID = types.StringValue(strconv.Itoa(order.ID))
for orderItemIndex, orderItem := range order.Items {
plan.Items[orderItemIndex] = orderItemModel{
Coffee: orderItemCoffeeModel{
ID: types.Int64Value(int64(orderItem.Coffee.ID)),
Name: types.StringValue(orderItem.Coffee.Name),
Teaser: types.StringValue(orderItem.Coffee.Teaser),
Description: types.StringValue(orderItem.Coffee.Description),
Price: types.Float64Value(orderItem.Coffee.Price),
Image: types.StringValue(orderItem.Coffee.Image),
},
Quantity: types.Int64Value(int64(orderItem.Quantity)),
}
}
plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850))
// Set state to fully populated data
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
Implement the Read
function
Terraform will call your resource type's Read
function to load data about an
instance of the resource from the cloud service's API. For example, if an end
user runs terraform refresh
to refresh their workspace's state, Terraform will
call your order resource's Read
function to load the current state of your
resource.
The Read
function performs the following steps:
- Gets the current state.
- Retrieves order ID from state.
- Gets the order’s information.
- Maps response body to resource schema attributes.
- Set state to new order.
In the internal/provider/order_resource.go
file, replace your Read
function
with the following.
internal/provider/order_resource.go
// Read resource information.
func (r *orderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Get current state
var state orderResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get refreshed order value from HashiCups
order, err := r.client.GetOrder(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Reading HashiCups Order",
"Could not read HashiCups order ID "+state.ID.ValueString()+": "+err.Error(),
)
return
}
// Overwrite items with refreshed state
state.Items = []orderItemModel{}
for _, item := range order.Items {
state.Items = append(state.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)),
})
}
// Set refreshed state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
Update imports
The functions you have added to your provider rely on some external libraries.
At the top of the internal/provider/order_resource.go
file, replace the import
section 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"
)
Now that your provider can create and read a coffee order using the HashiCups API client, in the next assignment you will build you provider and verify that it works as expected.
Build your provider
As you develop your provider, will will want to verify that it works as expected. To do so, build and install it locally, and then use it to provision infrastructure in your development or test API environment.
In normal usage, Terraform installs providers from the Terraform Registry. Configure Terraform to override this method for your provider during development, then build and test your provider with Terraform.
Prepare Terraform for local provider install
Add your provider to the dev_overrides
block in a configuration file called
.terraformrc
. This block overrides all other configured installation methods
for the specified provider.
Terraform searches for the .terraformrc
file in your home directory and
applies any configuration settings you set.
First, find the GOBIN
path where Go installs your binaries. Your path may vary depending on how your Go environment variables are configured.
$ go env GOBIN
/Users/<Username>/go/bin
If the GOBIN
go environment variable is not set, use the default path,
/Users/<Username>/go/bin
.
Create a new file called .terraformrc
in your home directory (~
), then add
the dev_overrides
block below. Change the <PATH>
to the value returned from
the go env GOBIN
command above.
~/.terraformrc
provider_installation {
dev_overrides {
"hashicorp.com/edu/hashicups" = "<PATH>"
}
# For all other providers, install them directly from their origin provider
# registries as normal. If you omit this, Terraform will _only_ use
# the dev_overrides block, and so no other providers will be available.
direct {}
}
In the terraform-provider-hashicups
directory, install the dependencies
required by your provider.
$ go mod vendor
Next, format your provider code.
$ go fmt ./...
Build and install the provider binary into your $GOPATH/bin
directory.
$ go install
Now that the provider is in your Go installation directory, you can use the provider in your Terraform configuration.
Apply your provider
The Terraform provider you just modified is ready to communicate with the HashiCups API endpoint to create an order.
In this step, you will run the standard Terraform workflow of planning and applying a configuration file to create your resources and observe how they map to your API schema.
In your terminal, navigate to the examples/order
directory. This directory
contains a sample configuration file you can apply to verify the create and read
functionality for your coffee order resource.
$ cd examples/order
Apply your configuration to create the order. Terraform will print a warning
about the provider being configured to use the dev_overrides
block. Confirm
the apply step with a yes
to create your order in the HashiCups API.
$ terraform apply
Once the apply completes, the provider saves the resource's state. View the
state with the show
subcommand.
$ terraform state show hashicups_order.edu
Verify the order
The provider performed three operations to create your order resource.
- The provider invoked the first
signin
operation when you ranterraform apply
to retrieve the current state of any resources in the workspace and create a plan for any changes to be applied. Because there are no resources in the workspace, it only authenticated the user. - The provider invoked the
signin
operation a second time after you confirmed the apply run. The provider authenticated using the provided credentials to retrieve and save the JWT token. - The provider invoked the
CreateOrder
operation to create the order defined by the Terraform configuration. Since this is a protected endpoint, it used the saved JWT token from thesignin
operation.
Verify that Terraform created the order by retrieving the order details via the API.
$ curl -X GET -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders/2 | jq .
The order's properties will be the same as those of your hashicups_order.edu
resource.
Implement a data source
Terraform providers use data sources to represent data loaded from a cloud
service API without managing infrastructure. To create data sources for your
provider, create a type that defines a schema and implements a Read
function.
Implement a data source to read order data from the HashiCups API.
Implement the Configure
function
Like your resource, the code that implements your data source will need access to the HashiCups API client to use it to make calls to the HashiCups API.
In your editor, open the internal/provider/coffees_data_source.go
file and
replace the Configure
function with the following.
internal/provider/coffees_data_source.go
// Configure adds the provider configured client to the data source.
func (d *coffeesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Add a nil check when handling ProviderData because Terraform
// sets that data after it calls the ConfigureProvider RPC.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*hashicups.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *hashicups.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = client
}
Implement the Schema
function
Review the data source implementation. This data source will return all the
coffees from the API's unprotected /coffees
endpoint.
Like resources, data sources use a schema to represent the data structure returned by the target service API client.
Replace the Schema
method in the coffees_data_source.go
file with the
following.
internal/provider/coffees_data_source.go
// Schema defines the schema for the data source.
func (d *coffeesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches the list of coffees.",
Attributes: map[string]schema.Attribute{
"coffees": schema.ListNestedAttribute{
Description: "List of coffees.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Description: "Numeric identifier of the coffee.",
Computed: true,
},
"name": schema.StringAttribute{
Description: "Product name of the coffee.",
Computed: true,
},
"teaser": schema.StringAttribute{
Description: "Fun tagline for the coffee.",
Computed: true,
},
"description": schema.StringAttribute{
Description: "Product description of the coffee.",
Computed: true,
},
"price": schema.Float64Attribute{
Description: "Suggested cost of the coffee.",
Computed: true,
},
"image": schema.StringAttribute{
Description: "URI for an image of the coffee.",
Computed: true,
},
"ingredients": schema.ListNestedAttribute{
Description: "List of ingredients in the coffee.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Description: "Numeric identifier of the coffee ingredient.",
Computed: true,
},
},
},
},
},
},
},
},
}
}
Add the Read
function
Replace the Read
function in internal/provider/order_data_source.go
with the
following.
internal/provider/coffees_data_source.go
// Read refreshes the Terraform state with the latest data.
func (d *coffeesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var state coffeesDataSourceModel
coffees, err := d.client.GetCoffees()
if err != nil {
resp.Diagnostics.AddError(
"Unable to Read HashiCups Coffees",
err.Error(),
)
return
}
// Map response body to model
for _, coffee := range coffees {
coffeeState := coffeesModel{
ID: types.Int64Value(int64(coffee.ID)),
Name: types.StringValue(coffee.Name),
Teaser: types.StringValue(coffee.Teaser),
Description: types.StringValue(coffee.Description),
Price: types.Float64Value(coffee.Price),
Image: types.StringValue(coffee.Image),
}
for _, ingredient := range coffee.Ingredient {
coffeeState.Ingredients = append(coffeeState.Ingredients, coffeesIngredientsModel{
ID: types.Int64Value(int64(ingredient.ID)),
})
}
state.Coffees = append(state.Coffees, coffeeState)
}
// Set state
diags := resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
The Read
function for this data source is similar to the order resource's
Read
function. The main difference is where the provider retrieves
information. Instead of reading information from the state file, the data source
reads information from the HashiCups API client.
Update imports
The functions you have added to your data source rely on some external libraries.
At the top of the internal/provider/coffees_data_source.go
file, replace the
import section with the following.
internal/provider/coffees_data_source.go
import (
"context"
"fmt"
"github.com/hashicorp-demoapp/hashicups-client-go"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
Add the data source type to the provider
Open the internal/provider/provider.go
file, and add the coffees data source
type you just created to the map returned by the DataSources
function. Replace
the DataSources
function with the following code.
internal/provider/provider.go
// DataSources defines the data sources implemented in the provider.
func (p *hashicupsProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewCoffeesDataSource,
}
}
Build the provider
Rebuild the provider with the new functionality.
In your terminal, format your provider code.
$ go fmt ./...
Then, rebuild and install the provider.
$ go install
Apply your new data source
Now that you have a resource order type, you can create a new order for the coffee shop API.
In your terminal, navigate to the examples/coffees
directory. This directory
contains a sample configuration file you can apply to verify the read
functionality for your coffees data source.
$ cd examples/coffees
Apply your configuration to read from the data source. Terraform will print a
warning about the provider being configured to use the dev_overrides
block.
Confirm the apply step with a yes
, and Terraform will print out data about the
coffees available from the HashiCups API.
$ terraform apply
Review the Terraform state with the show
subcommand.
$ terraform show
Implement delete
The lifecycle of infrastructure ends when the resources are destroyed. Terraform
will destroy resources when users remove their configuration and apply the
change, or when they destroy an entire workspace with the terraform destroy
.
The Delete
function makes the necessary API calls to destroy a resource and
then to remove that resource from the Terraform state. This function will read
data about the resource from the workspace's state, use the API client to delete
the corresponding infrastructure, and removes the resource from state.
Implement destroy functionality for your coffee order resource.
In your editor, open the internal/provider/order_resource.go
file and replace
the Delete
function with the following code.
internal/provider/order_resource.go
func (r *orderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Retrieve values from state
var state orderResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete existing order
err := r.client.DeleteOrder(state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting HashiCups Order",
"Could not delete order, unexpected error: "+err.Error(),
)
return
}
}
Build the provider
Rebuild the provider with the new functionality.
In your terminal, format your provider code.
$ go fmt ./...
Rebuild and install the provider.
$ go install
Destroy your resources
Navigate to your examples/order
directory.
$ cd examples/order
Use Terraform to destroy your resources from the HashiCups API. Remember to
confirm the apply step with a yes
.
$ terraform destroy
Review the Terraform state to confirm the resources no longer appear in the state file.
$ terraform state list
Next steps
After completing this lab, follow the plugin framework tutorial collection to learn more about provider development, including how to implement provider-defined functions, ephemeral resources, automated testing, and documentation generation.