appendix E Creating a Petstore data source

This appendix is a supplement to chapter 11. It explains how to implement a data source for the Petstore provider, which complements the pet resource. The data source described here allows users to query the ID of pet resources by name. An example of using the data source is as follows:

data "petstore_pet_ids" "all" {
    names = ["*"]
}
 
data "petstore_pet_ids" "my_pets" {
["snowball", "princess"]
}

The data source has a single required argument called names, which is a list of pet names to search for (an asterisk selects all pets). The data source exports an ids attribute, which is a list of pet IDs.

E.1 Registering the data source

As we did with the pet resource, we need to register the data source with the provider so that it can be exposed to Terraform. This is as easy as adding a DataSourcesMap attribute to the provider schema.

Listing E.1 provider.go

package petstore
 
import (
    "net/url"
 
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    sdk "github.com/terraform-in-action/go-petstore"
)
 
func Provider() *schema.Provider {
    return &schema.Provider{
        Schema: map[string]*schema.Schema{
            "address": &schema.Schema{
                Type:        schema.TypeString,
                Optional:    true,
                DefaultFunc: schema.EnvDefaultFunc("PETSTORE_ADDRESS", nil),
            },
        },
 
        DataSourcesMap: map[string]*schema.Resource{
            "petstore_pet_ids": dataSourcePSPetIDs(),           
        },
 
        ResourcesMap: map[string]*schema.Resource{
            "petstore_pet": resourcePSPet(),
        },
 
        ConfigureFunc: providerConfigure,
    }
}
 
func providerConfigure(d *schema.ResourceData) (interface{}, error) {
    hostname, _ := d.Get("address").(string)
    address, _ := url.Parse(hostname)
    cfg := &sdk.Config{
        Address: address.String(),
    }
    return sdk.NewClient(cfg)
}

Register data source with provider.

E.2 Creating the data source

Data sources come in two flavors: data sources that return a single resource and data sources that return a list of resources. An example data source that returns a single resource is tfe_workspace from the TFE provider, and a related data source that reads from a list of resources is tfe_workspace_ids:

data "tfe_workspace" "test" {
  name         = "my-workspace-name"
  organization = "my-org-name"
}
 
data "tfe_workspace_ids" "all" {
  names        = ["*"]
  organization = "my-org-name"
}

We could conceivably have two data sources as well: one called petstore_pet and another called petstore_pet_ids. Unfortunately, the Petstore API that I created has no way to uniquely identify pets except by ID, so we can’t have a data source that returns a single resource unless we already knew the ID (marginally useful, at best). This is why I think it makes more sense to just have a data source that returns a list of pet IDs.

Another strategy is to have a data source with a filters block that allows filtering based on other parameters such as name, species, and age and then returns a list of complete pet objects, although this would be more challenging to code. The following example filters by name as well as by species. You can also refer to the AWS provider source code for the aws_instances data source if you want to see an example implementation:

data "petstore_pets" "pets" {
  filter {
    name  = "name"
    value = "snowball"
  }
 
  filter {
    name  = "species"
    value = "cat"
  }
}

The source code for our petsource_pet_ids data source is shown next.

Listing E.2 datasource_ps_pet_ids.go

package petstore
 
import (
    "fmt"
    "strings"
 
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    sdk "github.com/terraform-in-action/go-petstore"
)
 
func dataSourcePSPetIDs() *schema.Resource {
    return &schema.Resource{
        Read: dataSourcePSPetIDsRead,
 
        Schema: map[string]*schema.Schema{
            "names": {
                Type:     schema.TypeList,
                Elem:     &schema.Schema{Type: schema.TypeString},
                Required: true,
            },
            "ids": {
                Type:     schema.TypeList,
                Computed: true,
                Elem:     &schema.Schema{Type: schema.TypeString},
            },
        },
    }
}
 
func dataSourcePSPetIDsRead(d *schema.ResourceData, meta interface{}) error {
    names := make(map[string]bool)
    for _, name := range d.Get("names").([]interface{}) {
        names[name.(string)] = true
    }
 
    conn := meta.(*sdk.Client)
    petList, err := conn.Pets.List(sdk.PetListOptions{})                   
    if err != nil {
        return err
    }
 
    var ids []string
    for _, pet := range petList.Items {                                    
        if names["*"] || names[pet.Name] {
            ids = append(ids, pet.ID)
        }
    }
    d.Set("ids", ids)
    id := fmt.Sprintf("%d", schema.HashString(strings.Join(ids, "")))      
    d.SetId(id)
    return nil
}

Lists all pet resources

Filters all that match either a name in the names list or the wildcard symbol

Invents an ID for this resource, since there isn’t a meaningful ID otherwise

E.3 Writing acceptance tests

Any acceptance test for a data source requires creating the complementary resource because otherwise there would be nothing to query. We can do that by creating a config with both the resource and data source. The following basic acceptance test creates a Petstore pet and then uses the petstore_pet_ids data source to verify that it can be read.

Listing E.3 datasource_ps_pet_ids_test.go

package petstore
 
import (
    "fmt"
    "math/rand"
    "testing"
    "time"
 
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)
 
func TestAccPSPetIDsDataSource_basic(t *testing.T) {
    rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
    resource.Test(t, resource.TestCase{
        PreCheck:     func() { testAccPreCheck(t) },
        Providers:    testAccProviders,
        CheckDestroy: testAccCheckPSPetDestroy,
        Steps: []resource.TestStep{
            {
                Config: testAccPSPetIDsDataSourceConfig(rInt),
                Check: resource.ComposeAggregateTestCheckFunc(
                    resource.TestCheckResourceAttr(
                        "data.petstore_pet_ids.pets", "ids.#", "1"),       
                    resource.TestCheckResourceAttrPair(
                        "petstore_pet.pet", "id",
                        "data.petstore_pet_ids.pets", "ids.0",
                    ),
                ),
            },
        },
    })
}
 
func testAccPSPetIDsDataSourceConfig(rInt int) string {
    return fmt.Sprintf(`      
    resource "petstore_pet" "pet" {
        name    = "%d"
        species = "cat"                                                    
        age     = 3
    }
    data "petstore_pet_ids" "pets" {
        names = [petstore_pet.pet.name]                                    
    }
    `, rInt)
}

Verify that the data source returns a list of length one.

Create dummy pet resource for testing.

Query dummy pet.

E.3.1 Running acceptance tests

Next, we have to download dependencies

$ go get

so that we can run acceptance tests:

$ go test -v ./petstore
=== RUN   TestAccPSPetIDsDataSource_basic
--- PASS: TestAccPSPetIDsDataSource_basic (3.33s)
=== RUN   TestProvider
--- PASS: TestProvider (0.00s)
=== RUN   TestProvider_impl
--- PASS: TestProvider_impl (0.00s)
=== RUN   TestAccPSPet_basic
--- PASS: TestAccPSPet_basic (2.61s)
PASS
ok      github.com/terraform-in-action/terraform-provider-petstore/petstore    
6.179s

Note Set TF_ACC=1 and PETSTORE_ADDRESS=<your petstore address> in your environment.

E.4 Using the data source

After building and installing the provider, just as in chapter 11, we can use the data source.

Note I have published the provider in the Terraform Registry, so the following code works as is.

Listing E.4 petstore.tf

terraform {
  required_providers {
    petstore = {
      source  = "terraform-in-action/petstore"
      version = "~> 1.0"
    }
  }
}
 
provider "petstore" {
  address = "https://w029yh67o2.execute-api.us-west-2.amazonaws.com/v1"    
}
 
resource "petstore_pet" "pet" {
  name    = "snowball"
  species = "cat"
  age     = 8
}
 
data "petstore_pet_ids" "pets" {
  depends_on = [petstore_pet.pet]
  names      = ["snowball"]
}
 
output "pet_ids" {
  value = data.petstore_pet_ids.pets.ids
}

This address will need to point to your petstore API.

The output of running this code is a list of IDs with length 1:

$ terraform output
petstore_pet.pet: Creating...
petstore_pet.pet: Creation complete after 1s [id=7e5a219b-9a77-4aa3-bcba-
6347abcdcb30]
data.petstore_pet_ids.pets: Reading...
data.petstore_pet_ids.pets: Read complete after 0s [id=1222408178]
 
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
 
Outputs:
 
pet_ids = tolist([
  "d1560fb3-6e39-4d6d-9bc1-f27f13efbb71",
])
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.13.201