Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 7 additions & 5 deletions src/handlers/resourceValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'
Expand All @@ -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'],
Expand Down Expand Up @@ -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'
Expand All @@ -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'],
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/IAppSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface IAppSettings {
AzureWebJobsStorage: Readonly<string>;
AzureWebJobsStorage__accountName: Readonly<string>;
AzureWebJobsStorage__blobServiceUri: Readonly<string>;
FUNCTIONS_WORKER_RUNTIME: Readonly<string>;
ENABLE_ORYX_BUILD: Readonly<string>;
SCM_DO_BUILD_DURING_DEPLOYMENT: Readonly<string>;
Expand Down
14 changes: 9 additions & 5 deletions src/publishers/websiteRunFromPackageDeploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
32 changes: 32 additions & 0 deletions src/utils/host_storage.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 2 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Logger } from './logger';
export { Parser } from './parser';
export { Sleeper } from './sleeper';
export { FuncIgnore } from './funcignore';
export { FuncIgnore } from './funcignore';
export { HostStorage } from './host_storage';
50 changes: 50 additions & 0 deletions tests/utils/host_storage.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
});
});
});