Data Sources Practice Questions

12 minute read

Data Source Fundamentals

Question 1: Data Source vs. Resource

What are the key differences between a data source and a resource?

View Answer | Aspect | Data Source | Resource | |--------|-------------|----------| | **Purpose** | Query/read existing | Create/manage lifecycle | | **Operations** | Read only | Create, Read, Update, Delete | | **Declaration** | `data "type" "name"` | `resource "type" "name"` | | **State** | Cached query results | Full lifecycle tracking | | **Modifications** | Cannot change infrastructure | Manages infrastructure | | **Refresh** | Every plan/apply | Explicit or on apply | | **Interface** | `datasource.DataSource` | `resource.Resource` | **Example:** ```hcl # Data source - query existing data "mycloud_vpc" "existing" { id = "vpc-12345" } # Resource - create new resource "mycloud_vpc" "new" { cidr_block = "10.0.0.0/16" } ```

Question 2: Required Interface

What is the minimum interface a data source must implement?

View Answer `datasource.DataSource` interface with three methods: ```go type DataSource interface { // Set data source type name Metadata(context.Context, MetadataRequest, *MetadataResponse) // Define schema Schema(context.Context, SchemaRequest, *SchemaResponse) // Query and populate state (ONLY operation) Read(context.Context, ReadRequest, *ReadResponse) } ``` **Additional useful interfaces:** ```go // Receive shared client from provider type DataSourceWithConfigure interface { DataSource Configure(context.Context, ConfigureRequest, *ConfigureResponse) } // Validate configuration type DataSourceWithValidateConfig interface { DataSource ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } ``` **Note:** Data sources have NO Create, Update, or Delete operations.

Question 3: Attribute Types

In a data source schema, when should attributes be Required vs. Optional vs. Computed?

View Answer **Required:** Search criteria that MUST be provided ```go "name": schema.StringAttribute{ Required: true, // User must provide MarkdownDescription: "Database name to look up", }, ``` **Optional:** Additional search criteria or filters ```go "region": schema.StringAttribute{ Optional: true, // User can provide MarkdownDescription: "Region to search in", }, "tags": schema.MapAttribute{ Optional: true, // User can provide ElementType: types.StringType, }, ``` **Computed:** Query results (most attributes) ```go "id": schema.StringAttribute{ Computed: true, // Set by Read operation MarkdownDescription: "Database ID", }, "endpoint": schema.StringAttribute{ Computed: true, // Set by Read operation MarkdownDescription: "Connection endpoint", }, ``` **Optional + Computed:** Search criteria that's also returned ```go "name": schema.StringAttribute{ Optional: true, // Can search by name Computed: true, // Also returned if searched by ID MarkdownDescription: "Database name", }, ```

Read Operation

Question 4: Parsing Configuration

How do you extract search criteria from configuration in the Read operation?

View Answer ```go func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config DatabaseDataSourceModel // 1. Get configuration diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } // 2. Extract simple attributes name := config.Name.ValueString() region := config.Region.ValueString() // 3. Check if attribute is set if config.Tags.IsNull() { // Tags not provided } // 4. Extract map attributes var tags map[string]string if !config.Tags.IsNull() { config.Tags.ElementsAs(ctx, &tags, false) } // 5. Extract list attributes var values []string if !config.Values.IsNull() { config.Values.ElementsAs(ctx, &values, false) } // Use extracted values to query API... } ```

Question 5: Not Found Error

How should you handle a “not found” error in a data source Read operation?

View Answer Return a clear error - data sources expect resources to exist: ```go func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config DatabaseDataSourceModel req.Config.Get(ctx, &config) name := config.Name.ValueString() db, err := d.client.GetDatabaseByName(ctx, name) if err != nil { if client.IsNotFoundError(err) { resp.Diagnostics.AddError( "Database Not Found", fmt.Sprintf( "No database found with name '%s'.\n\n"+ "Possible reasons:\n"+ " • Database doesn't exist\n"+ " • Database is in a different region\n"+ " • Insufficient permissions\n\n"+ "Check the database name and region configuration.", name, ), ) return } // Other errors resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Populate attributes... } ``` **Why error instead of null?** - Resources referenced by data sources must exist - Better to fail fast than allow configuration with missing dependencies - User can use `try()` if conditional existence is needed

Question 6: Setting Computed Attributes

What’s the correct way to set computed attributes from API responses?

View Answer Set ALL computed attributes with proper type conversions: ```go func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config DatabaseDataSourceModel req.Config.Get(ctx, &config) // Query API db, err := d.client.GetDatabase(ctx, config.ID.ValueString()) if err != nil { resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Set ALL computed attributes // Strings config.ID = types.StringValue(db.ID) config.Name = types.StringValue(db.Name) config.Endpoint = types.StringValue(db.Endpoint) // Numbers config.Port = types.Int64Value(int64(db.Port)) config.StorageGB = types.Int64Value(int64(db.StorageGB)) // Booleans config.MultiAZ = types.BoolValue(db.MultiAZ) // Handle nullable strings if db.Description == "" { config.Description = types.StringNull() } else { config.Description = types.StringValue(db.Description) } // Convert maps if len(db.Tags) > 0 { tagsMap := make(map[string]attr.Value) for k, v := range db.Tags { tagsMap[k] = types.StringValue(v) } mapValue, _ := types.MapValue(types.StringType, tagsMap) config.Tags = mapValue } else { config.Tags = types.MapNull(types.StringType) } // Convert lists if len(db.SecurityGroupIDs) > 0 { elements := make([]attr.Value, len(db.SecurityGroupIDs)) for i, id := range db.SecurityGroupIDs { elements[i] = types.StringValue(id) } listValue, _ := types.ListValue(types.StringType, elements) config.SecurityGroupIDs = listValue } else { config.SecurityGroupIDs = types.ListNull(types.StringType) } // Save state resp.State.Set(ctx, config) } ``` **Important:** Don't leave computed attributes unset!

Filtering

Question 7: Filter Block Implementation

How do you implement filter blocks for flexible querying?

View Answer **Schema:** ```go Blocks: map[string]schema.Block{ "filter": schema.ListNestedBlock{ MarkdownDescription: "Filter instances. Multiple filters are ANDed", NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ Required: true, MarkdownDescription: "Filter name", }, "values": schema.ListAttribute{ Required: true, ElementType: types.StringType, MarkdownDescription: "Filter values (OR within filter)", }, }, }, }, }, ``` **Model:** ```go 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"` } ``` **Implementation:** ```go func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config InstancesDataSourceModel req.Config.Get(ctx, &config) // Get all instances instances, err := d.client.ListInstances(ctx) // 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) // Apply filter (OR logic within filter) filtered = applyFilter(filtered, name, values) } // Convert to IDs... } func applyFilter(instances []Instance, filterName string, filterValues []string) []Instance { var result []Instance for _, instance := range instances { // Check if instance matches any value (OR) for _, value := range filterValues { if matchesFilterValue(instance, filterName, value) { result = append(result, instance) break } } } return result } ``` **Usage:** ```hcl data "mycloud_instances" "web" { filter { name = "tag:Role" values = ["web", "frontend"] # OR } filter { name = "tag:Environment" values = ["production"] } # Filters are ANDed together } ```

Question 8: Tag Filtering

Implement tag-based filtering where all specified tags must match.

View Answer ```go type DatabaseDataSourceModel struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` Tags types.Map `tfsdk:"tags"` } 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 match. 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) // Extract filter tags var filterTags map[string]string config.Tags.ElementsAs(ctx, &filterTags, false) // 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 matching []Database for _, db := range allDbs { if tagsMatch(db.Tags, filterTags) { matching = append(matching, db) } } // Handle results if len(matching) == 0 { resp.Diagnostics.AddError( "No Database Found", fmt.Sprintf("No database found with tags: %v", filterTags), ) return } if len(matching) > 1 { resp.Diagnostics.AddError( "Multiple Databases Found", fmt.Sprintf("Found %d databases with tags. Use more specific tags", len(matching)), ) return } db := matching[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:** ```hcl data "mycloud_database" "shared" { tags = { Environment = "production" Shared = "true" Team = "platform" } } ```

List Data Sources

Question 9: Empty Results

How should a list data source handle empty results?

View Answer **Return an empty list (recommended) - not an error:** ```go func (d *DatabasesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config DatabasesDataSourceModel req.Config.Get(ctx, &config) // Query with filters dbs, err := d.client.ListDatabases(ctx, filters) if err != nil { resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Empty list is valid - no error if len(dbs) == 0 { tflog.Info(ctx, "No databases found matching criteria") // Set null lists config.IDs = types.ListNull(types.StringType) config.Databases = types.ListNull(databaseObjectType) resp.State.Set(ctx, config) return } // Convert to lists... } ``` **Why not error?** - Empty result is valid for list queries - Users can handle empty lists with `count` or `for_each` - Errors should indicate actual problems, not absence of data **Usage:** ```hcl data "mycloud_databases" "filtered" { filter { name = "engine" values = ["postgres"] } } # Use count to handle empty lists resource "mycloud_backup" "db_backups" { count = length(data.mycloud_databases.filtered.ids) database_id = data.mycloud_databases.filtered.ids[count.index] } # Check if empty output "has_databases" { value = length(data.mycloud_databases.filtered.ids) > 0 } ```

Question 10: List of Objects

Implement a data source that returns a list of detailed objects, not just IDs.

View Answer ```go type InstancesDataSourceModel struct { IDs types.List `tfsdk:"ids"` Instances types.List `tfsdk:"instances"` } func (d *InstancesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "ids": schema.ListAttribute{ Computed: true, ElementType: types.StringType, }, "instances": schema.ListNestedAttribute{ Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, }, "name": schema.StringAttribute{ Computed: true, }, "instance_type": schema.StringAttribute{ Computed: true, }, "state": schema.StringAttribute{ Computed: true, }, "public_ip": schema.StringAttribute{ Computed: true, }, "tags": schema.MapAttribute{ Computed: true, ElementType: types.StringType, }, }, }, }, }, } } func (d *InstancesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config InstancesDataSourceModel req.Config.Get(ctx, &config) // Query instances instances, err := d.client.ListInstances(ctx) if err != nil { resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Build ID list ids := make([]attr.Value, len(instances)) for i, instance := range instances { ids[i] = types.StringValue(instance.ID) } config.IDs, _ = types.ListValue(types.StringType, ids) // Build detailed object list instanceObjectType := types.ObjectType{ AttrTypes: map[string]attr.Type{ "id": types.StringType, "name": types.StringType, "instance_type": types.StringType, "state": types.StringType, "public_ip": types.StringType, "tags": types.MapType{ElemType: types.StringType}, }, } instanceObjects := make([]attr.Value, len(instances)) for i, instance := range instances { // Convert tags tagsMap := make(map[string]attr.Value) for k, v := range instance.Tags { tagsMap[k] = types.StringValue(v) } tagsValue, _ := types.MapValue(types.StringType, tagsMap) // Create object objValue, _ := types.ObjectValue( instanceObjectType.AttrTypes, map[string]attr.Value{ "id": types.StringValue(instance.ID), "name": types.StringValue(instance.Name), "instance_type": types.StringValue(instance.Type), "state": types.StringValue(instance.State), "public_ip": types.StringValue(instance.PublicIP), "tags": tagsValue, }, ) instanceObjects[i] = objValue } config.Instances, _ = types.ListValue(instanceObjectType, instanceObjects) resp.State.Set(ctx, config) } ``` **Usage:** ```hcl data "mycloud_instances" "web" { filter { name = "tag:Role" values = ["web"] } } # Simple IDs output "instance_ids" { value = data.mycloud_instances.web.ids } # Detailed objects output "instance_details" { value = data.mycloud_instances.web.instances } # Access specific fields output "web_server_ips" { value = [for instance in data.mycloud_instances.web.instances : instance.public_ip] } ```

Real-World Scenarios

Scenario 1: Most Recent Pattern

Implement a “most recent” pattern for selecting the latest resource when multiple match.

View Solution ```go type AMIDataSourceModel struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` NamePattern types.String `tfsdk:"name_pattern"` MostRecent types.Bool `tfsdk:"most_recent"` CreatedAt types.String `tfsdk:"created_at"` } func (d *AMIDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "name_pattern": schema.StringAttribute{ Optional: true, MarkdownDescription: "Name pattern with wildcards", }, "most_recent": schema.BoolAttribute{ Optional: true, MarkdownDescription: "Return most recent if multiple match", }, "id": schema.StringAttribute{ Computed: true, }, "name": schema.StringAttribute{ Computed: true, }, "created_at": schema.StringAttribute{ Computed: true, }, }, } } 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 to regex regexPattern := "^" + strings.ReplaceAll(regexp.QuoteMeta(pattern), `\*`, ".*") + "$" nameRegex, _ := regexp.Compile(regexPattern) // Query AMIs amis, err := d.client.ListAMIs(ctx) if err != nil { resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Filter by pattern 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 } var ami AMI if config.MostRecent.ValueBool() { // Sort by creation time and take newest sort.Slice(matching, func(i, j int) bool { return matching[i].CreatedAt > matching[j].CreatedAt }) ami = matching[0] 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 pattern.\n"+ "Set most_recent = true or refine pattern.", len(matching), ), ) return } ami = matching[0] } // Set attributes config.ID = types.StringValue(ami.ID) config.Name = types.StringValue(ami.Name) config.CreatedAt = types.StringValue(ami.CreatedAt) resp.State.Set(ctx, config) } ``` **Usage:** ```hcl data "mycloud_ami" "latest_ubuntu" { name_pattern = "ubuntu-22.04-*-amd64-server-*" most_recent = true } resource "mycloud_instance" "web" { ami = data.mycloud_ami.latest_ubuntu.id instance_type = "t3.micro" } ```

Scenario 2: Availability Zones

Design a simple data source that returns a list of strings (availability zones).

View Solution ```go type AvailabilityZonesDataSourceModel struct { Names types.List `tfsdk:"names"` State types.String `tfsdk:"state"` } func (d *AvailabilityZonesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Fetches list of availability zones", Attributes: map[string]schema.Attribute{ "state": schema.StringAttribute{ Optional: true, MarkdownDescription: "Filter by state (available, impaired). Defaults to available", }, "names": schema.ListAttribute{ Computed: true, ElementType: types.StringType, MarkdownDescription: "List of availability zone names", }, }, } } func (d *AvailabilityZonesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { var config AvailabilityZonesDataSourceModel req.Config.Get(ctx, &config) state := "available" if !config.State.IsNull() { state = config.State.ValueString() } // Query availability zones zones, err := d.client.ListAvailabilityZones(ctx, state) if err != nil { resp.Diagnostics.AddError("Query Failed", err.Error()) return } // Convert to list of strings names := make([]attr.Value, len(zones)) for i, zone := range zones { names[i] = types.StringValue(zone.Name) } listValue, _ := types.ListValue(types.StringType, names) config.Names = listValue resp.State.Set(ctx, config) } ``` **Usage:** ```hcl data "mycloud_availability_zones" "available" { state = "available" } output "zones" { value = data.mycloud_availability_zones.available.names } # Deploy to each zone resource "mycloud_subnet" "multi_az" { for_each = toset(data.mycloud_availability_zones.available.names) availability_zone = each.value cidr_block = cidrsubnet("10.0.0.0/16", 8, index(data.mycloud_availability_zones.available.names, each.value)) } ```

Key Takeaways

  • ✅ Data sources query existing infrastructure (read-only)
  • ✅ Implement datasource.DataSource interface (Metadata, Schema, Read)
  • ✅ Search criteria are Required or Optional
  • ✅ Query results are Computed
  • ✅ Return clear errors when resources not found
  • ✅ List data sources return empty lists, not errors
  • ✅ Support flexible filtering with filter blocks
  • ✅ Filters combine with AND between blocks, OR within
  • ✅ Provide both simple ID lists and detailed objects
  • ✅ Log query operations for debugging

Next Steps

Congratulations on completing Module 7! Continue to Module 8:

Next: Module 8 - State Management →


Previous: ← Data Sources Lab

Last updated: June 30, 2026

Updated: