diff --git a/azurerm/internal/features/defaults.go b/azurerm/internal/features/defaults.go index 7a54dadb2f7f..e20667114f2d 100644 --- a/azurerm/internal/features/defaults.go +++ b/azurerm/internal/features/defaults.go @@ -17,10 +17,12 @@ func Default() UserFeatures { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: VirtualMachineScaleSetFeatures{ + ForceDelete: false, RollInstancesWhenRequired: true, }, } diff --git a/azurerm/internal/features/user_flags.go b/azurerm/internal/features/user_flags.go index 72157d354d99..d66c53e60891 100644 --- a/azurerm/internal/features/user_flags.go +++ b/azurerm/internal/features/user_flags.go @@ -10,11 +10,13 @@ type UserFeatures struct { } type VirtualMachineFeatures struct { - DeleteOSDiskOnDeletion bool - GracefulShutdown bool + DeleteOSDiskOnDeletion bool + GracefulShutdown bool + SkipShutdownAndForceDelete bool } type VirtualMachineScaleSetFeatures struct { + ForceDelete bool RollInstancesWhenRequired bool } diff --git a/azurerm/internal/provider/features.go b/azurerm/internal/provider/features.go index f16adef103a0..9acaae4f842c 100644 --- a/azurerm/internal/provider/features.go +++ b/azurerm/internal/provider/features.go @@ -1,6 +1,7 @@ package provider import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/pluginsdk" ) @@ -84,6 +85,10 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { Type: pluginsdk.TypeBool, Optional: true, }, + "skip_shutdown_and_force_delete": { + Type: schema.TypeBool, + Optional: true, + }, }, }, }, @@ -94,6 +99,10 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { MaxItems: 1, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ + "force_delete": { + Type: pluginsdk.TypeBool, + Optional: true, + }, "roll_instances_when_required": { Type: pluginsdk.TypeBool, Required: true, @@ -189,6 +198,9 @@ func expandFeatures(input []interface{}) features.UserFeatures { if v, ok := virtualMachinesRaw["graceful_shutdown"]; ok { features.VirtualMachine.GracefulShutdown = v.(bool) } + if v, ok := virtualMachinesRaw["skip_shutdown_and_force_delete"]; ok { + features.VirtualMachine.SkipShutdownAndForceDelete = v.(bool) + } } } @@ -199,6 +211,9 @@ func expandFeatures(input []interface{}) features.UserFeatures { if v, ok := scaleSetRaw["roll_instances_when_required"]; ok { features.VirtualMachineScaleSet.RollInstancesWhenRequired = v.(bool) } + if v, ok := scaleSetRaw["force_delete"]; ok { + features.VirtualMachineScaleSet.ForceDelete = v.(bool) + } } } diff --git a/azurerm/internal/provider/features_test.go b/azurerm/internal/provider/features_test.go index 86a35d915089..f429f83ad19e 100644 --- a/azurerm/internal/provider/features_test.go +++ b/azurerm/internal/provider/features_test.go @@ -22,6 +22,9 @@ func TestExpandFeatures(t *testing.T) { PurgeSoftDeleteOnDestroy: true, RecoverSoftDeletedKeyVaults: true, }, + LogAnalyticsWorkspace: features.LogAnalyticsWorkspaceFeatures{ + PermanentlyDeleteOnDestroy: false, + }, Network: features.NetworkFeatures{ RelaxedLocking: false, }, @@ -29,14 +32,14 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ + ForceDelete: false, RollInstancesWhenRequired: true, }, - LogAnalyticsWorkspace: features.LogAnalyticsWorkspaceFeatures{ - PermanentlyDeleteOnDestroy: false, - }, }, }, { @@ -66,13 +69,15 @@ func TestExpandFeatures(t *testing.T) { }, "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": true, - "graceful_shutdown": true, + "delete_os_disk_on_deletion": true, + "graceful_shutdown": true, + "skip_shutdown_and_force_delete": true, }, }, "virtual_machine_scale_set": []interface{}{ map[string]interface{}{ "roll_instances_when_required": true, + "force_delete": true, }, }, }, @@ -92,11 +97,13 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: true, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: true, + SkipShutdownAndForceDelete: true, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ RollInstancesWhenRequired: true, + ForceDelete: true, }, }, }, @@ -104,10 +111,15 @@ func TestExpandFeatures(t *testing.T) { Name: "Complete Disabled", Input: []interface{}{ map[string]interface{}{ - "virtual_machine": []interface{}{ + "key_vault": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": false, - "graceful_shutdown": false, + "purge_soft_delete_on_destroy": false, + "recover_soft_deleted_key_vaults": false, + }, + }, + "log_analytics_workspace": []interface{}{ + map[string]interface{}{ + "permanently_delete_on_destroy": false, }, }, "network_locking": []interface{}{ @@ -120,20 +132,17 @@ func TestExpandFeatures(t *testing.T) { "delete_nested_items_during_deletion": false, }, }, - "virtual_machine_scale_set": []interface{}{ - map[string]interface{}{ - "roll_instances_when_required": false, - }, - }, - "key_vault": []interface{}{ + "virtual_machine": []interface{}{ map[string]interface{}{ - "purge_soft_delete_on_destroy": false, - "recover_soft_deleted_key_vaults": false, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": false, }, }, - "log_analytics_workspace": []interface{}{ + "virtual_machine_scale_set": []interface{}{ map[string]interface{}{ - "permanently_delete_on_destroy": false, + "force_delete": false, + "roll_instances_when_required": false, }, }, }, @@ -153,10 +162,12 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: false, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ + ForceDelete: false, RollInstancesWhenRequired: false, }, }, @@ -388,46 +399,94 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, { - Name: "Delete OS Disk and Graceful Shutdown Enabled", + Name: "Delete OS Disk Enabled", Input: []interface{}{ map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ "delete_os_disk_on_deletion": true, - "graceful_shutdown": true, + "graceful_shutdown": false, + "force_delete": false, + "shutdown_before_deletion": false, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: true, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, { - Name: "Delete OS Disk and Graceful Shutdown Disabled", + Name: "Graceful Shutdown Enabled", Input: []interface{}{ map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ "delete_os_disk_on_deletion": false, - "graceful_shutdown": false, + "graceful_shutdown": true, + "force_delete": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + VirtualMachine: features.VirtualMachineFeatures{ + DeleteOSDiskOnDeletion: false, + GracefulShutdown: true, + SkipShutdownAndForceDelete: false, + }, + }, + }, + { + Name: "Skip Shutdown and Force Delete Enabled", + Input: []interface{}{ + map[string]interface{}{ + "virtual_machine": []interface{}{ + map[string]interface{}{ + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": true, + }, + }, + }, + }, + Expected: features.UserFeatures{ + VirtualMachine: features.VirtualMachineFeatures{ + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: true, + }, + }, + }, + { + Name: "All Disabled", + Input: []interface{}{ + map[string]interface{}{ + "virtual_machine": []interface{}{ + map[string]interface{}{ + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": false, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, @@ -462,12 +521,32 @@ func TestExpandFeaturesVirtualMachineScaleSet(t *testing.T) { }, }, }, + { + Name: "Force Delete Enabled", + Input: []interface{}{ + map[string]interface{}{ + "virtual_machine_scale_set": []interface{}{ + map[string]interface{}{ + "force_delete": true, + "roll_instances_when_required": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ + ForceDelete: true, + RollInstancesWhenRequired: false, + }, + }, + }, { Name: "Roll Instances Enabled", Input: []interface{}{ map[string]interface{}{ "virtual_machine_scale_set": []interface{}{ map[string]interface{}{ + "force_delete": false, "roll_instances_when_required": true, }, }, @@ -475,16 +554,18 @@ func TestExpandFeaturesVirtualMachineScaleSet(t *testing.T) { }, Expected: features.UserFeatures{ VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ + ForceDelete: false, RollInstancesWhenRequired: true, }, }, }, { - Name: "Roll Instances Disabled", + Name: "All Fields Disabled", Input: []interface{}{ map[string]interface{}{ "virtual_machine_scale_set": []interface{}{ map[string]interface{}{ + "force_delete": false, "roll_instances_when_required": false, }, }, @@ -492,6 +573,7 @@ func TestExpandFeaturesVirtualMachineScaleSet(t *testing.T) { }, Expected: features.UserFeatures{ VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ + ForceDelete: false, RollInstancesWhenRequired: false, }, }, diff --git a/azurerm/internal/services/compute/linux_virtual_machine_resource.go b/azurerm/internal/services/compute/linux_virtual_machine_resource.go index 6fad8ee3ce0b..7a10f060cb8d 100644 --- a/azurerm/internal/services/compute/linux_virtual_machine_resource.go +++ b/azurerm/internal/services/compute/linux_virtual_machine_resource.go @@ -1135,31 +1135,37 @@ func resourceLinuxVirtualMachineDelete(d *schema.ResourceData, meta interface{}) return fmt.Errorf("retrieving Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } - // If the VM was in a Failed state we can skip powering off, since that'll fail - if strings.EqualFold(*existing.ProvisioningState, "failed") { - log.Printf("[DEBUG] Powering Off Linux Virtual Machine was skipped because the VM was in %q state %q (Resource Group %q).", *existing.ProvisioningState, id.Name, id.ResourceGroup) - } else { - //ISSUE: 4920 - // shutting down the Virtual Machine prior to removing it means users are no longer charged for some Azure resources - // thus this can be a large cost-saving when deleting larger instances - // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle - log.Printf("[DEBUG] Powering Off Linux Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) - skipShutdown := !meta.(*clients.Client).Features.VirtualMachine.GracefulShutdown - powerOffFuture, err := client.PowerOff(ctx, id.ResourceGroup, id.Name, utils.Bool(skipShutdown)) - if err != nil { - return fmt.Errorf("powering off Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) - } - if err := powerOffFuture.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("waiting for power off of Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + if !meta.(*clients.Client).Features.VirtualMachine.SkipShutdownAndForceDelete { + // If the VM was in a Failed state we can skip powering off, since that'll fail + if strings.EqualFold(*existing.ProvisioningState, "failed") { + log.Printf("[DEBUG] Powering Off Linux Virtual Machine was skipped because the VM was in %q state %q (Resource Group %q).", *existing.ProvisioningState, id.Name, id.ResourceGroup) + } else { + //ISSUE: 4920 + // shutting down the Virtual Machine prior to removing it means users are no longer charged for some Azure resources + // thus this can be a large cost-saving when deleting larger instances + // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle + log.Printf("[DEBUG] Powering Off Linux Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) + skipShutdown := !meta.(*clients.Client).Features.VirtualMachine.GracefulShutdown + powerOffFuture, err := client.PowerOff(ctx, id.ResourceGroup, id.Name, utils.Bool(skipShutdown)) + if err != nil { + return fmt.Errorf("powering off Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + if err := powerOffFuture.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for power off of Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + log.Printf("[DEBUG] Powered Off Linux Virtual Machine %q (Resource Group %q).", id.Name, id.ResourceGroup) } - log.Printf("[DEBUG] Powered Off Linux Virtual Machine %q (Resource Group %q).", id.Name, id.ResourceGroup) } log.Printf("[DEBUG] Deleting Linux Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) - // @tombuildsstuff: sending `nil` here omits this value from being sent - which matches - // the previous behaviour - we're only splitting this out so it's clear why - // TODO: support force deletion once it's out of Preview, if applicable + + // Force Delete is in an opt-in Preview and can only be specified (true/false) if the feature is enabled + // as such we default this to `nil` which matches the previous behaviour (where this isn't sent) and + // conditionally set this if required var forceDeletion *bool = nil + if meta.(*clients.Client).Features.VirtualMachine.SkipShutdownAndForceDelete { + forceDeletion = utils.Bool(true) + } deleteFuture, err := client.Delete(ctx, id.ResourceGroup, id.Name, forceDeletion) if err != nil { return fmt.Errorf("deleting Linux Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) diff --git a/azurerm/internal/services/compute/linux_virtual_machine_resource_other_test.go b/azurerm/internal/services/compute/linux_virtual_machine_resource_other_test.go index 6c84a99f1591..563f035c262b 100644 --- a/azurerm/internal/services/compute/linux_virtual_machine_resource_other_test.go +++ b/azurerm/internal/services/compute/linux_virtual_machine_resource_other_test.go @@ -261,6 +261,21 @@ func TestAccLinuxVirtualMachine_otherCustomData(t *testing.T) { }) } +func TestAccLinuxVirtualMachine_otherSkipShutdownAndForceDelete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine", "test") + r := LinuxVirtualMachineResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.otherSkipShutdownAndForceDelete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + func TestAccLinuxVirtualMachine_otherLicenseType(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine", "test") r := LinuxVirtualMachineResource{} @@ -995,6 +1010,48 @@ resource "azurerm_linux_virtual_machine" "test" { `, r.template(data), data.RandomInteger) } +func (r LinuxVirtualMachineResource) otherSkipShutdownAndForceDelete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + virtual_machine { + skip_shutdown_and_force_delete = true + } + } +} + +%s + +resource "azurerm_linux_virtual_machine" "test" { + name = "acctestVM-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + admin_ssh_key { + username = "adminuser" + public_key = local.first_public_key + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } +} +`, r.template(data), data.RandomInteger) +} + func (r LinuxVirtualMachineResource) otherLicenseType(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/compute/linux_virtual_machine_resource_test.go b/azurerm/internal/services/compute/linux_virtual_machine_resource_test.go index 12272a87b8b3..b52e5d0c10a6 100644 --- a/azurerm/internal/services/compute/linux_virtual_machine_resource_test.go +++ b/azurerm/internal/services/compute/linux_virtual_machine_resource_test.go @@ -14,7 +14,7 @@ import ( type LinuxVirtualMachineResource struct { } -func (t LinuxVirtualMachineResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { +func (r LinuxVirtualMachineResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { id, err := parse.VirtualMachineID(state.ID) if err != nil { return nil, err diff --git a/azurerm/internal/services/compute/linux_virtual_machine_scale_set_other_resource_test.go b/azurerm/internal/services/compute/linux_virtual_machine_scale_set_other_resource_test.go index ea214aa13193..0048181f5d3f 100644 --- a/azurerm/internal/services/compute/linux_virtual_machine_scale_set_other_resource_test.go +++ b/azurerm/internal/services/compute/linux_virtual_machine_scale_set_other_resource_test.go @@ -154,6 +154,23 @@ func TestAccLinuxVirtualMachineScaleSet_otherCustomData(t *testing.T) { }) } +func TestAccLinuxVirtualMachineScaleSet_otherForceDelete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine_scale_set", "test") + r := LinuxVirtualMachineScaleSetResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.otherForceDelete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep( + "admin_password", + ), + }) +} + func TestAccLinuxVirtualMachineScaleSet_otherPrioritySpotDeallocate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine_scale_set", "test") r := LinuxVirtualMachineScaleSetResource{} @@ -900,6 +917,55 @@ resource "azurerm_linux_virtual_machine_scale_set" "test" { `, r.template(data), data.RandomInteger, customData) } +func (r LinuxVirtualMachineScaleSetResource) otherForceDelete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + virtual_machine_scale_set { + force_delete = true + } + } +} + +%s + +resource "azurerm_linux_virtual_machine_scale_set" "test" { + name = "acctestvmss-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "Standard_F2" + instances = 1 + admin_username = "adminuser" + admin_password = "P@ssword1234!" + + disable_password_authentication = false + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } + + os_disk { + storage_account_type = "Standard_LRS" + caching = "ReadWrite" + } + + network_interface { + name = "example" + primary = true + + ip_configuration { + name = "internal" + primary = true + subnet_id = azurerm_subnet.test.id + } + } +} +`, r.template(data), data.RandomInteger) +} + func (r LinuxVirtualMachineScaleSetResource) otherPrioritySpot(data acceptance.TestData, evictionPolicy string) string { return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/compute/linux_virtual_machine_scale_set_resource.go b/azurerm/internal/services/compute/linux_virtual_machine_scale_set_resource.go index c3df5166b1d3..800f7686d2c4 100644 --- a/azurerm/internal/services/compute/linux_virtual_machine_scale_set_resource.go +++ b/azurerm/internal/services/compute/linux_virtual_machine_scale_set_resource.go @@ -1110,10 +1110,13 @@ func resourceLinuxVirtualMachineScaleSetDelete(d *schema.ResourceData, meta inte } log.Printf("[DEBUG] Deleting Linux Virtual Machine Scale Set %q (Resource Group %q)..", id.Name, id.ResourceGroup) - // @ArcturusZhang (mimicking from linux_virtual_machine_resource.go): sending `nil` here omits this value from being sent - // which matches the previous behaviour - we're only splitting this out so it's clear why - // TODO: support force deletion once it's out of Preview, if applicable + // Force Delete is in an opt-in Preview and can only be specified (true/false) if the feature is enabled + // as such we default this to `nil` which matches the previous behaviour (where this isn't sent) and + // conditionally set this if required var forceDeletion *bool = nil + if meta.(*clients.Client).Features.VirtualMachineScaleSet.ForceDelete { + forceDeletion = utils.Bool(true) + } future, err := client.Delete(ctx, id.ResourceGroup, id.Name, forceDeletion) if err != nil { return fmt.Errorf("Error deleting Linux Virtual Machine Scale Set %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) diff --git a/azurerm/internal/services/compute/windows_virtual_machine_resource.go b/azurerm/internal/services/compute/windows_virtual_machine_resource.go index 7b8e0756a5aa..8af8793706e2 100644 --- a/azurerm/internal/services/compute/windows_virtual_machine_resource.go +++ b/azurerm/internal/services/compute/windows_virtual_machine_resource.go @@ -1193,31 +1193,37 @@ func resourceWindowsVirtualMachineDelete(d *schema.ResourceData, meta interface{ return fmt.Errorf("retrieving Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } - // If the VM was in a Failed state we can skip powering off, since that'll fail - if strings.EqualFold(*existing.ProvisioningState, "failed") { - log.Printf("[DEBUG] Powering Off Windows Virtual Machine was skipped because the VM was in %q state %q (Resource Group %q).", *existing.ProvisioningState, id.Name, id.ResourceGroup) - } else { - //ISSUE: 4920 - // shutting down the Virtual Machine prior to removing it means users are no longer charged for some Azure resources - // thus this can be a large cost-saving when deleting larger instances - // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle - log.Printf("[DEBUG] Powering Off Windows Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) - skipShutdown := !meta.(*clients.Client).Features.VirtualMachine.GracefulShutdown - powerOffFuture, err := client.PowerOff(ctx, id.ResourceGroup, id.Name, utils.Bool(skipShutdown)) - if err != nil { - return fmt.Errorf("powering off Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) - } - if err := powerOffFuture.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("waiting for power off of Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + if !meta.(*clients.Client).Features.VirtualMachine.SkipShutdownAndForceDelete { + // If the VM was in a Failed state we can skip powering off, since that'll fail + if strings.EqualFold(*existing.ProvisioningState, "failed") { + log.Printf("[DEBUG] Powering Off Windows Virtual Machine was skipped because the VM was in %q state %q (Resource Group %q).", *existing.ProvisioningState, id.Name, id.ResourceGroup) + } else { + //ISSUE: 4920 + // shutting down the Virtual Machine prior to removing it means users are no longer charged for some Azure resources + // thus this can be a large cost-saving when deleting larger instances + // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle + log.Printf("[DEBUG] Powering Off Windows Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) + skipShutdown := !meta.(*clients.Client).Features.VirtualMachine.GracefulShutdown + powerOffFuture, err := client.PowerOff(ctx, id.ResourceGroup, id.Name, utils.Bool(skipShutdown)) + if err != nil { + return fmt.Errorf("powering off Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + if err := powerOffFuture.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for power off of Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + log.Printf("[DEBUG] Powered Off Windows Virtual Machine %q (Resource Group %q).", id.Name, id.ResourceGroup) } - log.Printf("[DEBUG] Powered Off Windows Virtual Machine %q (Resource Group %q).", id.Name, id.ResourceGroup) } log.Printf("[DEBUG] Deleting Windows Virtual Machine %q (Resource Group %q)..", id.Name, id.ResourceGroup) - // @tombuildsstuff: sending `nil` here omits this value from being sent - which matches - // the previous behaviour - we're only splitting this out so it's clear why - // TODO: support force deletion once it's out of Preview, if applicable + + // Force Delete is in an opt-in Preview and can only be specified (true/false) if the feature is enabled + // as such we default this to `nil` which matches the previous behaviour (where this isn't sent) and + // conditionally set this if required var forceDeletion *bool = nil + if meta.(*clients.Client).Features.VirtualMachine.SkipShutdownAndForceDelete { + forceDeletion = utils.Bool(true) + } deleteFuture, err := client.Delete(ctx, id.ResourceGroup, id.Name, forceDeletion) if err != nil { return fmt.Errorf("deleting Windows Virtual Machine %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) diff --git a/azurerm/internal/services/compute/windows_virtual_machine_resource_other_test.go b/azurerm/internal/services/compute/windows_virtual_machine_resource_other_test.go index 3bbcfdfad2b9..f666ed53573c 100644 --- a/azurerm/internal/services/compute/windows_virtual_machine_resource_other_test.go +++ b/azurerm/internal/services/compute/windows_virtual_machine_resource_other_test.go @@ -437,6 +437,23 @@ func TestAccWindowsVirtualMachine_otherEnableAutomaticUpdatesDisabled(t *testing }) } +func TestAccWindowsVirtualMachine_otherSkipShutdownAndForceDelete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine", "test") + r := WindowsVirtualMachineResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.otherSkipShutdownAndForceDelete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep( + "admin_password", + ), + }) +} + func TestAccWindowsVirtualMachine_otherLicenseTypeNone(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine", "test") r := WindowsVirtualMachineResource{} @@ -1485,6 +1502,44 @@ resource "azurerm_windows_virtual_machine" "test" { `, r.template(data)) } +func (r WindowsVirtualMachineResource) otherSkipShutdownAndForceDelete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + virtual_machine { + skip_shutdown_and_force_delete = true + } + } +} + +%s + +resource "azurerm_windows_virtual_machine" "test" { + name = local.vm_name + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + admin_password = "P@$$w0rd1234!" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2016-Datacenter" + version = "latest" + } +} +`, r.template(data)) +} + func (r WindowsVirtualMachineResource) otherLicenseTypeDefault(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/compute/windows_virtual_machine_resource_test.go b/azurerm/internal/services/compute/windows_virtual_machine_resource_test.go index e8f3f8a41b58..b0a0324b74ee 100644 --- a/azurerm/internal/services/compute/windows_virtual_machine_resource_test.go +++ b/azurerm/internal/services/compute/windows_virtual_machine_resource_test.go @@ -14,7 +14,7 @@ import ( type WindowsVirtualMachineResource struct { } -func (t WindowsVirtualMachineResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { +func (r WindowsVirtualMachineResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { id, err := parse.VirtualMachineID(state.ID) if err != nil { return nil, err diff --git a/azurerm/internal/services/compute/windows_virtual_machine_scale_set_other_resource_test.go b/azurerm/internal/services/compute/windows_virtual_machine_scale_set_other_resource_test.go index 6bcd35d55bca..afa537bbc8a5 100644 --- a/azurerm/internal/services/compute/windows_virtual_machine_scale_set_other_resource_test.go +++ b/azurerm/internal/services/compute/windows_virtual_machine_scale_set_other_resource_test.go @@ -172,6 +172,23 @@ func TestAccWindowsVirtualMachineScaleSet_otherCustomData(t *testing.T) { }) } +func TestAccWindowsVirtualMachineScaleSet_otherForceDelete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine_scale_set", "test") + r := WindowsVirtualMachineScaleSetResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.otherForceDelete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep( + "admin_password", + ), + }) +} + func TestAccWindowsVirtualMachineScaleSet_otherEnableAutomaticUpdatesDisabled(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine_scale_set", "test") r := WindowsVirtualMachineScaleSetResource{} @@ -1077,6 +1094,53 @@ resource "azurerm_windows_virtual_machine_scale_set" "test" { `, r.template(data), customData) } +func (r WindowsVirtualMachineScaleSetResource) otherForceDelete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + virtual_machine_scale_set { + force_delete = true + } + } +} + +%s + +resource "azurerm_windows_virtual_machine_scale_set" "test" { + name = local.vm_name + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + sku = "Standard_F2" + instances = 1 + admin_username = "adminuser" + admin_password = "P@ssword1234!" + + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2019-Datacenter" + version = "latest" + } + + os_disk { + storage_account_type = "Standard_LRS" + caching = "ReadWrite" + } + + network_interface { + name = "example" + primary = true + + ip_configuration { + name = "internal" + primary = true + subnet_id = azurerm_subnet.test.id + } + } +} +`, r.template(data)) +} + func (r WindowsVirtualMachineScaleSetResource) otherEnableAutomaticUpdatesDisabled(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/azurerm/internal/services/compute/windows_virtual_machine_scale_set_resource.go b/azurerm/internal/services/compute/windows_virtual_machine_scale_set_resource.go index 80721ec59d0d..e0430569147b 100644 --- a/azurerm/internal/services/compute/windows_virtual_machine_scale_set_resource.go +++ b/azurerm/internal/services/compute/windows_virtual_machine_scale_set_resource.go @@ -1169,10 +1169,13 @@ func resourceWindowsVirtualMachineScaleSetDelete(d *schema.ResourceData, meta in } log.Printf("[DEBUG] Deleting Windows Virtual Machine Scale Set %q (Resource Group %q)..", id.Name, id.ResourceGroup) - // @ArcturusZhang (mimicking from windows_virtual_machine_resource.go): sending `nil` here omits this value from being sent - // which matches the previous behaviour - we're only splitting this out so it's clear why - // TODO: support force deletion once it's out of Preview, if applicable + // Force Delete is in an opt-in Preview and can only be specified (true/false) if the feature is enabled + // as such we default this to `nil` which matches the previous behaviour (where this isn't sent) and + // conditionally set this if required var forceDeletion *bool = nil + if meta.(*clients.Client).Features.VirtualMachineScaleSet.ForceDelete { + forceDeletion = utils.Bool(true) + } future, err := client.Delete(ctx, id.ResourceGroup, id.Name, forceDeletion) if err != nil { return fmt.Errorf("Error deleting Windows Virtual Machine Scale Set %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index aec6babf74ff..ce9e58f53c18 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -208,8 +208,16 @@ The `virtual_machine` block supports the following: ~> **Note:** When using a graceful shutdown, Azure gives the Virtual Machine a 5 minutes window in which to complete the shutdown process, at which point the machine will be force powered off - [more information can be found in this blog post](https://azure.microsoft.com/en-us/blog/linux-and-graceful-shutdowns-2/). +* `skip_shudown_and_force_delete` - Should the `azurerm_linux_virtual_machine` and `azurerm_windows_virtual_machine` skip the shutdown command and `Force Delete`, this provides the ability to forcefully and immediately delete the VM and detach all sub-resources associated with the virtual machine. This allows those freed resources to be reattached to another VM instance or deleted. Defaults to `false`. + +~> **Note:** Support for Force Delete is in an opt-in Preview. + --- The `virtual_machine_scale_set` block supports the following: +* `force_delete` - Should the `azurerm_linux_virtual_machine_scale_set` and `azurerm_windows_virtual_machine_scale_set` resources `Force Delete`, this provides the ability to forcefully and immediately delete the VM and detach all sub-resources associated with the virtual machine. This allows those freed resources to be reattached to another VM instance or deleted. Defaults to `false`. + +~> **Note:** Support for Force Delete is in an opt-in Preview. + * `roll_instances_when_required` - (Optional) Should the `azurerm_linux_virtual_machine_scale_set` and `azurerm_windows_virtual_machine_scale_set` resources automatically roll the instances in the Scale Set when Required (for example when updating the Sku/Image). Defaults to `true`.