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

provider - support for the recover_soft_deleted_backup_protected_vm feature #24157

Merged
merged 13 commits into from
May 9, 2024
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
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())
magodo marked this conversation as resolved.
Show resolved Hide resolved
}
}

}

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
}

magodo marked this conversation as resolved.
Show resolved Hide resolved
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
Loading