Skip to content

Commit

Permalink
Merge pull request #15871 from hashicorp/feature/apim-soft-delete
Browse files Browse the repository at this point in the history
azurerm_api_management: support recovery of soft deleted instances and enable purge on deletion by default
  • Loading branch information
manicminer committed Mar 22, 2022
2 parents 1a8f832 + 95a3471 commit 3a73e7a
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 23 deletions.
3 changes: 2 additions & 1 deletion internal/features/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions internal/features/user_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ type ResourceGroupFeatures struct {

type ApiManagementFeatures struct {
PurgeSoftDeleteOnDestroy bool
RecoverSoftDeleted bool
}
10 changes: 10 additions & 0 deletions internal/provider/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
18 changes: 14 additions & 4 deletions internal/provider/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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{}{
Expand Down Expand Up @@ -120,6 +122,7 @@ func TestExpandFeatures(t *testing.T) {
Expected: features.UserFeatures{
ApiManagement: features.ApiManagementFeatures{
PurgeSoftDeleteOnDestroy: true,
RecoverSoftDeleted: true,
},
CognitiveAccount: features.CognitiveAccountFeatures{
PurgeSoftDeleteOnDestroy: true,
Expand Down Expand Up @@ -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{}{
Expand Down Expand Up @@ -220,6 +224,7 @@ func TestExpandFeatures(t *testing.T) {
Expected: features.UserFeatures{
ApiManagement: features.ApiManagementFeatures{
PurgeSoftDeleteOnDestroy: false,
RecoverSoftDeleted: false,
},
CognitiveAccount: features.CognitiveAccountFeatures{
PurgeSoftDeleteOnDestroy: false,
Expand Down Expand Up @@ -282,41 +287,46 @@ 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,
},
},
},
},
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,
},
},
},
},
Expected: features.UserFeatures{
ApiManagement: features.ApiManagementFeatures{
PurgeSoftDeleteOnDestroy: false,
RecoverSoftDeleted: false,
},
},
},
Expand Down
70 changes: 69 additions & 1 deletion internal/services/apimanagement/api_management_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand Down Expand Up @@ -1934,6 +1984,7 @@ func flattenApiManagementTenantAccessSettings(input apimanagement.AccessInformat

if input.SecondaryKey != nil {
result["secondary_key"] = *input.SecondaryKey

}

return []interface{}{result}
Expand Down Expand Up @@ -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)
}
57 changes: 42 additions & 15 deletions internal/services/apimanagement/api_management_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package apimanagement_test
import (
"context"
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
Expand Down Expand Up @@ -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),
),
Expand All @@ -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 "[^"]+"`),
},
})
}

Expand Down Expand Up @@ -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" {
Expand All @@ -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
}
}
}
Expand Down
7 changes: 5 additions & 2 deletions website/docs/guides/features-block.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`

---

Expand Down

0 comments on commit 3a73e7a

Please sign in to comment.