Data Source Filtering
Why Filtering Matters
Filtering enables users to query infrastructure using flexible criteria without knowing exact IDs or names. This is essential for dynamic configurations and discovering resources.
Common filtering needs:
- Find resources by tags
- Filter by status or state
- Match name patterns
- Query by region or availability zone
- Filter by creation time
- Combine multiple criteria
Filter Block Pattern
Schema Definition
func (d *InstancesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches a list of compute instances matching filters",
Attributes: map[string]schema.Attribute{
"ids": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "List of instance IDs matching filters",
},
},
Blocks: map[string]schema.Block{
"filter": schema.ListNestedBlock{
MarkdownDescription: "Filter instances. Multiple filters are ANDed together",
NestedObject: schema.NestedBlockObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Name of the filter field (e.g., 'tag:Name', 'instance-state-name')",
},
"values": schema.ListAttribute{
Required: true,
ElementType: types.StringType,
MarkdownDescription: "Filter values. Multiple values are ORed within the filter",
},
},
},
},
},
}
}
Data Model
type InstancesDataSourceModel struct {
IDs types.List `tfsdk:"ids"`
Filters []FilterModel `tfsdk:"filter"`
}
type FilterModel struct {
Name types.String `tfsdk:"name"`
Values types.List `tfsdk:"values"`
}
Usage Example
data "mycloud_instances" "web_servers" {
# Find instances with tag Role=web OR Role=frontend
filter {
name = "tag:Role"
values = ["web", "frontend"]
}
# AND tag Environment=production
filter {
name = "tag:Environment"
values = ["production"]
}
# AND state is running
filter {
name = "instance-state-name"
values = ["running"]
}
}
output "instance_ids" {
value = data.mycloud_instances.web_servers.ids
}
Implementing Filter Logic
Parsing Filters
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
// Convert filters to usable format
filters := make(map[string][]string)
for _, filter := range config.Filters {
name := filter.Name.ValueString()
var values []string
diags := filter.Values.ElementsAs(ctx, &values, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
filters[name] = values
tflog.Debug(ctx, "Processing filter", map[string]any{
"name": name,
"values": values,
})
}
// Query API with filters
instances, err := d.client.ListInstances(ctx, filters)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Extract IDs
ids := make([]attr.Value, len(instances))
for i, instance := range instances {
ids[i] = types.StringValue(instance.ID)
}
listValue, diags := types.ListValue(types.StringType, ids)
resp.Diagnostics.Append(diags...)
config.IDs = listValue
resp.State.Set(ctx, config)
}
Client-Side Filtering
If your API doesn’t support filtering, implement it client-side:
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
// Get all instances
allInstances, err := d.client.ListAllInstances(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
tflog.Debug(ctx, "Retrieved instances", map[string]any{
"count": len(allInstances),
})
// Apply filters
filtered := allInstances
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),
})
}
// Convert to IDs
ids := make([]attr.Value, len(filtered))
for i, instance := range filtered {
ids[i] = types.StringValue(instance.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
resp.State.Set(ctx, config)
}
func applyFilter(instances []Instance, filterName string, filterValues []string) []Instance {
var result []Instance
for _, instance := range instances {
if matchesFilter(instance, filterName, filterValues) {
result = append(result, instance)
}
}
return result
}
func matchesFilter(instance Instance, name string, values []string) bool {
// Tag filters
if strings.HasPrefix(name, "tag:") {
tagKey := strings.TrimPrefix(name, "tag:")
tagValue, exists := instance.Tags[tagKey]
if !exists {
return false
}
// Check if tag value matches any filter value (OR logic)
for _, value := range values {
if tagValue == value {
return true
}
}
return false
}
// State filter
if name == "instance-state-name" {
for _, value := range values {
if instance.State == value {
return true
}
}
return false
}
// Type filter
if name == "instance-type" {
for _, value := range values {
if instance.Type == value {
return true
}
}
return false
}
// Add more filter types as needed
return false
}
Tag Filtering
Exact Tag Match
func (d *DatabaseDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"tags": schema.MapAttribute{
Optional: true,
ElementType: types.StringType,
MarkdownDescription: "Tags to filter by. All specified tags must match exactly",
},
"id": schema.StringAttribute{
Computed: true,
},
"name": schema.StringAttribute{
Computed: true,
},
},
}
}
func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabaseDataSourceModel
req.Config.Get(ctx, &config)
// Parse tags
var filterTags map[string]string
config.Tags.ElementsAs(ctx, &filterTags, false)
tflog.Debug(ctx, "Filtering by tags", map[string]any{
"tags": filterTags,
})
// Get all databases
allDbs, err := d.client.ListDatabases(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Filter by tags (all must match)
var matchingDbs []Database
for _, db := range allDbs {
if tagsMatch(db.Tags, filterTags) {
matchingDbs = append(matchingDbs, db)
}
}
if len(matchingDbs) == 0 {
resp.Diagnostics.AddError(
"No Database Found",
fmt.Sprintf("No database found with tags: %v", filterTags),
)
return
}
if len(matchingDbs) > 1 {
resp.Diagnostics.AddError(
"Multiple Databases Found",
fmt.Sprintf("Found %d databases with specified tags", len(matchingDbs)),
)
return
}
db := matchingDbs[0]
config.ID = types.StringValue(db.ID)
config.Name = types.StringValue(db.Name)
resp.State.Set(ctx, config)
}
func tagsMatch(dbTags, filterTags map[string]string) bool {
// All filter tags must exist and match
for key, value := range filterTags {
dbValue, exists := dbTags[key]
if !exists || dbValue != value {
return false
}
}
return true
}
Usage:
data "mycloud_database" "shared" {
tags = {
Environment = "production"
Shared = "true"
Team = "platform"
}
}
Tag Key Existence
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
// Filter instances that have specific tags (regardless of value)
requiredTagKeys := config.RequiredTags.Elements()
var filtered []Instance
for _, instance := range allInstances {
hasAllTags := true
for _, tagKey := range requiredTagKeys {
key := tagKey.(types.String).ValueString()
if _, exists := instance.Tags[key]; !exists {
hasAllTags = false
break
}
}
if hasAllTags {
filtered = append(filtered, instance)
}
}
// Set results...
}
Name Pattern Filtering
Wildcard Matching
"name_pattern": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Name pattern with wildcards (* matches any characters)",
},
func (d *AMIDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config AMIDataSourceModel
req.Config.Get(ctx, &config)
pattern := config.NamePattern.ValueString()
// Convert wildcard pattern to regex
regexPattern := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), `\*`, ".*") + "$"
nameRegex, err := regexp.Compile(regexPattern)
if err != nil {
resp.Diagnostics.AddError(
"Invalid Pattern",
fmt.Sprintf("Invalid name pattern: %s", err.Error()),
)
return
}
// Query and filter AMIs
amis, err := d.client.ListAMIs(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
var matching []AMI
for _, ami := range amis {
if nameRegex.MatchString(ami.Name) {
matching = append(matching, ami)
}
}
if len(matching) == 0 {
resp.Diagnostics.AddError(
"No AMI Found",
fmt.Sprintf("No AMI found matching pattern: %s", pattern),
)
return
}
// Use most recent if multiple matches
ami := findMostRecent(matching)
config.ID = types.StringValue(ami.ID)
config.Name = types.StringValue(ami.Name)
resp.State.Set(ctx, config)
}
Usage:
data "mycloud_ami" "ubuntu" {
name_pattern = "ubuntu-22.04-*-amd64-server-*"
most_recent = true
}
Regex Matching
"name_regex": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Regular expression to match AMI names",
},
func (d *AMIDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config AMIDataSourceModel
req.Config.Get(ctx, &config)
pattern := config.NameRegex.ValueString()
nameRegex, err := regexp.Compile(pattern)
if err != nil {
resp.Diagnostics.AddError(
"Invalid Regex",
fmt.Sprintf("Invalid regular expression: %s", err.Error()),
)
return
}
amis, err := d.client.ListAMIs(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
var matching []AMI
for _, ami := range amis {
if nameRegex.MatchString(ami.Name) {
matching = append(matching, ami)
}
}
// Handle results...
}
Usage:
data "mycloud_ami" "ubuntu" {
name_regex = "^ubuntu-22\\.04-\\d{8}-amd64-server-.*$"
most_recent = true
}
Most Recent Pattern
Schema
"most_recent": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "If multiple results match, return the most recent. Defaults to false",
},
"created_at": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Creation timestamp of the selected resource",
},
Implementation
func (d *AMIDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config AMIDataSourceModel
req.Config.Get(ctx, &config)
// Apply filters to get matching AMIs
matching := filterAMIs(allAMIs, config)
if len(matching) == 0 {
resp.Diagnostics.AddError(
"No AMI Found",
"No AMI found matching the specified criteria",
)
return
}
var ami AMI
if config.MostRecent.ValueBool() {
// Sort by creation time and take most recent
ami = findMostRecent(matching)
tflog.Info(ctx, "Selected most recent AMI", map[string]any{
"id": ami.ID,
"name": ami.Name,
"created_at": ami.CreatedAt,
"total": len(matching),
})
} else {
// Require exactly one match
if len(matching) > 1 {
resp.Diagnostics.AddError(
"Multiple AMIs Found",
fmt.Sprintf(
"Found %d AMIs matching criteria. "+
"Set most_recent = true or refine filters.\n\n"+
"Matching AMIs:\n%s",
len(matching),
formatAMIList(matching),
),
)
return
}
ami = matching[0]
}
// Set computed attributes
config.ID = types.StringValue(ami.ID)
config.Name = types.StringValue(ami.Name)
config.CreatedAt = types.StringValue(ami.CreatedAt)
resp.State.Set(ctx, config)
}
func findMostRecent(amis []AMI) AMI {
if len(amis) == 0 {
return AMI{}
}
mostRecent := amis[0]
for _, ami := range amis[1:] {
if ami.CreatedAt > mostRecent.CreatedAt {
mostRecent = ami
}
}
return mostRecent
}
Usage:
data "mycloud_ami" "latest_ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu-22.04-*"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}
output "ami_id" {
value = data.mycloud_ami.latest_ubuntu.id
}
output "ami_created" {
value = data.mycloud_ami.latest_ubuntu.created_at
}
Complex Filter Combinations
AND/OR Logic
// Multiple filters are ANDed
data "mycloud_instances" "specific" {
filter {
name = "tag:Environment"
values = ["production"] # Must match
}
filter {
name = "instance-state-name"
values = ["running", "pending"] # Must match one of these (OR)
}
filter {
name = "instance-type"
values = ["t3.large", "t3.xlarge"] # Must match one of these (OR)
}
}
Implementation
func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
instances, err := d.client.ListInstances(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Apply each filter (AND logic between filters)
filtered := instances
for _, filter := range config.Filters {
name := filter.Name.ValueString()
var values []string
filter.Values.ElementsAs(ctx, &values, false)
// Filter must match at least one value (OR logic within filter)
filtered = filterInstances(filtered, name, values)
}
// Convert to results
ids := make([]attr.Value, len(filtered))
for i, instance := range filtered {
ids[i] = types.StringValue(instance.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
resp.State.Set(ctx, config)
}
func filterInstances(instances []Instance, filterName string, filterValues []string) []Instance {
var result []Instance
for _, instance := range instances {
// Check if instance matches any value (OR logic)
matches := false
for _, value := range filterValues {
if instanceMatchesFilterValue(instance, filterName, value) {
matches = true
break
}
}
if matches {
result = append(result, instance)
}
}
return result
}
State/Status Filtering
"state": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Filter by state (available, creating, deleting, etc.)",
},
func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabasesDataSourceModel
req.Config.Get(ctx, &config)
dbs, err := d.client.ListDatabases(ctx)
if err != nil {
resp.Diagnostics.AddError("Query Failed", err.Error())
return
}
// Filter by state if specified
if !config.State.IsNull() {
state := config.State.ValueString()
var filtered []Database
for _, db := range dbs {
if db.Status == state {
filtered = append(filtered, db)
}
}
dbs = filtered
tflog.Debug(ctx, "Filtered by state", map[string]any{
"state": state,
"count": len(dbs),
})
}
// Convert to IDs
ids := make([]attr.Value, len(dbs))
for i, db := range dbs {
ids[i] = types.StringValue(db.ID)
}
listValue, _ := types.ListValue(types.StringType, ids)
config.IDs = listValue
resp.State.Set(ctx, config)
}
Usage:
data "mycloud_databases" "available" {
state = "available"
tags = {
Environment = "production"
}
}
Sorting Results
"sort_by": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Sort results by field (name, created_at, etc.)",
},
"sort_order": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Sort order (asc or desc). Defaults to asc",
},
func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config DatabasesDataSourceModel
req.Config.Get(ctx, &config)
// Query and filter databases
dbs := queryAndFilter(d.client, config)
// Sort if requested
if !config.SortBy.IsNull() {
sortBy := config.SortBy.ValueString()
sortOrder := "asc"
if !config.SortOrder.IsNull() {
sortOrder = config.SortOrder.ValueString()
}
sortDatabases(dbs, sortBy, sortOrder)
}
// Convert to results...
}
func sortDatabases(dbs []Database, sortBy, order string) {
sort.Slice(dbs, func(i, j int) bool {
var less bool
switch sortBy {
case "name":
less = dbs[i].Name < dbs[j].Name
case "created_at":
less = dbs[i].CreatedAt < dbs[j].CreatedAt
case "storage_gb":
less = dbs[i].StorageGB < dbs[j].StorageGB
default:
less = dbs[i].Name < dbs[j].Name
}
if order == "desc" {
return !less
}
return less
})
}
Validation
Filter Name Validation
func (d *InstancesDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var config InstancesDataSourceModel
req.Config.Get(ctx, &config)
validFilterNames := []string{
"instance-state-name",
"instance-type",
"availability-zone",
"tag:Name",
"tag:Environment",
"tag:Role",
}
for _, filter := range config.Filters {
name := filter.Name.ValueString()
// Check if it's a valid filter or tag filter
if !isValidFilterName(name, validFilterNames) {
resp.Diagnostics.AddAttributeWarning(
path.Root("filter"),
"Unknown Filter Name",
fmt.Sprintf(
"Filter name '%s' may not be supported.\n\n"+
"Valid filter names:\n%s\n\n"+
"Tag filters use format: tag:<TagKey>",
name,
strings.Join(validFilterNames, "\n"),
),
)
}
}
}
func isValidFilterName(name string, validNames []string) bool {
// Check exact match
for _, valid := range validNames {
if name == valid {
return true
}
}
// Check if it's a tag filter
if strings.HasPrefix(name, "tag:") {
return true
}
return false
}
Best Practices
1. Provide Clear Filter Documentation
resp.Schema = schema.Schema{
MarkdownDescription: "Fetches instances matching filters.\n\n" +
"## Supported Filters\n\n" +
"- `instance-state-name`: running, stopped, pending, etc.\n" +
"- `instance-type`: t3.micro, t3.small, etc.\n" +
"- `availability-zone`: us-east-1a, us-east-1b, etc.\n" +
"- `tag:<TagKey>`: Filter by tag key and value\n\n" +
"## Example\n\n" +
"```hcl\n" +
"data \"mycloud_instances\" \"web\" {\n" +
" filter {\n" +
" name = \"tag:Role\"\n" +
" values = [\"web\"]\n" +
" }\n" +
"}\n" +
"```",
}
2. Log Filter Application
tflog.Debug(ctx, "Applying filter", map[string]any{
"name": name,
"values": values,
"before_count": len(instances),
"after_count": len(filtered),
})
3. Handle Empty Results
if len(filtered) == 0 {
resp.Diagnostics.AddError(
"No Resources Found",
fmt.Sprintf(
"No resources found matching filters.\n\n"+
"Filters applied:\n%s\n\n"+
"Check filter names and values are correct.",
formatFilters(config.Filters),
),
)
return
}
4. Optimize Performance
// ✅ Good: Filter on server side
instances, err := d.client.ListInstances(ctx, &ListOptions{
Filters: convertFilters(config.Filters),
})
// ❌ Bad: Fetch all, filter client side
all, err := d.client.ListAllInstances(ctx)
filtered := filterClientSide(all, config.Filters)
Key Takeaways
- ✅ Use filter blocks for flexible querying
- ✅ Support multiple filter combinations (AND between filters, OR within)
- ✅ Implement tag filtering for resource discovery
- ✅ Support name pattern matching (wildcards or regex)
- ✅ Provide “most_recent” option for time-based selection
- ✅ Validate filter names and provide helpful errors
- ✅ Log filter application for debugging
- ✅ Handle empty results gracefully
- ✅ Optimize by filtering server-side when possible
- ✅ Document supported filters clearly
What’s Next?
Learn how to handle data sources that return lists of resources!
Previous: ← Data Source Read
Last updated: June 30, 2026