Data Sources Lab
Lab Overview
In this hands-on lab, you’ll build data sources for the CacheCloud provider:
What You’ll Build:
- Single result data source (
mycloud_cache) - List data source (
mycloud_caches) - Filtering support
- Multiple lookup methods
Time: 60-75 minutes
Prerequisites
Continue with the CacheCloud provider from Module 6 Resource Implementation Lab.
cd terraform-provider-cachecloud
Step 1: Single Cache Data Source
Create internal/provider/cache_data_source.go:
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/yourorg/terraform-provider-cachecloud/internal/client"
)
type CacheDataSource struct {
client *client.Client
}
type CacheDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Engine types.String `tfsdk:"engine"`
EngineVersion types.String `tfsdk:"engine_version"`
NodeType types.String `tfsdk:"node_type"`
NumNodes types.Int64 `tfsdk:"num_nodes"`
Port types.Int64 `tfsdk:"port"`
Status types.String `tfsdk:"status"`
Endpoint types.String `tfsdk:"endpoint"`
ARN types.String `tfsdk:"arn"`
Tags types.Map `tfsdk:"tags"`
}
var _ datasource.DataSource = &CacheDataSource{}
var _ datasource.DataSourceWithConfigure = &CacheDataSource{}
func NewCacheDataSource() datasource.DataSource {
return &CacheDataSource{}
}
func (d *CacheDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_cache"
}
func (d *CacheDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches information about a single cache cluster",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Cache cluster ID. Conflicts with `name`",
},
"name": schema.StringAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Cache cluster name. Conflicts with `id`",
},
"engine": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Cache engine (redis or memcached)",
},
"engine_version": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Engine version",
},
"node_type": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Node type",
},
"num_nodes": schema.Int64Attribute{
Computed: true,
MarkdownDescription: "Number of cache nodes",
},
"port": schema.Int64Attribute{
Computed: true,
MarkdownDescription: "Connection port",
},
"status": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Current status",
},
"endpoint": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Cache endpoint",
},
"arn": schema.StringAttribute{
Computed: true,
MarkdownDescription: "ARN of the cache cluster",
},
"tags": schema.MapAttribute{
Optional: true,
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "Tags to filter by or returned tags",
},
},
}
}
func (d *CacheDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *CacheDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config CacheDataSourceModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Validate that at least one search criterion is provided
hasID := !config.ID.IsNull()
hasName := !config.Name.IsNull()
hasTags := !config.Tags.IsNull()
if !hasID && !hasName && !hasTags {
resp.Diagnostics.AddError(
"Missing Search Criteria",
"One of 'id', 'name', or 'tags' must be specified",
)
return
}
if hasID && hasName {
resp.Diagnostics.AddError(
"Conflicting Search Criteria",
"Only one of 'id' or 'name' can be specified",
)
return
}
var cache *client.Cache
var err error
// Lookup by ID
if hasID {
id := config.ID.ValueString()
tflog.Debug(ctx, "Looking up cache by ID", map[string]any{"id": id})
cache, err = d.client.GetCache(ctx, id)
if err != nil {
resp.Diagnostics.AddError(
"Cache Not Found",
fmt.Sprintf("Could not find cache with ID '%s': %s", id, err.Error()),
)
return
}
}
// Lookup by name
if hasName {
name := config.Name.ValueString()
tflog.Debug(ctx, "Looking up cache by name", map[string]any{"name": name})
caches, err := d.client.ListCaches(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Find cache with matching name
var matching []client.Cache
for _, c := range caches {
if c.Name == name {
matching = append(matching, c)
}
}
if len(matching) == 0 {
resp.Diagnostics.AddError(
"Cache Not Found",
fmt.Sprintf("No cache found with name: %s", name),
)
return
}
if len(matching) > 1 {
resp.Diagnostics.AddError(
"Multiple Caches Found",
fmt.Sprintf("Found %d caches with name '%s'. Name must be unique", len(matching), name),
)
return
}
c := matching[0]
cache = &c
}
// Lookup by tags
if hasTags {
var filterTags map[string]string
config.Tags.ElementsAs(ctx, &filterTags, false)
tflog.Debug(ctx, "Looking up cache by tags", map[string]any{"tags": filterTags})
caches, err := d.client.ListCaches(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Filter by tags
var matching []client.Cache
for _, c := range caches {
if tagsMatch(c.Tags, filterTags) {
matching = append(matching, c)
}
}
if len(matching) == 0 {
resp.Diagnostics.AddError(
"No Cache Found",
fmt.Sprintf("No cache found with tags: %v", filterTags),
)
return
}
if len(matching) > 1 {
resp.Diagnostics.AddError(
"Multiple Caches Found",
fmt.Sprintf("Found %d caches with specified tags", len(matching)),
)
return
}
c := matching[0]
cache = &c
}
// Populate all attributes
config.ID = types.StringValue(cache.ID)
config.Name = types.StringValue(cache.Name)
config.Engine = types.StringValue(cache.Engine)
config.EngineVersion = types.StringValue(cache.EngineVersion)
config.NodeType = types.StringValue(cache.NodeType)
config.NumNodes = types.Int64Value(int64(cache.NumNodes))
config.Port = types.Int64Value(int64(cache.Port))
config.Status = types.StringValue(cache.Status)
config.Endpoint = types.StringValue(cache.Endpoint)
config.ARN = types.StringValue(cache.ARN)
// Convert tags
if len(cache.Tags) > 0 {
tagsMap := make(map[string]attr.Value)
for k, v := range cache.Tags {
tagsMap[k] = types.StringValue(v)
}
mapValue, _ := types.MapValue(types.StringType, tagsMap)
config.Tags = mapValue
} else {
config.Tags = types.MapNull(types.StringType)
}
// Save state
resp.State.Set(ctx, config)
tflog.Info(ctx, "Successfully read cache", map[string]any{
"id": cache.ID,
})
}
func tagsMatch(cacheTags, filterTags map[string]string) bool {
for key, value := range filterTags {
cacheValue, exists := cacheTags[key]
if !exists || cacheValue != value {
return false
}
}
return true
}
Step 2: List Caches Data Source
Create internal/provider/caches_data_source.go:
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/yourorg/terraform-provider-cachecloud/internal/client"
)
type CachesDataSource struct {
client *client.Client
}
type CachesDataSourceModel struct {
IDs types.List `tfsdk:"ids"`
Caches types.List `tfsdk:"caches"`
Filters []FilterModel `tfsdk:"filter"`
}
type FilterModel struct {
Name types.String `tfsdk:"name"`
Values types.List `tfsdk:"values"`
}
var _ datasource.DataSource = &CachesDataSource{}
var _ datasource.DataSourceWithConfigure = &CachesDataSource{}
func NewCachesDataSource() datasource.DataSource {
return &CachesDataSource{}
}
func (d *CachesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_caches"
}
func (d *CachesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a list of cache clusters matching filters",
Attributes: map[string]schema.Attribute{
"ids": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "List of cache cluster IDs",
},
"caches": schema.ListNestedAttribute{
Computed: true,
MarkdownDescription: "Detailed cache cluster information",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Computed: true,
},
"engine": schema.StringAttribute{
Computed: true,
},
"node_type": schema.StringAttribute{
Computed: true,
},
"num_nodes": schema.Int64Attribute{
Computed: true,
},
"status": schema.StringAttribute{
Computed: true,
},
"endpoint": schema.StringAttribute{
Computed: true,
},
},
},
},
},
Blocks: map[string]schema.Block{
"filter": schema.ListNestedBlock{
MarkdownDescription: "Filter caches. Multiple filters are ANDed",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Filter name (tag:<key>, status, engine)",
},
"values": schema.ListAttribute{
Required: true,
ElementType: types.StringType,
MarkdownDescription: "Filter values (OR within filter)",
},
},
},
},
},
}
}
func (d *CachesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *client.Client, got: %T", req.ProviderData),
)
return
}
d.client = client
}
func (d *CachesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config CachesDataSourceModel
req.Config.Get(ctx, &config)
tflog.Debug(ctx, "Reading caches data source")
// Get all caches
caches, err := d.client.ListCaches(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved caches", map[string]any{
"count": len(caches),
})
// Apply filters
filtered := caches
for _, filter := range config.Filters {
name := filter.Name.ValueString()
var values []string
filter.Values.ElementsAs(ctx, &values, false)
filtered = applyFilter(filtered, name, values)
tflog.Debug(ctx, "Applied filter", map[string]any{
"name": name,
"values": values,
"remaining": len(filtered),
})
}
// Handle empty results
if len(filtered) == 0 {
tflog.Info(ctx, "No caches found matching criteria")
config.IDs = types.ListNull(types.StringType)
config.Caches = types.ListNull(cacheObjectType)
resp.State.Set(ctx, config)
return
}
// Build ID list
ids := make([]attr.Value, len(filtered))
for i, cache := range filtered {
ids[i] = types.StringValue(cache.ID)
}
idsList, _ := types.ListValue(types.StringType, ids)
config.IDs = idsList
// Build detailed list
cacheObjectType := types.ObjectType{
AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"engine": types.StringType,
"node_type": types.StringType,
"num_nodes": types.Int64Type,
"status": types.StringType,
"endpoint": types.StringType,
},
}
cacheObjects := make([]attr.Value, len(filtered))
for i, cache := range filtered {
objValue, _ := types.ObjectValue(
cacheObjectType.AttrTypes,
map[string]attr.Value{
"id": types.StringValue(cache.ID),
"name": types.StringValue(cache.Name),
"engine": types.StringValue(cache.Engine),
"node_type": types.StringValue(cache.NodeType),
"num_nodes": types.Int64Value(int64(cache.NumNodes)),
"status": types.StringValue(cache.Status),
"endpoint": types.StringValue(cache.Endpoint),
},
)
cacheObjects[i] = objValue
}
cachesList, _ := types.ListValue(cacheObjectType, cacheObjects)
config.Caches = cachesList
// Save state
resp.State.Set(ctx, config)
tflog.Info(ctx, "Successfully read caches", map[string]any{
"count": len(filtered),
})
}
func applyFilter(caches []client.Cache, filterName string, filterValues []string) []client.Cache {
var result []client.Cache
for _, cache := range caches {
if matchesFilter(cache, filterName, filterValues) {
result = append(result, cache)
}
}
return result
}
func matchesFilter(cache client.Cache, name string, values []string) bool {
// Tag filter
if strings.HasPrefix(name, "tag:") {
tagKey := strings.TrimPrefix(name, "tag:")
tagValue, exists := cache.Tags[tagKey]
if !exists {
return false
}
for _, value := range values {
if tagValue == value {
return true
}
}
return false
}
// Status filter
if name == "status" {
for _, value := range values {
if cache.Status == value {
return true
}
}
return false
}
// Engine filter
if name == "engine" {
for _, value := range values {
if cache.Engine == value {
return true
}
}
return false
}
return false
}
Step 3: Update Client
Add ListCaches to internal/client/client.go:
func (c *Client) ListCaches(ctx context.Context) ([]Cache, error) {
// Simulate API call
fmt.Println("Listing all caches")
caches := []Cache{
{
ID: "cache-1",
Name: "prod-redis",
Engine: "redis",
EngineVersion: "7.0",
NodeType: "cache.t3.micro",
NumNodes: 2,
Port: 6379,
Status: "available",
Endpoint: "prod-redis.cache.example.com:6379",
ARN: "arn:aws:cache:us-east-1:123456789012:cache/prod-redis",
Tags: map[string]string{"Environment": "production", "Type": "redis"},
},
{
ID: "cache-2",
Name: "dev-redis",
Engine: "redis",
EngineVersion: "7.0",
NodeType: "cache.t3.micro",
NumNodes: 1,
Port: 6379,
Status: "available",
Endpoint: "dev-redis.cache.example.com:6379",
ARN: "arn:aws:cache:us-east-1:123456789012:cache/dev-redis",
Tags: map[string]string{"Environment": "development", "Type": "redis"},
},
{
ID: "cache-3",
Name: "prod-memcached",
Engine: "memcached",
EngineVersion: "1.6",
NodeType: "cache.t3.small",
NumNodes: 2,
Port: 11211,
Status: "available",
Endpoint: "prod-memcached.cache.example.com:11211",
ARN: "arn:aws:cache:us-east-1:123456789012:cache/prod-memcached",
Tags: map[string]string{"Environment": "production", "Type": "memcached"},
},
}
return caches, nil
}
Step 4: Register Data Sources
Update internal/provider/provider.go:
func (p *CacheCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewCacheDataSource,
NewCachesDataSource,
}
}
Step 5: Build Provider
go mod tidy
go build -o terraform-provider-cachecloud
Install locally:
# macOS ARM64
mkdir -p ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_arm64
cp terraform-provider-cachecloud ~/.terraform.d/plugins/registry.terraform.io/yourorg/cachecloud/1.0.0/darwin_arm64/
Step 6: Test Single Cache Lookup
Create examples/data-sources/cache.tf:
terraform {
required_providers {
cachecloud = {
source = "registry.terraform.io/yourorg/cachecloud"
}
}
}
provider "cachecloud" {
api_key = "test-key"
}
# Lookup by ID
data "cachecloud_cache" "by_id" {
id = "cache-1"
}
# Lookup by name
data "cachecloud_cache" "by_name" {
name = "prod-redis"
}
# Lookup by tags
data "cachecloud_cache" "by_tags" {
tags = {
Environment = "production"
Type = "redis"
}
}
# Outputs
output "cache_by_id" {
value = {
id = data.cachecloud_cache.by_id.id
name = data.cachecloud_cache.by_id.name
endpoint = data.cachecloud_cache.by_id.endpoint
}
}
output "cache_by_name" {
value = {
id = data.cachecloud_cache.by_name.id
endpoint = data.cachecloud_cache.by_name.endpoint
}
}
output "cache_by_tags" {
value = {
id = data.cachecloud_cache.by_tags.id
name = data.cachecloud_cache.by_tags.name
}
}
Test:
cd examples/data-sources
terraform init
terraform plan
terraform apply
Step 7: Test List Data Source
Create examples/data-sources/caches.tf:
# Get all production caches
data "cachecloud_caches" "production" {
filter {
name = "tag:Environment"
values = ["production"]
}
}
# Get all Redis caches
data "cachecloud_caches" "redis" {
filter {
name = "engine"
values = ["redis"]
}
}
# Get available production Redis caches
data "cachecloud_caches" "prod_redis" {
filter {
name = "tag:Environment"
values = ["production"]
}
filter {
name = "engine"
values = ["redis"]
}
filter {
name = "status"
values = ["available"]
}
}
# Outputs
output "production_cache_ids" {
value = data.cachecloud_caches.production.ids
}
output "redis_caches" {
value = data.cachecloud_caches.redis.caches
}
output "prod_redis_count" {
value = length(data.cachecloud_caches.prod_redis.ids)
}
# Use in resources
resource "cachecloud_backup" "production" {
for_each = toset(data.cachecloud_caches.production.ids)
cache_id = each.value
schedule = "daily"
}
Test:
terraform plan
terraform apply
Lab Validation
Test Checklist
- Single cache lookup by ID works
- Single cache lookup by name works
- Single cache lookup by tags works
- Error when multiple caches match (name)
- Error when no cache found
- List data source returns all caches
- Tag filtering works correctly
- Engine filtering works correctly
- Multiple filters combine with AND logic
- Empty filter results return empty list
- Can use data source IDs in
for_each
Expected Output
terraform apply
data.cachecloud_cache.by_id: Reading...
data.cachecloud_cache.by_name: Reading...
data.cachecloud_cache.by_tags: Reading...
data.cachecloud_caches.production: Reading...
data.cachecloud_caches.redis: Reading...
data.cachecloud_caches.prod_redis: Reading...
data.cachecloud_cache.by_id: Read complete after 0s
data.cachecloud_cache.by_name: Read complete after 0s
data.cachecloud_cache.by_tags: Read complete after 0s
data.cachecloud_caches.production: Read complete after 0s [id=2]
data.cachecloud_caches.redis: Read complete after 0s [id=2]
data.cachecloud_caches.prod_redis: Read complete after 0s [id=1]
Changes to Outputs:
+ cache_by_id = {
+ endpoint = "prod-redis.cache.example.com:6379"
+ id = "cache-1"
+ name = "prod-redis"
}
+ production_cache_ids = [
+ "cache-1",
+ "cache-3",
]
+ prod_redis_count = 1
Challenge Exercises
1. Add Most Recent
Add a most_recent attribute to return the newest cache when multiple match.
2. Add Name Pattern
Support wildcard name matching (e.g., prod-*).
3. Add Sorting
Add sort_by and sort_order attributes to the list data source.
4. Add Validation
Validate filter names and provide helpful error messages for unknown filters.
5. Add Region Filter
Add a region filter to the list data source.
Key Takeaways
- ✅ Data sources query existing infrastructure
- ✅ Single data sources return exactly one resource
- ✅ List data sources return zero or more resources
- ✅ Support multiple lookup methods (ID, name, tags)
- ✅ Implement filter blocks for flexible queries
- ✅ Filters combine with AND logic between blocks
- ✅ Values within a filter use OR logic
- ✅ Return empty lists, not errors, for no matches
- ✅ Provide detailed error messages
- ✅ Log query operations for debugging
What’s Next?
Test your knowledge with comprehensive practice questions!
Previous: ← Data Source Lists
Last updated: June 30, 2026