Skip to content

Commit

Permalink
provider - support for the recover_soft_deleted_backup_protected_vm
Browse files Browse the repository at this point in the history
… feature (#24157)

* `azurerm_backup_protected_vm` - support recover soft deleted vm

* update test case

* update per comment

* update per comments

* update per comment

* format code

* update test case

* revert site recovery test file

* `recover_soft_deleted_backup_protected_vm` defaults to `false`

* fix test
  • Loading branch information
ziyeqf authored May 9, 2024
1 parent fcccd64 commit d6cf769
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 14 deletions.
3 changes: 3 additions & 0 deletions internal/features/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func Default() UserFeatures {
ResourceGroup: ResourceGroupFeatures{
PreventDeletionIfContainsResources: true,
},
RecoveryServicesVault: RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: true,
},
TemplateDeployment: TemplateDeploymentFeatures{
DeleteNestedItemsDuringDeletion: true,
},
Expand Down
5 changes: 5 additions & 0 deletions internal/features/user_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type UserFeatures struct {
TemplateDeployment TemplateDeploymentFeatures
LogAnalyticsWorkspace LogAnalyticsWorkspaceFeatures
ResourceGroup ResourceGroupFeatures
RecoveryServicesVault RecoveryServicesVault
ManagedDisk ManagedDiskFeatures
Subscription SubscriptionFeatures
PostgresqlFlexibleServer PostgresqlFlexibleServerFeatures
Expand Down Expand Up @@ -84,6 +85,10 @@ type SubscriptionFeatures struct {
PreventCancellationOnDestroy bool
}

type RecoveryServicesVault struct {
RecoverSoftDeletedBackupProtectedVM bool
}

type PostgresqlFlexibleServerFeatures struct {
RestartServerOnConfigurationValueChange bool
}
Expand Down
29 changes: 27 additions & 2 deletions internal/provider/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema {
// NOTE: if there's only one nested field these want to be Required (since there's no point
// specifying the block otherwise) - however for 2+ they should be optional
featuresMap := map[string]*pluginsdk.Schema{
//lintignore:XS003
// lintignore:XS003
"api_management": {
Type: pluginsdk.TypeList,
Optional: true,
Expand Down Expand Up @@ -189,7 +189,7 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema {
},
},

//lintignore:XS003
// lintignore:XS003
"virtual_machine": {
Type: pluginsdk.TypeList,
Optional: true,
Expand Down Expand Up @@ -260,6 +260,21 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema {
},
},

"recovery_services_vaults": {
Type: pluginsdk.TypeList,
Optional: true,
MaxItems: 1,
Elem: &pluginsdk.Resource{
Schema: map[string]*schema.Schema{
"recover_soft_deleted_backup_protected_vm": {
Type: pluginsdk.TypeBool,
Optional: true,
Default: false,
},
},
},
},

"managed_disk": {
Type: pluginsdk.TypeList,
Optional: true,
Expand Down Expand Up @@ -518,6 +533,16 @@ func expandFeatures(input []interface{}) features.UserFeatures {
}
}

if raw, ok := val["recovery_services_vaults"]; ok {
items := raw.([]interface{})
if len(items) > 0 && items[0] != nil {
appConfRaw := items[0].(map[string]interface{})
if v, ok := appConfRaw["recover_soft_deleted_backup_protected_vm"]; ok {
featuresMap.RecoveryServicesVault.RecoverSoftDeletedBackupProtectedVM = v.(bool)
}
}
}

if raw, ok := val["managed_disk"]; ok {
items := raw.([]interface{})
if len(items) > 0 {
Expand Down
84 changes: 84 additions & 0 deletions internal/provider/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ func TestExpandFeatures(t *testing.T) {
ResourceGroup: features.ResourceGroupFeatures{
PreventDeletionIfContainsResources: true,
},
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: true,
},
Subscription: features.SubscriptionFeatures{
PreventCancellationOnDestroy: false,
},
Expand Down Expand Up @@ -143,6 +146,11 @@ func TestExpandFeatures(t *testing.T) {
"prevent_deletion_if_contains_resources": true,
},
},
"recovery_services_vaults": []interface{}{
map[string]interface{}{
"recover_soft_deleted_backup_protected_vm": true,
},
},
"subscription": []interface{}{
map[string]interface{}{
"prevent_cancellation_on_destroy": true,
Expand Down Expand Up @@ -216,6 +224,9 @@ func TestExpandFeatures(t *testing.T) {
ResourceGroup: features.ResourceGroupFeatures{
PreventDeletionIfContainsResources: true,
},
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: true,
},
Subscription: features.SubscriptionFeatures{
PreventCancellationOnDestroy: true,
},
Expand Down Expand Up @@ -304,6 +315,11 @@ func TestExpandFeatures(t *testing.T) {
"prevent_deletion_if_contains_resources": false,
},
},
"recovery_services_vaults": []interface{}{
map[string]interface{}{
"recover_soft_deleted_backup_protected_vm": false,
},
},
"subscription": []interface{}{
map[string]interface{}{
"prevent_cancellation_on_destroy": false,
Expand Down Expand Up @@ -377,6 +393,9 @@ func TestExpandFeatures(t *testing.T) {
ResourceGroup: features.ResourceGroupFeatures{
PreventDeletionIfContainsResources: false,
},
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: false,
},
Subscription: features.SubscriptionFeatures{
PreventCancellationOnDestroy: false,
},
Expand Down Expand Up @@ -1224,6 +1243,71 @@ func TestExpandFeaturesResourceGroup(t *testing.T) {
}
}

func TestExpandFeaturesRecoveryServicesVault(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_services_vaults": []interface{}{},
},
},
Expected: features.UserFeatures{
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: true,
},
},
},
{
Name: "Recover Soft Deleted Protected VM Enabled",
Input: []interface{}{
map[string]interface{}{
"recovery_services_vaults": []interface{}{
map[string]interface{}{
"recover_soft_deleted_backup_protected_vm": true,
},
},
},
},
Expected: features.UserFeatures{
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: true,
},
},
},
{
Name: "Recover Soft Deleted Protected VM Disabled",
Input: []interface{}{
map[string]interface{}{
"recovery_services_vaults": []interface{}{
map[string]interface{}{
"recover_soft_deleted_backup_protected_vm": false,
},
},
},
},
Expected: features.UserFeatures{
RecoveryServicesVault: features.RecoveryServicesVault{
RecoverSoftDeletedBackupProtectedVM: false,
},
},
},
}

for _, testCase := range testData {
t.Logf("[DEBUG] Test Case: %q", testCase.Name)
result := expandFeatures(testCase.Input)
if !reflect.DeepEqual(result.RecoveryServicesVault, testCase.Expected.RecoveryServicesVault) {
t.Fatalf("Expected %+v but got %+v", testCase.Expected.RecoveryServicesVault, result.RecoveryServicesVault)
}
}
}

func TestExpandFeaturesManagedDisk(t *testing.T) {
testData := []struct {
Name string
Expand Down
75 changes: 71 additions & 4 deletions internal/services/recoveryservices/backup_protected_vm_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func resourceRecoveryServicesBackupProtectedVMCreateUpdate(d *pluginsdk.Resource
log.Printf("[DEBUG] Creating/updating Azure Backup Protected VM %s (resource group %q)", protectedItemName, resourceGroup)

id := protecteditems.NewProtectedItemID(subscriptionId, resourceGroup, vaultName, "Azure", containerName, protectedItemName)

if d.IsNewResource() {
existing, err := client.Get(ctx, id, protecteditems.GetOperationOptions{})
if err != nil {
Expand All @@ -105,8 +106,29 @@ func resourceRecoveryServicesBackupProtectedVMCreateUpdate(d *pluginsdk.Resource
}

if !response.WasNotFound(existing.HttpResponse) {
return tf.ImportAsExistsError("azurerm_backup_protected_vm", id.ID())
isSoftDeleted := false
if existing.Model != nil && existing.Model.Properties != nil {
if prop, ok := existing.Model.Properties.(protecteditems.AzureIaaSComputeVMProtectedItem); ok {
isSoftDeleted = pointer.From(prop.IsScheduledForDeferredDelete)
}
}

if isSoftDeleted {
if meta.(*clients.Client).Features.RecoveryServicesVault.RecoverSoftDeletedBackupProtectedVM {
err = resourceRecoveryServicesVaultBackupProtectedVMRecoverSoftDeleted(ctx, client, opClient, id)
if err != nil {
return fmt.Errorf("recovering soft deleted %s: %+v", id, err)
}
} else {
return fmt.Errorf(optedOutOfRecoveringSoftDeletedBackupProtectedVMFmt(parsedVmId.ID(), vaultName))
}
}

if !isSoftDeleted {
return tf.ImportAsExistsError("azurerm_backup_protected_vm", id.ID())
}
}

}

item := protecteditems.ProtectedItemResource{
Expand Down Expand Up @@ -194,12 +216,14 @@ func resourceRecoveryServicesBackupProtectedVMRead(d *pluginsdk.ResourceData, me
return fmt.Errorf("making Read request on %s: %+v", id, err)
}

d.Set("resource_group_name", id.ResourceGroupName)
d.Set("recovery_vault_name", id.VaultName)

if model := resp.Model; model != nil {
if properties := model.Properties; properties != nil {
if vm, ok := properties.(protecteditems.AzureIaaSComputeVMProtectedItem); ok {
if vm.IsScheduledForDeferredDelete != nil && *vm.IsScheduledForDeferredDelete {
d.SetId("")
return nil
}

d.Set("source_vm_id", vm.SourceResourceId)
d.Set("protection_state", pointer.From(vm.ProtectionState))

Expand Down Expand Up @@ -228,6 +252,9 @@ func resourceRecoveryServicesBackupProtectedVMRead(d *pluginsdk.ResourceData, me
}
}

d.Set("resource_group_name", id.ResourceGroupName)
d.Set("recovery_vault_name", id.VaultName)

return nil
}

Expand Down Expand Up @@ -461,6 +488,46 @@ func expandDiskLunList(input []interface{}) []interface{} {
return result
}

func resourceRecoveryServicesVaultBackupProtectedVMRecoverSoftDeleted(ctx context.Context, client *protecteditems.ProtectedItemsClient, opClient *backup.ProtectedItemOperationResultsClient, id protecteditems.ProtectedItemId) (err error) {
resp, err := client.CreateOrUpdate(ctx, id, protecteditems.ProtectedItemResource{
Properties: &protecteditems.AzureIaaSComputeVMProtectedItem{
IsRehydrate: pointer.To(true),
},
},
)
if err != nil {
return fmt.Errorf("issuing request for %s: %+v", id, err)
}

operationId, err := parseBackupOperationId(resp.HttpResponse)
if err != nil {
return err
}

if err = resourceRecoveryServicesBackupProtectedVMWaitForStateCreateUpdate(ctx, opClient, id, operationId); err != nil {
return err
}

return nil
}

func optedOutOfRecoveringSoftDeletedBackupProtectedVMFmt(vmId string, vaultName string) string {
return fmt.Sprintf(`
An existing soft-deleted Backup Protected VM exists with the source VM %q in the recovery services
vault %q, however automatically recovering this Backup Protected VM has been disabled via the
"features" block.
Terraform can automatically recover the soft-deleted Backup Protected VM 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/guides/features-block
Alternatively you can manually recover this (e.g. using the Azure CLI) and then import
this into Terraform via "terraform import".
`, vmId, vaultName)
}

func resourceRecoveryServicesBackupProtectedVMSchema() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"resource_group_name": commonschema.ResourceGroupName(),
Expand Down
Loading

0 comments on commit d6cf769

Please sign in to comment.