Comprehensive documentation and best practices for building Terraform providers with terraform-plugin-framework (v1.17.0). Covers providers, resources, schemas, types, validators, testing, and common pitfalls.
Overall
score
97%
Implement read-only data sources for querying existing infrastructure and external APIs.
The datasource.DataSource interface defines a read-only Terraform data source.
type DataSource interface {
// Metadata returns data source type name
Metadata(context.Context, MetadataRequest, *MetadataResponse)
// Schema defines data source attributes
Schema(context.Context, SchemaRequest, *SchemaResponse)
// Read fetches data from remote API
Read(context.Context, ReadRequest, *ReadResponse)
}// DataSourceWithConfigure receives provider-configured data
type DataSourceWithConfigure interface {
DataSource
Configure(context.Context, ConfigureRequest, *ConfigureResponse)
}
// DataSourceWithValidateConfig validates configuration
type DataSourceWithValidateConfig interface {
DataSource
ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse)
}package datasource
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Ensure data source implements required interfaces
var (
_ datasource.DataSource = &PetDataSource{}
_ datasource.DataSourceWithConfigure = &PetDataSource{}
)
type PetDataSource struct {
client *APIClient
}
func NewPetDataSource() datasource.DataSource {
return &PetDataSource{}
}func (d *PetDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_pet"
}Usage in HCL:
data "example_pet" "my_pet" {
id = "pet-123"
}
output "pet_name" {
value = data.example_pet.my_pet.name
}func (d *PetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches a pet by ID",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Pet unique identifier",
Required: true,
},
"name": schema.StringAttribute{
Description: "Pet name",
Computed: true,
},
"species": schema.StringAttribute{
Description: "Pet species",
Computed: true,
},
"age": schema.Int64Attribute{
Description: "Pet age in years",
Computed: true,
},
"owner": schema.StringAttribute{
Description: "Pet owner name",
Computed: true,
},
},
}
}Key Points:
Required or OptionalComputed: trueCreate, Update, Delete)type PetDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Species types.String `tfsdk:"species"`
Age types.Int64 `tfsdk:"age"`
Owner types.String `tfsdk:"owner"`
}The Read method fetches data from the remote API and populates the state.
func (d *PetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config PetDataSourceModel
// Read configuration
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Fetch pet from API
pet, err := d.client.GetPet(ctx, config.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unable to Read Pet",
"An error occurred while fetching pet ID "+config.ID.ValueString()+": "+err.Error(),
)
return
}
// Map API response to state
state := PetDataSourceModel{
ID: types.StringValue(pet.ID),
Name: types.StringValue(pet.Name),
Species: types.StringValue(pet.Species),
Age: types.Int64Value(int64(pet.Age)),
Owner: types.StringValue(pet.Owner),
}
// Save state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}Key Steps:
req.Config.Get()resp.State.Set()Receive provider-configured client:
func (d *PetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*APIClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *APIClient, got: %T", req.ProviderData),
)
return
}
d.client = client
}Allow querying by name instead of ID:
func (d *PetDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches a pet by ID or name",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Pet ID (conflicts with name)",
Optional: true,
Computed: true,
},
"name": schema.StringAttribute{
Description: "Pet name (conflicts with id)",
Optional: true,
Computed: true,
},
"species": schema.StringAttribute{
Description: "Pet species",
Computed: true,
},
"age": schema.Int64Attribute{
Description: "Pet age",
Computed: true,
},
},
}
}
func (d *PetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config PetDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Validate: exactly one of id or name must be provided
if config.ID.IsNull() && config.Name.IsNull() {
resp.Diagnostics.AddError(
"Missing Required Attribute",
"Either 'id' or 'name' must be specified",
)
return
}
if !config.ID.IsNull() && !config.Name.IsNull() {
resp.Diagnostics.AddError(
"Conflicting Attributes",
"Only one of 'id' or 'name' can be specified",
)
return
}
// Fetch by ID or name
var pet *Pet
var err error
if !config.ID.IsNull() {
pet, err = d.client.GetPet(ctx, config.ID.ValueString())
} else {
pet, err = d.client.GetPetByName(ctx, config.Name.ValueString())
}
if err != nil {
resp.Diagnostics.AddError("Unable to Read Pet", err.Error())
return
}
// Map to state
state := PetDataSourceModel{
ID: types.StringValue(pet.ID),
Name: types.StringValue(pet.Name),
Species: types.StringValue(pet.Species),
Age: types.Int64Value(int64(pet.Age)),
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}Return multiple results:
type PetsDataSourceModel struct {
Species types.String `tfsdk:"species"` // Filter input
Pets []PetModel `tfsdk:"pets"` // List output
}
type PetModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Species types.String `tfsdk:"species"`
Age types.Int64 `tfsdk:"age"`
}
func (d *PetsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Fetches all pets, optionally filtered by species",
Attributes: map[string]schema.Attribute{
"species": schema.StringAttribute{
Description: "Filter by species",
Optional: true,
},
"pets": schema.ListNestedAttribute{
Description: "List of pets",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Computed: true,
},
"species": schema.StringAttribute{
Computed: true,
},
"age": schema.Int64Attribute{
Computed: true,
},
},
},
},
},
}
}
func (d *PetsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config PetsDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Fetch pets with optional filter
var pets []*Pet
var err error
if !config.Species.IsNull() {
pets, err = d.client.ListPetsBySpecies(ctx, config.Species.ValueString())
} else {
pets, err = d.client.ListAllPets(ctx)
}
if err != nil {
resp.Diagnostics.AddError("Unable to List Pets", err.Error())
return
}
// Map to state
state := PetsDataSourceModel{
Species: config.Species,
Pets: make([]PetModel, len(pets)),
}
for i, pet := range pets {
state.Pets[i] = PetModel{
ID: types.StringValue(pet.ID),
Name: types.StringValue(pet.Name),
Species: types.StringValue(pet.Species),
Age: types.Int64Value(int64(pet.Age)),
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}Usage:
data "example_pets" "all_cats" {
species = "cat"
}
output "cat_names" {
value = [for pet in data.example_pets.all_cats.pets : pet.name]
}Provide defaults when optional inputs are omitted:
func (d *PetsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config PetsDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Default limit to 100 if not specified
limit := 100
if !config.Limit.IsNull() {
limit = int(config.Limit.ValueInt64())
}
pets, err := d.client.ListPets(ctx, limit)
// ...
}# Data source: fetch existing VPC
data "aws_vpc" "main" {
default = true
}
# Resource: create subnet in VPC
resource "aws_subnet" "app" {
vpc_id = data.aws_vpc.main.id # Reference data source
cidr_block = "10.0.1.0/24"
}func (d *PetDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var config PetDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
// Custom validation: exactly one of id or name
idSet := !config.ID.IsNull()
nameSet := !config.Name.IsNull()
if !idSet && !nameSet {
resp.Diagnostics.AddError(
"Missing Required Attribute",
"Either 'id' or 'name' must be specified",
)
}
if idSet && nameSet {
resp.Diagnostics.AddError(
"Conflicting Attributes",
"Only one of 'id' or 'name' can be specified",
)
}
}Use built-in validators:
"species": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("dog", "cat", "bird", "fish"),
},
},See Validators for comprehensive validation patterns.
func TestPetDataSource_Read(t *testing.T) {
mockClient := &MockAPIClient{}
mockClient.On("GetPet", mock.Anything, "pet-123").Return(&Pet{
ID: "pet-123",
Name: "Fluffy",
Species: "cat",
Age: 3,
Owner: "Alice",
}, nil)
dataSource := &PetDataSource{client: mockClient}
req := datasource.ReadRequest{
Config: /* config with id="pet-123" */,
}
resp := &datasource.ReadResponse{
State: tfsdk.State{},
}
dataSource.Read(context.Background(), req, resp)
require.False(t, resp.Diagnostics.HasError())
mockClient.AssertExpectations(t)
var state PetDataSourceModel
resp.State.Get(context.Background(), &state)
require.Equal(t, "pet-123", state.ID.ValueString())
require.Equal(t, "Fluffy", state.Name.ValueString())
require.Equal(t, "cat", state.Species.ValueString())
}See Testing for comprehensive testing patterns.
pet, err := d.client.GetPet(ctx, config.ID.ValueString())
if err != nil {
if isNotFoundError(err) {
resp.Diagnostics.AddError(
"Pet Not Found",
"No pet exists with ID "+config.ID.ValueString(),
)
} else {
resp.Diagnostics.AddError(
"Unable to Read Pet",
"An unexpected error occurred: "+err.Error(),
)
}
return
}pets, err := d.client.ListPets(ctx)
if err != nil {
resp.Diagnostics.AddError("Unable to List Pets", err.Error())
return
}
if len(pets) == 0 {
resp.Diagnostics.AddWarning(
"No Pets Found",
"The query returned no results",
)
}Continue to Schema System to learn about designing schemas with attributes, blocks, and nested structures.