diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 4f061f1ca2b1..d8237ec7d42a 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -6,6 +6,9 @@ func Default() UserFeatures { ApiManagement: ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: false, }, + ApplicationInsights: ApplicationInsightFeatures{ + DisableGeneratedRule: false, + }, CognitiveAccount: CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, }, diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index 75f792721129..32676e0c4312 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -2,6 +2,7 @@ package features type UserFeatures struct { ApiManagement ApiManagementFeatures + ApplicationInsights ApplicationInsightFeatures CognitiveAccount CognitiveAccountFeatures VirtualMachine VirtualMachineFeatures VirtualMachineScaleSet VirtualMachineScaleSetFeatures @@ -53,3 +54,7 @@ type ResourceGroupFeatures struct { type ApiManagementFeatures struct { PurgeSoftDeleteOnDestroy bool } + +type ApplicationInsightFeatures struct { + DisableGeneratedRule bool +} diff --git a/internal/provider/features.go b/internal/provider/features.go index 65e34dc53692..fdf4b9e2fe75 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -25,6 +25,20 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { }, }, + "application_insights": { + Type: pluginsdk.TypeList, + Optional: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "disable_generated_rule": { + Type: pluginsdk.TypeBool, + Optional: true, + }, + }, + }, + }, + "cognitive_account": { Type: pluginsdk.TypeList, Optional: true, @@ -254,6 +268,16 @@ func expandFeatures(input []interface{}) features.UserFeatures { } } + if raw, ok := val["application_insights"]; ok { + items := raw.([]interface{}) + if len(items) > 0 && items[0] != nil { + applicationInsightsRaw := items[0].(map[string]interface{}) + if v, ok := applicationInsightsRaw["disable_generated_rule"]; ok { + featuresMap.ApplicationInsights.DisableGeneratedRule = v.(bool) + } + } + } + if raw, ok := val["cognitive_account"]; ok { items := raw.([]interface{}) if len(items) > 0 && items[0] != nil { diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index e4e75dd87b48..e1479c2b1d96 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -21,6 +21,9 @@ func TestExpandFeatures(t *testing.T) { ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: false, }, + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: false, + }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, }, @@ -64,6 +67,11 @@ func TestExpandFeatures(t *testing.T) { "purge_soft_delete_on_destroy": true, }, }, + "application_insights": []interface{}{ + map[string]interface{}{ + "disable_generated_rule": true, + }, + }, "cognitive_account": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": true, @@ -121,6 +129,9 @@ func TestExpandFeatures(t *testing.T) { ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: true, }, + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: true, + }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: true, }, @@ -164,6 +175,11 @@ func TestExpandFeatures(t *testing.T) { "purge_soft_delete_on_destroy": false, }, }, + "application_insights": []interface{}{ + map[string]interface{}{ + "disable_generated_rule": false, + }, + }, "cognitive_account": []interface{}{ map[string]interface{}{ "purge_soft_delete_on_destroy": false, @@ -221,6 +237,9 @@ func TestExpandFeatures(t *testing.T) { ApiManagement: features.ApiManagementFeatures{ PurgeSoftDeleteOnDestroy: false, }, + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: false, + }, CognitiveAccount: features.CognitiveAccountFeatures{ PurgeSoftDeleteOnDestroy: false, }, @@ -331,6 +350,71 @@ func TestExpandFeaturesApiManagement(t *testing.T) { } } +func TestExpandFeaturesApplicationInsights(t *testing.T) { + testData := []struct { + Name string + Input []interface{} + EnvVars map[string]interface{} + Expected features.UserFeatures + }{ + { + Name: "Empty Block", + Input: []interface{}{ + map[string]interface{}{ + "application_insights": []interface{}{}, + }, + }, + Expected: features.UserFeatures{ + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: false, + }, + }, + }, + { + Name: "Disable Generated Rule", + Input: []interface{}{ + map[string]interface{}{ + "application_insights": []interface{}{ + map[string]interface{}{ + "disable_generated_rule": true, + }, + }, + }, + }, + Expected: features.UserFeatures{ + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: true, + }, + }, + }, + { + Name: "Enable Generated Rule", + Input: []interface{}{ + map[string]interface{}{ + "application_insights": []interface{}{ + map[string]interface{}{ + "disable_generated_rule": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + ApplicationInsights: features.ApplicationInsightFeatures{ + DisableGeneratedRule: false, + }, + }, + }, + } + + for _, testCase := range testData { + t.Logf("[DEBUG] Test Case: %q", testCase.Name) + result := expandFeatures(testCase.Input) + if !reflect.DeepEqual(result.ApplicationInsights, testCase.Expected.ApplicationInsights) { + t.Fatalf("Expected %+v but got %+v", result.ApplicationInsights, testCase.Expected.ApplicationInsights) + } + } +} + func TestExpandFeaturesCognitiveServices(t *testing.T) { testData := []struct { Name string diff --git a/internal/services/applicationinsights/application_insights_resource.go b/internal/services/applicationinsights/application_insights_resource.go index 37f49061c186..9d6c53e926e0 100644 --- a/internal/services/applicationinsights/application_insights_resource.go +++ b/internal/services/applicationinsights/application_insights_resource.go @@ -7,14 +7,15 @@ import ( "strings" "time" - "github.com/hashicorp/go-azure-helpers/resourcemanager/location" - "github.com/Azure/azure-sdk-for-go/services/appinsights/mgmt/2020-02-02/insights" + "github.com/Azure/azure-sdk-for-go/services/preview/alertsmanagement/mgmt/2019-06-01-preview/alertsmanagement" + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/terraform-provider-azurerm/helpers/azure" "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" "github.com/hashicorp/terraform-provider-azurerm/internal/services/applicationinsights/migration" "github.com/hashicorp/terraform-provider-azurerm/internal/services/applicationinsights/parse" + monitorParse "github.com/hashicorp/terraform-provider-azurerm/internal/services/monitor/parse" "github.com/hashicorp/terraform-provider-azurerm/internal/tags" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" @@ -40,7 +41,7 @@ func resourceApplicationInsights() *pluginsdk.Resource { }), Timeouts: &pluginsdk.ResourceTimeout{ - Create: pluginsdk.DefaultTimeout(30 * time.Minute), + Create: pluginsdk.DefaultTimeout(60 * time.Minute), Read: pluginsdk.DefaultTimeout(5 * time.Minute), Update: pluginsdk.DefaultTimeout(30 * time.Minute), Delete: pluginsdk.DefaultTimeout(30 * time.Minute), @@ -169,6 +170,8 @@ func resourceApplicationInsights() *pluginsdk.Resource { func resourceApplicationInsightsCreateUpdate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).AppInsights.ComponentsClient + ruleClient := meta.(*clients.Client).Monitor.SmartDetectorAlertRulesClient + actionGroupClient := meta.(*clients.Client).Monitor.ActionGroupsClient billingClient := meta.(*clients.Client).AppInsights.BillingClient subscriptionId := meta.(*clients.Client).Account.SubscriptionId ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) @@ -179,7 +182,7 @@ func resourceApplicationInsightsCreateUpdate(d *pluginsdk.ResourceData, meta int name := d.Get("name").(string) resGroup := d.Get("resource_group_name").(string) - resourceId := parse.NewComponentID(subscriptionId, resGroup, name).ID() + resourceId := parse.NewComponentID(subscriptionId, resGroup, name) if d.IsNewResource() { existing, err := client.Get(ctx, resGroup, name) if err != nil { @@ -189,7 +192,7 @@ func resourceApplicationInsightsCreateUpdate(d *pluginsdk.ResourceData, meta int } if !utils.ResponseWasNotFound(existing.Response) { - return tf.ImportAsExistsError("azurerm_application_insights", resourceId) + return tf.ImportAsExistsError("azurerm_application_insights", resourceId.ID()) } } @@ -274,7 +277,69 @@ func resourceApplicationInsightsCreateUpdate(d *pluginsdk.ResourceData, meta int return fmt.Errorf("update Application Insights Billing Feature %q (Resource Group %q): %+v", name, resGroup, err) } - d.SetId(resourceId) + // https://github.com/hashicorp/terraform-provider-azurerm/issues/10563 + // Azure creates a rule and action group when creating this resource that are very noisy + // We would like to delete them but deleting them just causes them to be recreated after a few minutes. + // Instead, we'll opt to disable them here + if d.IsNewResource() && meta.(*clients.Client).Features.ApplicationInsights.DisableGeneratedRule { + // TODO: replace this with a StateWait func + err = pluginsdk.Retry(d.Timeout(pluginsdk.TimeoutCreate), func() *pluginsdk.RetryError { + time.Sleep(30 * time.Second) + actionGroupId := monitorParse.NewActionGroupID(resourceId.SubscriptionId, resourceId.ResourceGroup, "Application Insights Smart Detection") + + groupResult, err := actionGroupClient.Get(ctx, actionGroupId.ResourceGroup, actionGroupId.Name) + if err != nil { + if utils.ResponseWasNotFound(groupResult.Response) { + return pluginsdk.RetryableError(fmt.Errorf("expected %s to be created but was not found, retrying", actionGroupId)) + } + return pluginsdk.NonRetryableError(fmt.Errorf("making Read request for %s: %+v", actionGroupId, err)) + } + + if groupResult.ActionGroup != nil { + groupResult.ActionGroup.Enabled = utils.Bool(false) + updateActionGroupResult, err := actionGroupClient.CreateOrUpdate(ctx, actionGroupId.ResourceGroup, actionGroupId.Name, groupResult) + if err != nil { + if !utils.ResponseWasNotFound(updateActionGroupResult.Response) { + return pluginsdk.NonRetryableError(fmt.Errorf("issuing disable request for %s: %+v", actionGroupId, err)) + } + } + } + return nil + }) + if err != nil { + return err + } + + // TODO: replace this with a StateWait func + err = pluginsdk.Retry(d.Timeout(pluginsdk.TimeoutCreate), func() *pluginsdk.RetryError { + time.Sleep(30 * time.Second) + ruleName := fmt.Sprintf("Failure Anomalies - %s", resourceId.Name) + ruleId := monitorParse.NewSmartDetectorAlertRuleID(resourceId.SubscriptionId, resourceId.ResourceGroup, ruleName) + result, err := ruleClient.Get(ctx, ruleId.ResourceGroup, ruleId.Name, utils.Bool(true)) + if err != nil { + if utils.ResponseWasNotFound(result.Response) { + return pluginsdk.RetryableError(fmt.Errorf("expected %s to be created but was not found, retrying", ruleId)) + } + return pluginsdk.NonRetryableError(fmt.Errorf("making Read request for %s: %+v", ruleId, err)) + } + + if result.AlertRuleProperties != nil { + result.AlertRuleProperties.State = alertsmanagement.AlertRuleStateDisabled + updateRuleResult, err := ruleClient.CreateOrUpdate(ctx, ruleId.ResourceGroup, ruleId.Name, result) + if err != nil { + if !utils.ResponseWasNotFound(updateRuleResult.Response) { + return pluginsdk.NonRetryableError(fmt.Errorf("issuing disable request for %s: %+v", ruleId, err)) + } + } + } + return nil + }) + if err != nil { + return err + } + } + + d.SetId(resourceId.ID()) return resourceApplicationInsightsRead(d, meta) } diff --git a/internal/services/applicationinsights/application_insights_resource_test.go b/internal/services/applicationinsights/application_insights_resource_test.go index ed1bd6981cf5..ca9b2120e80f 100644 --- a/internal/services/applicationinsights/application_insights_resource_test.go +++ b/internal/services/applicationinsights/application_insights_resource_test.go @@ -243,6 +243,22 @@ func TestAccApplicationInsights_withInternetIngestionEnabled(t *testing.T) { }) } +func TestAccApplicationInsights_disableGeneratedRule(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_insights", "test") + r := AppInsightsResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.disableGeneratedRule(data, "web"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("application_type").HasValue("web"), + ), + }, + data.ImportStep(), + }) +} + func (AppInsightsResource) basic(data acceptance.TestData, applicationType string) string { return fmt.Sprintf(` provider "azurerm" { @@ -420,3 +436,27 @@ resource "azurerm_application_insights" "test" { } `, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } + +func (AppInsightsResource) disableGeneratedRule(data acceptance.TestData, applicationType string) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + application_insights { + disable_generated_rule = true + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-appinsights-%d" + location = "%s" +} + +resource "azurerm_application_insights" "test" { + name = "acctestappinsights-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "%s" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, applicationType) +} diff --git a/website/docs/guides/features-block.html.markdown b/website/docs/guides/features-block.html.markdown index 298c96016eec..7c2133733579 100644 --- a/website/docs/guides/features-block.html.markdown +++ b/website/docs/guides/features-block.html.markdown @@ -31,6 +31,10 @@ provider "azurerm" { purge_soft_delete_on_destroy = true } + application_insights { + disable_generated_rule = false + } + cognitive_account { purge_soft_delete_on_destroy = true } @@ -73,6 +77,8 @@ The `features` block supports the following: * `api_management` - (Optional) An `api_management` block as defined below. +* `application_insights` - (Optional) An `application_insights` block as defined below. + * `cognitive_account` - (Optional) A `cognitive_account` block as defined below. * `key_vault` - (Optional) A `key_vault` block as defined below. @@ -95,6 +101,12 @@ The `api_management` block supports the following: --- +The `application_insights` block supports the following: + +* `disable_generated_rule` - (Optional) Should the `azurerm_application_insights` resources disable the Azure generated Alert Rule and Action Group during the create step? Defaults to `false`. + +--- + The `cognitive_account` block supports the following: * `purge_soft_delete_on_destroy` - (Optional) Should the `azurerm_cognitive_account` resources be permanently deleted (e.g. purged) when destroyed? Defaults to `true`.