HashiCorp Learn
Infrastructure
  • TerraformTerraformLearn terraformDocs
  • PackerPackerLearn packerDocs
  • VagrantVagrantLearn vagrantDocs
Security
  • VaultVaultLearn vaultDocs
  • BoundaryBoundaryLearn boundaryDocs
Networking
  • ConsulConsulLearn consulDocs
Applications
  • NomadNomadLearn nomadDocs
  • WaypointWaypointLearn waypointDocs
  • HashiCorp Cloud Platform (HCP) LogoHashiCorp Cloud Platform (HCP)HashiCorp Cloud Platform (HCP)Docs
Type '/' to Search
Loading account...
  • Bookmarks
  • Manage Account
  • Overview
  • Prerequisites
  • Define order resource
  • Define order schema
  • Implement create
  • Implement read
  • Add order resource to provider
  • Test the provider
  • Next steps
DocsForum
Back to terraform
ProvidersView Collection
    Perform CRUD operations with ProvidersSetup and Implement ReadAdd Authentication to a ProviderImplement Complex ReadDebug a Terraform ProviderImplement CreateImplement UpdateImplement Delete

Implement Create

  • 11 min
  • Products Usedterraform

In this tutorial, you will create an order resource for a Terraform provider that interacts with the API of a fictional coffee-shop application, HashiCups. Then, you will use the schema to create a new order and populate the order. To do this, you will:

  1. Define order resource.
    You will add a scaffold that defines an empty schema and functions to create, read, update and delete orders.
  2. Define order schema.
    The schema contains fields used to create a new order and retrieved by the read functionality.
  3. Add create functionality.
    The create function invokes a POST request to /orders to create a new order.
  4. Add read functionality.
    The read function invokes a GET request to /orders/{orderID} to retrieve the order's data. The orders schema contains a nested object, therefore you will use multiple flattening functions to map the response to the order schema.
  5. Add order resource to the provider schema.
    This allows you to use the resource in your configuration.

»Prerequisites

To follow this tutorial, you need:

  • a Golang 1.13+ installed and configured.
  • the Terraform 0.14+ CLI installed locally. The Terraform binaries are located here.
  • Docker and Docker Compose to run an instance of HashiCups locally.

Navigate to your terraform-provider-hashicups directory. Then, checkout the debug-tf-providers branch. This step is optional but recommended to insure that you've accurately completed the previous steps.

$ git checkout debug-tf-providers

Your directory should have the following structure.

$ tree -L 2
.
├── Makefile
├── README.md
├── docker_compose
│   ├── conf.json
│   └── docker-compose.yml
├── examples
│   ├── coffee
│   ├── main.tf
├── go.mod
├── go.sum
├── hashicups
│   ├── data_source_coffee.go
│   ├── data_source_order.go
│   └── provider.go
├── main.go
└── vendor

If you’re stuck, refer to the implement-create branch to see the changes implemented in this tutorial.

»Define order resource

Add the following code snippet to a new file named resource_order.go in the hashicups directory. As a general convention, Terraform providers put each resource in their own file, named after the resource, prefixed with resource_.

package hashicups

import (
  "context"

  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceOrder() *schema.Resource {
  return &schema.Resource{
    CreateContext: resourceOrderCreate,
    ReadContext:   resourceOrderRead,
    UpdateContext: resourceOrderUpdate,
    DeleteContext: resourceOrderDelete,
    Schema: map[string]*schema.Schema{},
  }
}

func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}

func resourceOrderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}

func resourceOrderUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  return resourceOrderRead(ctx, d, m)
}

func resourceOrderDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  return diags
}

This serves as a scaffold for the order resource. Notice, the order resource defines create, read, update and delete (CRUD) functionality. In this tutorial, you will only implement create and read to create and read a new order resource.

»Define order schema

To create a HashiCups order, you would send a POST request to the /orders endpoint with a list of OrderItems, containing the coffee object and its respective quantity.

$ curl -X POST -H "Authorization: ${HASHICUPS_TOKEN}" localhost:19090/orders -d '[{"coffee": { "id":1 }, "quantity":4}, {"coffee": { "id":3 }, "quantity":3}]'

Replace the line Schema: map[string]*schema.Schema{}, in your resourceOrder function with the following schema. The order resource schema should resemble the request body.

Schema: map[string]*schema.Schema{
  "items": &schema.Schema{
    Type:     schema.TypeList,
    Required: true,
    Elem: &schema.Resource{
      Schema: map[string]*schema.Schema{
        "coffee": &schema.Schema{
          Type:     schema.TypeList,
          MaxItems: 1,
          Required: true,
          Elem: &schema.Resource{
            Schema: map[string]*schema.Schema{
              "id": &schema.Schema{
                Type:     schema.TypeInt,
                Required: true,
              },
              "name": &schema.Schema{
                Type:     schema.TypeString,
                Computed: true,
              },
              "teaser": &schema.Schema{
                Type:     schema.TypeString,
                Computed: true,
              },
              "description": &schema.Schema{
                Type:     schema.TypeString,
                Computed: true,
              },
              "price": &schema.Schema{
                Type:     schema.TypeInt,
                Computed: true,
              },
              "image": &schema.Schema{
                Type:     schema.TypeString,
                Computed: true,
              },
            },
          },
        },
        "quantity": &schema.Schema{
          Type:     schema.TypeInt,
          Required: true,
        },
      },
    },
  },
},

This order schema should be similar to the order data resource defined in the previous Implement Complex Read tutorial with two significant changes.

  1. The top level id attribute is missing in the order resource, but present in the order data resource. Unlike the order data source, you don't know the order ID ahead of time. When HashiCups provider invokes the HashiCups API, the API generates an order ID which is then assigned as the resource's ID by the HashiCups provider.
  2. items is a required field, not a computed one. This is because the OrderItems need to be defined to create an order.

There are multiple ways to nest maps.

  1. The first (see coffee object) is to define the nested object as an schema.TypeList with 1 item. This is currently the closest way to emulate a nested object.
  2. The second way is to use a schema.TypeMap. This method may be preferable if you only require a key value map of primitive types. However, you should use a validation function to enforce required keys.

Notice that the coffee object is represented as a schema.TypeList of one item. This method was selected because it closely matches the coffee object returned in the response.

»Implement create

Now that you have defined the order resource schema, replace the resourceOrderCreate function in resource_order.go with the following code snippet. This function will create a new HashiCups order and Terraform resource.

func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  c := m.(*hc.Client)

  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  items := d.Get("items").([]interface{})
  ois := []hc.OrderItem{}

  for _, item := range items {
    i := item.(map[string]interface{})

    co := i["coffee"].([]interface{})[0]
    coffee := co.(map[string]interface{})

    oi := hc.OrderItem{
      Coffee: hc.Coffee{
        ID: coffee["id"].(int),
      },
      Quantity: i["quantity"].(int),
    }

    ois = append(ois, oi)
  }

  o, err := c.CreateOrder(ois)
  if err != nil {
    return diag.FromErr(err)
  }

  d.SetId(strconv.Itoa(o.ID))

  return diags
}

Since this uses strconv to convert the ID into a string, remember to import the strconv library. Remember to also import the HashiCups API client library.

import (
  "context"
+ "strconv"

+ hc "github.com/hashicorp-demoapp/hashicups-client-go"
  "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
  "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

Notice the m (meta) input parameter contains the HashiCups API Client set by the ConfigureContextFunc defined above. If an unauthenticated API Client is provided (no username/password), this function will fail and return an error message.

The function retrieves the order attributes using d.Get("items") — order items are located in items as defined in the schema — and typecasts it to a []interface{}. Then, the function transforms the items into a slice of OrderItem, a struct required by the HashiCups API client to create an order.

Finally, upon successful order creation, SetID sets the resource ID to the order ID. The resource ID must be a non-blank string that can be used to read the resource again. If no ID is set, Terraform assumes the resource was not created successfully; as a result, no state will be saved for that resource.

»Implement read

Now that you have implemented create, you must implement read to retrieve the resource current state. This will be run during the plan process to determine whether the resource needs to be updated.

Replace the resourceOrderRead function in resource_order.go with the following code snippet.

func resourceOrderRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  c := m.(*hc.Client)

  // Warning or errors can be collected in a slice type
  var diags diag.Diagnostics

  orderID := d.Id()

  order, err := c.GetOrder(orderID)
  if err != nil {
    return diag.FromErr(err)
  }

  orderItems := flattenOrderItems(&order.Items)
  if err := d.Set("items", orderItems); err != nil {
    return diag.FromErr(err)
  }

  return diags
}

This function should be similar to the dataSourceOrderRead function in data_resource_order.go with two significant change.

  1. The order resource's read function (resourceOrderRead) uses d.ID() as the order ID. The order data resource's read function (dataSourceOrderRead) uses an order ID provider by the user.
  2. The function uses flattenOrderItems instead of flattenOrderItemsData because the order resource and the order data source uses different schemas. You will define the flattenOrderItems in the next section.

»Add flattening functions

The API Client's GetOrder function returns an order object that needs to be "flattened" so the provider can accurately map it to the order schema. An order consists of an order ID and a list of coffee objects and their respective quantities.

As a result, it must go through two flattening functions:

  1. The first flattening function populates the list of coffee objects and their quantities
  2. The second flattening function populates the actual coffee object itself.

Add the following function to your resource_order.go file. This is the first flattening function to return a list of order items. The flattenOrderItems function takes an *[]hc.OrderItem as orderItems. If orderItems is not nil, it will iterate through the slice and map its values into a map[string]interface{}.

func flattenOrderItems(orderItems *[]hc.OrderItem) []interface{} {
  if orderItems != nil {
    ois := make([]interface{}, len(*orderItems), len(*orderItems))

    for i, orderItem := range *orderItems {
      oi := make(map[string]interface{})

      oi["coffee"] = flattenCoffee(orderItem.Coffee)
      oi["quantity"] = orderItem.Quantity
      ois[i] = oi
    }

    return ois
  }

  return make([]interface{}, 0)
}

Then, add the second flattening function, flattenCoffee, to your resource_order.go file. This is called in the first flattening function. It takes a hc.Coffee and turns a slice with a single object. Notice how this mirrors the coffee schema — a schema.TypeList with a maximum of one item.

func flattenCoffee(coffee hc.Coffee) []interface{} {
  c := make(map[string]interface{})
  c["id"] = coffee.ID
  c["name"] = coffee.Name
  c["teaser"] = coffee.Teaser
  c["description"] = coffee.Description
  c["price"] = coffee.Price
  c["image"] = coffee.Image

  return []interface{}{c}
}

»Add read function to create function

Finally, add resourceOrderRead to the bottom of your resourceOrderCreate function. This will populate the Terraform state to its current state after the resource creation.

func resourceOrderCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
  // ...

  d.SetId(strconv.Itoa(o.ID))

+ resourceOrderRead(ctx, d, m)

  return diags
}

»Add order resource to provider

Now that you’ve defined the order resource, you can add it to your provider.

In your provider.go file, add the order resource to the ResourceMap. Resources and data sources names must follow the <provider>_<resource_name> convention.

// Provider -
func Provider() *schema.Provider {
  return &schema.Provider{
    Schema: map[string]*schema.Schema{
      "username": &schema.Schema{
        Type:        schema.TypeString,
        Optional:    true,
        DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_USERNAME", nil),
      },
      "password": &schema.Schema{
        Type:        schema.TypeString,
        Optional:    true,
        Sensitive:   true,
        DefaultFunc: schema.EnvDefaultFunc("HASHICUPS_PASSWORD", nil),
      },
    },
    ResourcesMap: map[string]*schema.Resource{
+     "hashicups_order": resourceOrder(),
    },
    DataSourcesMap: map[string]*schema.Resource{
      "hashicups_coffees":     dataSourceCoffees(),
      "hashicups_order":       dataSourceOrder(),
    },
    ConfigureContextFunc: providerConfigure,
  }
}

»Test the provider

Now that you’ve created the order resource with create and read capabilities, verify that it works.

First, navigate to the terraform-provider-hashicups root directory.

Then, build the binary and move it into your user Terraform plugins directory. This allows you to sideload and test your custom providers.

$ make install
go build -o terraform-provider-hashicups
mv terraform-provider-hashicups ~/.terraform.d/plugins/hashicorp.com/edu/hashicups/0.2/darwin_amd64

Navigate to the examples directory. This contains a sample Terraform configuration for the Terraform HashiCups provider.

$ cd examples

Add the following Terraform configuration to main.tf.

resource "hashicups_order" "edu" {
  items {
    coffee {
      id = 3
    }
    quantity = 2
  }
  items {
    coffee {
      id = 2
    }
    quantity = 2
  }
}

output "edu_order" {
  value = hashicups_order.edu
}

Then, initialize your workspace to refresh your HashiCups provider then. This should create an order and return its values in your output.

$ terraform init && terraform apply --auto-approve
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

»Next steps

Congratulations! You have created the order resource with create and read capabilities.

If you were stuck during this tutorial, checkout the implement-create branch to see the changes implemented in this tutorial.


PreviousDebug a Terraform ProviderNextImplement Update
HashiCorp
  • System Status
  • Terms of Use
  • Security
  • Privacy
stdin: is not a tty