diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 49c64d56659e..bb3564872208 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -60,5 +60,9 @@ func Default() UserFeatures { PostgresqlFlexibleServer: PostgresqlFlexibleServerFeatures{ RestartServerOnConfigurationValueChange: true, }, + RecoveryService: RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: false, + PurgeProtectedItemsFromVaultOnDestroy: false, + }, } } diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index 00d0199cd775..bd2916c009f3 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -17,6 +17,7 @@ type UserFeatures struct { ManagedDisk ManagedDiskFeatures Subscription SubscriptionFeatures PostgresqlFlexibleServer PostgresqlFlexibleServerFeatures + RecoveryService RecoveryServiceFeatures } type CognitiveAccountFeatures struct { @@ -85,3 +86,8 @@ type SubscriptionFeatures struct { type PostgresqlFlexibleServerFeatures struct { RestartServerOnConfigurationValueChange bool } + +type RecoveryServiceFeatures struct { + VMBackupStopProtectionAndRetainDataOnDestroy bool + PurgeProtectedItemsFromVaultOnDestroy bool +} diff --git a/internal/provider/features.go b/internal/provider/features.go index e16c7d6c1ad9..c1df7fe4b1fe 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -304,6 +304,26 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { }, }, }, + + "recovery_service": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "vm_backup_stop_protection_and_retain_data_on_destroy": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + "purge_protected_items_from_vault_on_destroy": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + }, + }, + }, } // this is a temporary hack to enable us to gradually add provider blocks to test configurations @@ -514,5 +534,18 @@ func expandFeatures(input []interface{}) features.UserFeatures { } } + if raw, ok := val["recovery_service"]; ok { + items := raw.([]interface{}) + if len(items) > 0 { + recoveryServicesRaw := items[0].(map[string]interface{}) + if v, ok := recoveryServicesRaw["vm_backup_stop_protection_and_retain_data_on_destroy"]; ok { + featuresMap.RecoveryService.VMBackupStopProtectionAndRetainDataOnDestroy = v.(bool) + } + if v, ok := recoveryServicesRaw["purge_protected_items_from_vault_on_destroy"]; ok { + featuresMap.RecoveryService.PurgeProtectedItemsFromVaultOnDestroy = v.(bool) + } + } + } + return featuresMap } diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index 544f514477a3..6988f87b8850 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -75,6 +75,10 @@ func TestExpandFeatures(t *testing.T) { PostgresqlFlexibleServer: features.PostgresqlFlexibleServerFeatures{ RestartServerOnConfigurationValueChange: true, }, + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: false, + PurgeProtectedItemsFromVaultOnDestroy: false, + }, }, }, { @@ -161,6 +165,12 @@ func TestExpandFeatures(t *testing.T) { "scale_to_zero_before_deletion": true, }, }, + "recovery_service": []interface{}{ + map[string]interface{}{ + "vm_backup_stop_protection_and_retain_data_on_destroy": true, + "purge_protected_items_from_vault_on_destroy": true, + }, + }, }, }, Expected: features.UserFeatures{ @@ -218,6 +228,10 @@ func TestExpandFeatures(t *testing.T) { PostgresqlFlexibleServer: features.PostgresqlFlexibleServerFeatures{ RestartServerOnConfigurationValueChange: true, }, + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: true, + PurgeProtectedItemsFromVaultOnDestroy: true, + }, }, }, { @@ -304,6 +318,12 @@ func TestExpandFeatures(t *testing.T) { "scale_to_zero_before_deletion": false, }, }, + "recovery_service": []interface{}{ + map[string]interface{}{ + "vm_backup_stop_protection_and_retain_data_on_destroy": false, + "purge_protected_items_from_vault_on_destroy": false, + }, + }, }, }, Expected: features.UserFeatures{ @@ -361,6 +381,10 @@ func TestExpandFeatures(t *testing.T) { PostgresqlFlexibleServer: features.PostgresqlFlexibleServerFeatures{ RestartServerOnConfigurationValueChange: false, }, + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: false, + PurgeProtectedItemsFromVaultOnDestroy: false, + }, }, }, } @@ -1358,3 +1382,73 @@ func TestExpandFeaturesPosgresqlFlexibleServer(t *testing.T) { } } } + +func TestExpandFeaturesRecoveryService(t *testing.T) { + testData := []struct { + Name string + Input []interface{} + EnvVars map[string]interface{} + Expected features.UserFeatures + }{ + { + Name: "Empty Block", + Input: []interface{}{ + map[string]interface{}{ + "recovery_service": []interface{}{}, + }, + }, + Expected: features.UserFeatures{ + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: false, + PurgeProtectedItemsFromVaultOnDestroy: false, + }, + }, + }, + { + Name: "Recovery Service Features Enabled", + Input: []interface{}{ + map[string]interface{}{ + "recovery_service": []interface{}{ + map[string]interface{}{ + "vm_backup_stop_protection_and_retain_data_on_destroy": true, + "purge_protected_items_from_vault_on_destroy": true, + }, + }, + }, + }, + Expected: features.UserFeatures{ + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: true, + PurgeProtectedItemsFromVaultOnDestroy: true, + }, + }, + }, + { + Name: "Recovery Service Features Disabled", + Input: []interface{}{ + map[string]interface{}{ + "recovery_service": []interface{}{ + map[string]interface{}{ + "vm_backup_stop_protection_and_retain_data_on_destroy": false, + "purge_protected_items_from_vault_on_destroy": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + RecoveryService: features.RecoveryServiceFeatures{ + VMBackupStopProtectionAndRetainDataOnDestroy: false, + PurgeProtectedItemsFromVaultOnDestroy: false, + }, + }, + }, + } + + for _, testCase := range testData { + t.Logf("[DEBUG] Test Case: %q", testCase.Name) + result := expandFeatures(testCase.Input) + if !reflect.DeepEqual(result.Subscription, testCase.Expected.Subscription) { + t.Fatalf("Expected %+v but got %+v", result.Subscription, testCase.Expected.Subscription) + } + } +} diff --git a/internal/services/recoveryservices/backup_protected_vm_resource.go b/internal/services/recoveryservices/backup_protected_vm_resource.go index 912bfd31d41d..244ee3b85094 100644 --- a/internal/services/recoveryservices/backup_protected_vm_resource.go +++ b/internal/services/recoveryservices/backup_protected_vm_resource.go @@ -234,6 +234,7 @@ func resourceRecoveryServicesBackupProtectedVMRead(d *pluginsdk.ResourceData, me func resourceRecoveryServicesBackupProtectedVMDelete(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).RecoveryServices.ProtectedItemsClient opResultClient := meta.(*clients.Client).RecoveryServices.BackupOperationResultsClient + opClient := meta.(*clients.Client).RecoveryServices.ProtectedItemOperationResultsClient ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() @@ -242,6 +243,49 @@ func resourceRecoveryServicesBackupProtectedVMDelete(d *pluginsdk.ResourceData, return err } + if meta.(*clients.Client).Features.RecoveryService.VMBackupStopProtectionAndRetainDataOnDestroy { + log.Printf("[DEBUG] Retaining Data and Stopping Protection for %s", id) + + existing, err := client.Get(ctx, *id, protecteditems.GetOperationOptions{}) + if err != nil { + if response.WasNotFound(existing.HttpResponse) { + d.SetId("") + return nil + } + + return fmt.Errorf("making Read request on %s: %+v", id, err) + } + + if model := existing.Model; model != nil { + if properties := model.Properties; properties != nil { + if vm, ok := properties.(protecteditems.AzureIaaSComputeVMProtectedItem); ok { + updateInput := protecteditems.ProtectedItemResource{ + Properties: &protecteditems.AzureIaaSComputeVMProtectedItem{ + ProtectionState: pointer.To(protecteditems.ProtectionStateProtectionStopped), + SourceResourceId: vm.SourceResourceId, + }, + } + + resp, err := client.CreateOrUpdate(ctx, *id, updateInput) + if err != nil { + return fmt.Errorf("stopping protection and retaining data for %s: %+v", id, err) + } + + operationId, err := parseBackupOperationId(resp.HttpResponse) + if err != nil { + return fmt.Errorf("issuing creating/updating request for %s: %+v", id, err) + } + + if err = resourceRecoveryServicesBackupProtectedVMWaitForStateCreateUpdate(ctx, opClient, *id, operationId); err != nil { + return err + } + + return nil + } + } + } + } + log.Printf("[DEBUG] Deleting %s", id) resp, err := client.Delete(ctx, *id) diff --git a/internal/services/recoveryservices/backup_protected_vm_resource_test.go b/internal/services/recoveryservices/backup_protected_vm_resource_test.go index d1d8c2e9e3fd..658b66fbffd8 100644 --- a/internal/services/recoveryservices/backup_protected_vm_resource_test.go +++ b/internal/services/recoveryservices/backup_protected_vm_resource_test.go @@ -232,6 +232,25 @@ func TestAccBackupProtectedVm_protectionStopped(t *testing.T) { }) } +func TestAccBackupProtectedVm_protectionStoppedOnDestroy(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_backup_protected_vm", "test") + r := BackupProtectedVmResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("resource_group_name").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.protectionStoppedOnDestroy(data), + }, + }) +} + func (t BackupProtectedVmResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := protecteditems.ParseProtectedItemID(state.ID) if err != nil { @@ -248,10 +267,6 @@ func (t BackupProtectedVmResource) Exists(ctx context.Context, clients *clients. func (BackupProtectedVmResource) base(data acceptance.TestData) string { return fmt.Sprintf(` -provider "azurerm" { - features {} -} - resource "azurerm_resource_group" "test" { name = "acctestRG-backup-%d" location = "%s" @@ -395,10 +410,6 @@ resource "azurerm_backup_policy_vm" "test" { func (BackupProtectedVmResource) baseWithoutVM(data acceptance.TestData) string { return fmt.Sprintf(` -provider "azurerm" { - features {} -} - resource "azurerm_resource_group" "test" { name = "acctestRG-backup-%d" location = "%s" @@ -475,6 +486,10 @@ resource "azurerm_backup_policy_vm" "test" { func (r BackupProtectedVmResource) basic(data acceptance.TestData) string { return fmt.Sprintf(` +provider "azurerm" { + features {} +} + %s resource "azurerm_backup_protected_vm" "test" { @@ -490,6 +505,10 @@ resource "azurerm_backup_protected_vm" "test" { func (r BackupProtectedVmResource) updateDiskExclusion(data acceptance.TestData) string { return fmt.Sprintf(` +provider "azurerm" { + features {} +} + %s resource "azurerm_backup_protected_vm" "test" { @@ -506,10 +525,6 @@ resource "azurerm_backup_protected_vm" "test" { // For update backup policy id test func (BackupProtectedVmResource) basePolicyTest(data acceptance.TestData) string { return fmt.Sprintf(` -provider "azurerm" { - features {} -} - resource "azurerm_resource_group" "test" { name = "acctestRG-backup-%d-1" location = "%s" @@ -594,6 +609,10 @@ resource "azurerm_backup_policy_vm" "test_change_backup" { // For update backup policy id test func (r BackupProtectedVmResource) linkFirstBackupPolicy(data acceptance.TestData) string { return fmt.Sprintf(` +provider "azurerm" { + features {} +} + %s resource "azurerm_backup_protected_vm" "test" { @@ -608,6 +627,10 @@ resource "azurerm_backup_protected_vm" "test" { // For update backup policy id test func (r BackupProtectedVmResource) linkSecondBackupPolicy(data acceptance.TestData) string { return fmt.Sprintf(` +provider "azurerm" { + features {} +} + %s resource "azurerm_backup_protected_vm" "test" { @@ -634,6 +657,10 @@ resource "azurerm_backup_protected_vm" "import" { func (r BackupProtectedVmResource) additionalVault(data acceptance.TestData) string { return fmt.Sprintf(` +provider "azurerm" { + features {} +} + %s resource "azurerm_resource_group" "test2" { @@ -734,3 +761,18 @@ resource "azurerm_backup_protected_vm" "test" { } `, r.base(data)) } + +func (r BackupProtectedVmResource) protectionStoppedOnDestroy(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + recovery_service { + vm_backup_stop_protection_and_retain_data_on_destroy = true + purge_protected_items_from_vault_on_destroy = true + } + } +} + +%s +`, r.base(data)) +} diff --git a/internal/services/recoveryservices/recovery_services_vault_resource.go b/internal/services/recoveryservices/recovery_services_vault_resource.go index 34d1bc62dd4a..3adbbcfa4573 100644 --- a/internal/services/recoveryservices/recovery_services_vault_resource.go +++ b/internal/services/recoveryservices/recovery_services_vault_resource.go @@ -18,7 +18,9 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservices/2024-01-01/vaults" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicesbackup/2023-02-01/backupprotecteditems" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicesbackup/2023-02-01/backupresourcevaultconfigs" + "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicesbackup/2023-02-01/protecteditems" "github.com/hashicorp/go-azure-sdk/resource-manager/recoveryservicessiterecovery/2022-10-01/replicationvaultsetting" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" @@ -667,6 +669,9 @@ func resourceRecoveryServicesVaultRead(d *pluginsdk.ResourceData, meta interface func resourceRecoveryServicesVaultDelete(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).RecoveryServices.VaultsClient + protectedItemsClient := meta.(*clients.Client).RecoveryServices.ProtectedItemsGroupClient + protectedItemClient := meta.(*clients.Client).RecoveryServices.ProtectedItemsClient + opResultClient := meta.(*clients.Client).RecoveryServices.BackupOperationResultsClient ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() @@ -675,6 +680,44 @@ func resourceRecoveryServicesVaultDelete(d *pluginsdk.ResourceData, meta interfa return err } + if meta.(*clients.Client).Features.RecoveryService.PurgeProtectedItemsFromVaultOnDestroy { + log.Printf("[DEBUG] Purging Protected Items from %s", id.String()) + + vaultId := backupprotecteditems.NewVaultID(id.SubscriptionId, id.ResourceGroupName, id.VaultName) + + protectedItems, err := protectedItemsClient.ListComplete(ctx, vaultId, backupprotecteditems.ListOperationOptions{}) + if err != nil { + return fmt.Errorf("listing protected items in %s: %+v", id, err) + } + + for _, item := range protectedItems.Items { + if item.Id != nil { + protectedItemId, err := protecteditems.ParseProtectedItemID(pointer.From(item.Id)) + if err != nil { + return err + } + + log.Printf("[DEBUG] Purging %s from %s", protectedItemId, id) + + resp, err := protectedItemClient.Delete(ctx, *protectedItemId) + if err != nil { + if !response.WasNotFound(resp.HttpResponse) { + return fmt.Errorf("issuing delete request for %s: %+v", protectedItemId, err) + } + } + + operationId, err := parseBackupOperationId(resp.HttpResponse) + if err != nil { + return fmt.Errorf("purging %s from %s: %+v", protectedItemId, id, err) + } + + if err = resourceRecoveryServicesBackupProtectedVMWaitForDeletion(ctx, protectedItemClient, opResultClient, *protectedItemId, operationId); err != nil { + return fmt.Errorf("waiting for %s to be purged from %s: %+v", protectedItemId, id, err) + } + } + } + } + log.Printf("[DEBUG] Deleting Recovery Service %s", id.String()) _, err = client.Delete(ctx, *id) diff --git a/website/docs/guides/features-block.html.markdown b/website/docs/guides/features-block.html.markdown index 831f2e6692b8..3d00321ea6cb 100644 --- a/website/docs/guides/features-block.html.markdown +++ b/website/docs/guides/features-block.html.markdown @@ -62,6 +62,11 @@ provider "azurerm" { restart_server_on_configuration_value_change = true } + recovery_service { + retain_data_and_stop_protection_on_back_vm_destroy = true + purge_protected_items_from_vault_on_destroy = true + } + resource_group { prevent_deletion_if_contains_resources = true } @@ -107,6 +112,8 @@ The `features` block supports the following: * `managed_disk` - (Optional) A `managed_disk` block as defined below. +* `recovery_service` - (Optional) A `recovery_service` block as defined below. + * `resource_group` - (Optional) A `resource_group` block as defined below. * `template_deployment` - (Optional) A `template_deployment` block as defined below. @@ -193,6 +200,14 @@ The `postgresql_flexible_server` block supports the following: --- +The `recovery_service` block supports the following: + +* `vm_backup_stop_protection_and_retain_data_on_destroy` - (Optional) Should we retain the data and stop protection instead of destroying the backup protected vm? Defaults to `false`. + +* `purge_protected_items_from_vault_on_destroy` - (Optional) Should we purge all protected items when destroying the vault. Defaults to `false`. + +--- + The `resource_group` block supports the following: * `prevent_deletion_if_contains_resources` - (Optional) Should the `azurerm_resource_group` resource check that there are no Resources within the Resource Group during deletion? This means that all Resources within the Resource Group must be deleted prior to deleting the Resource Group. Defaults to `true`.