internal/services/azapi_resource_data_source.go (261 lines of code) (raw):
package services
import (
"context"
"fmt"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/terraform-provider-azapi/internal/azure/identity"
"github.com/Azure/terraform-provider-azapi/internal/azure/location"
"github.com/Azure/terraform-provider-azapi/internal/azure/tags"
"github.com/Azure/terraform-provider-azapi/internal/clients"
"github.com/Azure/terraform-provider-azapi/internal/docstrings"
"github.com/Azure/terraform-provider-azapi/internal/retry"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/utils"
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
type AzapiResourceDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
ResourceID types.String `tfsdk:"resource_id"`
Type types.String `tfsdk:"type"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Location types.String `tfsdk:"location"`
Identity types.List `tfsdk:"identity"`
Output types.Dynamic `tfsdk:"output"`
Tags types.Map `tfsdk:"tags"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
Retry retry.RetryValue `tfsdk:"retry"`
Headers types.Map `tfsdk:"headers"`
QueryParameters types.Map `tfsdk:"query_parameters"`
}
type AzapiResourceDataSource struct {
ProviderData *clients.Client
}
var _ datasource.DataSource = &AzapiResourceDataSource{}
var _ datasource.DataSourceWithConfigure = &AzapiResourceDataSource{}
var _ datasource.DataSourceWithValidateConfig = &AzapiResourceDataSource{}
func (r *AzapiResourceDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) {
if v, ok := request.ProviderData.(*clients.Client); ok {
r.ProviderData = v
}
}
func (r *AzapiResourceDataSource) Metadata(ctx context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) {
response.TypeName = request.ProviderTypeName + "_resource"
}
func (r *AzapiResourceDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) {
response.Schema = schema.Schema{
MarkdownDescription: "This resource can access any existing Azure resource manager resource.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
MarkdownDescription: docstrings.ID(),
},
"type": schema.StringAttribute{
Required: true,
Validators: []validator.String{
myvalidator.StringIsResourceType(),
},
MarkdownDescription: docstrings.Type(),
},
"name": schema.StringAttribute{
Optional: true,
Computed: true,
Validators: []validator.String{
myvalidator.StringIsNotEmpty(),
},
MarkdownDescription: "Specifies the name of the Azure resource. Exactly one of the arguments `name` or `resource_id` must be set. It could be omitted if the `type` is `Microsoft.Resources/subscriptions`.",
},
"parent_id": schema.StringAttribute{
Optional: true,
Computed: true,
Validators: []validator.String{
myvalidator.StringIsResourceID(),
},
MarkdownDescription: docstrings.ParentID(),
},
"resource_id": schema.StringAttribute{
Optional: true,
Computed: true,
Validators: []validator.String{
myvalidator.StringIsResourceID(),
},
MarkdownDescription: "The ID of the Azure resource to retrieve. Exactly one of the arguments `name` or `resource_id` must be set. It could be omitted if the `type` is `Microsoft.Resources/subscriptions`.",
},
"location": schema.StringAttribute{
Computed: true,
MarkdownDescription: docstrings.Location(),
},
"identity": schema.ListNestedAttribute{
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Computed: true,
MarkdownDescription: docstrings.IdentityType(),
},
"principal_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: docstrings.IdentityPrincipalID(),
},
"tenant_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: docstrings.IdentityTenantID(),
},
"identity_ids": schema.ListAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: docstrings.IdentityIds(),
},
},
},
},
"response_export_values": schema.DynamicAttribute{
Optional: true,
MarkdownDescription: docstrings.ResponseExportValues(),
},
"output": schema.DynamicAttribute{
Computed: true,
MarkdownDescription: docstrings.Output("data.azapi_resource"),
},
"tags": schema.MapAttribute{
Computed: true,
ElementType: types.StringType,
MarkdownDescription: "A mapping of tags which are assigned to the Azure resource.",
},
"retry": retry.RetrySchema(ctx),
"headers": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
MarkdownDescription: "A map of headers to include in the request",
},
"query_parameters": schema.MapAttribute{
ElementType: types.ListType{
ElemType: types.StringType,
},
Optional: true,
MarkdownDescription: "A map of query parameters to include in the request",
},
},
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Read: true,
}),
},
}
}
func (r *AzapiResourceDataSource) ValidateConfig(ctx context.Context, request datasource.ValidateConfigRequest, response *datasource.ValidateConfigResponse) {
var config *AzapiResourceDataSourceModel
if response.Diagnostics.Append(request.Config.Get(ctx, &config)...); response.Diagnostics.HasError() {
return
}
if config == nil {
return
}
if config.Name.IsNull() && !config.ParentID.IsNull() {
response.Diagnostics.AddError("Invalid configuration", `The argument "name" is required when the argument "parent_id" is set`)
}
if !config.Name.IsNull() && config.ParentID.IsNull() {
resourceType := config.Type.ValueString()
azureResourceType, _, _ := utils.GetAzureResourceTypeApiVersion(resourceType)
// If the resource type is not a resource group, then the parent_id is required
if !strings.EqualFold(azureResourceType, arm.ResourceGroupResourceType.String()) {
response.Diagnostics.AddError("Invalid configuration", `The argument "parent_id" is required when the argument "name" is set`)
}
}
if config.Name.IsNull() && config.ResourceID.IsNull() {
resourceType := config.Type.ValueString()
azureResourceType, _, _ := utils.GetAzureResourceTypeApiVersion(resourceType)
// If the resource type is not a subscription, then at least one of the name or resource_id must be set
if !strings.EqualFold(azureResourceType, arm.SubscriptionResourceType.String()) {
response.Diagnostics.AddError("Invalid configuration", `One of the arguments "name" or "resource_id" must be set`)
}
}
if !config.Name.IsNull() && !config.ResourceID.IsNull() {
response.Diagnostics.AddError("Invalid configuration", `Exactly one of the arguments "name" or "resource_id" can be set`)
}
}
func (r *AzapiResourceDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) {
var model AzapiResourceDataSourceModel
if response.Diagnostics.Append(request.Config.Get(ctx, &model)...); response.Diagnostics.HasError() {
return
}
readTimeout, diags := model.Timeouts.Read(ctx, 5*time.Minute)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()
var id parse.ResourceId
resourceType := model.Type.ValueString()
azureResourceType, _, _ := utils.GetAzureResourceTypeApiVersion(resourceType)
if name := model.Name.ValueString(); len(name) != 0 {
parentId := model.ParentID.ValueString()
if parentId == "" && strings.EqualFold(azureResourceType, arm.ResourceGroupResourceType.String()) {
parentId = fmt.Sprintf("/subscriptions/%s", r.ProviderData.Account.GetSubscriptionId())
}
buildId, err := parse.NewResourceID(name, parentId, resourceType)
if err != nil {
response.Diagnostics.AddError("Invalid configuration", err.Error())
return
}
id = buildId
} else {
resourceId := model.ResourceID.ValueString()
if resourceId == "" && strings.EqualFold(azureResourceType, arm.SubscriptionResourceType.String()) {
resourceId = fmt.Sprintf("/subscriptions/%s", r.ProviderData.Account.GetSubscriptionId())
}
buildId, err := parse.ResourceIDWithResourceType(resourceId, resourceType)
if err != nil {
response.Diagnostics.AddError("Invalid configuration", err.Error())
return
}
id = buildId
}
ctx = tflog.SetField(ctx, "resource_id", id.ID())
// Ensure the context deadline has been set before calling ConfigureClientWithCustomRetry().
client := r.ProviderData.ResourceClient.ConfigureClientWithCustomRetry(ctx, model.Retry, false)
responseBody, err := client.Get(ctx, id.AzureResourceId, id.ApiVersion, clients.NewRequestOptions(AsMapOfString(model.Headers), AsMapOfLists(model.QueryParameters)))
if err != nil {
if utils.ResponseErrorWasNotFound(err) {
response.Diagnostics.AddError("Resource not found", fmt.Errorf("resource %q not found", id).Error())
return
}
response.Diagnostics.AddError("Failed to retrieve resource", fmt.Errorf("retrieving resource %q: %+v", id, err).Error())
return
}
model.ID = basetypes.NewStringValue(id.ID())
model.Name = basetypes.NewStringValue(id.Name)
model.ParentID = basetypes.NewStringValue(id.ParentId)
model.ResourceID = basetypes.NewStringValue(id.AzureResourceId)
if bodyMap, ok := responseBody.(map[string]interface{}); ok {
model.Tags = tags.FlattenTags(bodyMap["tags"])
model.Location = basetypes.NewStringNull()
if v := bodyMap["location"]; v != nil {
model.Location = basetypes.NewStringValue(location.Normalize(v.(string)))
}
model.Identity = basetypes.NewListNull(identity.Model{}.ModelType())
if v := identity.FlattenIdentity(bodyMap["identity"]); v != nil {
model.Identity = identity.ToList(*v)
}
}
var defaultOutput interface{}
if !r.ProviderData.Features.DisableDefaultOutput {
defaultOutput = id.ResourceDef.GetReadOnly(responseBody)
defaultOutput = utils.RemoveFields(defaultOutput, volatileFieldList())
}
output, err := buildOutputFromBody(responseBody, model.ResponseExportValues, defaultOutput)
if err != nil {
response.Diagnostics.AddError("Failed to build output", err.Error())
return
}
model.Output = output
response.Diagnostics.Append(response.State.Set(ctx, &model)...)
}