Guide: List Resource#
This guide covers how to add a List Resource for an existing resource, using azurerm_network_profile as an example. For more information on Lists, see Resources - List.
Prerequisites#
Before adding a List Resource, the resource must have Resource Identity implemented. For more information on implementing Resource Identity see Guide: Resource Identity.
Adding List Resource#
Note: There are some minor differences between the implementation of a List Resource for an untyped or typed resource. These differences are highlighted in separated code snippets.
-
In the resource, refactor the Read function to have a separate flatten function containing only the logic to set the attributes into state. This will be used by both the Read function and later in the List Resource.
For untyped resources:
func resourceNetworkProfileFlatten(d *pluginsdk.ResourceData, id *networkprofiles.NetworkProfileId, profile *networkprofiles.NetworkProfile) error { d.Set("name", id.NetworkProfileName) d.Set("resource_group_name", id.ResourceGroupName) if profile != nil { if props := profile.Properties; props != nil { cniConfigs := flattenNetworkProfileContainerNetworkInterface(props.ContainerNetworkInterfaceConfigurations) if err := d.Set("container_network_interface", cniConfigs); err != nil { return fmt.Errorf("setting `container_network_interface`: %+v", err) } cniIDs := flattenNetworkProfileContainerNetworkInterfaceIDs(props.ContainerNetworkInterfaces) if err := d.Set("container_network_interface_ids", cniIDs); err != nil { return fmt.Errorf("setting `container_network_interface_ids`: %+v", err) } } d.Set("location", location.NormalizeNilable(profile.Location)) if err := tags.FlattenAndSet(d, profile.Tags); err != nil { return err } } return pluginsdk.SetResourceIdentityData(d, id) }For typed resources:
func (ExampleResource) flatten(metadata sdk.ResourceMetaData, id *example.ExampleId, model *example.ExampleModel) error { // Instantiate state, set any fields with known values (e.g. ones we can derive from the ID) state := ExampleResourceModel{ Name: id.ExampleResourceName ResourceGroupName: id.ResourceGroupName } if model != nil { state.Location = location.Normalize(model.Location) if props := model.Properties; props != nil { // Set remaining properties into the Resource Model (`state`) } } // Set the Resource Identity Data if err := pluginsdk.SetResourceIdentityData(metadata.ResourceData, id); err != nil { return err } return metadata.Encode(&state) } -
Create a new file for the List Resource (for example,
network_profile_resource_list.go) and scaffold the empty resource:For untyped resources:
type NetworkProfileListResource struct{} var _ sdk.FrameworkListWrappedResource = new(NetworkProfileListResource) func (NetworkProfileListResource) ResourceFunc() *pluginsdk.Resource { return resourceNetworkProfile() } // set this with a const from the resource containing the resource name eg, `azurerm_network_profile` func (r NetworkProfileListResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { response.TypeName = azureNetworkProfileResourceName }For typed resources:
type ExampleListResource struct{} var _ sdk.FrameworkListWrappedResource = new(ExampleListResource) func (ExampleListResource) ResourceFunc() *pluginsdk.Resource { // Use the `sdk.WrappedResource` helper to convert a typed resource into `*pluginsdk.Resource` return sdk.WrappedResource(ExampleResource{}) } // Set the name using the `ResourceType()` function func (ExampleListResource) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { response.TypeName = ExampleResource{}.ResourceType() } -
Define any List Resource specific configuration options. This step can be omitted if using the DefaultListModel (which includes
subscription_idandresource_group_name). However, other resources may have different configuration options that need to be defined here and would look something like this:type NetworkProfileListModel struct { SubscriptionId types.String `tfsdk:"subscription_id"` ResourceGroupName types.String `tfsdk:"resource_group_name"` } func (NetworkProfileListResource) ListResourceConfigSchema(_ context.Context, _ list.ListResourceSchemaRequest, response *list.ListResourceSchemaResponse) { response.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "subscription_id": schema.StringAttribute{ Optional: true, Validators: []validator.String{ typehelpers.WrappedStringValidator{ Func: commonids.ValidateSubscriptionID, }, }, }, "resource_group_name": schema.StringAttribute{ Optional: true, Validators: []validator.String{ typehelpers.WrappedStringValidator{ Func: resourcegroups.ValidateName, }, }, }, }, } } -
Implement the List function.
For untyped resources:
func (NetworkProfileListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream, metadata sdk.ResourceMetadata) { client := metadata.Client.Network.NetworkProfiles // Read the list config data into the model var data sdk.DefaultListModel diags := request.Config.Get(ctx, &data) if diags.HasError() { stream.Results = list.ListResultsStreamDiagnostics(diags) return } // Initialize a list for the results of the API request results := make([]networkprofiles.NetworkProfile, 0) subscriptionID := metadata.SubscriptionId if !data.SubscriptionId.IsNull() { subscriptionID = data.SubscriptionId.ValueString() } // Make the request based on which list parameters have been set in the config switch { case !data.ResourceGroupName.IsNull(): resp, err := client.ListComplete(ctx, commonids.NewResourceGroupID(subscriptionID, data.ResourceGroupName.ValueString())) if err != nil { sdk.SetResponseErrorDiagnostic(stream, fmt.Sprintf("listing `%s`", azureNetworkProfileResourceName), err) return } results = resp.Items default: resp, err := client.ListAllComplete(ctx, commonids.NewSubscriptionID(subscriptionID)) if err != nil { sdk.SetResponseErrorDiagnostic(stream, fmt.Sprintf("listing `%s`", azureNetworkProfileResourceName), err) return } results = resp.Items } // Define the function that will push results into the stream stream.Results = func(push func(list.ListResult) bool) { for _, profile := range results { // Initialize a new result object for each resource in the list result := request.NewListResult(ctx) // Set the display name of the item as the resource name result.DisplayName = pointer.From(profile.Name) // Create a new ResourceData object to hold the state of the resource rd := resourceNetworkProfile().Data(&terraform.InstanceState{}) // Set the ID of the resource for the ResourceData object id, err := networkprofiles.ParseNetworkProfileID(pointer.From(profile.Id)) if err != nil { sdk.SetErrorDiagnosticAndPushListResult(result, push, "parsing Network Profile ID", err) return } rd.SetId(id.ID()) // Use the resource flatten function to set the attributes into the resource state if err := resourceNetworkProfileFlatten(rd, id, &profile); err != nil { sdk.SetErrorDiagnosticAndPushListResult(result, push, fmt.Sprintf("encoding `%s` resource data", azureNetworkProfileResourceName), err) return } // Convert and set the identity and resource state into the result sdk.EncodeListResult(ctx, rd, &result) if result.Diagnostics.HasError() { push(result) return } if !push(result) { return } } } }For typed resources:
func (ExampleListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream, metadata sdk.ResourceMetadata) { client := metadata.Client.Example.ExampleResourceClient var data sdk.DefaultListModel diags := request.Config.Get(ctx, &data) if diags.HasError() { stream.Results = list.ListResultsStreamDiagnostics(diags) return } var results []example.ExampleModel subscriptionID := metadata.SubscriptionId if !data.SubscriptionId.IsNull() { subscriptionID = data.SubscriptionId.ValueString() } r := ExampleResource{} switch { case !data.ResourceGroupName.IsNull(): resp, err := client.ListByResourceGroupComplete(ctx, commonids.NewResourceGroupID(subscriptionID, data.ResourceGroupName.ValueString())) if err != nil { sdk.SetResponseErrorDiagnostic(stream, fmt.Sprintf("listing `%s`", r.ResourceType()), err) return } results = resp.Items default: resp, err := client.ListComplete(ctx, commonids.NewSubscriptionID(subscriptionID)) if err != nil { sdk.SetResponseErrorDiagnostic(stream, fmt.Sprintf("listing `%s`", r.ResourceType()), err) return } results = resp.Items } stream.Results = func(push func(list.ListResult) bool) { for _, exampleResult := range results { result := request.NewListResult(ctx) result.DisplayName = pointer.From(exampleResult.Name) id, err := example.ParseExampleID(pointer.From(exampleResult.Id)) if err != nil { sdk.SetErrorDiagnosticAndPushListResult(result, push, "parsing Example ID", err) return } // Instantiate a new ResourceMetaData object to leverage the resource's `flatten` function // which uses the `(ResourceMetaData).Encode()` function to populate the resource state. rmd := sdk.NewResourceMetaData(metadata.Client, r) rmd.SetID(id) if err := r.flatten(rmd, id, &exampleResult); err != nil { sdk.SetErrorDiagnosticAndPushListResult(result, push, fmt.Sprintf("encoding `%s` resource data", r.ResourceType()), err) return } sdk.EncodeListResult(ctx, rmd.ResourceData, &result) if result.Diagnostics.HasError() { push(result) return } if !push(result) { return } } } -
Register the new List Resource
List Resources are registered within the registration.go within each Service Package - and should look something like this:
```
package network
import "github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
type Registration struct{}
var _ sdk.FrameworkServiceRegistration = Registration{}
// ...
// Resources returns a list of List Resources supported by this Service
func (r Registration) ListResources() []sdk.FrameworkListWrappedResource {
return []sdk.FrameworkListWrappedResource{
NetworkProfileListResource{},
}
}
```
-
Add Acceptance Tests for this List Resource
Create a new acceptance test file for the List Resource (for example,
network_profile_resource_list_test.go) and add tests to cover the List Resource functionality. The test should provision any prerequisite resources and multiple resources of the type of List Resource we want to test.The test should look something like this:
package network_test import ( "context" "fmt" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/querycheck" "github.com/hashicorp/terraform-plugin-testing/tfversion" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/provider/framework" ) func TestAccNetworkProfile_list_basic(t *testing.T) { r := NetworkProfileResource{} listResourceAddress := "azurerm_network_profile.list" data := acceptance.BuildTestData(t, "azurerm_network_profile", "test1") resource.Test(t, resource.TestCase{ TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.SkipBelow(tfversion.Version1_14_0), }, ProtoV5ProviderFactories: framework.ProtoV5ProviderFactoriesInit(context.Background(), "azurerm"), Steps: []resource.TestStep{ { Config: r.basicList(data), // provision multiple resources }, { Query: true, Config: r.basicQuery(), QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLengthAtLeast(listResourceAddress, 3), // expect at least the 3 we created }, }, { Query: true, Config: r.basicQueryByResourceGroupName(data), QueryResultChecks: []querycheck.QueryResultCheck{ querycheck.ExpectLength(listResourceAddress, 3), // expect exactly the 3 we created in that resource group }, }, }, }) } // provision multiple Network Profile resources for testing func (r NetworkProfileResource) basicList(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features {} } // Prerequisite Resources .... resource "azurerm_network_profile" "test" { // Where possible, use the `count` meta argument to provision multiple resources to query count = 3 name = "acctestnetprofile${count.index}-%[1]d" location = azurerm_resource_group.test.location resource_group_name = azurerm_resource_group.test.name container_network_interface { name = "acctesteth-%[1]d" ip_configuration { name = "acctestipconfig-%[1]d" subnet_id = azurerm_subnet.test.id } } } `, data.RandomInteger, data.Locations.Primary) } // define the basic list query for testing func (r NetworkProfileResource) basicQuery() string { return ` list "azurerm_network_profile" "list" { provider = azurerm config {} } ` } // define the list query for testing by resource group name func (r NetworkProfileResource) basicQueryByResourceGroupName(data acceptance.TestData) string { return fmt.Sprintf(` list "azurerm_network_profile" "list" { provider = azurerm config { resource_group_name = "acctestRG-%[1]d" } } `, data.RandomInteger) } -
Add documentation for this List Resource
Documentation should be written manually and added to the
./website/docs/list-resources/folder.It should include an example, arguments reference, and look something like this:
--- subcategory: "Network" layout: "azurerm" page_title: "Azure Resource Manager: azurerm_network_profile" description: |- Lists Network Profile resources. --- # List resource: azurerm_network_profile Lists Network Profile resources. ## Example Usage ### List all Network Profiles in the subscription ```hcl list "azurerm_network_profile" "example" { provider = azurerm config {} } ``` ### List all Network Profiles in a specific resource group ```hcl list "azurerm_network_profile" "example" { provider = azurerm config { resource_group_name = "example-rg" } } ``` ## Argument Reference This list resource supports the following arguments: * `resource_group_name` - (Optional) The name of the resource group to query. * `subscription_id` - (Optional) The Subscription ID to query. Defaults to the value specified in the Provider Configuration.
Known Issues and Considerations#
Cancelled Context#
Some resources need to send additional API requests in the flatten function, these API requests require a valid context (i.e. not cancelled or done). However, due to the way the List resources function, the context provided will be cancelled by the time Terraform calls the iterator (stream.Results).
In this scenario, you must instantiate a new context within the iterator using the deadline from the provided context, this should look like the below:
func (ExampleListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream, metadata sdk.ResourceMetadata) {
...
// retrieve the deadline from the supplied context
deadline, ok := ctx.Deadline()
if !ok {
// This *should* never happen given the List Wrapper instantiates a context with a timeout
sdk.SetResponseErrorDiagnostic(stream, "internal-error", "context had no deadline")
return
}
stream.Result = func(push func(list.ListResult) bool) {
// Instantiate a new context based on the deadline retrieved earlier
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
for _, example := range results {
// Remaining logic to retrieve and set the resource data
}
}
}