diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 4f061f1ca2b1..5c39b2d58fb5 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -4,7 +4,8 @@ func Default() UserFeatures { return UserFeatures{ // NOTE: ensure all nested objects are fully populated ApiManagement: ApiManagementFeatures{ - PurgeSoftDeleteOnDestroy: false, + PurgeSoftDeleteOnDestroy: true, + RecoverSoftDeleted: true, }, CognitiveAccount: CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index 75f792721129..04a73f4f9baf 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -52,4 +52,5 @@ type ResourceGroupFeatures struct { type ApiManagementFeatures struct { PurgeSoftDeleteOnDestroy bool + RecoverSoftDeleted bool } diff --git a/internal/provider/features.go b/internal/provider/features.go index 65e34dc53692..77f45d0ffa81 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -20,6 +20,13 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { "purge_soft_delete_on_destroy": { Type: pluginsdk.TypeBool, Optional: true, + Default: true, + }, + + "recover_soft_deleted": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, }, }, }, @@ -251,6 +258,9 @@ func expandFeatures(input []interface{}) features.UserFeatures { if v, ok := apimRaw["purge_soft_delete_on_destroy"]; ok { featuresMap.ApiManagement.PurgeSoftDeleteOnDestroy = v.(bool) } + if v, ok := apimRaw["recover_soft_deleted"]; ok { + featuresMap.ApiManagement.RecoverSoftDeleted = v.(bool) + } } } diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index e4e75dd87b48..48e978e8d1e0 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -19,7 +19,8 @@ func TestExpandFeatures(t *testing.T) { Input: []interface{}{}, Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ - PurgeSoftDeleteOnDestroy: false, + PurgeSoftDeleteOnDestroy: true, + RecoverSoftDeleted: true, }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, @@ -62,6 +63,7 @@ func TestExpandFeatures(t *testing.T) { "api_management": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": true, + "recover_soft_deleted": true, }, }, "cognitive_account": []interface{}{ @@ -120,6 +122,7 @@ func TestExpandFeatures(t *testing.T) { Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: true, + RecoverSoftDeleted: true, }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, @@ -162,6 +165,7 @@ func TestExpandFeatures(t *testing.T) { "api_management": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": false, + "recover_soft_deleted": false, }, }, "cognitive_account": []interface{}{ @@ -220,6 +224,7 @@ func TestExpandFeatures(t *testing.T) { Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: false, + RecoverSoftDeleted: false, }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: false, @@ -282,17 +287,19 @@ func TestExpandFeaturesApiManagement(t *testing.T) { }, Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ - PurgeSoftDeleteOnDestroy: false, + PurgeSoftDeleteOnDestroy: true, + RecoverSoftDeleted: true, }, }, }, { - Name: "Purge Soft Delete On Destroy Api Management Enabled", + Name: "Purge Soft Delete On Destroy and Recover Soft Deleted Api Management Enabled", Input: []interface{}{ map[string]interface{}{ "api_management": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": true, + "recover_soft_deleted": true, }, }, }, @@ -300,16 +307,18 @@ func TestExpandFeaturesApiManagement(t *testing.T) { Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: true, + RecoverSoftDeleted: true, }, }, }, { - Name: "Purge Soft Delete On Destroy Api Management Disabled", + Name: "Purge Soft Delete On Destroy and Recover Soft Deleted Api Management Disabled", Input: []interface{}{ map[string]interface{}{ "api_management": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": false, + "recover_soft_deleted": false, }, }, }, @@ -317,6 +326,7 @@ func TestExpandFeaturesApiManagement(t *testing.T) { Expected: features.UserFeatures{ ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: false, + RecoverSoftDeleted: false, }, }, }, diff --git a/internal/services/apimanagement/api_management_resource.go b/internal/services/apimanagement/api_management_resource.go index cafb773bc1d1..7b85b3216c0c 100644 --- a/internal/services/apimanagement/api_management_resource.go +++ b/internal/services/apimanagement/api_management_resource.go @@ -644,6 +644,7 @@ func resourceApiManagementSchema() map[string]*pluginsdk.Schema { func resourceApiManagementServiceCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).ApiManagement.ServiceClient apiClient := meta.(*clients.Client).ApiManagement.ApiClient + deletedServicesClient := meta.(*clients.Client).ApiManagement.DeletedServicesClient productsClient := meta.(*clients.Client).ApiManagement.ProductsClient subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) @@ -686,6 +687,55 @@ func resourceApiManagementServiceCreateUpdate(d *pluginsdk.ResourceData, meta in publicNetworkAccess = apimanagement.PublicNetworkAccessDisabled } + // before creating check to see if the resource exists in the soft delete state + softDeleted, err := deletedServicesClient.GetByName(ctx, id.ServiceName, location) + if err != nil { + // If Terraform lacks permission to read at the Subscription we'll get 403, not 404 + if !utils.ResponseWasNotFound(softDeleted.Response) && !utils.ResponseWasForbidden(softDeleted.Response) { + return fmt.Errorf("checking for the presence of an existing Soft-Deleted API Management %q (Location %q): %+v", id.ServiceName, location, err) + } + } + + // if so, does the user want us to recover it? + if !utils.ResponseWasNotFound(softDeleted.Response) && !utils.ResponseWasForbidden(softDeleted.Response) { + if !meta.(*clients.Client).Features.ApiManagement.RecoverSoftDeleted { + // this exists but the users opted out, so they must import this it out-of-band + return fmt.Errorf(optedOutOfRecoveringSoftDeletedApiManagementErrorFmt(id.ServiceName, location)) + } + + // First recover the deleted API Management, since all other properties are ignored during a restore operation + // (don't set the ID just yet to avoid tainting on failure) + params := apimanagement.ServiceResource{ + Location: utils.String(location), + ServiceProperties: &apimanagement.ServiceProperties{ + Restore: utils.Bool(true), + }, + } + + if _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.ServiceName, params); err != nil { + return fmt.Errorf("recovering %s: %+v", id, err) + } + + // Wait for the ProvisioningState to become "Succeeded" before attempting to update + log.Printf("[DEBUG] Waiting for %s to become ready", id) + stateConf := &pluginsdk.StateChangeConf{ + Pending: []string{"Deleted", "Activating", "Updating", "Unknown"}, + Target: []string{"Succeeded", "Ready"}, + Refresh: apiManagementRefreshFunc(ctx, client, id.ServiceName, id.ResourceGroup), + MinTimeout: 1 * time.Minute, + ContinuousTargetOccurence: 2, + } + if d.IsNewResource() { + stateConf.Timeout = d.Timeout(pluginsdk.TimeoutCreate) + } else { + stateConf.Timeout = d.Timeout(pluginsdk.TimeoutUpdate) + } + + if _, err = stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for %s to become ready: %+v", id, err) + } + } + properties := apimanagement.ServiceResource{ Location: utils.String(location), ServiceProperties: &apimanagement.ServiceProperties{ @@ -1070,6 +1120,7 @@ func resourceApiManagementServiceRead(d *pluginsdk.ResourceData, meta interface{ func resourceApiManagementServiceDelete(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).ApiManagement.ServiceClient + deletedServicesClient := meta.(*clients.Client).ApiManagement.DeletedServicesClient ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() @@ -1093,7 +1144,6 @@ func resourceApiManagementServiceDelete(d *pluginsdk.ResourceData, meta interfac // Purge the soft deleted Api Management permanently if the feature flag is enabled if meta.(*clients.Client).Features.ApiManagement.PurgeSoftDeleteOnDestroy { log.Printf("[DEBUG] %s marked for purge - executing purge", *id) - deletedServicesClient := meta.(*clients.Client).ApiManagement.DeletedServicesClient _, err := deletedServicesClient.GetByName(ctx, id.ServiceName, azure.NormalizeLocation(d.Get("location").(string))) if err != nil { return err @@ -1934,6 +1984,7 @@ func flattenApiManagementTenantAccessSettings(input apimanagement.AccessInformat if input.SecondaryKey != nil { result["secondary_key"] = *input.SecondaryKey + } return []interface{}{result} @@ -1979,3 +2030,20 @@ func flattenAPIManagementCertificates(d *pluginsdk.ResourceData, inputs *[]apima } return outputs } + +func optedOutOfRecoveringSoftDeletedApiManagementErrorFmt(name, location string) string { + message := ` +An existing soft-deleted API Management exists with the Name %q in the location %q, however +automatically recovering this API Management has been disabled via the "features" block. + +Terraform can automatically recover the soft-deleted API Management when this behaviour is +enabled within the "features" block (located within the "provider" block) - more +information can be found here: + +https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#features + +Alternatively you can manually recover this (e.g. using the Azure CLI) and then import +this into Terraform via "terraform import", or pick a different name/location. +` + return fmt.Sprintf(message, name, location) +} diff --git a/internal/services/apimanagement/api_management_resource_test.go b/internal/services/apimanagement/api_management_resource_test.go index 0713cc129c30..d8e31694d6fe 100644 --- a/internal/services/apimanagement/api_management_resource_test.go +++ b/internal/services/apimanagement/api_management_resource_test.go @@ -3,6 +3,7 @@ package apimanagement_test import ( "context" "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" @@ -475,23 +476,43 @@ func TestAccApiManagement_minApiVersion(t *testing.T) { }) } -func TestAccApiManagement_purgeSoftDelete(t *testing.T) { +func TestAccApiManagement_removeSamples(t *testing.T) { + if !features.ThreePointOh() { + t.Skip("Skipping since 3.0 mode is disabled") + } + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") r := ApiManagementResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.consumptionPurgeSoftDeleteRecovery(data), + Config: r.removeSamples(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + r.testCheckHasNoProductsOrApis(data.ResourceName), ), }, data.ImportStep(), + }) +} + +func TestAccApiManagement_softDeleteRecovery(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_api_management", "test") + r := ApiManagementResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.consumptionPurgeSoftDelete(data), + Config: r.consumption(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), }, + data.ImportStep(), { - Config: r.consumptionPurgeSoftDeleteRecovery(data), + Config: r.softDelete(data), + }, + { + Config: r.consumption(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -500,23 +521,25 @@ func TestAccApiManagement_purgeSoftDelete(t *testing.T) { }) } -func TestAccApiManagement_removeSamples(t *testing.T) { - if !features.ThreePointOh() { - t.Skip("Skipping since 3.0 mode is disabled") - } - +func TestAccApiManagement_softDeleteRecoveryDisabled(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_api_management", "test") r := ApiManagementResource{} data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.removeSamples(data), + Config: r.consumption(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - r.testCheckHasNoProductsOrApis(data.ResourceName), ), }, data.ImportStep(), + { + Config: r.softDelete(data), + }, + { + Config: r.consumptionRecoveryDisabled(data), + ExpectError: regexp.MustCompile(`An existing soft-deleted API Management exists with the Name "[^"]+" in the location "[^"]+"`), + }, }) } @@ -2129,10 +2152,14 @@ resource "azurerm_api_management" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } -func (r ApiManagementResource) consumptionPurgeSoftDeleteRecovery(data acceptance.TestData) string { +func (ApiManagementResource) consumptionRecoveryDisabled(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { - features {} + features { + api_management { + recover_soft_deleted = false + } + } } resource "azurerm_resource_group" "test" { @@ -2151,12 +2178,12 @@ resource "azurerm_api_management" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } -func (ApiManagementResource) consumptionPurgeSoftDelete(data acceptance.TestData) string { +func (ApiManagementResource) softDelete(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features { api_management { - purge_soft_delete_on_destroy = true + purge_soft_delete_on_destroy = false } } } diff --git a/website/docs/guides/features-block.html.markdown b/website/docs/guides/features-block.html.markdown index 298c96016eec..c7ce22c3ca01 100644 --- a/website/docs/guides/features-block.html.markdown +++ b/website/docs/guides/features-block.html.markdown @@ -28,7 +28,8 @@ Each of the blocks defined below can be optionally specified to configure the be provider "azurerm" { features { api_management { - purge_soft_delete_on_destroy = true + purge_soft_delete_on_destroy = true + recover_soft_deleted_api_managements = true } cognitive_account { @@ -91,7 +92,9 @@ The `features` block supports the following: The `api_management` block supports the following: -* `purge_soft_delete_on_destroy` - (Optional) Should the `azurerm_api_management` resources be permanently deleted (e.g. purged) when destroyed? Defaults to `false`. +* `purge_soft_delete_on_destroy` - (Optional) Should the `azurerm_api_management` resources be permanently deleted (e.g. purged) when destroyed? Defaults to `true`. + +* `recover_soft_deleted_api_managements` - (Optional) Should the `azurerm_api_management` resources recover a Soft-Deleted API Management service? Defaults to `true` ---