From fa99ccf29b9671019c6980886f030aa2872f0134 Mon Sep 17 00:00:00 2001 From: Ryan Pickett Date: Wed, 26 Jun 2019 21:14:05 +0100 Subject: [PATCH] Support app service application logs blob storage (#3520) --- azurerm/helpers/azure/app_service.go | 129 +++++++++++++++++++++++ azurerm/resource_arm_app_service.go | 43 +++++++- azurerm/resource_arm_app_service_test.go | 65 ++++++++++++ website/docs/r/app_service.html.markdown | 24 +++++ 4 files changed, 260 insertions(+), 1 deletion(-) diff --git a/azurerm/helpers/azure/app_service.go b/azurerm/helpers/azure/app_service.go index 16e2727d58e1..ba7729eed675 100644 --- a/azurerm/helpers/azure/app_service.go +++ b/azurerm/helpers/azure/app_service.go @@ -452,6 +452,57 @@ func SchemaAppServiceSiteConfig() *schema.Schema { } } +func SchemaAppServiceLogsConfig() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "application_logs": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "azure_blob_storage": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "level": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(web.Error), + string(web.Information), + string(web.Off), + string(web.Verbose), + string(web.Warning), + }, false), + }, + "sas_url": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + "retention_in_days": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func SchemaAppServiceDataSourceSiteConfig() *schema.Schema { return &schema.Schema{ Type: schema.TypeList, @@ -1014,6 +1065,84 @@ func FlattenAppServiceAuthSettings(input *web.SiteAuthSettingsProperties) []inte return append(results, result) } +func FlattenAppServiceLogs(input *web.SiteLogsConfigProperties) []interface{} { + results := make([]interface{}, 0) + if input == nil { + return results + } + + result := make(map[string]interface{}) + + if input.ApplicationLogs != nil { + appLogs := make([]interface{}, 0) + + appLogsItem := make(map[string]interface{}) + + if blobStorageInput := input.ApplicationLogs.AzureBlobStorage; blobStorageInput != nil { + blobStorage := make([]interface{}, 0) + + blobStorageItem := make(map[string]interface{}) + + blobStorageItem["level"] = string(blobStorageInput.Level) + + if blobStorageInput.SasURL != nil { + blobStorageItem["sas_url"] = *blobStorageInput.SasURL + } + + if blobStorageInput.RetentionInDays != nil { + blobStorageItem["retention_in_days"] = *blobStorageInput.RetentionInDays + } + + blobStorage = append(blobStorage, blobStorageItem) + + appLogsItem["azure_blob_storage"] = blobStorage + } + + appLogs = append(appLogs, appLogsItem) + + result["application_logs"] = appLogs + } + + return append(results, result) +} + +func ExpandAppServiceLogs(input interface{}) web.SiteLogsConfigProperties { + configs := input.([]interface{}) + logs := web.SiteLogsConfigProperties{} + + if len(configs) == 0 { + return logs + } + + config := configs[0].(map[string]interface{}) + + if v, ok := config["application_logs"]; ok { + appLogsConfigs := v.([]interface{}) + + for _, config := range appLogsConfigs { + appLogsConfig := config.(map[string]interface{}) + + logs.ApplicationLogs = &web.ApplicationLogsConfig{} + + if v, ok := appLogsConfig["azure_blob_storage"]; ok { + storageConfigs := v.([]interface{}) + + for _, config := range storageConfigs { + storageConfig := config.(map[string]interface{}) + + logs.ApplicationLogs.AzureBlobStorage = &web.AzureBlobStorageApplicationLogsConfig{ + Level: web.LogLevel(storageConfig["level"].(string)), + SasURL: utils.String(storageConfig["sas_url"].(string)), + RetentionInDays: utils.Int32(int32(storageConfig["retention_in_days"].(int))), + } + } + } + } + } + + return logs +} + func ExpandAppServiceSiteConfig(input interface{}) web.SiteConfig { configs := input.([]interface{}) siteConfig := web.SiteConfig{} diff --git a/azurerm/resource_arm_app_service.go b/azurerm/resource_arm_app_service.go index cf0cfc54fbd2..788658bb8fbc 100644 --- a/azurerm/resource_arm_app_service.go +++ b/azurerm/resource_arm_app_service.go @@ -71,6 +71,8 @@ func resourceArmAppService() *schema.Resource { "auth_settings": azure.SchemaAppServiceAuthSettings(), + "logs": azure.SchemaAppServiceLogsConfig(), + "client_affinity_enabled": { Type: schema.TypeBool, Optional: true, @@ -292,6 +294,16 @@ func resourceArmAppServiceCreate(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error updating auth settings for App Service %q (Resource Group %q): %+s", name, resGroup, err) } + logsConfig := azure.ExpandAppServiceLogs(d.Get("logs")) + + logs := web.SiteLogsConfig{ + ID: read.ID, + SiteLogsConfigProperties: &logsConfig} + + if _, err := client.UpdateDiagnosticLogsConfig(ctx, resGroup, name, logs); err != nil { + return fmt.Errorf("Error updating diagnostic logs config for App Service %q (Resource Group %q): %+s", name, resGroup, err) + } + return resourceArmAppServiceUpdate(d, meta) } @@ -365,6 +377,19 @@ func resourceArmAppServiceUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("logs") { + logs := azure.ExpandAppServiceLogs(d.Get("logs")) + id := d.Id() + logsResource := web.SiteLogsConfig{ + ID: &id, + SiteLogsConfigProperties: &logs, + } + + if _, err := client.UpdateDiagnosticLogsConfig(ctx, resGroup, name, logsResource); err != nil { + return fmt.Errorf("Error updating Diagnostics Logs for App Service %q: %+v", name, err) + } + } + if d.HasChange("client_affinity_enabled") { affinity := d.Get("client_affinity_enabled").(bool) @@ -465,6 +490,11 @@ func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error retrieving the AuthSettings for App Service %q (Resource Group %q): %+v", name, resGroup, err) } + logsResp, err := client.GetDiagnosticLogsConfiguration(ctx, resGroup, name) + if err != nil { + return fmt.Errorf("Error retrieving the DiagnosticsLogsConfiguration for App Service %q (Resource Group %q): %+v", name, resGroup, err) + } + appSettingsResp, err := client.ListApplicationSettings(ctx, resGroup, name) if err != nil { if utils.ResponseWasNotFound(appSettingsResp.Response) { @@ -515,7 +545,13 @@ func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error { d.Set("possible_outbound_ip_addresses", props.PossibleOutboundIPAddresses) } - if err := d.Set("app_settings", flattenAppServiceAppSettings(appSettingsResp.Properties)); err != nil { + appSettings := flattenAppServiceAppSettings(appSettingsResp.Properties) + + // remove DIAGNOSTICS* settings - Azure will sync these, so just maintain the logs block equivalents in the state + delete(appSettings, "DIAGNOSTICS_AZUREBLOBCONTAINERSASURL") + delete(appSettings, "DIAGNOSTICS_AZUREBLOBRETENTIONINDAYS") + + if err := d.Set("app_settings", appSettings); err != nil { return err } @@ -533,6 +569,11 @@ func resourceArmAppServiceRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error setting `auth_settings`: %s", err) } + logs := azure.FlattenAppServiceLogs(logsResp.SiteLogsConfigProperties) + if err := d.Set("logs", logs); err != nil { + return fmt.Errorf("Error setting `logs`: %s", err) + } + scm := flattenAppServiceSourceControl(scmResp.SiteSourceControlProperties) if err := d.Set("source_control", scm); err != nil { return err diff --git a/azurerm/resource_arm_app_service_test.go b/azurerm/resource_arm_app_service_test.go index ffa1927281c4..033fe5de6a65 100644 --- a/azurerm/resource_arm_app_service_test.go +++ b/azurerm/resource_arm_app_service_test.go @@ -780,6 +780,34 @@ func TestAccAzureRMAppService_localMySql(t *testing.T) { }) } +func TestAccAzureRMAppService_applicationBlobStorageLogs(t *testing.T) { + resourceName := "azurerm_app_service.test" + ri := tf.AccRandTimeInt() + config := testAccAzureRMAppService_applicationBlobStorageLogs(ri, testLocation()) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "logs.0.application_logs.0.azure_blob_storage.0.level", "Information"), + resource.TestCheckResourceAttr(resourceName, "logs.0.application_logs.0.azure_blob_storage.0.sas_url", "http://x.com/"), + resource.TestCheckResourceAttr(resourceName, "logs.0.application_logs.0.azure_blob_storage.0.retention_in_days", "3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccAzureRMAppService_managedPipelineMode(t *testing.T) { resourceName := "azurerm_app_service.test" ri := tf.AccRandTimeInt() @@ -2632,6 +2660,43 @@ resource "azurerm_app_service" "test" { `, rInt, location, rInt, rInt) } +func testAccAzureRMAppService_applicationBlobStorageLogs(rInt int, location string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_app_service_plan" "test" { + name = "acctestASP-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + + sku { + tier = "Standard" + size = "S1" + } +} + +resource "azurerm_app_service" "test" { + name = "acctestAS-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + app_service_plan_id = "${azurerm_app_service_plan.test.id}" + + logs { + application_logs { + azure_blob_storage { + level = "Information" + sas_url = "http://x.com/" + retention_in_days = 3 + } + } + } +} +`, rInt, location, rInt, rInt) +} + func testAccAzureRMAppService_managedPipelineMode(rInt int, location string) string { return fmt.Sprintf(` resource "azurerm_resource_group" "test" { diff --git a/website/docs/r/app_service.html.markdown b/website/docs/r/app_service.html.markdown index c86f45850c1d..bdeb19b653fa 100644 --- a/website/docs/r/app_service.html.markdown +++ b/website/docs/r/app_service.html.markdown @@ -83,6 +83,8 @@ The following arguments are supported: * `https_only` - (Optional) Can the App Service only be accessed via HTTPS? Defaults to `false`. +* `logs` - (Optional) A `logs` block as defined below. + * `site_config` - (Optional) A `site_config` block as defined below. * `tags` - (Optional) A mapping of tags to assign to the resource. @@ -109,6 +111,28 @@ A `identity` block supports the following: --- +A `logs` block supports the following: + +* `application_logs` - (Optional) An `application_logs` block as defined below. + +--- + +An `application_logs` block supports the following: + +* `azure_blob_storage` - (Optional) An `azure_blob_storage` block as defined below. + +--- + +An `azure_blob_storage` block supports the following: + +* `level` - (Required) The level at which to log. Possible values include `Error`, `Warning`, `Information`, `Verbose` and `Off`. + +* `sas_url` - (Required) The URL to the storage container, with a Service SAS token appended. **NOTE:** there is currently no means of generating Service SAS tokens with the `azurerm` provider. + +* `retention_in_days` - (Required) The number of days to retain logs for. + +--- + A `site_config` block supports the following: * `always_on` - (Optional) Should the app be loaded at all times? Defaults to `false`.