Skip to content

Commit

Permalink
azapi_data_plane_resource: support `replace_triggers_external_value…
Browse files Browse the repository at this point in the history
…s` field (#595)
  • Loading branch information
ms-henglu authored Aug 29, 2024
1 parent b8e200e commit 55188d1
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 85 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ FEATURES:
- **New Provider Function**: extension_resource_id

ENHANCEMENTS:
- `azapi_resource` resource: Support `replace_triggers_external_values` field which is used to trigger a replacement of the resource.
- `azapi_resource` and `azapi_data_plane_resource` resource: Support `replace_triggers_external_values` field which is used to trigger a replacement of the resource.
- `azapi` resources and data sources: Support `retry` field, which is used to specify the retry configuration.
- `azapi` resources and data sources: Support `headers` and `query_parameters` fields, which are used to specify the headers and query parameters.
- `azapi` resources and data sources: The `response_export_values` field supports JMESPath expressions.
Expand Down
23 changes: 23 additions & 0 deletions docs/resources/data_plane_resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,29 @@ resource "azapi_data_plane_resource" "dataset" {
- `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.
- `replace_triggers_external_values` (Dynamic) Will trigger a replace of the resource when the value changes and is not `null`. This can be used by practitioners to force a replace of the resource when certain values change, e.g. changing the SKU of a virtual machine based on the value of variables or locals. The value is a `dynamic`, so practitioners can compose the input however they wish. For a "break glass" set the value to `null` to prevent the plan modifier taking effect.
If you have `null` values that you do want to be tracked as affecting the resource replacement, include these inside an object.
Advanced use cases are possible and resource replacement can be triggered by values external to the resource, for example when a dependent resource changes.

e.g. to replace a resource when either the SKU or os_type attributes change:

```hcl
resource "azapi_data_plane_resource" "example" {
name = var.name
type = "Microsoft.AppConfiguration/configurationStores/[email protected]"
body = {
properties = {
sku = var.sku
zones = var.zones
}
}
replace_triggers_external_values = [
var.sku,
var.zones,
]
}
```
- `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.
Expand Down
72 changes: 52 additions & 20 deletions internal/services/azapi_data_plane_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/Azure/terraform-provider-azapi/internal/services/dynamic"
"github.com/Azure/terraform-provider-azapi/internal/services/migration"
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier"
"github.com/Azure/terraform-provider-azapi/internal/services/myplanmodifier/planmodifierdynamic"
"github.com/Azure/terraform-provider-azapi/internal/services/myvalidator"
"github.com/Azure/terraform-provider-azapi/internal/services/parse"
"github.com/Azure/terraform-provider-azapi/internal/tf"
Expand All @@ -34,26 +35,27 @@ import (
)

type DataPlaneResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
}

type DataPlaneResource struct {
Expand Down Expand Up @@ -151,6 +153,36 @@ func (r *DataPlaneResource) Schema(ctx context.Context, request resource.SchemaR

"retry": retry.SingleNestedAttribute(ctx),

"replace_triggers_external_values": schema.DynamicAttribute{
Optional: true,
MarkdownDescription: "Will trigger a replace of the resource when the value changes and is not `null`. This can be used by practitioners to force a replace of the resource when certain values change, e.g. changing the SKU of a virtual machine based on the value of variables or locals. " +
"The value is a `dynamic`, so practitioners can compose the input however they wish. For a \"break glass\" set the value to `null` to prevent the plan modifier taking effect. \n" +
"If you have `null` values that you do want to be tracked as affecting the resource replacement, include these inside an object. \n" +
"Advanced use cases are possible and resource replacement can be triggered by values external to the resource, for example when a dependent resource changes.\n\n" +
"e.g. to replace a resource when either the SKU or os_type attributes change:\n" +
"\n" +
"```hcl\n" +
"resource \"azapi_data_plane_resource\" \"example\" {\n" +
" name = var.name\n" +
" type = \"Microsoft.AppConfiguration/configurationStores/[email protected]\"\n" +
" body = {\n" +
" properties = {\n" +
" sku = var.sku\n" +
" zones = var.zones\n" +
" }\n" +
" }\n" +
"\n" +
" replace_triggers_external_values = [\n" +
" var.sku,\n" +
" var.zones,\n" +
" ]\n" +
"}\n" +
"```\n",
PlanModifiers: []planmodifier.Dynamic{
planmodifierdynamic.RequiresReplaceIfNotNull(),
},
},

"locks": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Expand Down
61 changes: 61 additions & 0 deletions internal/services/azapi_data_plane_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ func TestAccDataPlaneResource_queryParameters(t *testing.T) {
})
}

func TestAccDataPlaneResource_replaceTriggeredByExternalValues(t *testing.T) {
data := acceptance.BuildTestData(t, "azapi_data_plane_resource", "test")
r := DataPlaneResource{}

data.ResourceTest(t, r, []resource.TestStep{
{
Config: r.replaceTriggeredByExternalValues(data),
ExternalProviders: externalProvidersAzurerm(),
Check: resource.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
})
}

func (DataPlaneResource) Exists(ctx context.Context, client *clients.Client, state *terraform.InstanceState) (*bool, error) {
resourceType := state.Attributes["type"]
id, err := parse.DataPlaneResourceIDWithResourceType(state.ID, resourceType)
Expand Down Expand Up @@ -589,3 +604,49 @@ resource "azapi_data_plane_resource" "test" {
}
`, data.LocationPrimary, data.RandomString)
}

func (r DataPlaneResource) replaceTriggeredByExternalValues(data acceptance.TestData) string {
return fmt.Sprintf(`
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "acctest%[2]s"
location = "%[1]s"
}
resource "azurerm_app_configuration" "appconf" {
name = "acctest%[2]s"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
sku = "standard"
}
data "azurerm_client_config" "current" {}
resource "azurerm_role_assignment" "test" {
scope = azurerm_app_configuration.appconf.id
role_definition_name = "App Configuration Data Owner"
principal_id = data.azurerm_client_config.current.object_id
}
resource "azapi_data_plane_resource" "test" {
type = "Microsoft.AppConfiguration/configurationStores/[email protected]"
parent_id = replace(azurerm_app_configuration.appconf.endpoint, "https://", "")
name = "mykey"
body = {
content_type = ""
value = "myvalue"
}
replace_triggers_external_values = [
"value1"
]
depends_on = [
azurerm_role_assignment.test,
]
}
`, data.LocationPrimary, data.RandomString)
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,27 @@ func AzapiDataPlaneResourceMigrationV0ToV2(ctx context.Context) resource.StateUp
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
type newModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
}

var oldState OldModel
Expand Down Expand Up @@ -132,18 +133,19 @@ func AzapiDataPlaneResourceMigrationV0ToV2(ctx context.Context) resource.StateUp
}

newState := newModel{
ID: oldState.ID,
Name: oldState.Name,
ParentID: oldState.ParentID,
Type: oldState.Type,
Body: bodyVal,
Locks: oldState.Locks,
IgnoreCasing: oldState.IgnoreCasing,
IgnoreMissingProperty: oldState.IgnoreMissingProperty,
ResponseExportValues: responseExportValues,
Retry: retry.NewRetryValueNull(),
Output: outputVal,
Timeouts: oldState.Timeouts,
ID: oldState.ID,
Name: oldState.Name,
ParentID: oldState.ParentID,
Type: oldState.Type,
Body: bodyVal,
Locks: oldState.Locks,
IgnoreCasing: oldState.IgnoreCasing,
IgnoreMissingProperty: oldState.IgnoreMissingProperty,
ResponseExportValues: responseExportValues,
ReplaceTriggersExternalValues: types.DynamicNull(),
Retry: retry.NewRetryValueNull(),
Output: outputVal,
Timeouts: oldState.Timeouts,
}

response.Diagnostics.Append(response.State.Set(ctx, newState)...)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,26 +85,27 @@ func AzapiDataPlaneResourceMigrationV1ToV2(ctx context.Context) resource.StateUp
Timeouts timeouts.Value `tfsdk:"timeouts"`
}
type newModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
ParentID types.String `tfsdk:"parent_id"`
Type types.String `tfsdk:"type"`
Body types.Dynamic `tfsdk:"body"`
IgnoreCasing types.Bool `tfsdk:"ignore_casing"`
IgnoreMissingProperty types.Bool `tfsdk:"ignore_missing_property"`
ResponseExportValues types.Dynamic `tfsdk:"response_export_values"`
ReplaceTriggersExternalValues types.Dynamic `tfsdk:"replace_triggers_external_values"`
Retry retry.RetryValue `tfsdk:"retry"`
Locks types.List `tfsdk:"locks"`
Output types.Dynamic `tfsdk:"output"`
Timeouts timeouts.Value `tfsdk:"timeouts"`
CreateHeaders map[string]string `tfsdk:"create_headers"`
CreateQueryParameters map[string][]string `tfsdk:"create_query_parameters"`
UpdateHeaders map[string]string `tfsdk:"update_headers"`
UpdateQueryParameters map[string][]string `tfsdk:"update_query_parameters"`
DeleteHeaders map[string]string `tfsdk:"delete_headers"`
DeleteQueryParameters map[string][]string `tfsdk:"delete_query_parameters"`
ReadHeaders map[string]string `tfsdk:"read_headers"`
ReadQueryParameters map[string][]string `tfsdk:"read_query_parameters"`
}

var oldState OldModel
Expand All @@ -130,18 +131,19 @@ func AzapiDataPlaneResourceMigrationV1ToV2(ctx context.Context) resource.StateUp
}

newState := newModel{
ID: oldState.ID,
Name: oldState.Name,
ParentID: oldState.ParentID,
Type: oldState.Type,
Body: bodyVal,
Locks: oldState.Locks,
IgnoreCasing: oldState.IgnoreCasing,
IgnoreMissingProperty: oldState.IgnoreMissingProperty,
ResponseExportValues: responseExportValues,
Retry: retry.NewRetryValueNull(),
Output: outputVal,
Timeouts: oldState.Timeouts,
ID: oldState.ID,
Name: oldState.Name,
ParentID: oldState.ParentID,
Type: oldState.Type,
Body: bodyVal,
Locks: oldState.Locks,
IgnoreCasing: oldState.IgnoreCasing,
IgnoreMissingProperty: oldState.IgnoreMissingProperty,
ResponseExportValues: responseExportValues,
ReplaceTriggersExternalValues: types.DynamicNull(),
Retry: retry.NewRetryValueNull(),
Output: outputVal,
Timeouts: oldState.Timeouts,
}

response.Diagnostics.Append(response.State.Set(ctx, newState)...)
Expand Down

0 comments on commit 55188d1

Please sign in to comment.