diff --git a/internal/services/appservice/linux_function_app_resource.go b/internal/services/appservice/linux_function_app_resource.go index 0403d7a2c6cf..4951a32c53a9 100644 --- a/internal/services/appservice/linux_function_app_resource.go +++ b/internal/services/appservice/linux_function_app_resource.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/resourceproviders" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/webapps" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/helpers" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/migration" @@ -71,6 +72,8 @@ type LinuxFunctionAppModel struct { PublishingFTPBasicAuthEnabled bool `tfschema:"ftp_publish_basic_authentication_enabled"` Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` + // VnetImagePullEnabled bool `tfschema:"vnet_image_pull_enabled"` // TODO 4.0 not supported on Consumption plans + // Computed CustomDomainVerificationId string `tfschema:"custom_domain_verification_id"` DefaultHostname string `tfschema:"default_hostname"` @@ -105,7 +108,7 @@ func (r LinuxFunctionAppResource) IDValidationFunc() pluginsdk.SchemaValidateFun } func (r LinuxFunctionAppResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -305,6 +308,15 @@ func (r LinuxFunctionAppResource) Arguments() map[string]*pluginsdk.Schema { Description: "The local path and filename of the Zip packaged application to deploy to this Linux Function App. **Note:** Using this value requires either `WEBSITE_RUN_FROM_PACKAGE=1` or `SCM_DO_BUILD_DURING_DEPLOYMENT=true` to be set on the App in `app_settings`.", }, } + if features.FourPointOhBeta() { + s["vnet_image_pull_enabled"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Is container image pull over virtual network enabled? Defaults to `false`.", + } + } + return s } func (r LinuxFunctionAppResource) Attributes() map[string]*pluginsdk.Schema { @@ -425,6 +437,11 @@ func (r LinuxFunctionAppResource) Create() sdk.ResourceFunc { availabilityRequest.Name = fmt.Sprintf("%s.%s", functionApp.Name, nameSuffix) availabilityRequest.IsFqdn = pointer.To(true) + if features.FourPointOhBeta() { + if !metadata.ResourceData.Get("vnet_image_pull_enabled").(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } } } // Only send for ElasticPremium and Consumption plan @@ -525,6 +542,9 @@ func (r LinuxFunctionAppResource) Create() sdk.ResourceFunc { VnetRouteAllEnabled: siteConfig.VnetRouteAllEnabled, }, } + if features.FourPointOhBeta() { + siteEnvelope.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } pna := helpers.PublicNetworkAccessEnabled if !functionApp.PublicNetworkAccess { @@ -765,6 +785,9 @@ func (r LinuxFunctionAppResource) Read() sdk.ResourceFunc { state.DefaultHostname = pointer.From(props.DefaultHostName) state.PublicNetworkAccess = !strings.EqualFold(pointer.From(props.PublicNetworkAccess), helpers.PublicNetworkAccessDisabled) + if features.FourPointOhBeta() { + metadata.ResourceData.Set("vnet_image_pull_enabled", pointer.From(props.VnetImagePullEnabled)) + } servicePlanId, err := commonids.ParseAppServicePlanIDInsensitively(*props.ServerFarmId) if err != nil { return err @@ -930,6 +953,10 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc { } } + if metadata.ResourceData.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + model.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + if metadata.ResourceData.HasChange("client_certificate_enabled") { model.Properties.ClientCertEnabled = pointer.To(state.ClientCertEnabled) } @@ -1211,6 +1238,29 @@ func (r LinuxFunctionAppResource) CustomizeDiff() sdk.ResourceFunc { Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { client := metadata.Client.AppService.ServicePlanClient rd := metadata.ResourceDiff + if rd.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + planId := rd.Get("service_plan_id") + // the plan id is known after apply during the initial creation + if planId.(string) == "" { + return nil + } + _, newValue := rd.GetChange("vnet_image_pull_enabled") + servicePlanId, err := commonids.ParseAppServicePlanID(planId.(string)) + if err != nil { + return err + } + + asp, err := client.Get(ctx, *servicePlanId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", servicePlanId, err) + } + if aspModel := asp.Model; aspModel != nil { + if aspModel.Properties != nil && aspModel.Properties.HostingEnvironmentProfile != nil && + aspModel.Properties.HostingEnvironmentProfile.Id != nil && *(aspModel.Properties.HostingEnvironmentProfile.Id) != "" && !newValue.(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } + } if rd.HasChange("service_plan_id") { currentPlanIdRaw, newPlanIdRaw := rd.GetChange("service_plan_id") if newPlanIdRaw.(string) == "" { diff --git a/internal/services/appservice/linux_function_app_resource_test.go b/internal/services/appservice/linux_function_app_resource_test.go index 7decb0cab24d..7a7f1a253926 100644 --- a/internal/services/appservice/linux_function_app_resource_test.go +++ b/internal/services/appservice/linux_function_app_resource_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" ) @@ -604,6 +605,26 @@ func TestAccLinuxFunctionApp_consumptionCompleteUpdate(t *testing.T) { }) } +func TestAccLinuxFunctionApp_elasticPremiumCompleteWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") + r := LinuxFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.elasticCompleteWithVnetProperties(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("site_config.0.elastic_instance_minimum").HasValue("5"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionApp_elasticPremiumComplete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") r := LinuxFunctionAppResource{} @@ -1549,6 +1570,7 @@ func TestAccLinuxFunctionApp_storageAccountKeyVaultSecretVersionless(t *testing. }) } +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppASEv3_basic(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") r := LinuxFunctionAppResource{} @@ -1564,6 +1586,24 @@ func TestAccLinuxFunctionAppASEv3_basic(t *testing.T) { }) } +func TestAccLinuxFunctionAppASEv3_basicWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") + r := LinuxFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withASEV3VnetProperties(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + func TestAccLinuxFunctionApp_corsUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") r := LinuxFunctionAppResource{} @@ -1693,13 +1733,41 @@ func TestAccLinuxFunctionApp_basicPlanBackupShouldError(t *testing.T) { }) } +func TestAccLinuxFunctionApp_vNetIntegrationWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") + r := LinuxFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.vNetIntegration_subnetWithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( + check.That("azurerm_subnet.test1").Key("id"), + ), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionApp_vNetIntegration(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") r := LinuxFunctionAppResource{} + var vnetIntegrationProperties string + if features.FourPointOhBeta() { + vnetIntegrationProperties = r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan) + } else { + vnetIntegrationProperties = r.vNetIntegration_subnet1(data, SkuStandardPlan) + } data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.vNetIntegration_subnet1(data, SkuStandardPlan), + Config: vnetIntegrationProperties, Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( @@ -1711,6 +1779,7 @@ func TestAccLinuxFunctionApp_vNetIntegration(t *testing.T) { }) } +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionApp_vNetIntegrationUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") r := LinuxFunctionAppResource{} @@ -1753,6 +1822,48 @@ func TestAccLinuxFunctionApp_vNetIntegrationUpdate(t *testing.T) { }) } +func TestAccLinuxFunctionApp_vNetIntegrationUpdateWithVnetProperties(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_function_app", "test") + r := LinuxFunctionAppResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.vNetIntegration_basic(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( + check.That("azurerm_subnet.test1").Key("id"), + ), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_subnet2WithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( + check.That("azurerm_subnet.test2").Key("id"), + ), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_basic(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + // Outputs func TestAccLinuxFunctionApp_basicOutputs(t *testing.T) { @@ -3344,6 +3455,151 @@ resource "azurerm_linux_function_app" "test" { `, r.storageContainerTemplate(data, planSku), data.RandomInteger, data.Client().TenantID) } +func (r LinuxFunctionAppResource) elasticCompleteWithVnetProperties(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_user_assigned_identity" "test" { + name = "acct-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_application_insights" "test" { + name = "acctestappinsights-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "web" +} + +resource "azurerm_linux_function_app" "test" { + name = "acctest-LFA-%[2]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" + } + + backup { + name = "acctest" + storage_account_url = "https://${azurerm_storage_account.test.name}.blob.core.windows.net/${azurerm_storage_container.test.name}${data.azurerm_storage_account_sas.test.sas}&sr=b" + schedule { + frequency_interval = 7 + frequency_unit = "Day" + } + } + + connection_string { + name = "Example" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + site_config { + app_command_line = "whoami" + api_definition_url = "https://example.com/azure_function_app_def.json" + // api_management_api_id = "" // TODO + application_insights_connection_string = azurerm_application_insights.test.connection_string + + application_stack { + python_version = "3.8" + } + + elastic_instance_minimum = 5 + + container_registry_use_managed_identity = true + container_registry_managed_identity_client_id = azurerm_user_assigned_identity.test.client_id + + default_documents = [ + "first.html", + "second.jsp", + "third.aspx", + "hostingstart.html", + ] + + http2_enabled = true + + ip_restriction { + ip_address = "10.10.10.10/32" + name = "test-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + load_balancing_mode = "LeastResponseTime" + pre_warmed_instance_count = 2 + remote_debugging_enabled = true + remote_debugging_version = "VS2017" + + scm_ip_restriction { + ip_address = "10.20.20.20/32" + name = "test-scm-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + scm_ip_restriction { + ip_address = "fd80::/64" + name = "test-scm-restriction-v6" + priority = 124 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + use_32_bit_worker = true + websockets_enabled = true + ftps_state = "FtpsOnly" + health_check_path = "/health-check" + worker_count = 3 + + minimum_tls_version = "1.1" + scm_minimum_tls_version = "1.1" + + cors { + allowed_origins = [ + "https://www.contoso.com", + "www.contoso.com", + ] + + support_credentials = true + } + + vnet_route_all_enabled = true + } + vnet_image_pull_enabled = true + +} +`, r.storageContainerTemplate(data, SkuElasticPremiumPlan), data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_subnet1WithVnetProperties func (r LinuxFunctionAppResource) elasticComplete(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { @@ -4056,6 +4312,7 @@ resource "azurerm_user_assigned_identity" "test" { `, r.template(data, planSku), data.RandomInteger) } +// nolint:unparam func (r LinuxFunctionAppResource) vNetIntegration_basic(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -4120,6 +4377,70 @@ resource "azurerm_linux_function_app" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r LinuxFunctionAppResource) vNetIntegration_subnetWithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test1.id + + vnet_image_pull_enabled = true + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_subnet1WithVnetProperties func (r LinuxFunctionAppResource) vNetIntegration_subnet1(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -4182,6 +4503,7 @@ resource "azurerm_linux_function_app" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_subnet2WithVnetProperties func (r LinuxFunctionAppResource) vNetIntegration_subnet2(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -4244,6 +4566,133 @@ resource "azurerm_linux_function_app" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r LinuxFunctionAppResource) vNetIntegration_subnet1WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test1.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + vnet_image_pull_enabled = true + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +func (r LinuxFunctionAppResource) vNetIntegration_subnet2WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test2.id + + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + + vnet_image_pull_enabled = true + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by withASEV3VnetProperties func (r LinuxFunctionAppResource) withASEV3(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -4271,6 +4720,34 @@ resource "azurerm_linux_function_app" "test" { `, ServicePlanResource{}.aseV3Linux(data), data.RandomString, data.RandomInteger) } +func (r LinuxFunctionAppResource) withASEV3VnetProperties(data acceptance.TestData) string { + return fmt.Sprintf(` +%s +resource "azurerm_storage_account" "test" { + name = "acctestsa%s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +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 + + vnet_image_pull_enabled = true + site_config { + vnet_route_all_enabled = true + } +} +`, ServicePlanResource{}.aseV3Linux(data), data.RandomString, data.RandomInteger) +} + func (r LinuxFunctionAppResource) withStorageAccountSingle(data acceptance.TestData, planSKU string) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/internal/services/appservice/linux_function_app_slot_resource.go b/internal/services/appservice/linux_function_app_slot_resource.go index ce8366142a5f..a91e491a9f60 100644 --- a/internal/services/appservice/linux_function_app_slot_resource.go +++ b/internal/services/appservice/linux_function_app_slot_resource.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/resourceproviders" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/webapps" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/helpers" @@ -73,6 +74,7 @@ type LinuxFunctionAppSlotModel struct { SiteCredentials []helpers.SiteCredential `tfschema:"site_credential"` StorageAccounts []helpers.StorageAccount `tfschema:"storage_account"` Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` + // VnetImagePullEnabled bool `tfschema:"vnet_image_pull_enabled"` // TODO 4.0 not supported on Consumption plans } var _ sdk.ResourceWithUpdate = LinuxFunctionAppSlotResource{} @@ -92,7 +94,7 @@ func (r LinuxFunctionAppSlotResource) IDValidationFunc() pluginsdk.SchemaValidat } func (r LinuxFunctionAppSlotResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -281,6 +283,15 @@ func (r LinuxFunctionAppSlotResource) Arguments() map[string]*pluginsdk.Schema { ValidateFunc: commonids.ValidateSubnetID, }, } + if features.FourPointOhBeta() { + s["vnet_image_pull_enabled"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Is container image pull over virtual network enabled? Defaults to `false`.", + } + } + return s } func (r LinuxFunctionAppSlotResource) Attributes() map[string]*pluginsdk.Schema { @@ -427,6 +438,11 @@ func (r LinuxFunctionAppSlotResource) Create() sdk.ResourceFunc { availabilityRequest.Name = fmt.Sprintf("%s.%s", functionAppSlot.Name, nameSuffix) availabilityRequest.IsFqdn = pointer.To(true) + if features.FourPointOhBeta() { + if !metadata.ResourceData.Get("vnet_image_pull_enabled").(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } } } @@ -520,6 +536,10 @@ func (r LinuxFunctionAppSlotResource) Create() sdk.ResourceFunc { }, } + if features.FourPointOhBeta() { + siteEnvelope.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + pan := helpers.PublicNetworkAccessEnabled if !functionAppSlot.PublicNetworkAccess { pan = helpers.PublicNetworkAccessDisabled @@ -737,6 +757,9 @@ func (r LinuxFunctionAppSlotResource) Read() sdk.ResourceFunc { state.DefaultHostname = pointer.From(props.DefaultHostName) state.PublicNetworkAccess = !strings.EqualFold(pointer.From(props.PublicNetworkAccess), helpers.PublicNetworkAccessDisabled) + if features.FourPointOhBeta() { + metadata.ResourceData.Set("vnet_image_pull_enabled", pointer.From(props.VnetImagePullEnabled)) + } if hostingEnv := props.HostingEnvironmentProfile; hostingEnv != nil { state.HostingEnvId = pointer.From(hostingEnv.Id) } @@ -924,6 +947,10 @@ func (r LinuxFunctionAppSlotResource) Update() sdk.ResourceFunc { } } + if metadata.ResourceData.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + model.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + storageString := state.StorageAccountName if !state.StorageUsesMSI { if state.StorageKeyVaultSecretID != "" { @@ -1187,3 +1214,35 @@ func (r LinuxFunctionAppSlotResource) StateUpgraders() sdk.StateUpgradeData { }, } } + +func (r LinuxFunctionAppSlotResource) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + appClient := metadata.Client.AppService.WebAppsClient + rd := metadata.ResourceDiff + if rd.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + appId := rd.Get("function_app_id") + if appId.(string) == "" { + return nil + } + _, newValue := rd.GetChange("vnet_image_pull_enabled") + functionAppId, err := commonids.ParseAppServiceID(appId.(string)) + if err != nil { + return err + } + + functionApp, err := appClient.Get(ctx, *functionAppId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", functionAppId, err) + } + if functionAppModel := functionApp.Model; functionAppModel != nil && functionAppModel.Properties != nil { + if ase := functionAppModel.Properties.HostingEnvironmentProfile; ase != nil && ase.Id != nil && *(ase.Id) != "" && !newValue.(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app slot running in an app service environment.") + } + } + } + return nil + }, + } +} 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 4b0255238583..3644e920c10b 100644 --- a/internal/services/appservice/linux_function_app_slot_resource_test.go +++ b/internal/services/appservice/linux_function_app_slot_resource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" ) @@ -337,6 +338,25 @@ func TestAccLinuxFunctionAppSlot_consumptionCompleteUpdate(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlot_elasticPremiumCompleteWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.elasticCompleteWithVnetProperties(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppSlot_elasticPremiumComplete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -352,6 +372,25 @@ func TestAccLinuxFunctionAppSlot_elasticPremiumComplete(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlot_standardCompleteWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.standardCompleteWithVnetProperties(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppSlot_standardComplete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -385,6 +424,25 @@ func TestAccLinuxFunctionAppSlot_withAuthSettingsStandard(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlot_scmIpRestrictionSubnetWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.scmIpRestrictionSubnetWithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppSlot_scmIpRestrictionSubnet(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -1174,9 +1232,16 @@ func TestAccLinuxFunctionAppSlot_vNetIntegration(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} + var vnetIntegrationProperties string + if features.FourPointOhBeta() { + vnetIntegrationProperties = r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan) + } else { + vnetIntegrationProperties = r.vNetIntegration_subnet1(data, SkuStandardPlan) + } + data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.vNetIntegration_subnet1(data, SkuStandardPlan), + Config: vnetIntegrationProperties, Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( @@ -1188,6 +1253,7 @@ func TestAccLinuxFunctionAppSlot_vNetIntegration(t *testing.T) { }) } +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppSlot_vNetIntegrationUpdate(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -1230,6 +1296,52 @@ func TestAccLinuxFunctionAppSlot_vNetIntegrationUpdate(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlot_vNetIntegrationUpdateWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.vNetIntegration_basicWithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( + check.That("azurerm_subnet.test1").Key("id"), + ), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_subnet2WithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( + check.That("azurerm_subnet.test2").Key("id"), + ), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.vNetIntegration_basicWithVnetProperties(data, SkuStandardPlan), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +// TODO 4.0 remove post 4.0 func TestAccLinuxFunctionAppSlotASEv3_basic(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -1245,6 +1357,24 @@ func TestAccLinuxFunctionAppSlotASEv3_basic(t *testing.T) { }) } +func TestAccLinuxFunctionAppSlotASEv3_basicWithVnetProperties(t *testing.T) { + if !features.FourPointOhBeta() { + t.Skip("this test requires 4.0 mode") + } + data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") + r := LinuxFunctionAppSlotResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.withASEV3WithVnetProperties(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + func TestAccLinuxFunctionAppSlot_withStorageAccountBlock(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_function_app_slot", "test") r := LinuxFunctionAppSlotResource{} @@ -2282,7 +2412,7 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger, data.Client().TenantID) } -func (r LinuxFunctionAppSlotResource) standardComplete(data acceptance.TestData) string { +func (r LinuxFunctionAppSlotResource) standardCompleteWithVnetProperties(data acceptance.TestData) string { planSku := "S1" return fmt.Sprintf(` provider "azurerm" { @@ -2461,6 +2591,8 @@ resource "azurerm_linux_function_app_slot" "test" { vnet_route_all_enabled = true } + vnet_image_pull_enabled = true + tags = { terraform = "true" Env = "AccTest" @@ -2469,44 +2601,9 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.storageContainerTemplate(data, planSku), data.RandomInteger, data.Client().TenantID) } -func (r LinuxFunctionAppSlotResource) scmIpRestrictionSubnet(data acceptance.TestData, planSku string) string { - return fmt.Sprintf(` -provider "azurerm" { - features {} -} - -%s - -resource "azurerm_virtual_network" "test" { - name = "acctestvirtnet%[2]d" - address_space = ["10.0.0.0/16"] - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name -} - -resource "azurerm_subnet" "test" { - name = "acctestsubnet%[2]d" - resource_group_name = azurerm_resource_group.test.name - virtual_network_name = azurerm_virtual_network.test.name - address_prefixes = ["10.0.2.0/24"] -} - -resource "azurerm_linux_function_app_slot" "test" { - name = "acctest-LFAS-%[2]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 - - site_config { - scm_ip_restriction { - virtual_network_subnet_id = azurerm_subnet.test.id - } - } -} -`, r.template(data, planSku), data.RandomInteger) -} - -func (r LinuxFunctionAppSlotResource) elasticComplete(data acceptance.TestData) string { +// TODO 4.0 remove this test case as it's replaced by standardCompleteWithVnetProperties +func (r LinuxFunctionAppSlotResource) standardComplete(data acceptance.TestData) string { + planSku := "S1" return fmt.Sprintf(` provider "azurerm" { features {} @@ -2538,6 +2635,33 @@ resource "azurerm_linux_function_app_slot" "test" { secret = "sauce" } + auth_settings { + enabled = true + issuer = "https://sts.windows.net/%[3]s" + + additional_login_parameters = { + test_key = "test_value" + } + + active_directory { + client_id = "aadclientid" + client_secret = "aadsecret" + + allowed_audiences = [ + "activedirectorytokenaudiences", + ] + } + + facebook { + app_id = "facebookappid" + app_secret = "facebookappsecret" + + oauth_scopes = [ + "facebookscope", + ] + } + } + backup { name = "acctest" storage_account_url = "https://${azurerm_storage_account.test.name}.blob.core.windows.net/${azurerm_storage_container.test.name}${data.azurerm_storage_account_sas.test.sas}&sr=b" @@ -2547,13 +2671,28 @@ resource "azurerm_linux_function_app_slot" "test" { } } + builtin_logging_enabled = false + client_certificate_enabled = true + client_certificate_mode = "OptionalInteractiveUser" + client_certificate_exclusion_paths = "/foo;/bar;/hello;/world" + connection_string { - name = "Example" + name = "First" value = "some-postgresql-connection-string" type = "PostgreSQL" } + enabled = false + functions_extension_version = "~3" + https_only = true + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.test.id] + } + site_config { + always_on = true app_command_line = "whoami" api_definition_url = "https://example.com/azure_function_app_def.json" // api_management_api_id = "" // TODO @@ -2587,6 +2726,7 @@ resource "azurerm_linux_function_app_slot" "test" { x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] x_forwarded_host = ["example.com"] } + description = "Allow ip address 10.10.10.10/32" } load_balancing_mode = "LeastResponseTime" @@ -2640,11 +2780,16 @@ resource "azurerm_linux_function_app_slot" "test" { vnet_route_all_enabled = true } + + tags = { + terraform = "true" + Env = "AccTest" + } } -`, r.storageContainerTemplate(data, SkuElasticPremiumPlan), data.RandomInteger) +`, r.storageContainerTemplate(data, planSku), data.RandomInteger, data.Client().TenantID) } -func (r LinuxFunctionAppSlotResource) updateStorageAccount(data acceptance.TestData, planSku string) string { +func (r LinuxFunctionAppSlotResource) scmIpRestrictionSubnetWithVnetProperties(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -2652,18 +2797,39 @@ provider "azurerm" { %s +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%[2]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctestsubnet%[2]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] +} + resource "azurerm_linux_function_app_slot" "test" { - name = "acctest-LFAS-%d" + name = "acctest-LFAS-%[2]d" function_app_id = azurerm_linux_function_app.test.id - storage_account_name = azurerm_storage_account.update.name - storage_account_access_key = azurerm_storage_account.update.primary_access_key + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key - site_config {} + site_config { + scm_ip_restriction { + virtual_network_subnet_id = azurerm_subnet.test.id + } + } + + vnet_image_pull_enabled = true } -`, r.templateExtraStorageAccount(data, planSku), data.RandomInteger) +`, r.template(data, planSku), data.RandomInteger) } -func (r LinuxFunctionAppSlotResource) identitySystemAssigned(data acceptance.TestData, planSku string) string { +// TODO 4.0 remove this test case as it's replaced by scmIpRestrictionSubnetWithVnetProperties +func (r LinuxFunctionAppSlotResource) scmIpRestrictionSubnet(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -2671,22 +2837,36 @@ provider "azurerm" { %s +resource "azurerm_virtual_network" "test" { + name = "acctestvirtnet%[2]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctestsubnet%[2]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] +} + resource "azurerm_linux_function_app_slot" "test" { - name = "acctest-LFAS-%d" + name = "acctest-LFAS-%[2]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 - site_config {} - - identity { - type = "SystemAssigned" + site_config { + scm_ip_restriction { + virtual_network_subnet_id = azurerm_subnet.test.id + } } } -`, r.identityTemplate(data, planSku), data.RandomInteger) +`, r.template(data, planSku), data.RandomInteger) } -func (r LinuxFunctionAppSlotResource) identitySystemAssignedUserAssigned(data acceptance.TestData, planSku string) string { +func (r LinuxFunctionAppSlotResource) elasticCompleteWithVnetProperties(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { features {} @@ -2694,12 +2874,332 @@ provider "azurerm" { %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 - +resource "azurerm_user_assigned_identity" "test" { + name = "acct-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_application_insights" "test" { + name = "acctestappinsights-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "web" +} + +resource "azurerm_linux_function_app_slot" "test" { + name = "acctest-LFAS-%[2]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" + } + + backup { + name = "acctest" + storage_account_url = "https://${azurerm_storage_account.test.name}.blob.core.windows.net/${azurerm_storage_container.test.name}${data.azurerm_storage_account_sas.test.sas}&sr=b" + schedule { + frequency_interval = 7 + frequency_unit = "Day" + } + } + + connection_string { + name = "Example" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + site_config { + app_command_line = "whoami" + api_definition_url = "https://example.com/azure_function_app_def.json" + // api_management_api_id = "" // TODO + application_insights_key = azurerm_application_insights.test.instrumentation_key + application_insights_connection_string = azurerm_application_insights.test.connection_string + + application_stack { + python_version = "3.8" + } + + container_registry_use_managed_identity = true + container_registry_managed_identity_client_id = azurerm_user_assigned_identity.test.client_id + + default_documents = [ + "first.html", + "second.jsp", + "third.aspx", + "hostingstart.html", + ] + + http2_enabled = true + + ip_restriction { + ip_address = "10.10.10.10/32" + name = "test-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + load_balancing_mode = "LeastResponseTime" + pre_warmed_instance_count = 2 + remote_debugging_enabled = true + remote_debugging_version = "VS2017" + + scm_ip_restriction { + ip_address = "10.20.20.20/32" + name = "test-scm-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + scm_ip_restriction { + ip_address = "fd80::/64" + name = "test-scm-restriction-v6" + priority = 124 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + use_32_bit_worker = true + websockets_enabled = true + ftps_state = "FtpsOnly" + health_check_path = "/health-check" + worker_count = 3 + + minimum_tls_version = "1.1" + scm_minimum_tls_version = "1.1" + + cors { + allowed_origins = [ + "https://www.contoso.com", + "www.contoso.com", + ] + + support_credentials = true + } + + vnet_route_all_enabled = true + } + vnet_image_pull_enabled = true +} +`, r.storageContainerTemplate(data, SkuElasticPremiumPlan), data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by elasticCompleteWithVnetProperties +func (r LinuxFunctionAppSlotResource) elasticComplete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_user_assigned_identity" "test" { + name = "acct-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_application_insights" "test" { + name = "acctestappinsights-%[2]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "web" +} + +resource "azurerm_linux_function_app_slot" "test" { + name = "acctest-LFAS-%[2]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" + } + + backup { + name = "acctest" + storage_account_url = "https://${azurerm_storage_account.test.name}.blob.core.windows.net/${azurerm_storage_container.test.name}${data.azurerm_storage_account_sas.test.sas}&sr=b" + schedule { + frequency_interval = 7 + frequency_unit = "Day" + } + } + + connection_string { + name = "Example" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + site_config { + app_command_line = "whoami" + api_definition_url = "https://example.com/azure_function_app_def.json" + // api_management_api_id = "" // TODO + application_insights_key = azurerm_application_insights.test.instrumentation_key + application_insights_connection_string = azurerm_application_insights.test.connection_string + + application_stack { + python_version = "3.8" + } + + container_registry_use_managed_identity = true + container_registry_managed_identity_client_id = azurerm_user_assigned_identity.test.client_id + + default_documents = [ + "first.html", + "second.jsp", + "third.aspx", + "hostingstart.html", + ] + + http2_enabled = true + + ip_restriction { + ip_address = "10.10.10.10/32" + name = "test-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + load_balancing_mode = "LeastResponseTime" + pre_warmed_instance_count = 2 + remote_debugging_enabled = true + remote_debugging_version = "VS2017" + + scm_ip_restriction { + ip_address = "10.20.20.20/32" + name = "test-scm-restriction" + priority = 123 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + scm_ip_restriction { + ip_address = "fd80::/64" + name = "test-scm-restriction-v6" + priority = 124 + action = "Allow" + headers { + x_azure_fdid = ["55ce4ed1-4b06-4bf1-b40e-4638452104da"] + x_fd_health_probe = ["1"] + x_forwarded_for = ["9.9.9.9/32", "2002::1234:abcd:ffff:c0a8:101/64"] + x_forwarded_host = ["example.com"] + } + } + + use_32_bit_worker = true + websockets_enabled = true + ftps_state = "FtpsOnly" + health_check_path = "/health-check" + worker_count = 3 + + minimum_tls_version = "1.1" + scm_minimum_tls_version = "1.1" + + cors { + allowed_origins = [ + "https://www.contoso.com", + "www.contoso.com", + ] + + support_credentials = true + } + + vnet_route_all_enabled = true + } +} +`, r.storageContainerTemplate(data, SkuElasticPremiumPlan), data.RandomInteger) +} + +func (r LinuxFunctionAppSlotResource) updateStorageAccount(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.update.name + storage_account_access_key = azurerm_storage_account.update.primary_access_key + + site_config {} +} +`, r.templateExtraStorageAccount(data, planSku), data.RandomInteger) +} + +func (r LinuxFunctionAppSlotResource) identitySystemAssigned(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 + + site_config {} + + identity { + type = "SystemAssigned" + } +} +`, r.identityTemplate(data, planSku), data.RandomInteger) +} + +func (r LinuxFunctionAppSlotResource) identitySystemAssignedUserAssigned(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 + site_config {} identity { @@ -3172,6 +3672,7 @@ resource "azurerm_user_assigned_identity" "test" { `, r.template(data, planSku), data.RandomInteger) } +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_basicWithVnetProperties func (r LinuxFunctionAppSlotResource) vNetIntegration_basic(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -3231,6 +3732,68 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r LinuxFunctionAppSlotResource) vNetIntegration_basicWithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + + site_config {} + + vnet_image_pull_enabled = true +} + +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_subnet1WithVnetProperties func (r LinuxFunctionAppSlotResource) vNetIntegration_subnet1(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -3290,6 +3853,67 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r LinuxFunctionAppSlotResource) vNetIntegration_subnet1WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test1.id + + vnet_image_pull_enabled = true + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by vNetIntegration_subnet2WithVnetProperties func (r LinuxFunctionAppSlotResource) vNetIntegration_subnet2(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { @@ -3349,6 +3973,67 @@ resource "azurerm_linux_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r LinuxFunctionAppSlotResource) vNetIntegration_subnet2WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test2.id + + vnet_image_pull_enabled = true + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 remove this test case as it's replaced by withASEV3WithVnetProperties func (r LinuxFunctionAppSlotResource) withASEV3(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -3389,6 +4074,49 @@ resource "azurerm_linux_function_app_slot" "test" { `, ServicePlanResource{}.aseV3Linux(data), data.RandomString, data.RandomInteger) } +func (r LinuxFunctionAppSlotResource) withASEV3WithVnetProperties(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[2]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_linux_function_app" "test" { + name = "acctest-LFA-%[3]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 + + vnet_image_pull_enabled = true + + site_config { + vnet_route_all_enabled = true + } +} + +resource "azurerm_linux_function_app_slot" "test" { + name = "acctest-LFAS-%[3]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 + + vnet_image_pull_enabled = true + site_config { + vnet_route_all_enabled = true + } +} + +`, ServicePlanResource{}.aseV3Linux(data), data.RandomString, data.RandomInteger) +} + func (r LinuxFunctionAppSlotResource) withStorageAccountSingle(data acceptance.TestData, planSku string) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/internal/services/appservice/windows_function_app_resource.go b/internal/services/appservice/windows_function_app_resource.go index 384b5b106a89..ecffe74d8bc6 100644 --- a/internal/services/appservice/windows_function_app_resource.go +++ b/internal/services/appservice/windows_function_app_resource.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/resourceproviders" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/webapps" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/helpers" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/migration" @@ -70,6 +71,8 @@ type WindowsFunctionAppModel struct { PublishingDeployBasicAuthEnabled bool `tfschema:"webdeploy_publish_basic_authentication_enabled"` PublishingFTPBasicAuthEnabled bool `tfschema:"ftp_publish_basic_authentication_enabled"` + // VnetImagePullEnabled bool `tfschema:"vnet_image_pull_enabled"` // TODO 4.0 not supported on Consumption plans + // Computed CustomDomainVerificationId string `tfschema:"custom_domain_verification_id"` HostingEnvId string `tfschema:"hosting_environment_id"` @@ -104,7 +107,7 @@ func (r WindowsFunctionAppResource) IDValidationFunc() pluginsdk.SchemaValidateF } func (r WindowsFunctionAppResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -304,6 +307,15 @@ func (r WindowsFunctionAppResource) Arguments() map[string]*pluginsdk.Schema { Description: "The local path and filename of the Zip packaged application to deploy to this Windows Function App. **Note:** Using this value requires `WEBSITE_RUN_FROM_PACKAGE=1` to be set on the App in `app_settings`.", }, } + if features.FourPointOhBeta() { + s["vnet_image_pull_enabled"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Is container image pull over virtual network enabled? Defaults to `false`.", + } + } + return s } func (r WindowsFunctionAppResource) Attributes() map[string]*pluginsdk.Schema { @@ -429,6 +441,11 @@ func (r WindowsFunctionAppResource) Create() sdk.ResourceFunc { availabilityRequest.Name = fmt.Sprintf("%s.%s", functionApp.Name, nameSuffix) availabilityRequest.IsFqdn = pointer.To(true) + if features.FourPointOhBeta() { + if !metadata.ResourceData.Get("vnet_image_pull_enabled").(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } } } } @@ -527,6 +544,10 @@ func (r WindowsFunctionAppResource) Create() sdk.ResourceFunc { }, } + if features.FourPointOhBeta() { + siteEnvelope.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + pna := helpers.PublicNetworkAccessEnabled if !functionApp.PublicNetworkAccess { pna = helpers.PublicNetworkAccessDisabled @@ -752,6 +773,9 @@ func (r WindowsFunctionAppResource) Read() sdk.ResourceFunc { state.DefaultHostname = pointer.From(props.DefaultHostName) state.PublicNetworkAccess = !strings.EqualFold(pointer.From(props.PublicNetworkAccess), helpers.PublicNetworkAccessDisabled) + if features.FourPointOhBeta() { + metadata.ResourceData.Set("vnet_image_pull_enabled", pointer.From(props.VnetImagePullEnabled)) + } servicePlanId, err := commonids.ParseAppServicePlanIDInsensitively(pointer.From(props.ServerFarmId)) if err != nil { return err @@ -937,6 +961,10 @@ func (r WindowsFunctionAppResource) Update() sdk.ResourceFunc { model.Properties.HTTPSOnly = pointer.To(state.HttpsOnly) } + if metadata.ResourceData.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + model.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + if metadata.ResourceData.HasChange("client_certificate_enabled") { model.Properties.ClientCertEnabled = pointer.To(state.ClientCertEnabled) } @@ -1233,6 +1261,30 @@ func (r WindowsFunctionAppResource) CustomizeDiff() sdk.ResourceFunc { client := metadata.Client.AppService.ServicePlanClient rd := metadata.ResourceDiff + if rd.HasChange("vnet_image_pull_enabled") { + planId := rd.Get("service_plan_id") + // the plan id is known after apply during the initial creation + if planId.(string) == "" { + return nil + } + _, newValue := rd.GetChange("vnet_image_pull_enabled") + servicePlanId, err := commonids.ParseAppServicePlanID(planId.(string)) + if err != nil { + return err + } + + asp, err := client.Get(ctx, *servicePlanId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", servicePlanId, err) + } + if aspModel := asp.Model; aspModel != nil { + if aspModel.Properties != nil && aspModel.Properties.HostingEnvironmentProfile != nil && + aspModel.Properties.HostingEnvironmentProfile.Id != nil && *(aspModel.Properties.HostingEnvironmentProfile.Id) != "" && !newValue.(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } + } + if rd.HasChange("service_plan_id") { currentPlanIdRaw, newPlanIdRaw := rd.GetChange("service_plan_id") if newPlanIdRaw.(string) == "" { diff --git a/internal/services/appservice/windows_function_app_resource_test.go b/internal/services/appservice/windows_function_app_resource_test.go index 3cd0821971c9..6e50aa09a490 100644 --- a/internal/services/appservice/windows_function_app_resource_test.go +++ b/internal/services/appservice/windows_function_app_resource_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/utils" ) @@ -1469,9 +1470,15 @@ func TestAccWindowsFunctionApp_vNetIntegration(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_windows_function_app", "test") r := WindowsFunctionAppResource{} + var vnetIntegrationProperties string + if features.FourPointOhBeta() { + vnetIntegrationProperties = r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan) + } else { + vnetIntegrationProperties = r.vNetIntegration_subnet1(data, SkuStandardPlan) + } data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.vNetIntegration_subnet1(data, SkuStandardPlan), + Config: vnetIntegrationProperties, Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( @@ -3819,6 +3826,67 @@ resource "azurerm_windows_function_app" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r WindowsFunctionAppResource) vNetIntegration_subnet1WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +%s +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +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 + virtual_network_subnet_id = azurerm_subnet.test1.id + storage_account_name = azurerm_storage_account.test.name + storage_account_access_key = azurerm_storage_account.test.primary_access_key + vnet_image_pull_enabled = true + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 enable the vnet_image_pull_enabled property for app running in ase env func (r WindowsFunctionAppResource) withASEV3(data acceptance.TestData) string { return fmt.Sprintf(` %s @@ -3840,6 +3908,7 @@ resource "azurerm_windows_function_app" "test" { storage_account_name = azurerm_storage_account.test.name storage_account_access_key = azurerm_storage_account.test.primary_access_key + // vnet_image_pull_enabled = true site_config { vnet_route_all_enabled = true } diff --git a/internal/services/appservice/windows_function_app_slot_resource.go b/internal/services/appservice/windows_function_app_slot_resource.go index 61dda17359f7..172b555efc7c 100644 --- a/internal/services/appservice/windows_function_app_slot_resource.go +++ b/internal/services/appservice/windows_function_app_slot_resource.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-azure-helpers/resourcemanager/location" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/resourceproviders" "github.com/hashicorp/go-azure-sdk/resource-manager/web/2023-01-01/webapps" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/locks" "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/services/appservice/helpers" @@ -73,6 +74,7 @@ type WindowsFunctionAppSlotModel struct { SiteCredentials []helpers.SiteCredential `tfschema:"site_credential"` StorageAccounts []helpers.StorageAccount `tfschema:"storage_account"` VirtualNetworkSubnetID string `tfschema:"virtual_network_subnet_id"` + // VnetImagePullEnabled bool `tfschema:"vnet_image_pull_enabled"` // TODO 4.0 not supported on Consumption plans } var _ sdk.ResourceWithUpdate = WindowsFunctionAppSlotResource{} @@ -92,7 +94,7 @@ func (r WindowsFunctionAppSlotResource) IDValidationFunc() pluginsdk.SchemaValid } func (r WindowsFunctionAppSlotResource) Arguments() map[string]*pluginsdk.Schema { - return map[string]*pluginsdk.Schema{ + s := map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, Required: true, @@ -281,6 +283,15 @@ func (r WindowsFunctionAppSlotResource) Arguments() map[string]*pluginsdk.Schema ValidateFunc: commonids.ValidateSubnetID, }, } + if features.FourPointOhBeta() { + s["vnet_image_pull_enabled"] = &pluginsdk.Schema{ + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Is container image pull over virtual network enabled? Defaults to `false`.", + } + } + return s } func (r WindowsFunctionAppSlotResource) Attributes() map[string]*pluginsdk.Schema { @@ -444,6 +455,11 @@ func (r WindowsFunctionAppSlotResource) Create() sdk.ResourceFunc { availabilityRequest.Name = fmt.Sprintf("%s.%s", functionAppSlot.Name, nameSuffix) availabilityRequest.IsFqdn = pointer.To(true) + if features.FourPointOhBeta() { + if !metadata.ResourceData.Get("vnet_image_pull_enabled").(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app running in an app service environment.") + } + } } } } @@ -755,6 +771,9 @@ func (r WindowsFunctionAppSlotResource) Read() sdk.ResourceFunc { state.DefaultHostname = pointer.From(props.DefaultHostName) state.PublicNetworkAccess = !strings.EqualFold(pointer.From(props.PublicNetworkAccess), helpers.PublicNetworkAccessDisabled) + if features.FourPointOhBeta() { + metadata.ResourceData.Set("vnet_image_pull_enabled", pointer.From(props.VnetImagePullEnabled)) + } if hostingEnv := props.HostingEnvironmentProfile; hostingEnv != nil { state.HostingEnvId = pointer.From(hostingEnv.Id) } @@ -945,6 +964,10 @@ func (r WindowsFunctionAppSlotResource) Update() sdk.ResourceFunc { } } + if metadata.ResourceData.HasChange("vnet_image_pull_enabled") && features.FourPointOhBeta() { + model.Properties.VnetImagePullEnabled = pointer.To(metadata.ResourceData.Get("vnet_image_pull_enabled").(bool)) + } + if metadata.ResourceData.HasChange("storage_account") { storageAccountUpdate := helpers.ExpandStorageConfig(state.StorageAccounts) if _, err = client.UpdateAzureStorageAccountsSlot(ctx, *id, *storageAccountUpdate); err != nil { @@ -1199,3 +1222,35 @@ func (r WindowsFunctionAppSlotResource) StateUpgraders() sdk.StateUpgradeData { }, } } + +func (r WindowsFunctionAppSlotResource) CustomizeDiff() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + appClient := metadata.Client.AppService.WebAppsClient + rd := metadata.ResourceDiff + if rd.HasChange("vnet_image_pull_enabled") { + appId := rd.Get("function_app_id") + if appId.(string) == "" { + return nil + } + _, newValue := rd.GetChange("vnet_image_pull_enabled") + functionAppId, err := commonids.ParseAppServiceID(appId.(string)) + if err != nil { + return err + } + + functionApp, err := appClient.Get(ctx, *functionAppId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", functionAppId, err) + } + if functionAppModel := functionApp.Model; functionAppModel != nil && functionAppModel.Properties != nil { + if ase := functionAppModel.Properties.HostingEnvironmentProfile; ase != nil && ase.Id != nil && *(ase.Id) != "" && !newValue.(bool) { + return fmt.Errorf("`vnet_image_pull_enabled` cannot be disabled for app slot running in an app service environment.") + } + } + } + return nil + }, + } +} 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 355b39882578..821cac3fd967 100644 --- a/internal/services/appservice/windows_function_app_slot_resource_test.go +++ b/internal/services/appservice/windows_function_app_slot_resource_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/features" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azurerm/utils" ) @@ -1094,9 +1095,16 @@ func TestAccWindowsFunctionAppSlot_vNetIntegration(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_windows_function_app_slot", "test") r := WindowsFunctionAppSlotResource{} + var vnetIntegrationProperties string + if features.FourPointOhBeta() { + vnetIntegrationProperties = r.vNetIntegration_subnet1WithVnetProperties(data, SkuStandardPlan) + } else { + vnetIntegrationProperties = r.vNetIntegration_subnet1(data, SkuStandardPlan) + } + data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.vNetIntegration_subnet1(data, SkuStandardPlan), + Config: vnetIntegrationProperties, Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("virtual_network_subnet_id").MatchesOtherKey( @@ -3104,6 +3112,58 @@ resource "azurerm_windows_function_app_slot" "test" { `, r.template(data, planSku), data.RandomInteger, data.RandomInteger) } +func (r WindowsFunctionAppSlotResource) vNetIntegration_subnet1WithVnetProperties(data acceptance.TestData, planSku string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +%s +resource "azurerm_virtual_network" "test" { + name = "vnet-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} +resource "azurerm_subnet" "test1" { + name = "subnet1" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + delegation { + name = "delegation" + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} +resource "azurerm_subnet" "test2" { + name = "subnet2" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] + delegation { + name = "delegation" + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} +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 + virtual_network_subnet_id = azurerm_subnet.test1.id + vnet_image_pull_enabled = true + + site_config {} +} +`, r.template(data, planSku), data.RandomInteger, data.RandomInteger) +} + +// TODO 4.0 enable the vnet_image_pull_enabled property for app running in ase env func (r WindowsFunctionAppSlotResource) withASEV3(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -3125,6 +3185,7 @@ resource "azurerm_windows_function_app" "test" { storage_account_name = azurerm_storage_account.test.name storage_account_access_key = azurerm_storage_account.test.primary_access_key + // vnet_image_pull_enabled = true site_config { vnet_route_all_enabled = true } @@ -3136,6 +3197,7 @@ resource "azurerm_windows_function_app_slot" "test" { storage_account_name = azurerm_storage_account.test.name storage_account_access_key = azurerm_storage_account.test.primary_access_key + // vnet_image_pull_enabled = true site_config { vnet_route_all_enabled = true } diff --git a/website/docs/r/linux_function_app.html.markdown b/website/docs/r/linux_function_app.html.markdown index 54749723f888..b494ea9b8656 100644 --- a/website/docs/r/linux_function_app.html.markdown +++ b/website/docs/r/linux_function_app.html.markdown @@ -139,6 +139,11 @@ The following arguments are supported: ~> **Note:** Assigning the `virtual_network_subnet_id` property requires [RBAC permissions on the subnet](https://docs.microsoft.com/en-us/azure/app-service/overview-vnet-integration#permissions) +[//]: # (TODO 4.0 add it in 4.0 provider) +[//]: # (* `vnet_image_pull_enabled` - (Optional) Should the traffic for the image pull be routed over virtual network enabled. Defaults to `false`.) + +[//]: # (~> **Note:** The feature can also be enabled via the app setting `WEBSITE_PULL_IMAGE_OVER_VNET`. Must be set to `true` when running in an App Service Environment.) + * `webdeploy_publish_basic_authentication_enabled` - (Optional) Should the default WebDeploy Basic Authentication publishing credentials enabled. Defaults to `true`. ~> **NOTE:** Setting this value to true will disable the ability to use `zip_deploy_file` which currently relies on the default publishing profile. diff --git a/website/docs/r/linux_function_app_slot.html.markdown b/website/docs/r/linux_function_app_slot.html.markdown index fadd640500ef..af5faeebae08 100644 --- a/website/docs/r/linux_function_app_slot.html.markdown +++ b/website/docs/r/linux_function_app_slot.html.markdown @@ -131,7 +131,12 @@ The following arguments are supported: ~> **NOTE on regional virtual network integration:** The AzureRM Terraform provider provides regional virtual network integration via the standalone resource [app_service_virtual_network_swift_connection](app_service_virtual_network_swift_connection.html) and in-line within this resource using the `virtual_network_subnet_id` property. You cannot use both methods simultaneously. If the virtual network is set via the resource `app_service_virtual_network_swift_connection` then `ignore_changes` should be used in the function app slot configuration. ~> **Note:** Assigning the `virtual_network_subnet_id` property requires [RBAC permissions on the subnet](https://docs.microsoft.com/en-us/azure/app-service/overview-vnet-integration#permissions) - + +[//]: # (TODO 4.0 add it in 4.0 provider) +[//]: # (* `vnet_image_pull_enabled` - (Optional) Specifies whether traffic for the image pull should be routed over virtual network. Defaults to `false`.) + +[//]: # (~> **Note:** The feature can also be enabled via the app setting `WEBSITE_PULL_IMAGE_OVER_VNET`. The Setting is enabled by default for app running in the App Service Environment.) + * `webdeploy_publish_basic_authentication_enabled` - (Optional) Should the default WebDeploy Basic Authentication publishing credentials enabled. Defaults to `true`. --- diff --git a/website/docs/r/windows_function_app.html.markdown b/website/docs/r/windows_function_app.html.markdown index 052f52cd7b23..6051e5056efc 100644 --- a/website/docs/r/windows_function_app.html.markdown +++ b/website/docs/r/windows_function_app.html.markdown @@ -139,6 +139,11 @@ The following arguments are supported: ~> **Note:** Assigning the `virtual_network_subnet_id` property requires [RBAC permissions on the subnet](https://docs.microsoft.com/en-us/azure/app-service/overview-vnet-integration#permissions) +[//]: # (TODO 4.0 add it in 4.0 provider) +[//]: # (* `vnet_image_pull_enabled` - (Optional) Specifies whether traffic for the image pull should be routed over virtual network. Defaults to `false`.) + +[//]: # (~> **Note:** The feature can also be enabled via the app setting `WEBSITE_PULL_IMAGE_OVER_VNET`. The Setting is enabled by default for app running in the App Service Environment.) + * `webdeploy_publish_basic_authentication_enabled` - (Optional) Should the default WebDeploy Basic Authentication publishing credentials enabled. Defaults to `true`. ~> **NOTE:** Setting this value to true will disable the ability to use `zip_deploy_file` which currently relies on the default publishing profile. diff --git a/website/docs/r/windows_function_app_slot.html.markdown b/website/docs/r/windows_function_app_slot.html.markdown index 496322e7ca8f..2929865a4d29 100644 --- a/website/docs/r/windows_function_app_slot.html.markdown +++ b/website/docs/r/windows_function_app_slot.html.markdown @@ -131,6 +131,11 @@ The following arguments are supported: ~> **Note:** Assigning the `virtual_network_subnet_id` property requires [RBAC permissions on the subnet](https://docs.microsoft.com/en-us/azure/app-service/overview-vnet-integration#permissions) +[//]: # (TODO 4.0 add it in 4.0 provider) +[//]: # (* `vnet_image_pull_enabled` - (Optional) Specifies whether traffic for the image pull should be routed over virtual network. Defaults to `false`.) + +[//]: # (~> **Note:** The feature can also be enabled via the app setting `WEBSITE_PULL_IMAGE_OVER_VNET`. The Setting is enabled by default for app running in the App Service Environment.) + * `webdeploy_publish_basic_authentication_enabled` - (Optional) Should the default WebDeploy Basic Authentication publishing credentials enabled. Defaults to `true`. ---