Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azurerm_windows_function_app, azurerm_linux_function_app - fix content share not being created automatically issue #18258

Merged
merged 19 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func (r FunctionAppHybridConnectionResource) CustomImporter() sdk.ResourceRunFun

appId := parse.NewFunctionAppID(id.SubscriptionId, id.ResourceGroup, id.SiteName)

_, sku, err := helpers.ServicePlanInfoForApp(ctx, metadata, appId)
_, sku, err := helpers.ServicePlanInfoForApp(ctx, metadata, appId, "")
if err != nil {
return err
}
Expand Down
7 changes: 5 additions & 2 deletions internal/services/appservice/helpers/service_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func PlanTypeFromSku(input string) string {
}

// ServicePlanInfoForApp returns the OS type and Service Plan SKU for a given App Service Resource
func ServicePlanInfoForApp(ctx context.Context, metadata sdk.ResourceMetaData, id interface{}) (osType *string, planSku *string, err error) {
func ServicePlanInfoForApp(ctx context.Context, metadata sdk.ResourceMetaData, id interface{}, serviceFarmId string) (osType *string, planSku *string, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is intended to retrieve the Service Plan ID from the App ID, it should not take the ID directly to circumvent this.

client := metadata.Client.AppService.WebAppsClient
servicePlanClient := metadata.Client.AppService.ServicePlanClient
var rg, siteName string
Expand Down Expand Up @@ -165,7 +165,10 @@ func ServicePlanInfoForApp(ctx context.Context, metadata sdk.ResourceMetaData, i
if props.ServerFarmID == nil {
return nil, nil, fmt.Errorf("determining Service Plan ID for %s: %+v", id, err)
}
servicePlanId, err := parse.ServicePlanID(*props.ServerFarmID)
if serviceFarmId == "" {
serviceFarmId = *props.ServerFarmID
}
servicePlanId, err := parse.ServicePlanID(serviceFarmId)
if err != nil {
return nil, nil, err
}
Expand Down
110 changes: 100 additions & 10 deletions internal/services/appservice/linux_function_app_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,17 +406,28 @@ func (r LinuxFunctionAppResource) Create() sdk.ResourceFunc {
functionApp.AppSettings["AzureWebJobsDashboard__accountName"] = functionApp.StorageAccountName
}
}

// for function premium app with WEBSITE_CONTENTOVERVNET sets to 1, the user has to specify a predefined fire share.
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#website_contentovervnet
if sendContentSettings {
if functionApp.AppSettings == nil {
functionApp.AppSettings = make(map[string]string)
}
if !functionApp.StorageUsesMSI {
suffix := uuid.New().String()[0:4]
if _, present := functionApp.AppSettings["WEBSITE_CONTENTSHARE"]; !present {
_, contentOverVnetEnabled := functionApp.AppSettings["WEBSITE_CONTENTOVERVNET"]
_, contentSharePresent := functionApp.AppSettings["WEBSITE_CONTENTSHARE"]
_, contentShareConnectionString := functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]

if !contentSharePresent {
if contentOverVnetEnabled {
return fmt.Errorf("the value of WEBSITE_CONTENTSHARE must be set to a predefined share when the storage account is restricted to a virtual network")
}
functionApp.AppSettings["WEBSITE_CONTENTSHARE"] = fmt.Sprintf("%s-%s", strings.ToLower(functionApp.Name), suffix)
}
if _, present := functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]; !present {
if !contentShareConnectionString {
if contentOverVnetEnabled && contentSharePresent {
return fmt.Errorf("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING must be set when WEBSITE_CONTENTSHARE and WEBSITE_CONTENTOVERVNET is specified")
}
functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] = storageString
}
} else {
Expand Down Expand Up @@ -525,6 +536,7 @@ func (r LinuxFunctionAppResource) Read() sdk.ResourceFunc {
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.AppService.WebAppsClient
servicePlanClient := metadata.Client.AppService.ServicePlanClient
id, err := parse.FunctionAppID(metadata.ResourceData.Id())
if err != nil {
return err
Expand All @@ -537,6 +549,22 @@ func (r LinuxFunctionAppResource) Read() sdk.ResourceFunc {
return fmt.Errorf("reading Linux %s: %+v", id, err)
}

var planSku *string
if functionApp.ServerFarmID != nil {
servicePlanId, err := parse.ServicePlanID(*functionApp.ServerFarmID)
if err != nil {
return err
}
servicePlan, err := servicePlanClient.Get(ctx, servicePlanId.ResourceGroup, servicePlanId.ServerfarmName)
if err != nil {
return fmt.Errorf("reading %s: %+v", servicePlanId, err)
}
if sku := servicePlan.Sku; sku != nil && sku.Name != nil {
planSku = sku.Name
}
}
sendContentSettings := helpers.PlanIsElastic(planSku)

if functionApp.SiteProperties == nil {
return fmt.Errorf("reading properties of Linux %s", id)
}
Expand All @@ -547,6 +575,43 @@ func (r LinuxFunctionAppResource) Read() sdk.ResourceFunc {
return fmt.Errorf("reading App Settings for Linux %s: %+v", id, err)
}

var storageAccountName, storageFileShare string
if sendContentSettings || helpers.PlanIsConsumption(planSku) {
if appSettingsResp.Properties != nil {
if appSettingsResp.Properties["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] != nil {
saConnectionString := strings.Split(*appSettingsResp.Properties["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"], ";")
for _, part := range saConnectionString {
if strings.Contains(part, "AccountName") {
storageAccountName = strings.TrimPrefix(part, "AccountName=")
break
}
}
}
if appSettingsResp.Properties["WEBSITE_CONTENTSHARE"] != nil {
storageFileShare = *appSettingsResp.Properties["WEBSITE_CONTENTSHARE"]
}
saClient := metadata.Client.Storage
account, err := saClient.FindAccount(ctx, storageAccountName)
if err != nil {
return fmt.Errorf("retrieving Account %q for Share %q: %s", storageAccountName, storageFileShare, err)
}
if account == nil {
return fmt.Errorf("unable to locate Storage Account %s", storageAccountName)
}
saFileShareClient, err := metadata.Client.Storage.FileSharesClient(ctx, *account)
if err != nil {
return fmt.Errorf("building File Share Client: %s", err)
}
exists, err := saFileShareClient.Exists(ctx, account.ResourceGroup, storageAccountName, storageFileShare)
if err != nil {
return fmt.Errorf("checking for existence of Storage Share %q (Account %q) : %+v", storageFileShare, storageAccountName, err)
}
if exists == nil {
return fmt.Errorf("the storage file share %s for function app %s does not exists, please specify an existing one", storageFileShare, id.ID())
}
}
}

xiaxyi marked this conversation as resolved.
Show resolved Hide resolved
connectionStrings, err := client.ListConnectionStrings(ctx, id.ResourceGroup, id.SiteName)
if err != nil {
return fmt.Errorf("reading Connection String information for Linux %s: %+v", id, err)
Expand Down Expand Up @@ -687,7 +752,6 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc {
Timeout: 30 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.AppService.WebAppsClient

id, err := parse.FunctionAppID(metadata.ResourceData.Id())
if err != nil {
return err
Expand All @@ -703,19 +767,21 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc {
return fmt.Errorf("reading Linux %s: %v", id, err)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id)
// Some service plan updates are allowed - see customiseDiff for exceptions
var serviceFarmId string
if metadata.ResourceData.HasChange("service_plan_id") {
serviceFarmId = state.ServicePlanId
existing.SiteProperties.ServerFarmID = utils.String(serviceFarmId)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id, serviceFarmId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a call to the new service plan to get the SKU, and not repurpose the helper function.

if err != nil {
return err
}

// Only send for ElasticPremium
sendContentSettings := helpers.PlanIsElastic(planSKU) && !state.ForceDisableContentShare

// Some service plan updates are allowed - see customiseDiff for exceptions
if metadata.ResourceData.HasChange("service_plan_id") {
existing.SiteProperties.ServerFarmID = utils.String(state.ServicePlanId)
}

if metadata.ResourceData.HasChange("enabled") {
existing.SiteProperties.Enabled = utils.Bool(state.Enabled)
}
Expand Down Expand Up @@ -779,6 +845,30 @@ func (r LinuxFunctionAppResource) Update() sdk.ResourceFunc {
state.AppSettings = make(map[string]string)
}
state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings)

if !state.StorageUsesMSI {
suffix := uuid.New().String()[0:4]
_, contentOverVnetEnabled := state.AppSettings["WEBSITE_CONTENTOVERVNET"]
_, contentSharePresent := state.AppSettings["WEBSITE_CONTENTSHARE"]
_, contentShareConnectionString := state.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]

if !contentSharePresent {
if contentOverVnetEnabled {
return fmt.Errorf("the value of WEBSITE_CONTENTSHARE must be set to a predefined share when the storage account is restricted to a virtual network")
}
state.AppSettings["WEBSITE_CONTENTSHARE"] = fmt.Sprintf("%s-%s", strings.ToLower(state.Name), suffix)
}
if !contentShareConnectionString {
if contentOverVnetEnabled && contentSharePresent {
return fmt.Errorf("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING must be set when WEBSITE_CONTENTSHARE and WEBSITE_CONTENTOVERVNET is specified")
}
state.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] = storageString
}
} else {
if _, present := state.AppSettings["AzureWebJobsStorage__accountName"]; !present {
state.AppSettings["AzureWebJobsStorage__accountName"] = storageString
}
}
}

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ func (r LinuxFunctionAppSlotResource) Update() sdk.ResourceFunc {
return fmt.Errorf("reading Linux %s: %v", id, err)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id)
_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id, "")
if err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func (r WebAppHybridConnectionResource) CustomImporter() sdk.ResourceRunFunc {
}
appId := parse.NewWebAppID(id.SubscriptionId, id.ResourceGroup, id.SiteName)

_, sku, err := helpers.ServicePlanInfoForApp(ctx, metadata, appId)
_, sku, err := helpers.ServicePlanInfoForApp(ctx, metadata, appId, "")
if err != nil {
return err
}
Expand Down
100 changes: 97 additions & 3 deletions internal/services/appservice/windows_function_app_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,10 +412,20 @@ func (r WindowsFunctionAppResource) Create() sdk.ResourceFunc {
}
if !functionApp.StorageUsesMSI {
suffix := uuid.New().String()[0:4]
if _, present := functionApp.AppSettings["WEBSITE_CONTENTSHARE"]; !present {
_, contentOverVnetEnabled := functionApp.AppSettings["WEBSITE_CONTENTOVERVNET"]
_, contentSharePresent := functionApp.AppSettings["WEBSITE_CONTENTSHARE"]
_, contentShareConnectionString := functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]

if !contentSharePresent {
if contentOverVnetEnabled {
return fmt.Errorf("the value of WEBSITE_CONTENTSHARE must be set to a predefined share when the storage account is restricted to a virtual network")
}
functionApp.AppSettings["WEBSITE_CONTENTSHARE"] = fmt.Sprintf("%s-%s", strings.ToLower(functionApp.Name), suffix)
}
if _, present := functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]; !present {
if !contentShareConnectionString {
if contentOverVnetEnabled && contentSharePresent {
return fmt.Errorf("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING must be set when WEBSITE_CONTENTSHARE and WEBSITE_CONTENTOVERVNET is specified")
}
functionApp.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] = storageString
}
} else {
Expand Down Expand Up @@ -524,6 +534,7 @@ func (r WindowsFunctionAppResource) Read() sdk.ResourceFunc {
Timeout: 5 * time.Minute,
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.AppService.WebAppsClient
servicePlanClient := metadata.Client.AppService.ServicePlanClient
id, err := parse.FunctionAppID(metadata.ResourceData.Id())
if err != nil {
return err
Expand All @@ -536,6 +547,22 @@ func (r WindowsFunctionAppResource) Read() sdk.ResourceFunc {
return fmt.Errorf("reading Windows %s: %+v", id, err)
}

var planSku *string
if functionApp.ServerFarmID != nil {
servicePlanId, err := parse.ServicePlanID(*functionApp.ServerFarmID)
if err != nil {
return err
}
servicePlan, err := servicePlanClient.Get(ctx, servicePlanId.ResourceGroup, servicePlanId.ServerfarmName)
if err != nil {
return fmt.Errorf("reading %s: %+v", servicePlanId, err)
}
if sku := servicePlan.Sku; sku != nil && sku.Name != nil {
planSku = sku.Name
}
}
sendContentSettings := helpers.PlanIsElastic(planSku)

xiaxyi marked this conversation as resolved.
Show resolved Hide resolved
if functionApp.SiteProperties == nil {
return fmt.Errorf("reading properties of Windows %s", id)
}
Expand All @@ -546,6 +573,43 @@ func (r WindowsFunctionAppResource) Read() sdk.ResourceFunc {
return fmt.Errorf("reading App Settings for Windows %s: %+v", id, err)
}

var storageAccountName, storageFileShare string
if sendContentSettings || helpers.PlanIsConsumption(planSku) {
if appSettingsResp.Properties != nil {
if appSettingsResp.Properties["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] != nil {
saConnectionString := strings.Split(*appSettingsResp.Properties["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"], ";")
for _, part := range saConnectionString {
if strings.Contains(part, "AccountName") {
storageAccountName = strings.TrimPrefix(part, "AccountName=")
break
}
}
}
if appSettingsResp.Properties["WEBSITE_CONTENTSHARE"] != nil {
storageFileShare = *appSettingsResp.Properties["WEBSITE_CONTENTSHARE"]
}
saClient := metadata.Client.Storage
account, err := saClient.FindAccount(ctx, storageAccountName)
if err != nil {
return fmt.Errorf("retrieving Account %q for Share %q: %s", storageAccountName, storageFileShare, err)
}
if account == nil {
return fmt.Errorf("unable to locate Storage Account %s", storageAccountName)
}
saFileShareClient, err := metadata.Client.Storage.FileSharesClient(ctx, *account)
if err != nil {
return fmt.Errorf("building File Share Client: %s", err)
}
exists, err := saFileShareClient.Exists(ctx, account.ResourceGroup, storageAccountName, storageFileShare)
if err != nil {
return fmt.Errorf("checking for existence of Storage Share %q (Account %q) : %+v", storageFileShare, storageAccountName, err)
}
if exists == nil {
return fmt.Errorf("the storage file share %s for function app %s does not exists, please specify an existing one", storageFileShare, id.ID())
}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, this shouldn't be in the Read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, this is to address the timeout issue. if the user doesn't specify a predefined file share and also set the WEBSITE_CONTENTOVERVNET in the app_setting, the provider will timeout by trying to access a non-existence file share. I can add the 4.0 condition to this check block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed - this shouldn't be in the Read function - we don't retrieve the file share in the Read, so this is unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tombuildsstuff for the comments. If we don't check the storage file share, shall we just let the read function timeout?

Copy link
Contributor

@tombuildsstuff tombuildsstuff Oct 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xiaxyi why will the read function timeout? it's calling the App Service ARM API and not interacting with the Storage Account in the read function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function app is trying to connect to the file share specified in the [WEBSITE_CONTENTSHARE](https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings), Generally, the file share is created by function app automatically, however, the it cannot be created if user specify the key WEBSITE_CONTENTOVERVNET in app_setting, it requires the user to create a valid file share in advance and put the value in WEBSITE_CONTENTSHARE`. We didn't add such notes to user in the current provider, so the function(Read function) times out.

If we don't check the file share in read function, then what's left is to add notes in docs and let the read function timeout.

connectionStrings, err := client.ListConnectionStrings(ctx, id.ResourceGroup, id.SiteName)
if err != nil {
return fmt.Errorf("reading Connection String information for Windows %s: %+v", id, err)
Expand Down Expand Up @@ -702,7 +766,13 @@ func (r WindowsFunctionAppResource) Update() sdk.ResourceFunc {
return fmt.Errorf("reading Windows %s: %v", id, err)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id)
var serviceFarmId string
if metadata.ResourceData.HasChange("service_plan_id") {
serviceFarmId = state.ServicePlanId
existing.SiteProperties.ServerFarmID = utils.String(serviceFarmId)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id, serviceFarmId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, the helper should not be repurposed, and the SKU should be read from the new service plan ID. A new helper for that would be acceptable.

if err != nil {
return err
}
Expand Down Expand Up @@ -778,6 +848,30 @@ func (r WindowsFunctionAppResource) Update() sdk.ResourceFunc {
state.AppSettings = make(map[string]string)
}
state.AppSettings = helpers.ParseContentSettings(appSettingsResp, state.AppSettings)

if !state.StorageUsesMSI {
suffix := uuid.New().String()[0:4]
_, contentOverVnetEnabled := state.AppSettings["WEBSITE_CONTENTOVERVNET"]
_, contentSharePresent := state.AppSettings["WEBSITE_CONTENTSHARE"]
_, contentShareConnectionString := state.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"]

if !contentSharePresent {
if contentOverVnetEnabled {
return fmt.Errorf("the value of WEBSITE_CONTENTSHARE must be set to a predefined share when the storage account is restricted to a virtual network")
}
state.AppSettings["WEBSITE_CONTENTSHARE"] = fmt.Sprintf("%s-%s", strings.ToLower(state.Name), suffix)
}
if !contentShareConnectionString {
if contentOverVnetEnabled && contentSharePresent {
return fmt.Errorf("WEBSITE_CONTENTAZUREFILECONNECTIONSTRING must be set when WEBSITE_CONTENTSHARE and WEBSITE_CONTENTOVERVNET is specified")
}
state.AppSettings["WEBSITE_CONTENTAZUREFILECONNECTIONSTRING"] = storageString
}
} else {
if _, present := state.AppSettings["AzureWebJobsStorage__accountName"]; !present {
state.AppSettings["AzureWebJobsStorage__accountName"] = storageString
}
}
}

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ func (r WindowsFunctionAppSlotResource) Update() sdk.ResourceFunc {
return fmt.Errorf("reading Windows %s: %v", id, err)
}

_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id)
_, planSKU, err := helpers.ServicePlanInfoForApp(ctx, metadata, *id, "")
if err != nil {
return err
}
Expand Down
2 changes: 2 additions & 0 deletions website/docs/r/linux_function_app.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,8 @@ A `site_config` block supports the following:

* `always_on` - (Optional) If this Linux Web App is Always On enabled. Defaults to `false`.

~> **NOTE:** when running in a Consumption or Premium Plan, `always_on` feature should be turned off. Please turn it off before upgrading the service plan from standard to premium.

* `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.
Expand Down
2 changes: 2 additions & 0 deletions website/docs/r/windows_function_app.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ A `site_config` block supports the following:

* `always_on` - (Optional) If this Windows Function App is Always On enabled. Defaults to `false`.

~> **NOTE:** when running in a Consumption or Premium Plan, `always_on` feature should be turned off. Please turn it off before upgrading the service plan from standard to premium.

* `api_definition_url` - (Optional) The URL of the API definition that describes this Windows Function App.

* `api_management_api_id` - (Optional) The ID of the API Management API for this Windows Function App.
Expand Down