diff --git a/internal/services/appservice/function_app_hybrid_connection_resource.go b/internal/services/appservice/function_app_hybrid_connection_resource.go index 631b3ea2ee5d..87b9c2b9879f 100644 --- a/internal/services/appservice/function_app_hybrid_connection_resource.go +++ b/internal/services/appservice/function_app_hybrid_connection_resource.go @@ -337,7 +337,7 @@ func (r FunctionAppHybridConnectionResource) CustomImporter() sdk.ResourceRunFun return err } - if helpers.PlanIsConsumption(*sku) || helpers.PlanIsElastic(*sku) { + if helpers.PlanIsConsumption(sku) || helpers.PlanIsElastic(sku) { return fmt.Errorf("unsupported plan type. Hybrid Connections are not supported on Consumption or Elastic service plans") } diff --git a/internal/services/appservice/helpers/function_app_schema.go b/internal/services/appservice/helpers/function_app_schema.go index 83e711de146e..292e88c2e62a 100644 --- a/internal/services/appservice/helpers/function_app_schema.go +++ b/internal/services/appservice/helpers/function_app_schema.go @@ -2153,3 +2153,22 @@ func FlattenFunctionAppAppServiceLogs(input web.SiteLogsConfig) []FunctionAppApp return []FunctionAppAppServiceLogs{} } + +func ParseContentSettings(input web.StringDictionary, existing map[string]string) map[string]string { + if input.Properties == nil { + return nil + } + + out := existing + for k, v := range input.Properties { + switch k { + case "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": + out[k] = utils.NormalizeNilableString(v) + + case "WEBSITE_CONTENTSHARE": + out[k] = utils.NormalizeNilableString(v) + } + } + + return out +} diff --git a/internal/services/appservice/helpers/service_plan.go b/internal/services/appservice/helpers/service_plan.go index 727d69a160a5..3e0e0eb01135 100644 --- a/internal/services/appservice/helpers/service_plan.go +++ b/internal/services/appservice/helpers/service_plan.go @@ -65,9 +65,12 @@ func AllKnownServicePlanSkus() []string { return allSkus } -func PlanIsConsumption(input string) bool { +func PlanIsConsumption(input *string) bool { + if input == nil { + return false + } for _, v := range consumptionSkus { - if strings.EqualFold(input, v) { + if strings.EqualFold(*input, v) { return true } } @@ -75,9 +78,12 @@ func PlanIsConsumption(input string) bool { return false } -func PlanIsElastic(input string) bool { +func PlanIsElastic(input *string) bool { + if input == nil { + return false + } for _, v := range elasticSkus { - if strings.EqualFold(input, v) { + if strings.EqualFold(*input, v) { return true } } @@ -85,9 +91,12 @@ func PlanIsElastic(input string) bool { return false } -func PlanIsIsolated(input string) bool { +func PlanIsIsolated(input *string) bool { + if input == nil { + return false + } for _, v := range isolatedSkus { - if strings.EqualFold(input, v) { + if strings.EqualFold(*input, v) { return true } } @@ -95,9 +104,12 @@ func PlanIsIsolated(input string) bool { return false } -func PlanIsAppPlan(input string) bool { +func PlanIsAppPlan(input *string) bool { + if input == nil { + return false + } for _, v := range appServicePlanSkus { - if strings.EqualFold(input, v) { + if strings.EqualFold(*input, v) { return true } } @@ -106,19 +118,19 @@ func PlanIsAppPlan(input string) bool { } func PlanTypeFromSku(input string) string { - if PlanIsConsumption(input) { + if PlanIsConsumption(&input) { return ServicePlanTypeConsumption } - if PlanIsElastic(input) { + if PlanIsElastic(&input) { return ServicePlanTypeElastic } - if PlanIsIsolated(input) { + if PlanIsIsolated(&input) { return ServicePlanTypeIsolated } - if PlanIsAppPlan(input) { + if PlanIsAppPlan(&input) { return ServicePlanTypeAppPlan } @@ -131,12 +143,17 @@ func ServicePlanInfoForApp(ctx context.Context, metadata sdk.ResourceMetaData, i servicePlanClient := metadata.Client.AppService.ServicePlanClient var rg, siteName string - if appId, ok := id.(parse.WebAppId); ok { + switch appId := id.(type) { + case parse.WebAppId: rg = appId.ResourceGroup siteName = appId.SiteName - } - - if appId, ok := id.(parse.FunctionAppId); ok { + case parse.WebAppSlotId: + rg = appId.ResourceGroup + siteName = appId.SiteName + case parse.FunctionAppId: + rg = appId.ResourceGroup + siteName = appId.SiteName + case parse.FunctionAppSlotId: rg = appId.ResourceGroup siteName = appId.SiteName } diff --git a/internal/services/appservice/helpers/service_plan_test.go b/internal/services/appservice/helpers/service_plan_test.go index 13a7bd7b50b3..95cbb1cf722c 100644 --- a/internal/services/appservice/helpers/service_plan_test.go +++ b/internal/services/appservice/helpers/service_plan_test.go @@ -4,148 +4,149 @@ import ( "testing" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/helpers" + "github.com/hashicorp/terraform-provider-azurerm/utils" ) func TestPlanIsConsumption(t *testing.T) { input := []struct { - name string + name *string isConsumption bool }{ { - name: "", + name: utils.String(""), isConsumption: false, }, { - name: "Y1", + name: utils.String("Y1"), isConsumption: true, }, { - name: "EP1", + name: utils.String("EP1"), isConsumption: false, }, { - name: "S1", + name: utils.String("S1"), isConsumption: false, }, } for _, v := range input { if actual := helpers.PlanIsConsumption(v.name); actual != v.isConsumption { - t.Fatalf("expected %s to be %t, got %t", v.name, v.isConsumption, actual) + t.Fatalf("expected %s to be %t, got %t", *v.name, v.isConsumption, actual) } } } func TestPlanIsElastic(t *testing.T) { input := []struct { - name string + name *string isElastic bool }{ { - name: "", + name: utils.String(""), isElastic: false, }, { - name: "Y1", + name: utils.String("Y1"), isElastic: false, }, { - name: "EP1", + name: utils.String("EP1"), isElastic: true, }, { - name: "S1", + name: utils.String("S1"), isElastic: false, }, } for _, v := range input { if actual := helpers.PlanIsElastic(v.name); actual != v.isElastic { - t.Fatalf("expected %s to be %t, got %t", v.name, v.isElastic, actual) + t.Fatalf("expected %s to be %t, got %t", *v.name, v.isElastic, actual) } } } func TestPlanIsIsolated(t *testing.T) { input := []struct { - name string + name *string isIsolated bool }{ { - name: "", + name: utils.String(""), isIsolated: false, }, { - name: "Y1", + name: utils.String("Y1"), isIsolated: false, }, { - name: "EP1", + name: utils.String("EP1"), isIsolated: false, }, { - name: "S1", + name: utils.String("S1"), isIsolated: false, }, { - name: "I1", + name: utils.String("I1"), isIsolated: true, }, { - name: "I1v2", + name: utils.String("I1v2"), isIsolated: true, }, } for _, v := range input { if actual := helpers.PlanIsIsolated(v.name); actual != v.isIsolated { - t.Fatalf("expected %s to be %t, got %t", v.name, v.isIsolated, actual) + t.Fatalf("expected %s to be %t, got %t", *v.name, v.isIsolated, actual) } } } func TestPlanIsAppPlan(t *testing.T) { input := []struct { - name string + name *string isAppPlan bool }{ { - name: "", + name: utils.String(""), isAppPlan: false, }, { - name: "Y1", + name: utils.String("Y1"), isAppPlan: false, }, { - name: "EP1", + name: utils.String("EP1"), isAppPlan: false, }, { - name: "B1", + name: utils.String("B1"), isAppPlan: true, }, { - name: "S1", + name: utils.String("S1"), isAppPlan: true, }, { - name: "P1v3", + name: utils.String("P1v3"), isAppPlan: true, }, { - name: "I1", + name: utils.String("I1"), isAppPlan: false, }, { - name: "I1v2", + name: utils.String("I1v2"), isAppPlan: false, }, } for _, v := range input { if actual := helpers.PlanIsAppPlan(v.name); actual != v.isAppPlan { - t.Fatalf("expected %s to be %t, got %t", v.name, v.isAppPlan, actual) + t.Fatalf("expected %s to be %t, got %t", *v.name, v.isAppPlan, actual) } } } diff --git a/internal/services/appservice/linux_function_app_resource.go b/internal/services/appservice/linux_function_app_resource.go index b13c6a8bf8cb..ebee788ba30f 100644 --- a/internal/services/appservice/linux_function_app_resource.go +++ b/internal/services/appservice/linux_function_app_resource.go @@ -658,6 +658,14 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc { return fmt.Errorf("reading Linux %s: %v", id, err) } + _, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id) + if err != nil { + return err + } + + // Only send for ElasticPremium + sendContentSettings := helpers.PlanIsElastic(planSKU) + // Some service plan updates are allowed - see customiseDiff for exceptions if metadata.ResourceData.HasChange("service_plan_id") { existing.SiteProperties.ServerFarmID = utils.String(state.ServicePlanId) @@ -704,13 +712,28 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc { } } + if sendContentSettings { + appSettingsResp, err := client.ListApplicationSettings(ctx, id.ResourceGroup, id.SiteName) + if err != nil { + return fmt.Errorf("reading App Settings for Windows %s: %+v", id, err) + } + if state.AppSettings == nil { + state.AppSettings = make(map[string]string) + } + state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings) + } + // Note: We process this regardless to give us a "clean" view of service-side app_settings, so we can reconcile the user-defined entries later siteConfig, err := helpers.ExpandSiteConfigLinuxFunctionApp(state.SiteConfig, existing.SiteConfig, metadata, state.FunctionExtensionsVersion, storageString, state.StorageUsesMSI) if state.BuiltinLogging { if state.AppSettings == nil && !state.StorageUsesMSI { state.AppSettings = make(map[string]string) } - state.AppSettings["AzureWebJobsDashboard"] = storageString + if !state.StorageUsesMSI { + state.AppSettings["AzureWebJobsDashboard"] = storageString + } else { + state.AppSettings["AzureWebJobsDashboard__accountName"] = state.StorageAccountName + } } if metadata.ResourceData.HasChange("site_config") { diff --git a/internal/services/appservice/linux_function_app_resource_test.go b/internal/services/appservice/linux_function_app_resource_test.go index becfe5a5947b..8259d247337f 100644 --- a/internal/services/appservice/linux_function_app_resource_test.go +++ b/internal/services/appservice/linux_function_app_resource_test.go @@ -211,6 +211,32 @@ func TestAccLinuxFunctionApp_withAppSettingsStandardPlan(t *testing.T) { }) } +func TestAccLinuxFunctionApp_withAppSettingsUserSettingUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") + r := LinuxFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettingsUserSettings(data, SkuElasticPremiumPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + check.That(data.ResourceName).Key("app_settings.%").DoesNotExist(), + ), + }, + data.ImportStep(), + { + Config: r.appSettingsUserSettingsUpdate(data, SkuElasticPremiumPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + check.That(data.ResourceName).Key("app_settings.%").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + // backup by plan type func TestAccLinuxFunctionApp_withBackupElasticPremiumPlan(t *testing.T) { @@ -1118,6 +1144,57 @@ resource "azurerm_linux_function_app" "test" { `, r.template(data, planSku), data.RandomInteger) } +func (r LinuxFunctionAppResource) appSettingsUserSettings(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_linux_function_app" "test" { + name = "acctest-LFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = {} + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + +func (r LinuxFunctionAppResource) appSettingsUserSettingsUpdate(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_linux_function_app" "test" { + name = "acctest-LFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = { + foo = "bar" + secret = "sauce" + } + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + func (r LinuxFunctionAppResource) appSettingsCustomContentShare(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -2496,6 +2573,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.template(data, planSku)) diff --git a/internal/services/appservice/linux_function_app_slot_resource.go b/internal/services/appservice/linux_function_app_slot_resource.go index 404ddde5fcfe..24c32cab9805 100644 --- a/internal/services/appservice/linux_function_app_slot_resource.go +++ b/internal/services/appservice/linux_function_app_slot_resource.go @@ -655,6 +655,13 @@ func (r LinuxFunctionAppSlotResource) Update() sdk.ResourceFunc { return fmt.Errorf("reading Linux %s: %v", id, err) } + _, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id) + if err != nil { + return err + } + + sendContentSettings := !helpers.PlanIsElastic(planSKU) + if metadata.ResourceData.HasChange("enabled") { existing.SiteProperties.Enabled = utils.Bool(state.Enabled) } @@ -696,13 +703,29 @@ func (r LinuxFunctionAppSlotResource) Update() sdk.ResourceFunc { } } + if sendContentSettings { + appSettingsResp, err := client.ListApplicationSettings(ctx, id.ResourceGroup, id.SiteName) + if err != nil { + return fmt.Errorf("reading App Settings for Windows %s: %+v", id, err) + } + if state.AppSettings == nil { + state.AppSettings = make(map[string]string) + } + state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings) + } + // Note: We process this regardless to give us a "clean" view of service-side app_settings, so we can reconcile the user-defined entries later siteConfig, err := helpers.ExpandSiteConfigLinuxFunctionAppSlot(state.SiteConfig, existing.SiteConfig, metadata, state.FunctionExtensionsVersion, storageString, state.StorageUsesMSI) if state.BuiltinLogging { if state.AppSettings == nil && !state.StorageUsesMSI { state.AppSettings = make(map[string]string) } - state.AppSettings["AzureWebJobsDashboard"] = storageString + if !state.StorageUsesMSI { + state.AppSettings["AzureWebJobsDashboard"] = storageString + } else { + state.AppSettings["AzureWebJobsDashboard__accountName"] = state.StorageAccountName + } + } if metadata.ResourceData.HasChange("site_config") { diff --git a/internal/services/appservice/linux_function_app_slot_resource_test.go b/internal/services/appservice/linux_function_app_slot_resource_test.go index 7f0ec58d47b9..1c8ad8e76dde 100644 --- a/internal/services/appservice/linux_function_app_slot_resource_test.go +++ b/internal/services/appservice/linux_function_app_slot_resource_test.go @@ -170,6 +170,32 @@ func TestAccLinuxFunctionAppSlot_withAppSettingsStandardPlan(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlot_withAppSettingsUserSettingUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettingsUserSettings(data, SkuElasticPremiumPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + check.That(data.ResourceName).Key("app_settings.%").DoesNotExist(), + ), + }, + data.ImportStep(), + { + Config: r.appSettingsUserSettingsUpdate(data, SkuElasticPremiumPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + check.That(data.ResourceName).Key("app_settings.%").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + // backup by plan type func TestAccLinuxFunctionAppSlot_withBackupElasticPremiumPlan(t *testing.T) { @@ -1021,6 +1047,51 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger) } +func (r LinuxFunctionAppSlotResource) appSettingsUserSettings(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_linux_function_app_slot" "test" { + name = "acctest-LFAS-%d" + function_app_id = azurerm_linux_function_app.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = {} + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + +func (r LinuxFunctionAppSlotResource) appSettingsUserSettingsUpdate(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_linux_function_app_slot" "test" { + name = "acctest-LFAS-%d" + function_app_id = azurerm_linux_function_app.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = { + foo = "bar" + secret = "sauce" + } + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + func (r LinuxFunctionAppSlotResource) connectionStrings(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -2262,6 +2333,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.template(data, planSku)) diff --git a/internal/services/appservice/linux_web_app_resource_test.go b/internal/services/appservice/linux_web_app_resource_test.go index 2d600da2df50..edbfe48a4bd3 100644 --- a/internal/services/appservice/linux_web_app_resource_test.go +++ b/internal/services/appservice/linux_web_app_resource_test.go @@ -2512,6 +2512,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.standardPlanTemplate(data), data.RandomInteger, data.RandomString) diff --git a/internal/services/appservice/linux_web_app_slot_resource_test.go b/internal/services/appservice/linux_web_app_slot_resource_test.go index ff5a89dd2d18..cf6fbda9149c 100644 --- a/internal/services/appservice/linux_web_app_slot_resource_test.go +++ b/internal/services/appservice/linux_web_app_slot_resource_test.go @@ -1988,6 +1988,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.baseTemplate(data), data.RandomInteger, data.RandomString) diff --git a/internal/services/appservice/web_app_hybrid_connection_resource.go b/internal/services/appservice/web_app_hybrid_connection_resource.go index 49ff9519dfcf..ca34a974147d 100644 --- a/internal/services/appservice/web_app_hybrid_connection_resource.go +++ b/internal/services/appservice/web_app_hybrid_connection_resource.go @@ -336,7 +336,7 @@ func (r WebAppHybridConnectionResource) CustomImporter() sdk.ResourceRunFunc { return err } - if helpers.PlanIsConsumption(*sku) || helpers.PlanIsElastic(*sku) { + if helpers.PlanIsConsumption(sku) || helpers.PlanIsElastic(sku) { return fmt.Errorf("unsupported plan type. Hybrid Connections are not supported on Consumption or Elastic service plans") } diff --git a/internal/services/appservice/windows_function_app_resource.go b/internal/services/appservice/windows_function_app_resource.go index 69bc965f66e8..7bc061c34f1d 100644 --- a/internal/services/appservice/windows_function_app_resource.go +++ b/internal/services/appservice/windows_function_app_resource.go @@ -657,6 +657,12 @@ func (r WindowsFunctionAppResource) Update() sdk.ResourceFunc { return fmt.Errorf("reading Windows %s: %v", id, err) } + _, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id) + if err != nil { + return err + } + sendContentSettings := !helpers.PlanIsAppPlan(planSKU) + // Some service plan updates are allowed - see customiseDiff for exceptions if metadata.ResourceData.HasChange("service_plan_id") { existing.SiteProperties.ServerFarmID = utils.String(state.ServicePlanId) @@ -703,13 +709,28 @@ func (r WindowsFunctionAppResource) Update() sdk.ResourceFunc { } } + if sendContentSettings { + appSettingsResp, err := client.ListApplicationSettings(ctx, id.ResourceGroup, id.SiteName) + if err != nil { + return fmt.Errorf("reading App Settings for Windows %s: %+v", id, err) + } + if state.AppSettings == nil { + state.AppSettings = make(map[string]string) + } + state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings) + } + // Note: We process this regardless to give us a "clean" view of service-side app_settings, so we can reconcile the user-defined entries later siteConfig, err := helpers.ExpandSiteConfigWindowsFunctionApp(state.SiteConfig, existing.SiteConfig, metadata, state.FunctionExtensionsVersion, storageString, state.StorageUsesMSI) if state.BuiltinLogging { if state.AppSettings == nil && !state.StorageUsesMSI { state.AppSettings = make(map[string]string) } - state.AppSettings["AzureWebJobsDashboard"] = storageString + if !state.StorageUsesMSI { + state.AppSettings["AzureWebJobsDashboard"] = storageString + } else { + state.AppSettings["AzureWebJobsDashboard__accountName"] = state.StorageAccountName + } } if metadata.ResourceData.HasChange("site_config") { diff --git a/internal/services/appservice/windows_function_app_resource_test.go b/internal/services/appservice/windows_function_app_resource_test.go index a049f87b7049..95e82fd97435 100644 --- a/internal/services/appservice/windows_function_app_resource_test.go +++ b/internal/services/appservice/windows_function_app_resource_test.go @@ -202,6 +202,32 @@ func TestAccWindowsFunctionApp_withAppSettingsStandardPlan(t *testing.T) { }) } +func TestAccWindowsFunctionApp_withAppSettingsUserSettingUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_function_app", "test") + r := WindowsFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettingsUserSettings(data, SkuConsumptionPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp"), + check.That(data.ResourceName).Key("app_settings.%").DoesNotExist(), + ), + }, + data.ImportStep(), + { + Config: r.appSettingsUserSettingsUpdate(data, SkuConsumptionPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp"), + check.That(data.ResourceName).Key("app_settings.%").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + // backup by plan type func TestAccWindowsFunctionApp_withBackupElasticPremiumPlan(t *testing.T) { @@ -940,6 +966,57 @@ resource "azurerm_windows_function_app" "test" { `, r.template(data, planSku), data.RandomInteger) } +func (r WindowsFunctionAppResource) appSettingsUserSettings(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_windows_function_app" "test" { + name = "acctest-WFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = {} + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + +func (r WindowsFunctionAppResource) appSettingsUserSettingsUpdate(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_windows_function_app" "test" { + name = "acctest-WFA-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = { + foo = "bar" + secret = "sauce" + } + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + func (r WindowsFunctionAppResource) appSettingsCustomContentShare(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -2238,6 +2315,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } diff --git a/internal/services/appservice/windows_function_app_slot_resource.go b/internal/services/appservice/windows_function_app_slot_resource.go index a624dfdd960c..8b7b95531a04 100644 --- a/internal/services/appservice/windows_function_app_slot_resource.go +++ b/internal/services/appservice/windows_function_app_slot_resource.go @@ -661,6 +661,12 @@ func (r WindowsFunctionAppSlotResource) Update() sdk.ResourceFunc { return fmt.Errorf("reading Windows %s: %v", id, err) } + _, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id) + if err != nil { + return err + } + sendContentSettings := !helpers.PlanIsAppPlan(planSKU) + // Some service plan updates are allowed - see customiseDiff for exceptions if metadata.ResourceData.HasChange("enabled") { existing.SiteProperties.Enabled = utils.Bool(state.Enabled) @@ -703,13 +709,28 @@ func (r WindowsFunctionAppSlotResource) Update() sdk.ResourceFunc { } } + if sendContentSettings { + appSettingsResp, err := client.ListApplicationSettings(ctx, id.ResourceGroup, id.SiteName) + if err != nil { + return fmt.Errorf("reading App Settings for Windows %s: %+v", id, err) + } + if state.AppSettings == nil { + state.AppSettings = make(map[string]string) + } + state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings) + } + // Note: We process this regardless to give us a "clean" view of service-side app_settings, so we can reconcile the user-defined entries later siteConfig, err := helpers.ExpandSiteConfigWindowsFunctionAppSlot(state.SiteConfig, existing.SiteConfig, metadata, state.FunctionExtensionsVersion, storageString, state.StorageUsesMSI) if state.BuiltinLogging { if state.AppSettings == nil && !state.StorageUsesMSI { state.AppSettings = make(map[string]string) } - state.AppSettings["AzureWebJobsDashboard"] = storageString + if !state.StorageUsesMSI { + state.AppSettings["AzureWebJobsDashboard"] = storageString + } else { + state.AppSettings["AzureWebJobsDashboard__accountName"] = state.StorageAccountName + } } if metadata.ResourceData.HasChange("site_config") { diff --git a/internal/services/appservice/windows_function_app_slot_resource_test.go b/internal/services/appservice/windows_function_app_slot_resource_test.go index 193463b11f8c..2d2f1e171451 100644 --- a/internal/services/appservice/windows_function_app_slot_resource_test.go +++ b/internal/services/appservice/windows_function_app_slot_resource_test.go @@ -170,6 +170,32 @@ func TestAccWindowsFunctionAppSlot_withCustomContentShareElasticPremiumPlan(t *t }) } +func TestAccWindowsFunctionAppSlot_withAppSettingsUserSettingUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_function_app_slot", "test") + r := WindowsFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettingsUserSettings(data, SkuConsumptionPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp"), + check.That(data.ResourceName).Key("app_settings.%").DoesNotExist(), + ), + }, + data.ImportStep(), + { + Config: r.appSettingsUserSettingsUpdate(data, SkuConsumptionPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp"), + check.That(data.ResourceName).Key("app_settings.%").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + // backup by plan type func TestAccWindowsFunctionAppSlot_withBackupElasticPremiumPlan(t *testing.T) { @@ -582,7 +608,7 @@ func TestAccWindowsFunctionAppSlot_appStackNode(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.appStackNode(data, SkuStandardPlan, "14"), + Config: r.appStackNode(data, SkuStandardPlan, "~14"), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("kind").HasValue("functionapp"), @@ -598,7 +624,7 @@ func TestAccWindowsFunctionAppSlot_appStackNodeUpdate(t *testing.T) { data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.appStackNode(data, SkuStandardPlan, "12"), + Config: r.appStackNode(data, SkuStandardPlan, "~12"), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("kind").HasValue("functionapp"), @@ -606,7 +632,7 @@ func TestAccWindowsFunctionAppSlot_appStackNodeUpdate(t *testing.T) { }, data.ImportStep(), { - Config: r.appStackNode(data, SkuStandardPlan, "14"), + Config: r.appStackNode(data, SkuStandardPlan, "~14"), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("kind").HasValue("functionapp"), @@ -887,6 +913,51 @@ resource "azurerm_windows_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger) } +func (r WindowsFunctionAppSlotResource) appSettingsUserSettings(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_windows_function_app_slot" "test" { + name = "acctest-WFAS-%d" + function_app_id = azurerm_windows_function_app.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = {} + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + +func (r WindowsFunctionAppSlotResource) appSettingsUserSettingsUpdate(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_windows_function_app_slot" "test" { + name = "acctest-WFAS-%d" + function_app_id = azurerm_windows_function_app.test.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + app_settings = { + foo = "bar" + secret = "sauce" + } + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger) +} + func (r WindowsFunctionAppSlotResource) appSettingsCustomContentShare(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -2087,6 +2158,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } diff --git a/internal/services/appservice/windows_web_app_resource_test.go b/internal/services/appservice/windows_web_app_resource_test.go index eadd91bc03dc..e86909f06036 100644 --- a/internal/services/appservice/windows_web_app_resource_test.go +++ b/internal/services/appservice/windows_web_app_resource_test.go @@ -2447,6 +2447,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.baseTemplate(data), data.RandomInteger, data.RandomString) diff --git a/internal/services/appservice/windows_web_app_slot_resource_test.go b/internal/services/appservice/windows_web_app_slot_resource_test.go index 819560b313f3..8b8555971cca 100644 --- a/internal/services/appservice/windows_web_app_slot_resource_test.go +++ b/internal/services/appservice/windows_web_app_slot_resource_test.go @@ -1820,6 +1820,8 @@ data "azurerm_storage_account_sas" "test" { create = false update = false process = false + tag = false + filter = false } } `, r.baseTemplate(data), data.RandomInteger, data.RandomString)