diff --git a/internal/features/defaults.go b/internal/features/defaults.go index d1d094c78ba0..133d6a01a887 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -53,5 +53,8 @@ func Default() UserFeatures { RollInstancesWhenRequired: true, ScaleToZeroOnDelete: true, }, + Subscription: SubscriptionFeatures{ + PreventCancellationOnDestroy: false, + }, } } diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index e91379846937..a0b1cfe9f448 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -15,6 +15,7 @@ type UserFeatures struct { LogAnalyticsWorkspace LogAnalyticsWorkspaceFeatures ResourceGroup ResourceGroupFeatures ManagedDisk ManagedDiskFeatures + Subscription SubscriptionFeatures } type CognitiveAccountFeatures struct { @@ -74,3 +75,7 @@ type AppConfigurationFeatures struct { PurgeSoftDeleteOnDestroy bool RecoverSoftDeleted bool } + +type SubscriptionFeatures struct { + PreventCancellationOnDestroy bool +} diff --git a/internal/provider/features.go b/internal/provider/features.go index bdb0cbfe3a9f..872386ce9a03 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -268,6 +268,21 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { }, }, }, + + "subscription": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "prevent_cancellation_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 @@ -455,5 +470,15 @@ func expandFeatures(input []interface{}) features.UserFeatures { } } + if raw, ok := val["subscription"]; ok { + items := raw.([]interface{}) + if len(items) > 0 { + subscriptionRaw := items[0].(map[string]interface{}) + if v, ok := subscriptionRaw["prevent_cancellation_on_destroy"]; ok { + featuresMap.Subscription.PreventCancellationOnDestroy = v.(bool) + } + } + } + return featuresMap } diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index 1b5c2a949e68..2e35abf8a385 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -68,6 +68,9 @@ func TestExpandFeatures(t *testing.T) { ResourceGroup: features.ResourceGroupFeatures{ PreventDeletionIfContainsResources: true, }, + Subscription: features.SubscriptionFeatures{ + PreventCancellationOnDestroy: false, + }, }, }, { @@ -124,6 +127,11 @@ func TestExpandFeatures(t *testing.T) { "prevent_deletion_if_contains_resources": true, }, }, + "subscription": []interface{}{ + map[string]interface{}{ + "prevent_cancellation_on_destroy": true, + }, + }, "template_deployment": []interface{}{ map[string]interface{}{ "delete_nested_items_during_deletion": true, @@ -180,6 +188,9 @@ func TestExpandFeatures(t *testing.T) { ResourceGroup: features.ResourceGroupFeatures{ PreventDeletionIfContainsResources: true, }, + Subscription: features.SubscriptionFeatures{ + PreventCancellationOnDestroy: true, + }, TemplateDeployment: features.TemplateDeploymentFeatures{ DeleteNestedItemsDuringDeletion: true, }, @@ -249,6 +260,11 @@ func TestExpandFeatures(t *testing.T) { "prevent_deletion_if_contains_resources": false, }, }, + "subscription": []interface{}{ + map[string]interface{}{ + "prevent_cancellation_on_destroy": false, + }, + }, "template_deployment": []interface{}{ map[string]interface{}{ "delete_nested_items_during_deletion": false, @@ -305,6 +321,9 @@ func TestExpandFeatures(t *testing.T) { ResourceGroup: features.ResourceGroupFeatures{ PreventDeletionIfContainsResources: false, }, + Subscription: features.SubscriptionFeatures{ + PreventCancellationOnDestroy: false, + }, TemplateDeployment: features.TemplateDeploymentFeatures{ DeleteNestedItemsDuringDeletion: false, }, @@ -1196,3 +1215,51 @@ func TestExpandFeaturesManagedDisk(t *testing.T) { } } } + +func TestExpandFeaturesSubscription(t *testing.T) { + testData := []struct { + Name string + Input []interface{} + EnvVars map[string]interface{} + Expected features.UserFeatures + }{ + { + Name: "Empty Block", + Input: []interface{}{ + map[string]interface{}{ + "subscription": []interface{}{}, + }, + }, + Expected: features.UserFeatures{ + Subscription: features.SubscriptionFeatures{ + PreventCancellationOnDestroy: false, + }, + }, + }, + { + Name: "No Downtime Resize Enabled", + Input: []interface{}{ + map[string]interface{}{ + "subscription": []interface{}{ + map[string]interface{}{ + "prevent_cancellation_on_destroy": true, + }, + }, + }, + }, + Expected: features.UserFeatures{ + Subscription: features.SubscriptionFeatures{ + PreventCancellationOnDestroy: true, + }, + }, + }, + } + + 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/subscription/subscription_resource.go b/internal/services/subscription/subscription_resource.go index a7238e610807..96ae76b8b98b 100644 --- a/internal/services/subscription/subscription_resource.go +++ b/internal/services/subscription/subscription_resource.go @@ -411,15 +411,21 @@ func resourceSubscriptionDelete(d *pluginsdk.ResourceData, meta interface{}) err } // Cancel the Subscription - if _, err := subscriptionClient.Cancel(ctx, subscriptionId); err != nil { - return fmt.Errorf("failed to cancel Subscription: %+v", err) - } + if !meta.(*clients.Client).Features.Subscription.PreventCancellationOnDestroy { + log.Printf("[DEBUG] Cancelling subscription %s", subscriptionId) - deadline, _ := ctx.Deadline() - deleteDeadline := time.Until(deadline) + if _, err := subscriptionClient.Cancel(ctx, subscriptionId); err != nil { + return fmt.Errorf("failed to cancel Subscription: %+v", err) + } - if err := waitForSubscriptionStateToSettle(ctx, meta.(*clients.Client), subscriptionId, "Cancelled", deleteDeadline); err != nil { - return fmt.Errorf("failed to cancel Subscription %q (Alias %q): %+v", subscriptionId, id.AliasName, err) + deadline, _ := ctx.Deadline() + deleteDeadline := time.Until(deadline) + + if err := waitForSubscriptionStateToSettle(ctx, meta.(*clients.Client), subscriptionId, "Cancelled", deleteDeadline); err != nil { + return fmt.Errorf("failed to cancel Subscription %q (Alias %q): %+v", subscriptionId, id.AliasName, err) + } + } else { + log.Printf("[DEBUG] Skipping subscription %s cancellation due to feature flag.", *id) } return nil diff --git a/website/docs/guides/features-block.html.markdown b/website/docs/guides/features-block.html.markdown index 2e2c41ffaf36..36ace635190a 100644 --- a/website/docs/guides/features-block.html.markdown +++ b/website/docs/guides/features-block.html.markdown @@ -62,6 +62,10 @@ provider "azurerm" { prevent_deletion_if_contains_resources = true } + subscription { + prevent_cancellation_on_destroy = false + } + template_deployment { delete_nested_items_during_deletion = true } @@ -185,6 +189,12 @@ The `resource_group` block supports the following: --- +The `subscription` block supports the following: + +* `prevent_cancellation_on_destroy` - (Optional) Should the `azurerm_subscription` resource prevent a subscription to be cancelled on destroy? Defaults to `false`. + +--- + The `template_deployment` block supports the following: * `delete_nested_items_during_deletion` - (Optional) Should the `azurerm_resource_group_template_deployment` resource attempt to delete resources that have been provisioned by the ARM Template, when the Resource Group Template Deployment is deleted? Defaults to `true`. diff --git a/website/docs/r/subscription.html.markdown b/website/docs/r/subscription.html.markdown index 4bbafff3c706..90c527a40a15 100644 --- a/website/docs/r/subscription.html.markdown +++ b/website/docs/r/subscription.html.markdown @@ -14,6 +14,8 @@ Manages an Alias for a Subscription - which adds an Alias to an existing Subscri ~> **NOTE:** It is not possible to destroy (cancel) a subscription if it contains resources. If resources are present that are not managed by Terraform then these will need to be removed before the Subscription can be destroyed. +~> **Note:** This resource will automatically attempt to cancel a subscription when it is deleted. This behavior can be disabled in the provider `features` block by setting the `prevent_cancellation_on_destroy` field to `true` within the `subscription` block. + ~> **NOTE:** Azure supports Multiple Aliases per Subscription, however, to reliably manage this resource in Terraform only a single Alias is supported. ~> **NOTE:** When using this resource across tenants the `client_id` and `tenant_id` of the `provider` config block should be for the home tenant details for the SPN / User or a permissions error will likely be encountered. See [the official documentation](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/programmatically-create-subscription) for more details.