From 4a05d266693b29cd6aaa0ec88f4b7a743f71703a Mon Sep 17 00:00:00 2001 From: Xiaxin <92154856+xiaxyi@users.noreply.github.com> Date: Thu, 27 Feb 2025 19:51:45 +1100 Subject: [PATCH] New Resource - `azurerm_linux_function_app_flex_consumption` (#28199) * upgrade api version from 2023-01-01 to 2023-12-01 for function and web app/ app slot * update code * update vendor * add azure linux function app new feature flex consumption * update expanding error for flex consumption config * update vendor * fix linter * sort imports * remove validation check for instance count property * resolving golinter error * merging upstream * add new resource flex consumption linux function app * update code to fix linter error * go generate yml * fix fmt * update pr based on review * fix generate and remove linux * update code based on review comment * update code based on review comments * confilcts * remove servicebus code change * remove legacy changes * fix fmt * fix runtime update failure * update test cases * update docs * hard code test location --- .github/labeler-issue-triage.yml | 2 +- .../function_app_flex_consumption_resource.go | 1001 ++++++++++++++++ ...tion_app_flex_consumption_resource_test.go | 1022 +++++++++++++++++ .../appservice/helpers/function_app_schema.go | 518 ++++++++- internal/services/appservice/registration.go | 1 + .../container_app_resource_test.go | 2 +- ...unction_app_flex_consumption.html.markdown | 762 ++++++++++++ .../docs/r/linux_function_app.html.markdown | 2 + 8 files changed, 3295 insertions(+), 15 deletions(-) create mode 100644 internal/services/appservice/function_app_flex_consumption_resource.go create mode 100644 internal/services/appservice/function_app_flex_consumption_resource_test.go create mode 100644 website/docs/r/function_app_flex_consumption.html.markdown diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index 4ed2475cf41c..4052014369d4 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -27,7 +27,7 @@ service/app-configuration: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_app_configuration((.|\n)*)###' service/app-service: - - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_environment_v3\W+|app_service_environment_v3\W+|app_service_source_control\W+|app_service_source_control_slot\W+|function_app_active_slot\W+|function_app_function\W+|function_app_hybrid_connection\W+|linux_function_app\W+|linux_function_app\W+|linux_function_app_slot\W+|linux_web_app\W+|linux_web_app\W+|linux_web_app_slot\W+|service_plan|source_control_token|static_web_app|web_app_|windows_function_app\W+|windows_function_app\W+|windows_function_app_slot\W+|windows_web_app\W+|windows_web_app\W+|windows_web_app_slot\W+)((.|\n)*)###' + - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_(app_service_environment_v3\W+|app_service_environment_v3\W+|app_service_source_control\W+|app_service_source_control_slot\W+|function_app_active_slot\W+|function_app_flex_consumption\W+|function_app_function\W+|function_app_hybrid_connection\W+|linux_function_app\W+|linux_function_app\W+|linux_function_app_slot\W+|linux_web_app\W+|linux_web_app\W+|linux_web_app_slot\W+|service_plan|source_control_token|static_web_app|web_app_|windows_function_app\W+|windows_function_app\W+|windows_function_app_slot\W+|windows_web_app\W+|windows_web_app\W+|windows_web_app_slot\W+)((.|\n)*)###' service/application-insights: - '### (|New or )Affected Resource\(s\)\/Data Source\(s\)((.|\n)*)azurerm_application_insights((.|\n)*)###' diff --git a/internal/services/appservice/function_app_flex_consumption_resource.go b/internal/services/appservice/function_app_flex_consumption_resource.go new file mode 100644 index 000000000000..859fc9d775f7 --- /dev/null +++ b/internal/services/appservice/function_app_flex_consumption_resource.go @@ -0,0 +1,1001 @@ +package appservice + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" + "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-12-01/webapps" + "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/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tags" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/jackofallops/kermit/sdk/web/2022-09-01/web" +) + +const ( + StorageStringFmt = "DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=%s" +) + +type FunctionAppFlexConsumptionResource struct{} + +type FunctionAppFlexConsumptionModel struct { + Name string `tfschema:"name"` + ResourceGroup string `tfschema:"resource_group_name"` + Location string `tfschema:"location"` + ServicePlanId string `tfschema:"service_plan_id"` + + Enabled bool `tfschema:"enabled"` + AppSettings map[string]string `tfschema:"app_settings"` + StickySettings []helpers.StickySettings `tfschema:"sticky_settings"` + AuthSettings []helpers.AuthSettings `tfschema:"auth_settings"` + AuthV2Settings []helpers.AuthV2Settings `tfschema:"auth_settings_v2"` + ClientCertEnabled bool `tfschema:"client_certificate_enabled"` + ClientCertMode string `tfschema:"client_certificate_mode"` + ClientCertExclusionPaths string `tfschema:"client_certificate_exclusion_paths"` + ConnectionStrings []helpers.ConnectionString `tfschema:"connection_string"` + PublicNetworkAccess bool `tfschema:"public_network_access_enabled"` + VirtualNetworkSubnetID string `tfschema:"virtual_network_subnet_id"` + ZipDeployFile string `tfschema:"zip_deploy_file"` + PublishingDeployBasicAuthEnabled bool `tfschema:"webdeploy_publish_basic_authentication_enabled"` + + StorageContainerType string `tfschema:"storage_container_type"` + StorageContainerEndpoint string `tfschema:"storage_container_endpoint"` + StorageAuthType string `tfschema:"storage_authentication_type"` + StorageAccessKey string `tfschema:"storage_access_key"` + StorageUserAssignedIdentityID string `tfschema:"storage_user_assigned_identity_id"` + RuntimeName string `tfschema:"runtime_name"` + RuntimeVersion string `tfschema:"runtime_version"` + MaximumInstanceCount int64 `tfschema:"maximum_instance_count"` + InstanceMemoryInMB int64 `tfschema:"instance_memory_in_mb"` + SiteConfig []helpers.SiteConfigFunctionAppFlexConsumption `tfschema:"site_config"` + Identity []identity.ModelSystemAssignedUserAssigned `tfschema:"identity"` + Tags map[string]string `tfschema:"tags"` + + CustomDomainVerificationId string `tfschema:"custom_domain_verification_id"` + DefaultHostname string `tfschema:"default_hostname"` + HostingEnvId string `tfschema:"hosting_environment_id"` + Kind string `tfschema:"kind"` + OutboundIPAddresses string `tfschema:"outbound_ip_addresses"` + OutboundIPAddressList []string `tfschema:"outbound_ip_address_list"` + PossibleOutboundIPAddresses string `tfschema:"possible_outbound_ip_addresses"` + PossibleOutboundIPAddressList []string `tfschema:"possible_outbound_ip_address_list"` + + SiteCredentials []helpers.SiteCredential `tfschema:"site_credential"` +} + +var _ sdk.ResourceWithUpdate = FunctionAppFlexConsumptionResource{} + +func (r FunctionAppFlexConsumptionResource) ModelObject() interface{} { + return &FunctionAppFlexConsumptionModel{} +} + +func (r FunctionAppFlexConsumptionResource) ResourceType() string { + return "azurerm_function_app_flex_consumption" +} + +func (r FunctionAppFlexConsumptionResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return commonids.ValidateFunctionAppID +} + +func (r FunctionAppFlexConsumptionResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.WebAppName, + Description: "Specifies the name of the Function App.", + }, + + "resource_group_name": commonschema.ResourceGroupName(), + + "location": commonschema.Location(), + + "service_plan_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: commonids.ValidateAppServicePlanID, + Description: "The ID of the App Service Plan within which to create this Function App", + }, + + "storage_container_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(webapps.FunctionsDeploymentStorageTypeBlobContainer), + }, false), + Description: "The type of the storage container where the function app's code is hosted. Only `blobContainer` is supported currently.", + }, + + "storage_container_endpoint": { + Type: pluginsdk.TypeString, + Required: true, + Description: "The endpoint of the storage container where the function app's code is hosted.", + }, + + "storage_authentication_type": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(webapps.AuthenticationTypeSystemAssignedIdentity), + string(webapps.AuthenticationTypeStorageAccountConnectionString), + string(webapps.AuthenticationTypeUserAssignedIdentity), + }, false), + }, + + "storage_access_key": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "storage_user_assigned_identity_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: commonids.ValidateUserAssignedIdentityID, + }, + + "runtime_name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(webapps.RuntimeNameDotnetNegativeisolated), + string(webapps.RuntimeNameJava), + string(webapps.RuntimeNameNode), + string(webapps.RuntimeNamePowershell), + string(webapps.RuntimeNamePython), + string(webapps.RuntimeNameCustom), + }, false), + }, + + "runtime_version": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "instance_memory_in_mb": { + Type: pluginsdk.TypeInt, + Optional: true, + Default: 2048, + }, + + "maximum_instance_count": { + Type: pluginsdk.TypeInt, + Optional: true, + Default: 100, + ValidateFunc: validation.IntBetween(40, 1000), + }, + + "site_config": helpers.SiteConfigSchemaFunctionAppFlexConsumption(), + + "sticky_settings": helpers.StickySettingsSchema(), + + "app_settings": { + Type: pluginsdk.TypeMap, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + Description: "A map of key-value pairs for [App Settings](https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings) and custom values.", + }, + + "auth_settings": helpers.AuthSettingsSchema(), + + "auth_settings_v2": helpers.AuthV2SettingsSchema(), + + "client_certificate_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should the function app use Client Certificates", + }, + + "client_certificate_mode": { + Type: pluginsdk.TypeString, + Optional: true, + Default: web.ClientCertModeOptional, + ValidateFunc: validation.StringInSlice([]string{ + string(web.ClientCertModeOptional), + string(web.ClientCertModeRequired), + string(web.ClientCertModeOptionalInteractiveUser), + }, false), + Description: "The mode of the Function App's client certificates requirement for incoming requests. Possible values are `Required`, `Optional`, and `OptionalInteractiveUser` ", + }, + + "client_certificate_exclusion_paths": { + Type: pluginsdk.TypeString, + Optional: true, + Description: "Paths to exclude when using client certificates, separated by ;", + }, + + "connection_string": helpers.ConnectionStringSchema(), + + "enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + Description: "Is the Function App enabled.", + }, + + "identity": commonschema.SystemAssignedUserAssignedIdentityOptional(), + + "public_network_access_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "webdeploy_publish_basic_authentication_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: true, + }, + + "virtual_network_subnet_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: commonids.ValidateSubnetID, + }, + + "zip_deploy_file": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The local path and filename of the Zip packaged application to deploy to this 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`.", + }, + + "tags": tags.Schema(), + } +} + +func (r FunctionAppFlexConsumptionResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "custom_domain_verification_id": { + Type: pluginsdk.TypeString, + Computed: true, + Sensitive: true, + }, + + "default_hostname": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "hosting_environment_id": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "kind": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "outbound_ip_addresses": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "outbound_ip_address_list": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "possible_outbound_ip_addresses": { + Type: pluginsdk.TypeString, + Computed: true, + }, + + "possible_outbound_ip_address_list": { + Type: pluginsdk.TypeList, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + }, + + "site_credential": helpers.SiteCredentialSchema(), + } +} + +func (r FunctionAppFlexConsumptionResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.AppService.WebAppsClient + resourcesClient := metadata.Client.AppService.ResourceProvidersClient + servicePlanClient := metadata.Client.AppService.ServicePlanClient + subscriptionId := metadata.Client.Account.SubscriptionId + + storageDomainSuffix, ok := metadata.Client.Account.Environment.Storage.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Storage domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } + + var functionAppFlexConsumption FunctionAppFlexConsumptionModel + if err := metadata.Decode(&functionAppFlexConsumption); err != nil { + return err + } + + id := commonids.NewAppServiceID(subscriptionId, functionAppFlexConsumption.ResourceGroup, functionAppFlexConsumption.Name) + + servicePlanId, err := commonids.ParseAppServicePlanID(functionAppFlexConsumption.ServicePlanId) + if err != nil { + return err + } + + servicePlan, err := servicePlanClient.Get(ctx, *servicePlanId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", servicePlanId, err) + } + + var planSKU *string + availabilityRequest := resourceproviders.ResourceNameAvailabilityRequest{ + Name: functionAppFlexConsumption.Name, + Type: resourceproviders.CheckNameResourceTypesMicrosoftPointWebSites, + } + + if servicePlanModel := servicePlan.Model; servicePlanModel != nil { + if sku := servicePlanModel.Sku; sku != nil && sku.Name != nil { + planSKU = sku.Name + } + } + + isFlexConsumptionSku := helpers.PlanIsFlexConsumption(planSKU) + if !isFlexConsumptionSku { + return fmt.Errorf("the sku name is %s which is not valid for a flex consumption function app", *planSKU) + } + + existing, err := client.Get(ctx, id) + if err != nil && !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + checkName, err := resourcesClient.CheckNameAvailability(ctx, commonids.NewSubscriptionID(subscriptionId), availabilityRequest) + if err != nil { + return fmt.Errorf("checking name availability for %s: %+v", id, err) + } + if model := checkName.Model; model != nil && model.NameAvailable != nil && !*model.NameAvailable { + return fmt.Errorf("the Site Name %q failed the availability check: %+v", id.SiteName, *model.Message) + } + + expandedIdentity, err := identity.ExpandSystemAndUserAssignedMapFromModel(functionAppFlexConsumption.Identity) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + + blobContainerType := webapps.FunctionsDeploymentStorageType(functionAppFlexConsumption.StorageContainerType) + storageDeployment := &webapps.FunctionsDeployment{ + Storage: &webapps.FunctionsDeploymentStorage{ + Type: &blobContainerType, + Value: &functionAppFlexConsumption.StorageContainerEndpoint, + }, + } + storageAuthType := webapps.AuthenticationType(functionAppFlexConsumption.StorageAuthType) + storageConnStringForFCApp := "DEPLOYMENT_STORAGE_CONNECTION_STRING" + endpoint := strings.TrimPrefix(functionAppFlexConsumption.StorageContainerEndpoint, "https://") + var storageString string + if storageNameIndex := strings.Index(endpoint, "."); storageNameIndex != -1 { + storageName := endpoint[:storageNameIndex] + storageString = fmt.Sprintf(StorageStringFmt, storageName, functionAppFlexConsumption.StorageAccessKey, *storageDomainSuffix) + } else { + return fmt.Errorf("retrieving storage container endpoint error, the expected format is https://storagename.blob.core.windows.net/containername, the received value is %s", functionAppFlexConsumption.StorageContainerEndpoint) + } + storageAuth := webapps.FunctionsDeploymentStorageAuthentication{ + Type: &storageAuthType, + } + + if functionAppFlexConsumption.StorageAuthType == string(webapps.AuthenticationTypeStorageAccountConnectionString) { + if functionAppFlexConsumption.StorageAccessKey == "" { + return fmt.Errorf("the storage account access key must be specified when using the storage key based access") + } + } else { + storageConnStringForFCApp = "" + if functionAppFlexConsumption.StorageAuthType == string(webapps.AuthenticationTypeUserAssignedIdentity) { + if functionAppFlexConsumption.StorageUserAssignedIdentityID == "" { + return fmt.Errorf("the user assigned identity id must be specified when using the user assigned identity to access the storage account") + } + storageAuth.UserAssignedIdentityResourceId = &functionAppFlexConsumption.StorageUserAssignedIdentityID + } + } + + storageAuth.StorageAccountConnectionStringName = &storageConnStringForFCApp + storageDeployment.Storage.Authentication = &storageAuth + runtimeName := webapps.RuntimeName(functionAppFlexConsumption.RuntimeName) + runtime := webapps.FunctionsRuntime{ + Name: &runtimeName, + Version: &functionAppFlexConsumption.RuntimeVersion, + } + + scaleAndConcurrencyConfig := webapps.FunctionsScaleAndConcurrency{ + InstanceMemoryMB: &functionAppFlexConsumption.InstanceMemoryInMB, + MaximumInstanceCount: &functionAppFlexConsumption.MaximumInstanceCount, + } + + flexFunctionAppConfig := &webapps.FunctionAppConfig{ + Deployment: storageDeployment, + Runtime: &runtime, + ScaleAndConcurrency: &scaleAndConcurrencyConfig, + } + + siteConfig, err := helpers.ExpandSiteConfigFunctionFlexConsumptionApp(functionAppFlexConsumption.SiteConfig, nil, metadata, false, storageString, storageConnStringForFCApp) + if err != nil { + return fmt.Errorf("expanding `site_config` for %s: %+v", id, err) + } + + siteConfig.AppSettings = helpers.MergeUserAppSettings(siteConfig.AppSettings, functionAppFlexConsumption.AppSettings) + + siteEnvelope := webapps.Site{ + Location: location.Normalize(functionAppFlexConsumption.Location), + Tags: pointer.To(functionAppFlexConsumption.Tags), + Kind: pointer.To("functionapp,linux"), + Identity: expandedIdentity, + Properties: &webapps.SiteProperties{ + ServerFarmId: pointer.To(functionAppFlexConsumption.ServicePlanId), + Enabled: pointer.To(functionAppFlexConsumption.Enabled), + SiteConfig: siteConfig, + FunctionAppConfig: flexFunctionAppConfig, + ClientCertEnabled: pointer.To(functionAppFlexConsumption.ClientCertEnabled), + ClientCertMode: pointer.To(webapps.ClientCertMode(functionAppFlexConsumption.ClientCertMode)), + }, + } + + pna := helpers.PublicNetworkAccessEnabled + if !functionAppFlexConsumption.PublicNetworkAccess { + pna = helpers.PublicNetworkAccessDisabled + } + + siteEnvelope.Properties.PublicNetworkAccess = pointer.To(pna) + siteEnvelope.Properties.SiteConfig.PublicNetworkAccess = siteEnvelope.Properties.PublicNetworkAccess + + if functionAppFlexConsumption.VirtualNetworkSubnetID != "" { + siteEnvelope.Properties.VirtualNetworkSubnetId = pointer.To(functionAppFlexConsumption.VirtualNetworkSubnetID) + } + + if functionAppFlexConsumption.ClientCertExclusionPaths != "" { + siteEnvelope.Properties.ClientCertExclusionPaths = pointer.To(functionAppFlexConsumption.ClientCertExclusionPaths) + } + + if err = client.CreateOrUpdateThenPoll(ctx, id, siteEnvelope); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + + if !functionAppFlexConsumption.PublishingDeployBasicAuthEnabled { + sitePolicy := webapps.CsmPublishingCredentialsPoliciesEntity{ + Properties: &webapps.CsmPublishingCredentialsPoliciesEntityProperties{ + Allow: false, + }, + } + if _, err := client.UpdateScmAllowed(ctx, id, sitePolicy); err != nil { + return fmt.Errorf("setting basic auth for deploy publishing credentials for %s: %+v", id, err) + } + } + + stickySettings := helpers.ExpandStickySettings(functionAppFlexConsumption.StickySettings) + + if stickySettings != nil { + stickySettingsUpdate := webapps.SlotConfigNamesResource{ + Properties: stickySettings, + } + if _, err := client.UpdateSlotConfigurationNames(ctx, id, stickySettingsUpdate); err != nil { + return fmt.Errorf("updating Sticky Settings for %s: %+v", id, err) + } + } + + auth := helpers.ExpandAuthSettings(functionAppFlexConsumption.AuthSettings) + if auth.Properties != nil { + if _, err := client.UpdateAuthSettings(ctx, id, *auth); err != nil { + return fmt.Errorf("setting Authorisation Settings for %s: %+v", id, err) + } + } + + authv2 := helpers.ExpandAuthV2Settings(functionAppFlexConsumption.AuthV2Settings) + if authv2.Properties != nil { + if _, err = client.UpdateAuthSettingsV2(ctx, id, *authv2); err != nil { + return fmt.Errorf("updating AuthV2 settings for %s: %+v", id, err) + } + } + + connectionStrings := helpers.ExpandConnectionStrings(functionAppFlexConsumption.ConnectionStrings) + if connectionStrings.Properties != nil { + if _, err := client.UpdateConnectionStrings(ctx, id, *connectionStrings); err != nil { + return fmt.Errorf("setting Connection Strings for %s: %+v", id, err) + } + } + + if functionAppFlexConsumption.ZipDeployFile != "" { + if err = helpers.GetCredentialsAndPublish(ctx, client, id, functionAppFlexConsumption.ZipDeployFile); err != nil { + return err + } + } + return nil + }, + } +} + +func (r FunctionAppFlexConsumptionResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.AppService.WebAppsClient + id, err := commonids.ParseFunctionAppID(metadata.ResourceData.Id()) + if err != nil { + return err + } + functionAppFlexConsumption, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(functionAppFlexConsumption.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + appSettingsResp, err := client.ListApplicationSettings(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving App Settings for %s: %+v", id, err) + } + + connectionStrings, err := client.ListConnectionStrings(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving Connection String information for %s: %+v", id, err) + } + + stickySettings, err := client.ListSlotConfigurationNames(ctx, *id) + if err != nil || stickySettings.Model == nil { + return fmt.Errorf("retrieving Sticky Settings for %s: %+v", id, err) + } + + siteCredentials, err := helpers.ListPublishingCredentials(ctx, client, *id) + if err != nil { + return fmt.Errorf("listing Site Publishing Credential information for %s: %+v", *id, err) + } + + auth, err := client.GetAuthSettings(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving Auth Settings for %s: %+v", id, err) + } + + var authV2 webapps.SiteAuthSettingsV2 + if auth.Model != nil && auth.Model.Properties != nil && strings.EqualFold(pointer.From(auth.Model.Properties.ConfigVersion), "v2") { + authV2Resp, err := client.GetAuthSettingsV2(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving authV2 settings for %s: %+v", *id, err) + } + authV2 = *authV2Resp.Model + } + + basicAuthWebDeploy := true + if basicAuthWebDeployResp, err := client.GetScmAllowed(ctx, *id); err != nil && basicAuthWebDeployResp.Model != nil { + return fmt.Errorf("retrieving state of WebDeploy Basic Auth for %s: %+v", id, err) + } else if csmProps := basicAuthWebDeployResp.Model.Properties; csmProps != nil { + basicAuthWebDeploy = csmProps.Allow + } + + model := functionAppFlexConsumption.Model + if model == nil { + return fmt.Errorf("function app %s : model is nil", id) + } + flattenedIdentity, err := identity.FlattenSystemAndUserAssignedMapToModel(model.Identity) + if err != nil { + return fmt.Errorf("flattening `identity`: %+v", err) + } + state := FunctionAppFlexConsumptionModel{ + Name: id.SiteName, + ResourceGroup: id.ResourceGroupName, + Location: location.Normalize(model.Location), + ConnectionStrings: helpers.FlattenConnectionStrings(connectionStrings.Model), + StickySettings: helpers.FlattenStickySettings(stickySettings.Model.Properties), + SiteCredentials: helpers.FlattenSiteCredentials(siteCredentials), + AuthSettings: helpers.FlattenAuthSettings(auth.Model), + AuthV2Settings: helpers.FlattenAuthV2Settings(authV2), + PublishingDeployBasicAuthEnabled: basicAuthWebDeploy, + Tags: pointer.From(model.Tags), + Kind: pointer.From(model.Kind), + Identity: pointer.From(flattenedIdentity), + } + + if props := model.Properties; props != nil { + state.Enabled = pointer.From(props.Enabled) + state.ClientCertMode = string(pointer.From(props.ClientCertMode)) + state.ClientCertExclusionPaths = pointer.From(props.ClientCertExclusionPaths) + state.CustomDomainVerificationId = pointer.From(props.CustomDomainVerificationId) + state.DefaultHostname = pointer.From(props.DefaultHostName) + state.PublicNetworkAccess = !strings.EqualFold(pointer.From(props.PublicNetworkAccess), helpers.PublicNetworkAccessDisabled) + + servicePlanId, err := commonids.ParseAppServicePlanIDInsensitively(*props.ServerFarmId) + if err != nil { + return err + } + state.ServicePlanId = servicePlanId.ID() + + if v := props.OutboundIPAddresses; v != nil { + state.OutboundIPAddresses = *v + state.OutboundIPAddressList = strings.Split(*v, ",") + } + + if v := props.PossibleOutboundIPAddresses; v != nil { + state.PossibleOutboundIPAddresses = *v + state.PossibleOutboundIPAddressList = strings.Split(*v, ",") + } + + configResp, err := client.GetConfiguration(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving Function App Configuration %q: %+v", id.SiteName, err) + } + + siteConfig, err := helpers.FlattenSiteConfigFunctionAppFlexConsumption(configResp.Model.Properties) + if err != nil { + return fmt.Errorf("retrieving Site Config for %s: %+v", id, err) + } + state.SiteConfig = []helpers.SiteConfigFunctionAppFlexConsumption{*siteConfig} + + if functionAppConfig := props.FunctionAppConfig; functionAppConfig != nil { + if faConfigDeployment := functionAppConfig.Deployment; faConfigDeployment != nil && faConfigDeployment.Storage != nil { + storageConfig := *faConfigDeployment.Storage + state.StorageContainerType = string(pointer.From(storageConfig.Type)) + state.StorageContainerEndpoint = pointer.From(storageConfig.Value) + if storageConfig.Authentication != nil && storageConfig.Authentication.Type != nil { + state.StorageAuthType = string(pointer.From(storageConfig.Authentication.Type)) + if storageConfig.Authentication.UserAssignedIdentityResourceId != nil { + state.StorageUserAssignedIdentityID = pointer.From(storageConfig.Authentication.UserAssignedIdentityResourceId) + } + } + } + + if faConfigRuntime := functionAppConfig.Runtime; faConfigRuntime != nil { + state.RuntimeName = string(pointer.From(faConfigRuntime.Name)) + state.RuntimeVersion = pointer.From(faConfigRuntime.Version) + } + + if faConfigScale := functionAppConfig.ScaleAndConcurrency; faConfigScale != nil { + state.InstanceMemoryInMB = pointer.From(faConfigScale.InstanceMemoryMB) + state.MaximumInstanceCount = pointer.From(faConfigScale.MaximumInstanceCount) + } + } + + state.unpackFunctionAppFlexConsumptionSettings(*appSettingsResp.Model) + + state.ClientCertEnabled = pointer.From(props.ClientCertEnabled) + + if props.VirtualNetworkSubnetId != nil && pointer.From(props.VirtualNetworkSubnetId) != "" { + subnetId, err := commonids.ParseSubnetID(*props.VirtualNetworkSubnetId) + if err != nil { + return err + } + state.VirtualNetworkSubnetID = subnetId.ID() + } + + // Zip Deploys are not retrievable, so attempt to get from config. This doesn't matter for imports as an unexpected value here could break the deployment. + if deployFile, ok := metadata.ResourceData.Get("zip_deploy_file").(string); ok { + state.ZipDeployFile = deployFile + } + } + return metadata.Encode(&state) + }, + } +} + +func (r FunctionAppFlexConsumptionResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.AppService.WebAppsClient + id, err := commonids.ParseFunctionAppID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + metadata.Logger.Infof("deleting %s", *id) + + delOptions := webapps.DeleteOperationOptions{ + DeleteEmptyServerFarm: pointer.To(false), + DeleteMetrics: pointer.To(false), + } + if _, err = client.Delete(ctx, *id, delOptions); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + return nil + }, + } +} + +func (r FunctionAppFlexConsumptionResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + storageDomainSuffix, ok := metadata.Client.Account.Environment.Storage.DomainSuffix() + if !ok { + return fmt.Errorf("could not determine Storage domain suffix for environment %q", metadata.Client.Account.Environment.Name) + } + + client := metadata.Client.AppService.WebAppsClient + id, err := commonids.ParseFunctionAppID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var state FunctionAppFlexConsumptionModel + if err := metadata.Decode(&state); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + existing, err := client.Get(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving %s: %v", id, err) + } + if existing.Model == nil { + return fmt.Errorf("retrieving %s: `model` was nil", id) + } + + if existing.Model.Properties == nil { + return fmt.Errorf("retrieving %s: `properties` was nil", id) + } + + model := *existing.Model + + if metadata.ResourceData.HasChange("enabled") { + model.Properties.Enabled = pointer.To(state.Enabled) + } + + if metadata.ResourceData.HasChange("virtual_network_subnet_id") { + subnetId := metadata.ResourceData.Get("virtual_network_subnet_id").(string) + if subnetId == "" { + if _, err := client.DeleteSwiftVirtualNetwork(ctx, *id); err != nil { + return fmt.Errorf("removing `virtual_network_subnet_id` association for %s: %+v", *id, err) + } + var empty *string + model.Properties.VirtualNetworkSubnetId = empty + } else { + model.Properties.VirtualNetworkSubnetId = pointer.To(subnetId) + } + } + + if metadata.ResourceData.HasChange("client_certificate_enabled") { + model.Properties.ClientCertEnabled = pointer.To(state.ClientCertEnabled) + } + + if metadata.ResourceData.HasChange("client_certificate_mode") { + model.Properties.ClientCertMode = pointer.To(webapps.ClientCertMode(state.ClientCertMode)) + } + + if metadata.ResourceData.HasChange("client_certificate_exclusion_paths") { + model.Properties.ClientCertExclusionPaths = pointer.To(state.ClientCertExclusionPaths) + } + + if metadata.ResourceData.HasChange("identity") { + expandedIdentity, err := identity.ExpandSystemAndUserAssignedMapFromModel(state.Identity) + if err != nil { + return fmt.Errorf("expanding `identity`: %+v", err) + } + model.Identity = expandedIdentity + } + + if metadata.ResourceData.HasChange("tags") { + model.Tags = pointer.To(state.Tags) + } + + var storageString string + if state.StorageContainerEndpoint != "" || metadata.ResourceData.HasChange("storage_container_endpoint") { + endpoint := strings.TrimPrefix(state.StorageContainerEndpoint, "https://") + model.Properties.FunctionAppConfig.Deployment.Storage.Value = pointer.To(state.StorageContainerEndpoint) + if storageNameIndex := strings.Index(endpoint, "."); storageNameIndex != -1 { + storageName := endpoint[:storageNameIndex] + storageString = fmt.Sprintf(StorageStringFmt, storageName, state.StorageAccessKey, *storageDomainSuffix) + } else { + return fmt.Errorf("retrieving storage container endpoint error, the expected format is https://storagename.blob.core.windows.net/containername, the received value is %s", state.StorageContainerEndpoint) + } + } + + storageConnStringForFCApp := "DEPLOYMENT_STORAGE_CONNECTION_STRING" + if metadata.ResourceData.HasChange("storage_authentication_type") { + storageAuthType := webapps.AuthenticationType(state.StorageAuthType) + storageAuth := webapps.FunctionsDeploymentStorageAuthentication{ + Type: &storageAuthType, + } + if state.StorageAuthType == string(webapps.AuthenticationTypeStorageAccountConnectionString) { + if state.StorageAccessKey == "" { + return fmt.Errorf("the storage account access key must be specified when using the storage key based access") + } + storageAuth.StorageAccountConnectionStringName = pointer.To(storageConnStringForFCApp) + } else { + storageConnStringForFCApp = "" + if state.StorageAuthType == string(webapps.AuthenticationTypeUserAssignedIdentity) { + if state.StorageUserAssignedIdentityID == "" { + return fmt.Errorf("the user assigned identity id must be specified when using the user assigned identity to access the storage account") + } + storageAuth.UserAssignedIdentityResourceId = &state.StorageUserAssignedIdentityID + } + } + model.Properties.FunctionAppConfig.Deployment.Storage.Authentication = &storageAuth + } + + if metadata.ResourceData.HasChange("storage_user_assigned_identity_id") { + model.Properties.FunctionAppConfig.Deployment.Storage.Authentication.UserAssignedIdentityResourceId = &state.StorageUserAssignedIdentityID + } + + // 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.ExpandSiteConfigFunctionFlexConsumptionApp(state.SiteConfig, model.Properties.SiteConfig, metadata, false, storageString, storageConnStringForFCApp) + if err != nil { + return fmt.Errorf("expanding Site Config for %s: %+v", id, err) + } + + if metadata.ResourceData.HasChange("site_config") { + model.Properties.SiteConfig = siteConfig + } + + if metadata.ResourceData.HasChange("runtime_name") { + runtimeName := webapps.RuntimeName(state.RuntimeName) + model.Properties.FunctionAppConfig.Runtime.Name = pointer.To(runtimeName) + } + + if metadata.ResourceData.HasChange("runtime_version") { + model.Properties.FunctionAppConfig.Runtime.Version = pointer.To(state.RuntimeVersion) + } + + model.Properties.SiteConfig.AppSettings = helpers.MergeUserAppSettings(siteConfig.AppSettings, state.AppSettings) + + if metadata.ResourceData.HasChange("public_network_access_enabled") { + pna := helpers.PublicNetworkAccessEnabled + if !state.PublicNetworkAccess { + pna = helpers.PublicNetworkAccessDisabled + } + + // (@jackofallops) - Values appear to need to be set in both SiteProperties and SiteConfig for now? https://github.com/Azure/azure-rest-api-specs/issues/24681 + model.Properties.PublicNetworkAccess = pointer.To(pna) + model.Properties.SiteConfig.PublicNetworkAccess = model.Properties.PublicNetworkAccess + } + + if err := client.CreateOrUpdateThenPoll(ctx, *id, model); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + if metadata.ResourceData.HasChange("webdeploy_publish_basic_authentication_enabled") { + sitePolicy := webapps.CsmPublishingCredentialsPoliciesEntity{ + Properties: &webapps.CsmPublishingCredentialsPoliciesEntityProperties{ + Allow: state.PublishingDeployBasicAuthEnabled, + }, + } + if _, err := client.UpdateScmAllowed(ctx, *id, sitePolicy); err != nil { + return fmt.Errorf("setting basic auth for deploy publishing credentials for %s: %+v", id, err) + } + } + + if _, err := client.UpdateConfiguration(ctx, *id, webapps.SiteConfigResource{Properties: model.Properties.SiteConfig}); err != nil { + return fmt.Errorf("updating Site Config for %s: %+v", id, err) + } + + if metadata.ResourceData.HasChange("connection_string") { + connectionStringUpdate := helpers.ExpandConnectionStrings(state.ConnectionStrings) + if connectionStringUpdate.Properties == nil { + connectionStringUpdate.Properties = pointer.To(map[string]webapps.ConnStringValueTypePair{}) + } + if _, err := client.UpdateConnectionStrings(ctx, *id, *connectionStringUpdate); err != nil { + return fmt.Errorf("updating Connection Strings for %s: %+v", id, err) + } + } + + if metadata.ResourceData.HasChange("sticky_settings") { + emptySlice := make([]string, 0) + stickySettings := helpers.ExpandStickySettings(state.StickySettings) + stickySettingsUpdate := webapps.SlotConfigNamesResource{ + Properties: &webapps.SlotConfigNames{ + AppSettingNames: &emptySlice, + ConnectionStringNames: &emptySlice, + }, + } + + if stickySettings != nil { + if stickySettings.AppSettingNames != nil { + stickySettingsUpdate.Properties.AppSettingNames = stickySettings.AppSettingNames + } + if stickySettings.ConnectionStringNames != nil { + stickySettingsUpdate.Properties.ConnectionStringNames = stickySettings.ConnectionStringNames + } + } + + if _, err := client.UpdateSlotConfigurationNames(ctx, *id, stickySettingsUpdate); err != nil { + return fmt.Errorf("updating Sticky Settings for %s: %+v", id, err) + } + } + + if metadata.ResourceData.HasChange("auth_settings") { + authUpdate := helpers.ExpandAuthSettings(state.AuthSettings) + // (@jackofallops) - in the case of a removal of this block, we need to zero these settings + if authUpdate.Properties == nil { + authUpdate.Properties = &webapps.SiteAuthSettingsProperties{ + Enabled: pointer.To(false), + ClientSecret: pointer.To(""), + ClientSecretSettingName: pointer.To(""), + ClientSecretCertificateThumbprint: pointer.To(""), + GoogleClientSecret: pointer.To(""), + FacebookAppSecret: pointer.To(""), + GitHubClientSecret: pointer.To(""), + TwitterConsumerSecret: pointer.To(""), + MicrosoftAccountClientSecret: pointer.To(""), + } + } + if _, err := client.UpdateAuthSettings(ctx, *id, *authUpdate); err != nil { + return fmt.Errorf("updating Auth Settings for %s: %+v", id, err) + } + } + + if metadata.ResourceData.HasChange("auth_settings_v2") { + authV2Update := helpers.ExpandAuthV2Settings(state.AuthV2Settings) + if _, err := client.UpdateAuthSettingsV2(ctx, *id, *authV2Update); err != nil { + return fmt.Errorf("updating AuthV2 Settings for %s: %+v", id, err) + } + } + + if metadata.ResourceData.HasChange("site_config.0.app_service_logs") { + appServiceLogs := helpers.ExpandFunctionAppAppServiceLogs(state.SiteConfig[0].AppServiceLogs) + if _, err := client.UpdateDiagnosticLogsConfig(ctx, *id, appServiceLogs); err != nil { + return fmt.Errorf("updating App Service Log Settings for %s: %+v", id, err) + } + } + + if metadata.ResourceData.HasChange("zip_deploy_file") { + if err = helpers.GetCredentialsAndPublish(ctx, client, *id, state.ZipDeployFile); err != nil { + return err + } + } + + return nil + }, + } +} + +func (m *FunctionAppFlexConsumptionModel) unpackFunctionAppFlexConsumptionSettings(input webapps.StringDictionary) { + if input.Properties == nil { + return + } + + appSettings := make(map[string]string) + + for k, v := range *input.Properties { + switch k { + case "APPINSIGHTS_INSTRUMENTATIONKEY": + m.SiteConfig[0].AppInsightsInstrumentationKey = v + + case "APPLICATIONINSIGHTS_CONNECTION_STRING": + m.SiteConfig[0].AppInsightsConnectionString = v + + case "AzureWebJobsStorage": + _, m.StorageAccessKey = helpers.ParseWebJobsStorageString(v) + + case "WEBSITE_HEALTHCHECK_MAXPINGFAILURES": + i, _ := strconv.Atoi(v) + m.SiteConfig[0].HealthCheckEvictionTime = int64(i) + + case "DEPLOYMENT_STORAGE_CONNECTION_STRING": + // Filter out - not user faced + + default: + appSettings[k] = v + } + } + m.AppSettings = appSettings +} diff --git a/internal/services/appservice/function_app_flex_consumption_resource_test.go b/internal/services/appservice/function_app_flex_consumption_resource_test.go new file mode 100644 index 000000000000..4de93cf707a3 --- /dev/null +++ b/internal/services/appservice/function_app_flex_consumption_resource_test.go @@ -0,0 +1,1022 @@ +package appservice_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "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/tf/pluginsdk" +) + +type FunctionAppFlexConsumptionResource struct{} + +func TestAccFunctionAppFlexConsumption_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_connectionString(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.connectionString(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_stickySettings(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.stickySettings(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_connectionStringUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.connectionString(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.connectionStringUpdate(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.connectionString(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_appSettings(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettings(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_appSettingsUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.appSettings(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.appSettingsAddKvps(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.appSettingsRemoveKvps(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimePython(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.python(data, "3.10"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimeNode(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.runtimeNode(data, "20"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimeJava(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.java(data, "11"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimeJavaUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.java(data, "11"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.java(data, "17"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.java(data, "11"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimeDotNet(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.dotNet(data, "8.0"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_runtimePowerShell(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.powerShell(data, "7.4"), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_systemAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.storageSystemAssignedIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_userAssignedIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.storageUserAssignedIdentity1(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func TestAccFunctionAppFlexConsumption_userAssignedIdentityUpdate(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_function_app_flex_consumption", "test") + r := FunctionAppFlexConsumptionResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.storageUserAssignedIdentity1(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + { + Config: r.storageUserAssignedIdentity2(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("kind").HasValue("functionapp,linux"), + ), + }, + data.ImportStep("site_credential.0.password"), + }) +} + +func (r FunctionAppFlexConsumptionResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := commonids.ParseFunctionAppID(state.ID) + if err != nil { + return nil, err + } + + resp, err := client.AppService.WebAppsClient.Get(ctx, *id) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + return pointer.To(resp.Model != nil), nil +} + +func (r FunctionAppFlexConsumptionResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) connectionString(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + connection_string { + name = "Example" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) stickySettings(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + app_settings = { + foo = "bar" + secret = "sauce" + third = "degree" + } + + connection_string { + name = "First" + value = "first-connection-string" + type = "Custom" + } + + connection_string { + name = "Second" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + connection_string { + name = "Third" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + sticky_settings { + app_setting_names = ["foo", "secret"] + connection_string_names = ["First", "Third"] + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) connectionStringUpdate(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + connection_string { + name = "Example" + value = "some-postgresql-connection-string" + type = "PostgreSQL" + } + + connection_string { + name = "AnotherExample" + value = "some-other-connection-string" + type = "Custom" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + app_settings = { + foo = "bar" + secret = "sauce" + } + + 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" + application_insights_connection_string = azurerm_application_insights.test.connection_string + + 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" + remote_debugging_enabled = true + remote_debugging_version = "VS2022" + + 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"] + } + } + + websockets_enabled = true + health_check_path = "/health-check" + health_check_eviction_time_in_min = 7 + worker_count = 3 + + minimum_tls_version = "1.2" + scm_minimum_tls_version = "1.2" + + cors { + allowed_origins = [ + "https://www.contoso.com", + "www.contoso.com", + ] + + support_credentials = true + } + + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) appSettings(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + app_settings = { + "tftest" : "tftestvalue" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) appSettingsAddKvps(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + app_settings = { + "tftest" : "tftestvalue", + "tftestkvp1" : "tftestkvpvalue1" + "tftestkvp2" : "tftestkvpvalue2" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) appSettingsRemoveKvps(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "test" { + name = "acctest-LFA-tf%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + service_plan_id = azurerm_service_plan.test.id + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} + + app_settings = { + "tftest" : "tftestvalue", + "tftestkvp1" : "tftestkvpvalue1" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) python(data acceptance.TestData, pythonVersion string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} +%s +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "python" + runtime_version = "%s" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger, pythonVersion) +} + +func (r FunctionAppFlexConsumptionResource) java(data acceptance.TestData, javaVersion string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "java" + runtime_version = "%s" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger, javaVersion) +} + +func (r FunctionAppFlexConsumptionResource) dotNet(data acceptance.TestData, dotNetVersion string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "dotnet-isolated" + runtime_version = "%s" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger, dotNetVersion) +} + +func (r FunctionAppFlexConsumptionResource) powerShell(data acceptance.TestData, powerShellVersion string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.test.primary_access_key + runtime_name = "powershell" + runtime_version = "%s" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger, powerShellVersion) +} + +func (r FunctionAppFlexConsumptionResource) storageSystemAssignedIdentity(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "SystemAssignedIdentity" + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) storageUserAssignedIdentity1(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s +resource "azurerm_user_assigned_identity" "test1" { + name = "acct-uai1-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "UserAssignedIdentity" + storage_user_assigned_identity_id = azurerm_user_assigned_identity.test1.id + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) storageUserAssignedIdentity2(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_user_assigned_identity" "test2" { + name = "acct-uai2-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "UserAssignedIdentity" + storage_user_assigned_identity_id = azurerm_user_assigned_identity.test2.id + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger) +} + +func (r FunctionAppFlexConsumptionResource) runtimeNode(data acceptance.TestData, nodeVersion string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +%s + +resource "azurerm_user_assigned_identity" "test2" { + name = "acct-uai2-%[2]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_function_app_flex_consumption" "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_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.test.id + storage_authentication_type = "UserAssignedIdentity" + storage_user_assigned_identity_id = azurerm_user_assigned_identity.test2.id + runtime_name = "node" + runtime_version = "%s" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 + + site_config {} +} +`, r.template(data), data.RandomInteger, nodeVersion) +} + +func (FunctionAppFlexConsumptionResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-LFA-%[1]d" + location = "%s" +} + +resource "azurerm_application_insights" "test" { + name = "acctestappinsights-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + application_type = "web" +} + +resource "azurerm_storage_account" "test" { + name = "acctestsa%[3]s" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "test" { + name = "acctestblobforfc" + storage_account_name = azurerm_storage_account.test.name + container_access_type = "private" +} + +data "azurerm_storage_account_sas" "test" { + connection_string = azurerm_storage_account.test.primary_connection_string + https_only = true + + resource_types { + service = false + container = false + object = true + } + + services { + blob = true + queue = false + table = false + file = false + } + + start = "2021-04-01" + expiry = "2024-03-30" + + permissions { + read = false + write = true + delete = false + list = false + add = false + create = false + update = false + process = false + tag = false + filter = false + } +} + +resource "azurerm_service_plan" "test" { + name = "acctestASP-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + os_type = "Linux" + sku_name = "FC1" +} +`, data.RandomInteger, "eastus2", data.RandomString, data.RandomInteger) // location needs to be hardcoded for the moment because flex isn't available in all regions yet and appservice already has location overrides in TC +} diff --git a/internal/services/appservice/helpers/function_app_schema.go b/internal/services/appservice/helpers/function_app_schema.go index cef268de8c82..dc04c4e480a9 100644 --- a/internal/services/appservice/helpers/function_app_schema.go +++ b/internal/services/appservice/helpers/function_app_schema.go @@ -274,18 +274,29 @@ func SiteConfigSchemaLinuxFunctionApp() *pluginsdk.Schema { }, "health_check_path": { - Type: pluginsdk.TypeString, - Optional: true, - Description: "The path to be checked for this function app health.", - RequiredWith: []string{"site_config.0.health_check_eviction_time_in_min"}, + Type: pluginsdk.TypeString, + Optional: true, + Description: "The path to be checked for this function app health.", + RequiredWith: func() []string { + if features.FourPointOhBeta() { + return []string{"site_config.0.health_check_eviction_time_in_min"} + } + return []string{} + }(), }, "health_check_eviction_time_in_min": { // NOTE: Will evict the only node in single node configurations. Type: pluginsdk.TypeInt, Optional: true, + Computed: !features.FourPointOhBeta(), ValidateFunc: validation.IntBetween(2, 10), - RequiredWith: []string{"site_config.0.health_check_path"}, - Description: "The amount of time in minutes that a node is unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`", + RequiredWith: func() []string { + if features.FourPointOhBeta() { + return []string{"site_config.0.health_check_path"} + } + return []string{} + }(), + Description: "The amount of time in minutes that a node is unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`", }, "worker_count": { @@ -537,6 +548,272 @@ func SiteConfigSchemaLinuxFunctionAppComputed() *pluginsdk.Schema { } } +type SiteConfigFunctionAppFlexConsumption struct { + AppCommandLine string `tfschema:"app_command_line"` + ApiDefinition string `tfschema:"api_definition_url"` + ApiManagementConfigId string `tfschema:"api_management_api_id"` + AppInsightsInstrumentationKey string `tfschema:"application_insights_key"` + AppInsightsConnectionString string `tfschema:"application_insights_connection_string"` + AppServiceLogs []FunctionAppAppServiceLogs `tfschema:"app_service_logs"` + UseManagedIdentityACR bool `tfschema:"container_registry_use_managed_identity"` + ContainerRegistryMSI string `tfschema:"container_registry_managed_identity_client_id"` + DefaultDocuments []string `tfschema:"default_documents"` + ElasticInstanceMinimum int64 `tfschema:"elastic_instance_minimum"` + Http2Enabled bool `tfschema:"http2_enabled"` + IpRestriction []IpRestriction `tfschema:"ip_restriction"` + IpRestrictionDefaultAction string `tfschema:"ip_restriction_default_action"` + LoadBalancing string `tfschema:"load_balancing_mode"` + ManagedPipelineMode string `tfschema:"managed_pipeline_mode"` + RemoteDebugging bool `tfschema:"remote_debugging_enabled"` + RemoteDebuggingVersion string `tfschema:"remote_debugging_version"` + RuntimeScaleMonitoring bool `tfschema:"runtime_scale_monitoring_enabled"` + ScmIpRestriction []IpRestriction `tfschema:"scm_ip_restriction"` + ScmIpRestrictionDefaultAction string `tfschema:"scm_ip_restriction_default_action"` + ScmType string `tfschema:"scm_type"` + ScmUseMainIpRestriction bool `tfschema:"scm_use_main_ip_restriction"` + Use32BitWorker bool `tfschema:"use_32_bit_worker"` + WebSockets bool `tfschema:"websockets_enabled"` + HealthCheckPath string `tfschema:"health_check_path"` + HealthCheckEvictionTime int64 `tfschema:"health_check_eviction_time_in_min"` + WorkerCount int64 `tfschema:"worker_count"` + MinTlsVersion string `tfschema:"minimum_tls_version"` + ScmMinTlsVersion string `tfschema:"scm_minimum_tls_version"` + Cors []CorsSetting `tfschema:"cors"` + DetailedErrorLogging bool `tfschema:"detailed_error_logging_enabled"` +} + +func SiteConfigSchemaFunctionAppFlexConsumption() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Required: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "api_management_api_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: apimValidate.ApiID, + Description: "The ID of the API Management API for this Linux Function App.", + }, + + "api_definition_url": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsURLWithHTTPorHTTPS, + Description: "The URL of the API definition that describes this Linux Function App.", + }, + + "app_command_line": { + Type: pluginsdk.TypeString, + Optional: true, + Description: "The program and any arguments used to launch this app via the command line. (Example `node myapp.js`).", + }, + + "application_insights_key": { + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The Instrumentation Key for connecting the Linux Function App to Application Insights.", + }, + + "application_insights_connection_string": { + Type: pluginsdk.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The Connection String for linking the Linux Function App to Application Insights.", + }, + + "app_service_logs": FunctionAppAppServiceLogsSchema(), + + "container_registry_use_managed_identity": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should connections for Azure Container Registry use Managed Identity.", + }, + + "container_registry_managed_identity_client_id": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.IsUUID, + Description: "The Client ID of the Managed Service Identity to use for connections to the Azure Container Registry.", + }, + + "default_documents": { + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + }, + Description: "Specifies a list of Default Documents for the Linux Web App.", + }, + + "elastic_instance_minimum": { + Type: pluginsdk.TypeInt, + Optional: true, + Computed: true, + Description: "The number of minimum instances for this Linux Function App. Only affects apps on Elastic Premium plans.", + }, + + "http2_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Specifies if the http2 protocol should be enabled. Defaults to `false`.", + }, + + "ip_restriction": IpRestrictionSchema(), + + "ip_restriction_default_action": { + Type: pluginsdk.TypeString, + Optional: true, + Default: webapps.DefaultActionAllow, + ValidateFunc: validation.StringInSlice(webapps.PossibleValuesForDefaultAction(), false), + }, + + "scm_use_main_ip_restriction": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should the Linux Function App `ip_restriction` configuration be used for the SCM also.", + }, + + "scm_ip_restriction": IpRestrictionSchema(), + + "scm_ip_restriction_default_action": { + Type: pluginsdk.TypeString, + Optional: true, + Default: webapps.DefaultActionAllow, + ValidateFunc: validation.StringInSlice(webapps.PossibleValuesForDefaultAction(), false), + }, + + "load_balancing_mode": { // Supported on Function Apps? + Type: pluginsdk.TypeString, + Optional: true, + Default: "LeastRequests", + ValidateFunc: validation.StringInSlice([]string{ + "LeastRequests", // Service default + "WeightedRoundRobin", + "LeastResponseTime", + "WeightedTotalTraffic", + "RequestHash", + "PerSiteRoundRobin", + }, false), + Description: "The Site load balancing mode. Possible values include: `WeightedRoundRobin`, `LeastRequests`, `LeastResponseTime`, `WeightedTotalTraffic`, `RequestHash`, `PerSiteRoundRobin`. Defaults to `LeastRequests` if omitted.", + }, + + "managed_pipeline_mode": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(webapps.ManagedPipelineModeIntegrated), + ValidateFunc: validation.StringInSlice([]string{ + string(webapps.ManagedPipelineModeClassic), + string(webapps.ManagedPipelineModeIntegrated), + }, false), + Description: "The Managed Pipeline mode. Possible values include: `Integrated`, `Classic`. Defaults to `Integrated`.", + }, + + "remote_debugging_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should Remote Debugging be enabled. Defaults to `false`.", + }, + + "remote_debugging_version": { + Type: pluginsdk.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + "VS2017", + "VS2019", + "VS2022", + }, false), + Description: "The Remote Debugging Version. Possible values include `VS2017`, `VS2019`, and `VS2022``", + }, + + "runtime_scale_monitoring_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Description: "Should Functions Runtime Scale Monitoring be enabled.", + }, + + "scm_type": { + Type: pluginsdk.TypeString, + Computed: true, + Description: "The SCM Type in use by the Linux Function App.", + }, + + "use_32_bit_worker": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should the Linux Web App use a 32-bit worker.", + }, + + "websockets_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + Description: "Should Web Sockets be enabled. Defaults to `false`.", + }, + + "health_check_path": { + Type: pluginsdk.TypeString, + Optional: true, + Description: "The path to be checked for this function app health.", + RequiredWith: []string{"site_config.0.health_check_eviction_time_in_min"}, + }, + + "health_check_eviction_time_in_min": { // NOTE: Will evict the only node in single node configurations. + Type: pluginsdk.TypeInt, + Optional: true, + Computed: !features.FourPointOhBeta(), + ValidateFunc: validation.IntBetween(2, 10), + RequiredWith: []string{"site_config.0.health_check_path"}, + + Description: "The amount of time in minutes that a node is unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`", + }, + + "worker_count": { + Type: pluginsdk.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(1, 100), + Description: "The number of Workers for this Linux Function App.", + }, + + "minimum_tls_version": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(webapps.SupportedTlsVersionsOnePointTwo), + ValidateFunc: validation.StringInSlice(webapps.PossibleValuesForSupportedTlsVersions(), false), + Description: "The configures the minimum version of TLS required for SSL requests. Possible values include: `1.0`, `1.1`, and `1.2`. Defaults to `1.2`.", + }, + + "scm_minimum_tls_version": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(webapps.SupportedTlsVersionsOnePointTwo), + ValidateFunc: validation.StringInSlice(webapps.PossibleValuesForSupportedTlsVersions(), false), + Description: "Configures the minimum version of TLS required for SSL requests to the SCM site Possible values include: `1.0`, `1.1`, and `1.2`. Defaults to `1.2`.", + }, + + "cors": CorsSettingsSchema(), + + "detailed_error_logging_enabled": { + Type: pluginsdk.TypeBool, + Computed: true, + Description: "Is detailed error logging enabled", + }, + }, + }, + } +} + type SiteConfigWindowsFunctionApp struct { AlwaysOn bool `tfschema:"always_on"` AppCommandLine string `tfschema:"app_command_line"` @@ -769,18 +1046,29 @@ func SiteConfigSchemaWindowsFunctionApp() *pluginsdk.Schema { }, "health_check_path": { - Type: pluginsdk.TypeString, - Optional: true, - Description: "The path to be checked for this function app health.", - RequiredWith: []string{"site_config.0.health_check_eviction_time_in_min"}, + Type: pluginsdk.TypeString, + Optional: true, + Description: "The path to be checked for this function app health.", + RequiredWith: func() []string { + if features.FourPointOhBeta() { + return []string{"site_config.0.health_check_eviction_time_in_min"} + } + return []string{} + }(), }, "health_check_eviction_time_in_min": { // NOTE: Will evict the only node in single node configurations. Type: pluginsdk.TypeInt, Optional: true, + Computed: !features.FourPointOhBeta(), ValidateFunc: validation.IntBetween(2, 10), - RequiredWith: []string{"site_config.0.health_check_path"}, - Description: "The amount of time in minutes that a node is unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`", + RequiredWith: func() []string { + if features.FourPointOhBeta() { + return []string{"site_config.0.health_check_path"} + } + return []string{} + }(), + Description: "The amount of time in minutes that a node is unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`", }, "worker_count": { @@ -1126,7 +1414,6 @@ func linuxFunctionAppStackSchema() *pluginsdk.Schema { "16", "18", "20", - "22", }, false), ExactlyOneOf: []string{ "site_config.0.application_stack.0.dotnet_version", @@ -1781,6 +2068,160 @@ func ExpandSiteConfigLinuxFunctionApp(siteConfig []SiteConfigLinuxFunctionApp, e return expanded, nil } +func ExpandSiteConfigFunctionFlexConsumptionApp(siteConfigFlexConsumption []SiteConfigFunctionAppFlexConsumption, existing *webapps.SiteConfig, metadata sdk.ResourceMetaData, storageUsesMSI bool, storageStringFlex string, storageConnStringForFCApp string) (*webapps.SiteConfig, error) { + if len(siteConfigFlexConsumption) == 0 { + return nil, nil + } + + expanded := &webapps.SiteConfig{} + if existing != nil { + expanded = existing + } + + appSettings := make([]webapps.NameValuePair, 0) + + if existing != nil && existing.AppSettings != nil { + appSettings = *existing.AppSettings + } + + if storageStringFlex != "" { + appSettings = updateOrAppendAppSettings(appSettings, "AzureWebJobsStorage", storageStringFlex, false) + if storageConnStringForFCApp != "" { + appSettings = updateOrAppendAppSettings(appSettings, storageConnStringForFCApp, storageStringFlex, false) + } + } + + FlexConsumptionSiteConfig := siteConfigFlexConsumption[0] + + v := strconv.FormatInt(FlexConsumptionSiteConfig.HealthCheckEvictionTime, 10) + if v == "0" || FlexConsumptionSiteConfig.HealthCheckPath == "" { + appSettings = updateOrAppendAppSettings(appSettings, "WEBSITE_HEALTHCHECK_MAXPINGFAILURES", v, true) + } else { + appSettings = updateOrAppendAppSettings(appSettings, "WEBSITE_HEALTHCHECK_MAXPINGFAILURES", v, false) + } + + if FlexConsumptionSiteConfig.AppInsightsConnectionString == "" { + appSettings = updateOrAppendAppSettings(appSettings, "APPLICATIONINSIGHTS_CONNECTION_STRING", FlexConsumptionSiteConfig.AppInsightsConnectionString, true) + } else { + appSettings = updateOrAppendAppSettings(appSettings, "APPLICATIONINSIGHTS_CONNECTION_STRING", FlexConsumptionSiteConfig.AppInsightsConnectionString, false) + } + + if FlexConsumptionSiteConfig.AppInsightsInstrumentationKey == "" { + appSettings = updateOrAppendAppSettings(appSettings, "APPINSIGHTS_INSTRUMENTATIONKEY", FlexConsumptionSiteConfig.AppInsightsInstrumentationKey, true) + } else { + appSettings = updateOrAppendAppSettings(appSettings, "APPINSIGHTS_INSTRUMENTATIONKEY", FlexConsumptionSiteConfig.AppInsightsInstrumentationKey, false) + } + + if metadata.ResourceData.HasChange("site_config.0.api_management_api_id") { + expanded.ApiManagementConfig = &webapps.ApiManagementConfig{ + Id: pointer.To(FlexConsumptionSiteConfig.ApiManagementConfigId), + } + } + + if metadata.ResourceData.HasChange("site_config.0.api_definition_url") { + expanded.ApiDefinition = &webapps.ApiDefinitionInfo{ + Url: pointer.To(FlexConsumptionSiteConfig.ApiDefinition), + } + } + + if metadata.ResourceData.HasChange("site_config.0.app_command_line") { + expanded.AppCommandLine = pointer.To(FlexConsumptionSiteConfig.AppCommandLine) + } + + if metadata.ResourceData.HasChange("site_config.0.container_registry_use_managed_identity") { + expanded.AcrUseManagedIdentityCreds = pointer.To(FlexConsumptionSiteConfig.UseManagedIdentityACR) + } + + if metadata.ResourceData.HasChange("site_config.0.default_documents") { + expanded.DefaultDocuments = &FlexConsumptionSiteConfig.DefaultDocuments + } + + if metadata.ResourceData.HasChange("site_config.0.http2_enabled") { + expanded.HTTP20Enabled = pointer.To(FlexConsumptionSiteConfig.Http2Enabled) + } + + if metadata.ResourceData.HasChange("site_config.0.ip_restriction") { + ipRestrictions, err := ExpandIpRestrictions(FlexConsumptionSiteConfig.IpRestriction) + if err != nil { + return nil, err + } + expanded.IPSecurityRestrictions = ipRestrictions + } + + if metadata.ResourceData.HasChange("site_config.0.ip_restriction_default_action") { + expanded.IPSecurityRestrictionsDefaultAction = pointer.To(webapps.DefaultAction(FlexConsumptionSiteConfig.IpRestrictionDefaultAction)) + } + + if metadata.ResourceData.HasChange("site_config.0.scm_use_main_ip_restriction") { + expanded.ScmIPSecurityRestrictionsUseMain = pointer.To(FlexConsumptionSiteConfig.ScmUseMainIpRestriction) + } + + if metadata.ResourceData.HasChange("site_config.0.scm_ip_restriction") { + scmIpRestrictions, err := ExpandIpRestrictions(FlexConsumptionSiteConfig.ScmIpRestriction) + if err != nil { + return nil, err + } + expanded.ScmIPSecurityRestrictions = scmIpRestrictions + } + + if metadata.ResourceData.HasChange("site_config.0.scm_ip_restriction_default_action") { + expanded.ScmIPSecurityRestrictionsDefaultAction = pointer.To(webapps.DefaultAction(FlexConsumptionSiteConfig.ScmIpRestrictionDefaultAction)) + } + + if metadata.ResourceData.HasChange("site_config.0.load_balancing_mode") { + expanded.LoadBalancing = pointer.To(webapps.SiteLoadBalancing(FlexConsumptionSiteConfig.LoadBalancing)) + } + + if metadata.ResourceData.HasChange("site_config.0.managed_pipeline_mode") { + expanded.ManagedPipelineMode = pointer.To(webapps.ManagedPipelineMode(FlexConsumptionSiteConfig.ManagedPipelineMode)) + } + + if metadata.ResourceData.HasChange("site_config.0.remote_debugging_enabled") { + expanded.RemoteDebuggingEnabled = pointer.To(FlexConsumptionSiteConfig.RemoteDebugging) + } + + if metadata.ResourceData.HasChange("site_config.0.remote_debugging_version") { + expanded.RemoteDebuggingVersion = pointer.To(FlexConsumptionSiteConfig.RemoteDebuggingVersion) + } + + if metadata.ResourceData.HasChange("site_config.0.websockets_enabled") { + expanded.WebSocketsEnabled = pointer.To(FlexConsumptionSiteConfig.WebSockets) + } + + if metadata.ResourceData.HasChange("site_config.0.health_check_path") { + expanded.HealthCheckPath = pointer.To(FlexConsumptionSiteConfig.HealthCheckPath) + } + + if metadata.ResourceData.HasChange("site_config.0.worker_count") { + expanded.NumberOfWorkers = pointer.To(FlexConsumptionSiteConfig.WorkerCount) + } + + if metadata.ResourceData.HasChange("site_config.0.minimum_tls_version") { + expanded.MinTlsVersion = pointer.To(webapps.SupportedTlsVersions(FlexConsumptionSiteConfig.MinTlsVersion)) + } + + if metadata.ResourceData.HasChange("site_config.0.scm_minimum_tls_version") { + expanded.ScmMinTlsVersion = pointer.To(webapps.SupportedTlsVersions(FlexConsumptionSiteConfig.ScmMinTlsVersion)) + } + + if metadata.ResourceData.HasChange("site_config.0.cors") { + cors := ExpandCorsSettings(FlexConsumptionSiteConfig.Cors) + expanded.Cors = cors + } + + if metadata.ResourceData.HasChange("site_config.0.elastic_instance_minimum") { + expanded.MinimumElasticInstanceCount = pointer.To(FlexConsumptionSiteConfig.ElasticInstanceMinimum) + } + + if metadata.ResourceData.HasChange("site_config.0.runtime_scale_monitoring_enabled") { + expanded.FunctionsRuntimeScaleMonitoringEnabled = pointer.To(FlexConsumptionSiteConfig.RuntimeScaleMonitoring) + } + + expanded.AppSettings = &appSettings + + return expanded, nil +} + // updateOrAppendAppSettings is used to modify a collection of webapps.NameValuePair items. func updateOrAppendAppSettings(input []webapps.NameValuePair, name string, value string, remove bool) []webapps.NameValuePair { for k, v := range input { @@ -2090,6 +2531,57 @@ func FlattenSiteConfigLinuxFunctionApp(functionAppSiteConfig *webapps.SiteConfig return result, nil } +func FlattenSiteConfigFunctionAppFlexConsumption(functionAppFlexConsumptionSiteConfig *webapps.SiteConfig) (*SiteConfigFunctionAppFlexConsumption, error) { + if functionAppFlexConsumptionSiteConfig == nil { + return nil, fmt.Errorf("flattening site config: SiteConfig was nil") + } + + result := &SiteConfigFunctionAppFlexConsumption{ + AppCommandLine: pointer.From(functionAppFlexConsumptionSiteConfig.AppCommandLine), + ContainerRegistryMSI: pointer.From(functionAppFlexConsumptionSiteConfig.AcrUserManagedIdentityID), + Cors: FlattenCorsSettings(functionAppFlexConsumptionSiteConfig.Cors), + DetailedErrorLogging: pointer.From(functionAppFlexConsumptionSiteConfig.DetailedErrorLoggingEnabled), + HealthCheckPath: pointer.From(functionAppFlexConsumptionSiteConfig.HealthCheckPath), + IpRestrictionDefaultAction: string(pointer.From(functionAppFlexConsumptionSiteConfig.IPSecurityRestrictionsDefaultAction)), + ScmIpRestrictionDefaultAction: string(pointer.From(functionAppFlexConsumptionSiteConfig.ScmIPSecurityRestrictionsDefaultAction)), + LoadBalancing: string(pointer.From(functionAppFlexConsumptionSiteConfig.LoadBalancing)), + ManagedPipelineMode: string(pointer.From(functionAppFlexConsumptionSiteConfig.ManagedPipelineMode)), + WorkerCount: pointer.From(functionAppFlexConsumptionSiteConfig.NumberOfWorkers), + ScmType: string(pointer.From(functionAppFlexConsumptionSiteConfig.ScmType)), + RuntimeScaleMonitoring: pointer.From(functionAppFlexConsumptionSiteConfig.FunctionsRuntimeScaleMonitoringEnabled), + MinTlsVersion: string(pointer.From(functionAppFlexConsumptionSiteConfig.MinTlsVersion)), + ScmMinTlsVersion: string(pointer.From(functionAppFlexConsumptionSiteConfig.ScmMinTlsVersion)), + WebSockets: pointer.From(functionAppFlexConsumptionSiteConfig.WebSocketsEnabled), + ScmUseMainIpRestriction: pointer.From(functionAppFlexConsumptionSiteConfig.ScmIPSecurityRestrictionsUseMain), + UseManagedIdentityACR: pointer.From(functionAppFlexConsumptionSiteConfig.AcrUseManagedIdentityCreds), + RemoteDebugging: pointer.From(functionAppFlexConsumptionSiteConfig.RemoteDebuggingEnabled), + RemoteDebuggingVersion: strings.ToUpper(pointer.From(functionAppFlexConsumptionSiteConfig.RemoteDebuggingVersion)), + Http2Enabled: pointer.From(functionAppFlexConsumptionSiteConfig.HTTP20Enabled), + } + + if v := functionAppFlexConsumptionSiteConfig.ApiDefinition; v != nil && v.Url != nil { + result.ApiDefinition = *v.Url + } + + if v := functionAppFlexConsumptionSiteConfig.ApiManagementConfig; v != nil && v.Id != nil { + result.ApiManagementConfigId = *v.Id + } + + if functionAppFlexConsumptionSiteConfig.IPSecurityRestrictions != nil { + result.IpRestriction = FlattenIpRestrictions(functionAppFlexConsumptionSiteConfig.IPSecurityRestrictions) + } + + if functionAppFlexConsumptionSiteConfig.ScmIPSecurityRestrictions != nil { + result.ScmIpRestriction = FlattenIpRestrictions(functionAppFlexConsumptionSiteConfig.ScmIPSecurityRestrictions) + } + + if v := functionAppFlexConsumptionSiteConfig.DefaultDocuments; v != nil { + result.DefaultDocuments = *v + } + + return result, nil +} + func FlattenSiteConfigWindowsFunctionApp(functionAppSiteConfig *webapps.SiteConfig) (*SiteConfigWindowsFunctionApp, error) { if functionAppSiteConfig == nil { return nil, fmt.Errorf("flattening site config: SiteConfig was nil") diff --git a/internal/services/appservice/registration.go b/internal/services/appservice/registration.go index f55694b5e943..54d68abe0134 100644 --- a/internal/services/appservice/registration.go +++ b/internal/services/appservice/registration.go @@ -41,6 +41,7 @@ func (r Registration) Resources() []sdk.Resource { AppServiceEnvironmentV3Resource{}, AppServiceSourceControlTokenResource{}, FunctionAppActiveSlotResource{}, + FunctionAppFlexConsumptionResource{}, FunctionAppFunctionResource{}, FunctionAppHybridConnectionResource{}, LinuxFunctionAppResource{}, diff --git a/internal/services/containerapps/container_app_resource_test.go b/internal/services/containerapps/container_app_resource_test.go index 703b24a6c671..8837fc2194e7 100644 --- a/internal/services/containerapps/container_app_resource_test.go +++ b/internal/services/containerapps/container_app_resource_test.go @@ -2906,7 +2906,7 @@ func (r ContainerAppResource) latestRevisionFalseRevisionSuffixEmpty() string { return ` traffic_weight { latest_revision = false - percentage = 100 + percentage = 100 } ` } diff --git a/website/docs/r/function_app_flex_consumption.html.markdown b/website/docs/r/function_app_flex_consumption.html.markdown new file mode 100644 index 000000000000..cf2993655c7c --- /dev/null +++ b/website/docs/r/function_app_flex_consumption.html.markdown @@ -0,0 +1,762 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_function_app_flex_consumption" +description: |- + Manages a Function App Running on a Flex Consumption Plan. +--- + +# azurerm_function_app_flex_consumption + +Manages a Function App Running on a Flex Consumption Plan. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_storage_account" "example" { + name = "examplelinuxfunctionappsa" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_container" "example" { + name = "example-flexcontainer" + storage_account_id = azurerm_storage_account.example.id + container_access_type = "private" +} + +resource "azurerm_service_plan" "example" { + name = "example-app-service-plan" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku_name = "FC1" + os_type = "Linux" +} + +resource "azurerm_function_app_flex_consumption" "example" { + name = "example-linux-function-app" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + + storage_container_type = "blobContainer" + storage_container_endpoint = azurerm_storage_container.example.id + storage_authentication_type = "StorageAccountConnectionString" + storage_access_key = azurerm_storage_account.example.primary_access_key + runtime_name = "node" + runtime_version = "20" + maximum_instance_count = 50 + instance_memory_in_mb = 2048 +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `location` - (Required) The Azure Region where the Function App should exist. Changing this forces a new Function App to be created. + +* `name` - (Required) The name which should be used for this Function App. Changing this forces a new Function App to be created. Limit the function name to 32 characters to avoid naming collisions. For more information about [Function App naming rule](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftweb) and [Host ID Collisions](https://github.com/Azure/azure-functions-host/wiki/Host-IDs#host-id-collisions) + +* `resource_group_name` - (Required) The name of the Resource Group where the Function App should exist. Changing this forces a new Linux Function App to be created. + +* `service_plan_id` - (Required) The ID of the App Service Plan within which to create this Function App. + +* `site_config` - (Required) A `site_config` block as defined below. + +--- + +* `app_settings` - (Optional) A map of key-value pairs for [App Settings](https://docs.microsoft.com/azure/azure-functions/functions-app-settings) and custom values. + +~> **Note:** For storage related settings, please use related properties that are available such as `storage_account_access_key`, terraform will assign the value to keys such as `WEBSITE_CONTENTAZUREFILECONNECTIONSTRING`, `AzureWebJobsStorage` in app_setting. + +~> **Note:** For application insight related settings, please use `application_insights_connection_string` and `application_insights_key`, terraform will assign the value to the key `APPINSIGHTS_INSTRUMENTATIONKEY` and `APPLICATIONINSIGHTS_CONNECTION_STRING` in app setting. + +~> **Note:** For health check related settings, please use `health_check_eviction_time_in_min`, terraform will assign the value to the key `WEBSITE_HEALTHCHECK_MAXPINGFAILURES` in app setting. + +~> **Note:** For those app settings that are deprecated or replaced by another properties for flex consumption function app, please check https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings. + +* `auth_settings` - (Optional) A `auth_settings` block as defined below. + +* `auth_settings_v2` - (Optional) An `auth_settings_v2` block as defined below. + +* `backup` - (Optional) A `backup` block as defined below. + +* `client_certificate_enabled` - (Optional) Should the function app use Client Certificates. + +* `client_certificate_mode` - (Optional) The mode of the Function App's client certificates requirement for incoming requests. Possible values are `Required`, `Optional`, and `OptionalInteractiveUser`. Defaults to `Optional`. + +* `client_certificate_exclusion_paths` - (Optional) Paths to exclude when using client certificates, separated by ; + +* `connection_string` - (Optional) One or more `connection_string` blocks as defined below. + +* `daily_memory_time_quota` - (Optional) The amount of memory in gigabyte-seconds that your application is allowed to consume per day. Setting this value only affects function apps under the consumption plan. Defaults to `0`. + +* `enabled` - (Optional) Is the Function App enabled? Defaults to `true`. + +* `content_share_force_disabled` - (Optional) Should the settings for linking the Function App to storage be suppressed. + +* `https_only` - (Optional) Can the Function App only be accessed via HTTPS? Defaults to `false`. + +* `public_network_access_enabled` - (Optional) Should public network access be enabled for the Function App. Defaults to `true`. + +* `identity` - (Optional) A `identity` block as defined below. + +* `key_vault_reference_identity_id` - (Optional) The User Assigned Identity ID used for accessing KeyVault secrets. The identity must be assigned to the application in the `identity` block. [For more information see - Access vaults with a user-assigned identity](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references#access-vaults-with-a-user-assigned-identity) + +* `sticky_settings` - (Optional) A `sticky_settings` block as defined below. + +* `storage_container_type` - (Optional) The storage container type used for the Function App. The current supported type is `blobContainer`. + +* `storage_container_endpoint` - (Optional) The backend storage container endpoint which will be used by this Function App. + +* `storage_authentication_type` - (Optional) The authentication type which will be used to access the backend storage account for the Function App. Possible values are `storageaccountconnectionstring`, `systemassignedidentity`, and `userassignedidentity`. + +* `storage_access_key` - (Optional) The access key which will be used to access the backend storage account for the Function App. + +~> **Note:** The`storage_access_key` must be specified when `storage_authentication_type` sets to `storageaccountconnectionstring`. + +* `storage_user_assigned_identity_id` - (Optional) The user assigned Managed Identity to access the storage account. Conflicts with `storage_account_access_key`. + +~> **Note:** The`storage_user_assigned_identity_id` must be specified when `storage_authentication_type` sets to `userassignedidentity`. + +* `runtime_name` - (Optional) The Runtime of the Linux Function App. Possible values are `node`, `dotnet-isolated`, `powershell`, `python`, `java`. + +* `runtime_version` - (Optional) The Runtime version of the Linux Function App. The values are diff from different runtime version. The supported values are `8.0`, `9.0` for `dotnet-isolated`, `20` for `node`, `3.10`, `3.11` for `python`, `11`, `17` for `java`, `7.4` for `powershell`. + +* `maximum_instance_count` - (Optional) The number of workers this function app can scale out to. + +* `instance_memory_in_mb` - (Optional) A mapping of tags which should be assigned to the Linux Function App. + +* `tags` - (Optional) A mapping of tags which should be assigned to the Linux Function App. + +* `virtual_network_subnet_id` - (Optional) The subnet id which will be used by this Function App for [regional virtual network integration](https://docs.microsoft.com/en-us/azure/app-service/overview-vnet-integration#regional-virtual-network-integration). + +~> **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 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) + +* `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. + +* `zip_deploy_file` - (Optional) 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`. Refer to the [Azure docs](https://learn.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies) for further details. + +--- + +An `active_directory` block supports the following: + +* `client_id` - (Required) The ID of the Client to use to authenticate with Azure Active Directory. + +* `allowed_audiences` - (Optional) Specifies a list of Allowed audience values to consider when validating JWTs issued by Azure Active Directory. + +~> **Note:** The `client_id` value is always considered an allowed audience. + +* `client_secret` - (Optional) The Client Secret for the Client ID. Cannot be used with `client_secret_setting_name`. + +* `client_secret_setting_name` - (Optional) The App Setting name that contains the client secret of the Client. Cannot be used with `client_secret`. + +--- + +An `app_service_logs` block supports the following: + +* `disk_quota_mb` - (Optional) The amount of disk space to use for logs. Valid values are between `25` and `100`. Defaults to `35`. + +* `retention_period_days` - (Optional) The retention period for logs in days. Valid values are between `0` and `99999`.(never delete). + +~> **Note:** This block is not supported on Consumption plans. + +--- + +An `auth_settings` block supports the following: + +* `enabled` - (Required) Should the Authentication / Authorization feature be enabled for the Linux Web App? + +* `active_directory` - (Optional) An `active_directory` block as defined above. + +* `additional_login_parameters` - (Optional) Specifies a map of login Parameters to send to the OpenID Connect authorization endpoint when a user logs in. + +* `allowed_external_redirect_urls` - (Optional) Specifies a list of External URLs that can be redirected to as part of logging in or logging out of the Linux Web App. + +* `default_provider` - (Optional) The default authentication provider to use when multiple providers are configured. Possible values include: `AzureActiveDirectory`, `Facebook`, `Google`, `MicrosoftAccount`, `Twitter`, `Github` + +~> **Note:** This setting is only needed if multiple providers are configured, and the `unauthenticated_client_action` is set to "RedirectToLoginPage". + +* `facebook` - (Optional) A `facebook` block as defined below. + +* `github` - (Optional) A `github` block as defined below. + +* `google` - (Optional) A `google` block as defined below. + +* `issuer` - (Optional) The OpenID Connect Issuer URI that represents the entity which issues access tokens for this Linux Web App. + +~> **Note:** When using Azure Active Directory, this value is the URI of the directory tenant, e.g. . + +* `microsoft` - (Optional) A `microsoft` block as defined below. + +* `runtime_version` - (Optional) The RuntimeVersion of the Authentication / Authorization feature in use for the Linux Web App. + +* `token_refresh_extension_hours` - (Optional) The number of hours after session token expiration that a session token can be used to call the token refresh API. Defaults to `72` hours. + +* `token_store_enabled` - (Optional) Should the Linux Web App durably store platform-specific security tokens that are obtained during login flows? Defaults to `false`. + +* `twitter` - (Optional) A `twitter` block as defined below. + +* `unauthenticated_client_action` - (Optional) The action to take when an unauthenticated client attempts to access the app. Possible values include: `RedirectToLoginPage`, `AllowAnonymous`. + +--- + +An `auth_settings_v2` block supports the following: + +* `auth_enabled` - (Optional) Should the AuthV2 Settings be enabled. Defaults to `false`. + +* `runtime_version` - (Optional) The Runtime Version of the Authentication and Authorisation feature of this App. Defaults to `~1`. + +* `config_file_path` - (Optional) The path to the App Auth settings. + +~> **Note:** Relative Paths are evaluated from the Site Root directory. + +* `require_authentication` - (Optional) Should the authentication flow be used for all requests. + +* `unauthenticated_action` - (Optional) The action to take for requests made without authentication. Possible values include `RedirectToLoginPage`, `AllowAnonymous`, `Return401`, and `Return403`. Defaults to `RedirectToLoginPage`. + +* `default_provider` - (Optional) The Default Authentication Provider to use when the `unauthenticated_action` is set to `RedirectToLoginPage`. Possible values include: `apple`, `azureactivedirectory`, `facebook`, `github`, `google`, `twitter` and the `name` of your `custom_oidc_v2` provider. + +~> **Note:** Whilst any value will be accepted by the API for `default_provider`, it can leave the app in an unusable state if this value does not correspond to the name of a known provider (either built-in value, or custom_oidc name) as it is used to build the auth endpoint URI. + +* `excluded_paths` - (Optional) The paths which should be excluded from the `unauthenticated_action` when it is set to `RedirectToLoginPage`. + +~> **Note:** This list should be used instead of setting `WEBSITE_WARMUP_PATH` in `app_settings` as it takes priority. + +* `require_https` - (Optional) Should HTTPS be required on connections? Defaults to `true`. + +* `http_route_api_prefix` - (Optional) The prefix that should precede all the authentication and authorisation paths. Defaults to `/.auth`. + +* `forward_proxy_convention` - (Optional) The convention used to determine the url of the request made. Possible values include `NoProxy`, `Standard`, `Custom`. Defaults to `NoProxy`. + +* `forward_proxy_custom_host_header_name` - (Optional) The name of the custom header containing the host of the request. + +* `forward_proxy_custom_scheme_header_name` - (Optional) The name of the custom header containing the scheme of the request. + +* `apple_v2` - (Optional) An `apple_v2` block as defined below. + +* `active_directory_v2` - (Optional) An `active_directory_v2` block as defined below. + +* `azure_static_web_app_v2` - (Optional) An `azure_static_web_app_v2` block as defined below. + +* `custom_oidc_v2` - (Optional) Zero or more `custom_oidc_v2` blocks as defined below. + +* `facebook_v2` - (Optional) A `facebook_v2` block as defined below. + +* `github_v2` - (Optional) A `github_v2` block as defined below. + +* `google_v2` - (Optional) A `google_v2` block as defined below. + +* `microsoft_v2` - (Optional) A `microsoft_v2` block as defined below. + +* `twitter_v2` - (Optional) A `twitter_v2` block as defined below. + +* `login` - (Required) A `login` block as defined below. + +--- + +An `apple_v2` block supports the following: + +* `client_id` - (Required) The OpenID Connect Client ID for the Apple web application. + +* `client_secret_setting_name` - (Required) The app setting name that contains the `client_secret` value used for Apple Login. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `login_scopes` - A list of Login Scopes provided by this Authentication Provider. + +~> **Note:** This is configured on the Authentication Provider side and is Read Only here. + +--- + +An `active_directory_v2` block supports the following: + +* `client_id` - (Required) The ID of the Client to use to authenticate with Azure Active Directory. + +* `tenant_auth_endpoint` - (Required) The Azure Tenant Endpoint for the Authenticating Tenant. e.g. `https://login.microsoftonline.com/{tenant-guid}/v2.0/` + +~> **Note:** [Here](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-national-cloud#microsoft-entra-authentication-endpoints) is a list of possible authentication endpoints based on the cloud environment. [Here](https://learn.microsoft.com/en-us/azure/app-service/configure-authentication-provider-aad?tabs=workforce-tenant) is more information to better understand how to configure authentication for Azure App Service or Azure Functions. + +* `client_secret_setting_name` - (Optional) The App Setting name that contains the client secret of the Client. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `client_secret_certificate_thumbprint` - (Optional) The thumbprint of the certificate used for signing purposes. + +!> **Note:** If one `client_secret_setting_name` or `client_secret_certificate_thumbprint` is specified, terraform won't write the client secret or secret certificate thumbprint back to `app_setting`, so make sure they are existed in `app_settings` to function correctly. + +* `jwt_allowed_groups` - (Optional) A list of Allowed Groups in the JWT Claim. + +* `jwt_allowed_client_applications` - (Optional) A list of Allowed Client Applications in the JWT Claim. + +* `www_authentication_disabled` - (Optional) Should the www-authenticate provider should be omitted from the request? Defaults to `false`. + +* `allowed_groups` - (Optional) The list of allowed Group Names for the Default Authorisation Policy. + +* `allowed_identities` - (Optional) The list of allowed Identities for the Default Authorisation Policy. + +* `allowed_applications` - (Optional) The list of allowed Applications for the Default Authorisation Policy. + +* `login_parameters` - (Optional) A map of key-value pairs to send to the Authorisation Endpoint when a user logs in. + +* `allowed_audiences` - (Optional) Specifies a list of Allowed audience values to consider when validating JWTs issued by Azure Active Directory. + +~> **Note:** This is configured on the Authentication Provider side and is Read Only here. + +--- + +An `azure_static_web_app_v2` block supports the following: + +* `client_id` - (Required) The ID of the Client to use to authenticate with Azure Static Web App Authentication. + +--- + +A `custom_oidc_v2` block supports the following: + +* `name` - (Required) The name of the Custom OIDC Authentication Provider. + +~> **Note:** An `app_setting` matching this value in upper case with the suffix of `_PROVIDER_AUTHENTICATION_SECRET` is required. e.g. `MYOIDC_PROVIDER_AUTHENTICATION_SECRET` for a value of `myoidc`. + +* `client_id` - (Required) The ID of the Client to use to authenticate with the Custom OIDC. + +* `openid_configuration_endpoint` - (Required) The app setting name that contains the `client_secret` value used for the Custom OIDC Login. + +* `name_claim_type` - (Optional) The name of the claim that contains the users name. + +* `scopes` - (Optional) The list of the scopes that should be requested while authenticating. + +* `client_credential_method` - The Client Credential Method used. + +* `client_secret_setting_name` - The App Setting name that contains the secret for this Custom OIDC Client. This is generated from `name` above and suffixed with `_PROVIDER_AUTHENTICATION_SECRET`. + +* `authorisation_endpoint` - The endpoint to make the Authorisation Request as supplied by `openid_configuration_endpoint` response. + +* `token_endpoint` - The endpoint used to request a Token as supplied by `openid_configuration_endpoint` response. + +* `issuer_endpoint` - The endpoint that issued the Token as supplied by `openid_configuration_endpoint` response. + +* `certification_uri` - The endpoint that provides the keys necessary to validate the token as supplied by `openid_configuration_endpoint` response. + +--- + +A `facebook_v2` block supports the following: + +* `app_id` - (Required) The App ID of the Facebook app used for login. + +* `app_secret_setting_name` - (Required) The app setting name that contains the `app_secret` value used for Facebook Login. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `graph_api_version` - (Optional) The version of the Facebook API to be used while logging in. + +* `login_scopes` - (Optional) The list of scopes that should be requested as part of Facebook Login authentication. + +--- + +A `github_v2` block supports the following: + +* `client_id` - (Required) The ID of the GitHub app used for login.. + +* `client_secret_setting_name` - (Required) The app setting name that contains the `client_secret` value used for GitHub Login. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `login_scopes` - (Optional) The list of OAuth 2.0 scopes that should be requested as part of GitHub Login authentication. + +--- + +A `google_v2` block supports the following: + +* `client_id` - (Required) The OpenID Connect Client ID for the Google web application. + +* `client_secret_setting_name` - (Required) The app setting name that contains the `client_secret` value used for Google Login. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `allowed_audiences` - (Optional) Specifies a list of Allowed Audiences that should be requested as part of Google Sign-In authentication. + +* `login_scopes` - (Optional) The list of OAuth 2.0 scopes that should be requested as part of Google Sign-In authentication. + +--- + +A `microsoft_v2` block supports the following: + +* `client_id` - (Required) The OAuth 2.0 client ID that was created for the app used for authentication. + +* `client_secret_setting_name` - (Required) The app setting name containing the OAuth 2.0 client secret that was created for the app used for authentication. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +* `allowed_audiences` - (Optional) Specifies a list of Allowed Audiences that will be requested as part of Microsoft Sign-In authentication. + +* `login_scopes` - (Optional) The list of Login scopes that should be requested as part of Microsoft Account authentication. + +--- + +A `twitter_v2` block supports the following: + +* `consumer_key` - (Required) The OAuth 1.0a consumer key of the Twitter application used for sign-in. + +* `consumer_secret_setting_name` - (Required) The app setting name that contains the OAuth 1.0a consumer secret of the Twitter application used for sign-in. + +!> **Note:** A setting with this name must exist in `app_settings` to function correctly. + +--- + +A `login` block supports the following: + +* `logout_endpoint` - (Optional) The endpoint to which logout requests should be made. + +* `token_store_enabled` - (Optional) Should the Token Store configuration Enabled. Defaults to `false` + +* `token_refresh_extension_time` - (Optional) The number of hours after session token expiration that a session token can be used to call the token refresh API. Defaults to `72` hours. + +* `token_store_path` - (Optional) The directory path in the App Filesystem in which the tokens will be stored. + +* `token_store_sas_setting_name` - (Optional) The name of the app setting which contains the SAS URL of the blob storage containing the tokens. + +* `preserve_url_fragments_for_logins` - (Optional) Should the fragments from the request be preserved after the login request is made. Defaults to `false`. + +* `allowed_external_redirect_urls` - (Optional) External URLs that can be redirected to as part of logging in or logging out of the app. This is an advanced setting typically only needed by Windows Store application backends. + +~> **Note:** URLs within the current domain are always implicitly allowed. + +* `cookie_expiration_convention` - (Optional) The method by which cookies expire. Possible values include: `FixedTime`, and `IdentityProviderDerived`. Defaults to `FixedTime`. + +* `cookie_expiration_time` - (Optional) The time after the request is made when the session cookie should expire. Defaults to `08:00:00`. + +* `validate_nonce` - (Optional) Should the nonce be validated while completing the login flow. Defaults to `true`. + +* `nonce_expiration_time` - (Optional) The time after the request is made when the nonce should expire. Defaults to `00:05:00`. + +--- + +A `backup` block supports the following: + +* `name` - (Required) The name which should be used for this Backup. + +* `schedule` - (Required) A `schedule` block as defined below. + +* `storage_account_url` - (Required) The SAS URL to the container. + +* `enabled` - (Optional) Should this backup job be enabled? Defaults to `true`. + +--- + +A `connection_string` block supports the following: + +* `name` - (Required) The name which should be used for this Connection. + +* `type` - (Required) Type of database. Possible values include: `MySQL`, `SQLServer`, `SQLAzure`, `Custom`, `NotificationHub`, `ServiceBus`, `EventHub`, `APIHub`, `DocDb`, `RedisCache`, and `PostgreSQL`. + +* `value` - (Required) The connection string value. + +--- + +A `cors` block supports the following: + +* `allowed_origins` - (Optional) Specifies a list of origins that should be allowed to make cross-origin calls. + +* `support_credentials` - (Optional) Are credentials allowed in CORS requests? Defaults to `false`. + +--- + +A `facebook` block supports the following: + +* `app_id` - (Required) The App ID of the Facebook app used for login. + +* `app_secret` - (Optional) The App Secret of the Facebook app used for Facebook login. Cannot be specified with `app_secret_setting_name`. + +* `app_secret_setting_name` - (Optional) The app setting name that contains the `app_secret` value used for Facebook login. Cannot be specified with `app_secret`. + +* `oauth_scopes` - (Optional) Specifies a list of OAuth 2.0 scopes to be requested as part of Facebook login authentication. + +--- + +A `github` block supports the following: + +* `client_id` - (Required) The ID of the GitHub app used for login. + +* `client_secret` - (Optional) The Client Secret of the GitHub app used for GitHub login. Cannot be specified with `client_secret_setting_name`. + +* `client_secret_setting_name` - (Optional) The app setting name that contains the `client_secret` value used for GitHub login. Cannot be specified with `client_secret`. + +* `oauth_scopes` - (Optional) Specifies a list of OAuth 2.0 scopes that will be requested as part of GitHub login authentication. + +--- + +A `google` block supports the following: + +* `client_id` - (Required) The OpenID Connect Client ID for the Google web application. + +* `client_secret` - (Optional) The client secret associated with the Google web application. Cannot be specified with `client_secret_setting_name`. + +* `client_secret_setting_name` - (Optional) The app setting name that contains the `client_secret` value used for Google login. Cannot be specified with `client_secret`. + +* `oauth_scopes` - (Optional) Specifies a list of OAuth 2.0 scopes that will be requested as part of Google Sign-In authentication. If not specified, `openid`, `profile`, and `email` are used as default scopes. + +--- + +A `headers` block supports the following: + +~> **Note:** Please see the [official Azure Documentation](https://docs.microsoft.com/azure/app-service/app-service-ip-restrictions#filter-by-http-header) for details on using header filtering. + +* `x_azure_fdid` - (Optional) Specifies a list of Azure Front Door IDs. + +* `x_fd_health_probe` - (Optional) Specifies if a Front Door Health Probe should be expected. The only possible value is `1`. + +* `x_forwarded_for` - (Optional) Specifies a list of addresses for which matching should be applied. Omitting this value means allow any. + +* `x_forwarded_host` - (Optional) Specifies a list of Hosts for which matching should be applied. + +--- + +An `identity` block supports the following: + +* `type` - (Required) Specifies the type of Managed Service Identity that should be configured on this Linux Function App. Possible values are `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned` (to enable both). + +* `identity_ids` - (Optional) A list of User Assigned Managed Identity IDs to be assigned to this Linux Function App. + +~> **Note:** This is required when `type` is set to `UserAssigned` or `SystemAssigned, UserAssigned`. + +--- + +An `ip_restriction` block supports the following: + +* `action` - (Optional) The action to take. Possible values are `Allow` or `Deny`. Defaults to `Allow`. + +* `headers` - (Optional) A `headers` block as defined above. + +* `ip_address` - (Optional) The CIDR notation of the IP or IP Range to match. For example: `10.0.0.0/24` or `192.168.10.1/32` + +* `name` - (Optional) The name which should be used for this `ip_restriction`. + +* `priority` - (Optional) The priority value of this `ip_restriction`. Defaults to `65000`. + +* `service_tag` - (Optional) The Service Tag used for this IP Restriction. + +* `virtual_network_subnet_id` - (Optional) The Virtual Network Subnet ID used for this IP Restriction. + +~> **Note:** One and only one of `ip_address`, `service_tag` or `virtual_network_subnet_id` must be specified. + +* `description` - (Optional) The Description of this IP Restriction. + +--- + +A `microsoft` block supports the following: + +* `client_id` - (Required) The OAuth 2.0 client ID that was created for the app used for authentication. + +* `client_secret` - (Optional) The OAuth 2.0 client secret that was created for the app used for authentication. Cannot be specified with `client_secret_setting_name`. + +* `client_secret_setting_name` - (Optional) The app setting name containing the OAuth 2.0 client secret that was created for the app used for authentication. Cannot be specified with `client_secret`. + +* `oauth_scopes` - (Optional) Specifies a list of OAuth 2.0 scopes that will be requested as part of Microsoft Account authentication. If not specified, `wl.basic` is used as the default scope. + +--- + +A `schedule` block supports the following: + +* `frequency_interval` - (Required) How often the backup should be executed (e.g. for weekly backup, this should be set to `7` and `frequency_unit` should be set to `Day`). + +~> **Note:** Not all intervals are supported on all Linux Function App SKUs. Please refer to the official documentation for appropriate values. + +* `frequency_unit` - (Required) The unit of time for how often the backup should take place. Possible values include: `Day` and `Hour`. + +* `keep_at_least_one_backup` - (Optional) Should the service keep at least one backup, regardless of age of backup. Defaults to `false`. + +* `retention_period_days` - (Optional) After how many days backups should be deleted. Defaults to `30`. + +* `start_time` - (Optional) When the schedule should start working in RFC-3339 format. + +--- + +A `scm_ip_restriction` block supports the following: + +* `action` - (Optional) The action to take. Possible values are `Allow` or `Deny`. Defaults to `Allow`. + +* `headers` - (Optional) A `headers` block as defined above. + +* `ip_address` - (Optional) The CIDR notation of the IP or IP Range to match. For example: `10.0.0.0/24` or `192.168.10.1/32` + +* `name` - (Optional) The name which should be used for this `ip_restriction`. + +* `priority` - (Optional) The priority value of this `ip_restriction`. Defaults to `65000`. + +* `service_tag` - (Optional) The Service Tag used for this IP Restriction. + +* `virtual_network_subnet_id` - (Optional) The Virtual Network Subnet ID used for this IP Restriction. + +~> **Note:** One and only one of `ip_address`, `service_tag` or `virtual_network_subnet_id` must be specified. + +* `description` - (Optional) The Description of this IP Restriction. + +--- + +A `site_config` block supports the following: + +* `api_definition_url` - (Optional) The URL of the API definition that describes this Linux Function App. + +* `api_management_api_id` - (Optional) The ID of the API Management API for this Linux Function App. + +* `app_command_line` - (Optional) The App command line to launch. + +* `application_insights_connection_string` - (Optional) The Connection String for linking the Linux Function App to Application Insights. + +* `application_insights_key` - (Optional) The Instrumentation Key for connecting the Linux Function App to Application Insights. + +* `app_service_logs` - (Optional) An `app_service_logs` block as defined above. + +* `container_registry_managed_identity_client_id` - (Optional) The Client ID of the Managed Service Identity to use for connections to the Azure Container Registry. + +* `container_registry_use_managed_identity` - (Optional) Should connections for Azure Container Registry use Managed Identity. + +* `cors` - (Optional) A `cors` block as defined above. + +* `default_documents` - (Optional) Specifies a list of Default Documents for the Linux Web App. + +* `health_check_path` - (Optional) The path to be checked for this function app health. + +* `health_check_eviction_time_in_min` - (Optional) The amount of time in minutes that a node can be unhealthy before being removed from the load balancer. Possible values are between `2` and `10`. Only valid in conjunction with `health_check_path`. + +* `http2_enabled` - (Optional) Specifies if the HTTP2 protocol should be enabled. Defaults to `false`. + +* `ip_restriction` - (Optional) One or more `ip_restriction` blocks as defined above. + +* `ip_restriction_default_action` - (Optional) The Default action for traffic that does not match any `ip_restriction` rule. possible values include `Allow` and `Deny`. Defaults to `Allow`. + +* `load_balancing_mode` - (Optional) The Site load balancing mode. Possible values include: `WeightedRoundRobin`, `LeastRequests`, `LeastResponseTime`, `WeightedTotalTraffic`, `RequestHash`, `PerSiteRoundRobin`. Defaults to `LeastRequests` if omitted. + +* `managed_pipeline_mode` - (Optional) Managed pipeline mode. Possible values include: `Integrated`, `Classic`. Defaults to `Integrated`. + +* `minimum_tls_version` - (Optional) The configures the minimum version of TLS required for SSL requests. Possible values include: `1.0`, `1.1`, `1.2` and `1.3`. Defaults to `1.2`. + +* `remote_debugging_enabled` - (Optional) Should Remote Debugging be enabled. Defaults to `false`. + +* `remote_debugging_version` - (Optional) The Remote Debugging Version. Possible values include `VS2017`, `VS2019`, and `VS2022`. + +* `runtime_scale_monitoring_enabled` - (Optional) Should Scale Monitoring of the Functions Runtime be enabled? + +~> **Note:** Functions runtime scale monitoring can only be enabled for Elastic Premium Function Apps or Workflow Standard Logic Apps and requires a minimum prewarmed instance count of 1. + +* `scm_ip_restriction` - (Optional) One or more `scm_ip_restriction` blocks as defined above. + +* `scm_ip_restriction_default_action` - (Optional) The Default action for traffic that does not match any `scm_ip_restriction` rule. possible values include `Allow` and `Deny`. Defaults to `Allow`. + +* `scm_minimum_tls_version` - (Optional) Configures the minimum version of TLS required for SSL requests to the SCM site Possible values include: `1.0`, `1.1`, and `1.2`. Defaults to `1.2`. + +* `scm_use_main_ip_restriction` - (Optional) Should the Linux Function App `ip_restriction` configuration be used for the SCM also. + +* `websockets_enabled` - (Optional) Should Web Sockets be enabled. Defaults to `false`. + +* `worker_count` - (Optional) The number of Workers for this Linux Function App. + +--- + +A `sticky_settings` block supports the following: + +* `app_setting_names` - (Optional) A list of `app_setting` names that the Linux Function App will not swap between Slots when a swap operation is triggered. + +* `connection_string_names` - (Optional) A list of `connection_string` names that the Linux Function App will not swap between Slots when a swap operation is triggered. + +--- + +A `storage_account` block supports the following: + +* `access_key` - (Required) The Access key for the storage account. + +* `account_name` - (Required) The Name of the Storage Account. + +* `name` - (Required) The name which should be used for this Storage Account. + +* `share_name` - (Required) The Name of the File Share or Container Name for Blob storage. + +* `type` - (Required) The Azure Storage Type. Possible values include `AzureFiles` and `AzureBlob`. + +* `mount_path` - (Optional) The path at which to mount the storage share. + +--- + +A `twitter` block supports the following: + +* `consumer_key` - (Required) The OAuth 1.0a consumer key of the Twitter application used for sign-in. + +* `consumer_secret` - (Optional) The OAuth 1.0a consumer secret of the Twitter application used for sign-in. Cannot be specified with `consumer_secret_setting_name`. + +* `consumer_secret_setting_name` - (Optional) The app setting name that contains the OAuth 1.0a consumer secret of the Twitter application used for sign-in. Cannot be specified with `consumer_secret`. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Linux Function App. + +* `custom_domain_verification_id` - The identifier used by App Service to perform domain ownership verification via DNS TXT record. + +* `default_hostname` - The default hostname of the Linux Function App. + +* `hosting_environment_id` - The ID of the App Service Environment used by Function App. + +* `identity` - An `identity` block as defined below. + +* `kind` - The Kind value for this Linux Function App. + +* `outbound_ip_address_list` - A list of outbound IP addresses. For example `["52.23.25.3", "52.143.43.12"]` + +* `outbound_ip_addresses` - A comma separated list of outbound IP addresses as a string. For example `52.23.25.3,52.143.43.12`. + +* `possible_outbound_ip_address_list` - A list of possible outbound IP addresses, not all of which are necessarily in use. This is a superset of `outbound_ip_address_list`. For example `["52.23.25.3", "52.143.43.12"]`. + +* `possible_outbound_ip_addresses` - A comma separated list of possible outbound IP addresses as a string. For example `52.23.25.3,52.143.43.12,52.143.43.17`. This is a superset of `outbound_ip_addresses`. + +* `site_credential` - A `site_credential` block as defined below. + +--- + +An `identity` block exports the following: + +* `principal_id` - The Principal ID associated with this Managed Service Identity. + +* `tenant_id` - The Tenant ID associated with this Managed Service Identity. + +--- + +A `site_credential` block exports the following: + +* `name` - The Site Credentials Username used for publishing. + +* `password` - The Site Credentials Password used for publishing. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Function Flex Consumption App. +* `read` - (Defaults to 5 minutes) Used when retrieving the Function Flex Consumption App. +* `update` - (Defaults to 30 minutes) Used when updating the Function Flex Consumption App. +* `delete` - (Defaults to 30 minutes) Used when deleting the Function Flex Consumption App. + +## Import + +The Function Apps can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_function_app_flex_consumption.example /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Web/sites/site1 +``` diff --git a/website/docs/r/linux_function_app.html.markdown b/website/docs/r/linux_function_app.html.markdown index a2503028329f..9ded93f22669 100644 --- a/website/docs/r/linux_function_app.html.markdown +++ b/website/docs/r/linux_function_app.html.markdown @@ -89,6 +89,8 @@ The following arguments are supported: * `builtin_logging_enabled` - (Optional) Should built in logging be enabled. Configures `AzureWebJobsDashboard` app setting based on the configured storage setting. Defaults to `true`. +~> **Note:** `builtin_logging_enabled` is only supported for function app whose function runtime is running on version 1.x. + * `client_certificate_enabled` - (Optional) Should the function app use Client Certificates. * `client_certificate_mode` - (Optional) The mode of the Function App's client certificates requirement for incoming requests. Possible values are `Required`, `Optional`, and `OptionalInteractiveUser`. Defaults to `Optional`.