From 8651d30aa41613618cc9f7f72543d789fc582f7f Mon Sep 17 00:00:00 2001 From: Matt White <16320656+matt-FFFFFF@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:44:01 +0000 Subject: [PATCH 1/2] wip: working data source --- docs/data-sources/data_plane_data_source.md | 137 ++++++++++++ internal/provider/provider.go | 3 + .../services/azapi_data_plane_data_source.go | 205 ++++++++++++++++++ .../azapi_data_plane_data_source_test.go | 1 + 4 files changed, 346 insertions(+) create mode 100644 docs/data-sources/data_plane_data_source.md create mode 100644 internal/services/azapi_data_plane_data_source.go create mode 100644 internal/services/azapi_data_plane_data_source_test.go diff --git a/docs/data-sources/data_plane_data_source.md b/docs/data-sources/data_plane_data_source.md new file mode 100644 index 000000000..bf253c477 --- /dev/null +++ b/docs/data-sources/data_plane_data_source.md @@ -0,0 +1,137 @@ +--- +page_title: "azapi_data_plane_data_source Data Source - terraform-provider-azapi" +subcategory: "" +description: |- + This data source can read some Azure data plane resources. +--- + +# azapi_data_plane_data_source (Data Source) + +This data source can read some Azure data plane resources. + + +## Schema + +### Required + +- `name` (String) Specifies the name of the Azure data plane resource. +- `parent_id` (String) The ID of the Azure resource to get the data from. +- `type` (String) In a format like `@`. `` is the Azure resource type, for example, `Microsoft.Storage/storageAccounts`. `` is version of the API used to manage this azure resource. + +### Optional + +- `ignore_casing` (Boolean) A dynamic attribute that contains the request body. +- `ignore_missing_property` (Boolean) Whether ignore not returned properties like credentials in `body` to suppress plan-diff. Defaults to `true`. It's recommend to enable this option when some sensitive properties are not returned in response body, instead of setting them in `lifecycle.ignore_changes` because it will make the sensitive fields unable to update. +- `locks` (List of String) A list of ARM resource IDs which are used to avoid create/modify/delete azapi resources at the same time. +- `read_headers` (Map of String) A mapping of headers to be sent with the read request. +- `read_query_parameters` (Map of List of String) A mapping of query parameters to be sent with the read request. +- `response_export_values` (Dynamic) The attribute can accept either a list or a map. + +- **List**: A list of paths that need to be exported from the response body. Setting it to `["*"]` will export the full response body. Here's an example. If it sets to `["properties.loginServer", "properties.policies.quarantinePolicy.status"]`, it will set the following HCL object to the computed property output. + + ```text + { + properties = { + loginServer = "registry1.azurecr.io" + policies = { + quarantinePolicy = { + status = "disabled" + } + } + } + } + ``` + +- **Map**: A map where the key is the name for the result and the value is a JMESPath query string to filter the response. Here's an example. If it sets to `{"login_server": "properties.loginServer", "quarantine_status": "properties.policies.quarantinePolicy.status"}`, it will set the following HCL object to the computed property output. + + ```text + { + "login_server" = "registry1.azurecr.io" + "quarantine_status" = "disabled" + } + ``` + +To learn more about JMESPath, visit [JMESPath](https://jmespath.org/). +- `retry` (Attributes) The retry object supports the following attributes: (see [below for nested schema](#nestedatt--retry)) +- `sensitive_response_export_values` (Dynamic) The attribute can accept either a list or a map. + +- **List**: A list of paths that need to be exported from the response body. Setting it to `["*"]` will export the full response body. Here's an example. If it sets to `["properties.loginServer", "properties.policies.quarantinePolicy.status"]`, it will set the following HCL object to the computed property sensitive_output. + + ```text + { + properties = { + loginServer = "registry1.azurecr.io" + policies = { + quarantinePolicy = { + status = "disabled" + } + } + } + } + ``` + +- **Map**: A map where the key is the name for the result and the value is a JMESPath query string to filter the response. Here's an example. If it sets to `{"login_server": "properties.loginServer", "quarantine_status": "properties.policies.quarantinePolicy.status"}`, it will set the following HCL object to the computed property sensitive_output. + + ```text + { + "login_server" = "registry1.azurecr.io" + "quarantine_status" = "disabled" + } + ``` + +To learn more about JMESPath, visit [JMESPath](https://jmespath.org/). +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of the Azure resource. +- `output` (Dynamic) The output HCL object containing the properties specified in `response_export_values`. Here are some examples to use the values. + + ```terraform + // it will output "registry1.azurecr.io" + output "login_server" { + value = data.azapi_data_plane_data_source.example.output.properties.loginServer + } + + // it will output "disabled" + output "quarantine_policy" { + value = data.azapi_data_plane_data_source.example.output.properties.policies.quarantinePolicy.status + } + ``` +- `sensitive_output` (Dynamic, Sensitive) The output HCL object containing the properties specified in `sensitive_response_export_values`. Here are some examples to use the values. + + ```terraform + // it will output "registry1.azurecr.io" + output "login_server" { + value = data.azapi_data_plane_data_source.example.sensitive_output.properties.loginServer + sensitive = true + } + + // it will output "disabled" + output "quarantine_policy" { + value = data.azapi_data_plane_data_source.example.sensitive_output.properties.policies.quarantinePolicy.status + sensitive = true + } + ``` + + +### Nested Schema for `retry` + +Required: + +- `error_message_regex` (List of String) A list of regular expressions to match against error messages. If any of the regular expressions match, the request will be retried. + +Optional: + +- `interval_seconds` (Number) The base number of seconds to wait between retries. Default is `10`. +- `max_interval_seconds` (Number) The maximum number of seconds to wait between retries. Default is `180`. +- `multiplier` (Number) The multiplier to apply to the interval between retries. Default is `1.5`. +- `randomization_factor` (Number) The randomization factor to apply to the interval between retries. The formula for the randomized interval is: `RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])`. Therefore set to zero `0.0` for no randomization. Default is `0.5`. + + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7b990e4ff..2ffa5b236 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -732,6 +732,9 @@ func (p Provider) DataSources(ctx context.Context) []func() datasource.DataSourc func() datasource.DataSource { return &services.ClientConfigDataSource{} }, + func() datasource.DataSource { + return &services.DataPlaneDataSource{} + }, } } diff --git a/internal/services/azapi_data_plane_data_source.go b/internal/services/azapi_data_plane_data_source.go new file mode 100644 index 000000000..b40cad26e --- /dev/null +++ b/internal/services/azapi_data_plane_data_source.go @@ -0,0 +1,205 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "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-validators/listvalidator" + "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 DataPlaneDataSourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + ParentID types.String `tfsdk:"parent_id"` + Type types.String `tfsdk:"type"` + ResponseExportValues types.Dynamic `tfsdk:"response_export_values"` + SensitiveResponseExportValues types.Dynamic `tfsdk:"sensitive_response_export_values"` + Retry retry.RetryValue `tfsdk:"retry"` + Locks types.List `tfsdk:"locks"` + Output types.Dynamic `tfsdk:"output"` + SensitiveOutput types.Dynamic `tfsdk:"sensitive_output"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Headers types.Map `tfsdk:"headers"` + QueryParameters types.Map `tfsdk:"query_parameters"` +} + +type DataPlaneDataSource struct { + ProviderData *clients.Client +} + +var _ datasource.DataSource = &DataPlaneDataSource{} +var _ datasource.DataSourceWithConfigure = &DataPlaneDataSource{} + +func (r *DataPlaneDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { + tflog.Debug(ctx, "Configuring azapi_data_plane_data_source") + if v, ok := request.ProviderData.(*clients.Client); ok { + r.ProviderData = v + } +} + +func (r *DataPlaneDataSource) Metadata(ctx context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = request.ProviderTypeName + "_data_plane_resource" +} + +func (r *DataPlaneDataSource) Schema(ctx context.Context, request datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + MarkdownDescription: "This data source can read some Azure data plane resources.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: docstrings.ID(), + }, + + "name": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Specifies the name of the Azure data plane resource.", + }, + + "parent_id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + myvalidator.StringIsNotEmpty(), + }, + MarkdownDescription: "The ID of the Azure resource to get the data from.", + }, + + "type": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + myvalidator.StringIsResourceType(), + }, + MarkdownDescription: docstrings.Type(), + }, + + "response_export_values": schema.DynamicAttribute{ + Optional: true, + MarkdownDescription: docstrings.ResponseExportValues(), + }, + + "sensitive_response_export_values": schema.DynamicAttribute{ + Optional: true, + MarkdownDescription: docstrings.SensitiveResponseExportValues(), + }, + + "retry": retry.RetrySchema(ctx), + + "locks": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.ValueStringsAre(myvalidator.StringIsNotEmpty()), + }, + MarkdownDescription: docstrings.Locks(), + }, + + "output": schema.DynamicAttribute{ + Computed: true, + MarkdownDescription: docstrings.Output("data.azapi_data_plane_data_source"), + }, + + "sensitive_output": schema.DynamicAttribute{ + Computed: true, + Sensitive: true, + MarkdownDescription: docstrings.SensitiveOutput("data.azapi_data_plane_data_source"), + }, + + "headers": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "A mapping of headers to be sent with the read request.", + }, + + "query_parameters": schema.MapAttribute{ + ElementType: types.ListType{ + ElemType: types.StringType, + }, + Optional: true, + MarkdownDescription: "A mapping of query parameters to be sent with the read request.", + }, + }, + + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Read: true, + }), + }, + } +} + +func (r *DataPlaneDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + var model *DataPlaneDataSourceModel + 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() + + id, err := parse.NewDataPlaneResourceId(model.Name.ValueString(), model.ParentID.ValueString(), model.Type.ValueString()) + if err != nil { + response.Diagnostics.AddError("Error parsing ID", err.Error()) + return + } + ctx = tflog.SetField(ctx, "resource_id", id.ID()) + + // Ensure that the context deadline has been set before calling ConfigureClientWithCustomRetry(). + client := r.ProviderData.DataPlaneClient.ConfigureClientWithCustomRetry(ctx, model.Retry, false) + + responseBody, err := client.Get(ctx, id, 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 + } + + if _, err := json.Marshal(responseBody); err != nil { + response.Diagnostics.AddError("Invalid body", err.Error()) + return + } + + model.ID = basetypes.NewStringValue(id.ID()) + + output, err := buildOutputFromBody(responseBody, model.ResponseExportValues, nil) + if err != nil { + response.Diagnostics.AddError("Failed to build output", err.Error()) + return + } + model.Output = output + + sensitiveOutput, err := buildOutputFromBody(responseBody, model.SensitiveResponseExportValues, nil) + if err != nil { + response.Diagnostics.AddError("Failed to build sensitive output", err.Error()) + return + } + model.SensitiveOutput = sensitiveOutput + + model.Name = basetypes.NewStringValue(id.Name) + model.ParentID = basetypes.NewStringValue(id.ParentId) + model.Type = basetypes.NewStringValue(fmt.Sprintf("%s@%s", id.AzureResourceType, id.ApiVersion)) + + response.Diagnostics.Append(response.State.Set(ctx, model)...) +} diff --git a/internal/services/azapi_data_plane_data_source_test.go b/internal/services/azapi_data_plane_data_source_test.go new file mode 100644 index 000000000..5e568ea85 --- /dev/null +++ b/internal/services/azapi_data_plane_data_source_test.go @@ -0,0 +1 @@ +package services From 8e37259965b3d3902bca8aa938f93433481e97bd Mon Sep 17 00:00:00 2001 From: Matt White <16320656+matt-FFFFFF@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:48:10 +0000 Subject: [PATCH 2/2] docs: gen docs --- ...ata_plane_data_source.md => data_plane_resource.md} | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) rename docs/data-sources/{data_plane_data_source.md => data_plane_resource.md} (87%) diff --git a/docs/data-sources/data_plane_data_source.md b/docs/data-sources/data_plane_resource.md similarity index 87% rename from docs/data-sources/data_plane_data_source.md rename to docs/data-sources/data_plane_resource.md index bf253c477..5624b3cae 100644 --- a/docs/data-sources/data_plane_data_source.md +++ b/docs/data-sources/data_plane_resource.md @@ -1,11 +1,11 @@ --- -page_title: "azapi_data_plane_data_source Data Source - terraform-provider-azapi" +page_title: "azapi_data_plane_resource Data Source - terraform-provider-azapi" subcategory: "" description: |- This data source can read some Azure data plane resources. --- -# azapi_data_plane_data_source (Data Source) +# azapi_data_plane_resource (Data Source) This data source can read some Azure data plane resources. @@ -20,11 +20,9 @@ This data source can read some Azure data plane resources. ### Optional -- `ignore_casing` (Boolean) A dynamic attribute that contains the request body. -- `ignore_missing_property` (Boolean) Whether ignore not returned properties like credentials in `body` to suppress plan-diff. Defaults to `true`. It's recommend to enable this option when some sensitive properties are not returned in response body, instead of setting them in `lifecycle.ignore_changes` because it will make the sensitive fields unable to update. +- `headers` (Map of String) A mapping of headers to be sent with the read request. - `locks` (List of String) A list of ARM resource IDs which are used to avoid create/modify/delete azapi resources at the same time. -- `read_headers` (Map of String) A mapping of headers to be sent with the read request. -- `read_query_parameters` (Map of List of String) A mapping of query parameters to be sent with the read request. +- `query_parameters` (Map of List of String) A mapping of query parameters to be sent with the read request. - `response_export_values` (Dynamic) The attribute can accept either a list or a map. - **List**: A list of paths that need to be exported from the response body. Setting it to `["*"]` will export the full response body. Here's an example. If it sets to `["properties.loginServer", "properties.policies.quarantinePolicy.status"]`, it will set the following HCL object to the computed property output.