Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_api_management: support recovery of soft deleted instances and enable purge on deletion by default #15871

Merged
merged 1 commit into from
Mar 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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