From 79b6538dfd370dcf128ee06a78b633c58cc03db7 Mon Sep 17 00:00:00 2001 From: Gabriel Bider <1554615+silkfire@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:28:14 +0100 Subject: [PATCH] Recognize identity-based AzureWebJobsStorage settings The host storage warning only checked AzureWebJobsStorage and AzureWebJobsStorage__accountName, so apps using the URI-based identity configuration (__blobServiceUri, __queueServiceUri, etc.) got a false positive warning. Treat any AzureWebJobsStorage__* setting as configured host storage, and let the run-from-package publisher use AzureWebJobsStorage__blobServiceUri for blob access, failing with a clear error instead of building an 'undefined' endpoint when no blob endpoint can be determined. --- src/handlers/resourceValidator.ts | 12 +++-- src/interfaces/IAppSettings.ts | 1 + src/publishers/websiteRunFromPackageDeploy.ts | 14 ++++-- src/utils/host_storage.ts | 32 ++++++++++++ src/utils/index.ts | 3 +- tests/utils/host_storage.spec.ts | 50 +++++++++++++++++++ 6 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/utils/host_storage.ts create mode 100644 tests/utils/host_storage.spec.ts diff --git a/src/handlers/resourceValidator.ts b/src/handlers/resourceValidator.ts index fa47e8119..f4ab399f2 100644 --- a/src/handlers/resourceValidator.ts +++ b/src/handlers/resourceValidator.ts @@ -16,7 +16,7 @@ import { IOrchestratable } from '../interfaces/IOrchestratable'; import { IScmCredentials } from '../interfaces/IScmCredentials'; import { Kudu } from '../appservice-rest/Kudu/azure-app-kudu-service'; import { KuduServiceUtility } from '../appservice-rest/Utilities/KuduServiceUtility'; -import { Logger } from '../utils'; +import { HostStorage, Logger } from '../utils'; import { RuntimeStackConstant } from '../constants/runtime_stack'; import { StateConstant } from '../constants/state'; @@ -152,9 +152,9 @@ export class ResourceValidator implements IOrchestratable { ); } - if (!appSettings.properties['AzureWebJobsStorage'] && !appSettings.properties['AzureWebJobsStorage__accountName']) { + if (!HostStorage.isConfigured(appSettings.properties)) { Logger.Warn( - 'Neither AzureWebJobsStorage nor AzureWebJobsStorage__accountName exist in app settings (from Azure Resource Manager with RBAC credential). ' + + 'Neither AzureWebJobsStorage nor any identity-based AzureWebJobsStorage__* setting (e.g. AzureWebJobsStorage__accountName, AzureWebJobsStorage__blobServiceUri) exist in app settings (from Azure Resource Manager with RBAC credential). ' + 'Please ensure one of them is configured as it is critical for function runtime. ' + 'For more information, please visit the function app settings reference page: ' + 'https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurewebjobsstorage' @@ -171,6 +171,7 @@ export class ResourceValidator implements IOrchestratable { const result: IAppSettings = { AzureWebJobsStorage: appSettings.properties['AzureWebJobsStorage'], AzureWebJobsStorage__accountName: appSettings.properties['AzureWebJobsStorage__accountName'], + AzureWebJobsStorage__blobServiceUri: appSettings.properties['AzureWebJobsStorage__blobServiceUri'], FUNCTIONS_WORKER_RUNTIME: appSettings.properties['FUNCTIONS_WORKER_RUNTIME'], ENABLE_ORYX_BUILD: appSettings.properties['ENABLE_ORYX_BUILD'], SCM_DO_BUILD_DURING_DEPLOYMENT: appSettings.properties['SCM_DO_BUILD_DURING_DEPLOYMENT'], @@ -199,9 +200,9 @@ export class ResourceValidator implements IOrchestratable { ); } - if (!appSettings['AzureWebJobsStorage'] && !appSettings['AzureWebJobsStorage__accountName']) { + if (!HostStorage.isConfigured(appSettings)) { Logger.Warn( - 'Neither AzureWebJobsStorage nor AzureWebJobsStorage__accountName exist in app settings (from Kudu SCM site with publish-profile credential). ' + + 'Neither AzureWebJobsStorage nor any identity-based AzureWebJobsStorage__* setting (e.g. AzureWebJobsStorage__accountName, AzureWebJobsStorage__blobServiceUri) exist in app settings (from Kudu SCM site with publish-profile credential). ' + 'Please ensure one of them is configured as it is critical for function runtime. ' + 'For more information, please visit the function app settings reference page: ' + 'https://docs.microsoft.com/en-us/azure/azure-functions/functions-app-settings#azurewebjobsstorage' @@ -218,6 +219,7 @@ export class ResourceValidator implements IOrchestratable { const result: IAppSettings = { AzureWebJobsStorage: appSettings['AzureWebJobsStorage'], AzureWebJobsStorage__accountName: appSettings['AzureWebJobsStorage__accountName'], + AzureWebJobsStorage__blobServiceUri: appSettings['AzureWebJobsStorage__blobServiceUri'], FUNCTIONS_WORKER_RUNTIME: appSettings['FUNCTIONS_WORKER_RUNTIME'], ENABLE_ORYX_BUILD: appSettings['ENABLE_ORYX_BUILD'], SCM_DO_BUILD_DURING_DEPLOYMENT: appSettings['SCM_DO_BUILD_DURING_DEPLOYMENT'], diff --git a/src/interfaces/IAppSettings.ts b/src/interfaces/IAppSettings.ts index 078c9f146..29b688e85 100644 --- a/src/interfaces/IAppSettings.ts +++ b/src/interfaces/IAppSettings.ts @@ -1,6 +1,7 @@ export interface IAppSettings { AzureWebJobsStorage: Readonly; AzureWebJobsStorage__accountName: Readonly; + AzureWebJobsStorage__blobServiceUri: Readonly; FUNCTIONS_WORKER_RUNTIME: Readonly; ENABLE_ORYX_BUILD: Readonly; SCM_DO_BUILD_DURING_DEPLOYMENT: Readonly; diff --git a/src/publishers/websiteRunFromPackageDeploy.ts b/src/publishers/websiteRunFromPackageDeploy.ts index e02b4e507..76f31e4e3 100644 --- a/src/publishers/websiteRunFromPackageDeploy.ts +++ b/src/publishers/websiteRunFromPackageDeploy.ts @@ -5,7 +5,7 @@ import { StorageManagementClient, StorageAccountListKeysResult } from '@azure/ar import { DefaultAzureCredential } from '@azure/identity'; import { AzureResourceError } from '../exceptions'; -import { Logger, Sleeper } from '../utils'; +import { HostStorage, Logger, Sleeper } from '../utils'; import { AzureAppService } from '../appservice-rest/Arm/azure-app-service'; import { ConfigurationConstant } from '../constants/configuration'; import { IActionContext } from '../interfaces/IActionContext'; @@ -19,11 +19,15 @@ export class WebsiteRunFromPackageDeploy { Logger.Info("Using AzureWebJobsStorage for Blob access."); blobServiceClient = BlobServiceClient.fromConnectionString(context.appSettings.AzureWebJobsStorage); } else { - Logger.Info("Using AzureWebJobsStorage__accountName and RBAC for Blob access."); - blobServiceClient = new BlobServiceClient( - `https://${context.appSettings.AzureWebJobsStorage__accountName}.blob.core.windows.net`, - new DefaultAzureCredential() + const blobServiceUri: string | undefined = HostStorage.getBlobServiceUri( + context.appSettings.AzureWebJobsStorage__blobServiceUri, + context.appSettings.AzureWebJobsStorage__accountName ); + if (!blobServiceUri) { + throw new AzureResourceError(state, "Get Blob Service", "Neither AzureWebJobsStorage, AzureWebJobsStorage__blobServiceUri nor AzureWebJobsStorage__accountName exist in app settings, cannot determine the blob endpoint to upload the package to."); + } + Logger.Info("Using identity-based AzureWebJobsStorage settings and RBAC for Blob access."); + blobServiceClient = new BlobServiceClient(blobServiceUri, new DefaultAzureCredential()); } const containerClient: ContainerClient = blobServiceClient.getContainerClient(ConfigurationConstant.BlobContainerName); await containerClient.createIfNotExists(); diff --git a/src/utils/host_storage.ts b/src/utils/host_storage.ts new file mode 100644 index 000000000..fb7cc819d --- /dev/null +++ b/src/utils/host_storage.ts @@ -0,0 +1,32 @@ +export class HostStorage { + private static readonly IdentityPrefix = 'AzureWebJobsStorage__'; + + /** + * True when the host storage is configured in any supported form: the AzureWebJobsStorage connection string, + * or any identity-based AzureWebJobsStorage__* setting (accountName, blobServiceUri, queueServiceUri, + * tableServiceUri, credential, clientId). + */ + public static isConfigured(appSettings: { [key: string]: string }): boolean { + if (!appSettings) { + return false; + } + + if (appSettings['AzureWebJobsStorage']) { + return true; + } + + return Object.keys(appSettings).some(key => key.startsWith(HostStorage.IdentityPrefix) && !!appSettings[key]); + } + + /** + * Blob service endpoint for identity-based host storage. AzureWebJobsStorage__blobServiceUri takes precedence, + * falling back to the endpoint derived from AzureWebJobsStorage__accountName. Undefined when neither is set. + */ + public static getBlobServiceUri(blobServiceUri: string, accountName: string): string | undefined { + if (blobServiceUri) { + return blobServiceUri; + } + + return accountName ? `https://${accountName}.blob.core.windows.net` : undefined; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index fe7757f7f..1471671b7 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ export { Logger } from './logger'; export { Parser } from './parser'; export { Sleeper } from './sleeper'; -export { FuncIgnore } from './funcignore'; \ No newline at end of file +export { FuncIgnore } from './funcignore'; +export { HostStorage } from './host_storage'; \ No newline at end of file diff --git a/tests/utils/host_storage.spec.ts b/tests/utils/host_storage.spec.ts new file mode 100644 index 000000000..c37036222 --- /dev/null +++ b/tests/utils/host_storage.spec.ts @@ -0,0 +1,50 @@ +import { expect } from 'chai'; +import { HostStorage } from '../../src/utils'; + +describe('host storage', function () { + describe('isConfigured', function () { + it('should accept a connection string', function () { + expect(HostStorage.isConfigured({ AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=x' })).is.true; + }); + + [ + 'AzureWebJobsStorage__accountName', + 'AzureWebJobsStorage__blobServiceUri', + 'AzureWebJobsStorage__queueServiceUri', + 'AzureWebJobsStorage__tableServiceUri', + 'AzureWebJobsStorage__credential', + 'AzureWebJobsStorage__clientId' + ].forEach(key => { + it(`should accept ${key}`, function () { + expect(HostStorage.isConfigured({ [key]: 'value' })).is.true; + }); + }); + + it('should reject settings without host storage', function () { + expect(HostStorage.isConfigured({ FUNCTIONS_WORKER_RUNTIME: 'dotnet-isolated' })).is.false; + }); + + it('should reject empty values and unrelated prefixes', function () { + expect(HostStorage.isConfigured({ AzureWebJobsStorage: '', AzureWebJobsStorage__accountName: '' })).is.false; + expect(HostStorage.isConfigured({ AzureWebJobsStorageOther: 'value' })).is.false; + }); + + it('should reject undefined settings', function () { + expect(HostStorage.isConfigured(undefined)).is.false; + }); + }); + + describe('getBlobServiceUri', function () { + it('should prefer the blob service uri', function () { + expect(HostStorage.getBlobServiceUri('https://a.blob.core.windows.net', 'b')).equals('https://a.blob.core.windows.net'); + }); + + it('should derive the uri from the account name', function () { + expect(HostStorage.getBlobServiceUri(undefined, 'b')).equals('https://b.blob.core.windows.net'); + }); + + it('should return undefined when neither is set', function () { + expect(HostStorage.getBlobServiceUri(undefined, undefined)).is.undefined; + }); + }); +});