Skip to content
Merged
17 changes: 8 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@
"@azure/storage-blob": "^12.5.0",
"@microsoft/vscode-azext-azureappservice": "^3.3.1",
"@microsoft/vscode-azext-azureappsettings": "^0.2.2",
"@microsoft/vscode-azext-azureutils": "^3.1.3",
"@microsoft/vscode-azext-azureutils": "^3.1.5",
"@microsoft/vscode-azext-serviceconnector": "^0.1.3",
"@microsoft/vscode-azext-utils": "^2.6.3",
"@microsoft/vscode-azureresources-api": "^2.0.4",
Expand Down
88 changes: 52 additions & 36 deletions src/commands/createFunctionApp/FunctionAppCreateStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { type NameValuePair, type Site, type SiteConfig, type WebSiteManagementClient } from '@azure/arm-appservice';
import { type Identity } from '@azure/arm-resources';
import { BlobServiceClient } from '@azure/storage-blob';
import { ParsedSite, WebsiteOS, type CustomLocation, type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice';
import { LocationListStep } from '@microsoft/vscode-azext-azureutils';
Expand All @@ -16,6 +17,7 @@ import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { createWebSiteClient } from '../../utils/azureClients';
import { getRandomHexString } from '../../utils/fs';
import { createAzureWebJobsStorageManagedIdentitySettings } from '../../utils/managedIdentityUtils';
import { nonNullProp } from '../../utils/nonNull';
import { getStorageConnectionString } from '../appSettings/connectionSettings/getLocalConnectionSetting';
import { enableFileLogging } from '../logstream/enableFileLogging';
Expand Down Expand Up @@ -56,7 +58,6 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
context.telemetry.properties.fileLoggingError = maskUserInfo(parseError(error).message, []);
}
}

showSiteCreated(site, context);
}

Expand All @@ -65,16 +66,8 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
}

private async getNewSite(context: IFunctionAppWizardContext, stack: FullFunctionAppStack): Promise<Site> {
const location = await LocationListStep.getLocation(context, webProvider);
const site: Site = {
name: context.newSiteName,
kind: getSiteKind(context),
location: nonNullProp(location, 'name'),
serverFarmId: context.plan?.id,
clientAffinityEnabled: false,
siteConfig: await this.getNewSiteConfig(context, stack),
reserved: context.newSiteOS === WebsiteOS.linux // The secret property - must be set to true to make it a Linux plan. Confirmed by the team who owns this API.
};
const site: Site = await this.createNewSite(context, stack);
site.reserved = context.newSiteOS === WebsiteOS.linux; // The secret property - must be set to true to make it a Linux plan. Confirmed by the team who owns this API.

if (context.customLocation) {
this.addCustomLocationProperties(site, context.customLocation);
Expand All @@ -99,16 +92,7 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
}

private async getNewFlexSite(context: IFlexFunctionAppWizardContext, sku: Sku): Promise<Site> {
const location = await LocationListStep.getLocation(context, webProvider);
const site: Site = {
name: context.newSiteName,
kind: getSiteKind(context),
location: nonNullProp(location, 'name'),
serverFarmId: context.plan?.id,
clientAffinityEnabled: false,
siteConfig: await this.getNewSiteConfig(context)
};

const site: Site = await this.createNewSite(context);
site.functionAppConfig = {
deployment: {
storage: {
Expand Down Expand Up @@ -136,16 +120,42 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
return site;
}

private async createNewSite(context: IFunctionAppWizardContext, stack?: FullFunctionAppStack): Promise<Site> {
const location = await LocationListStep.getLocation(context, webProvider);
let identity: Identity | undefined = undefined;
if (context.managedIdentity) {
const userAssignedIdentities = {};
userAssignedIdentities[nonNullProp(context.managedIdentity, 'id')] =
{ principalId: context.managedIdentity?.principalId, clientId: context.managedIdentity?.clientId };
identity = { type: 'UserAssigned', userAssignedIdentities }
}

return {
name: context.newSiteName,
kind: getSiteKind(context),
location: nonNullProp(location, 'name'),
serverFarmId: context.plan?.id,
clientAffinityEnabled: false,
siteConfig: await this.getNewSiteConfig(context, stack),
identity
};
}

private async getNewSiteConfig(context: IFunctionAppWizardContext, stack?: FullFunctionAppStack): Promise<SiteConfig> {
let newSiteConfig: SiteConfig = {};

const storageConnectionString: string = (await getStorageConnectionString(context)).connectionString;
let appSettings: NameValuePair[] = [
{

let appSettings: NameValuePair[] = [];
if (context.managedIdentity) {
appSettings.push(...createAzureWebJobsStorageManagedIdentitySettings(context));
} else {
appSettings.push({
name: ConnectionKey.Storage,
value: storageConnectionString
}
];
});
}


if (stack) {
const stackSettings: FunctionAppRuntimeSettings = nonNullProp(stack.minorVersion.stackSettings, context.newSiteOS === WebsiteOS.linux ? 'linuxRuntimeSettings' : 'windowsRuntimeSettings');
Expand Down Expand Up @@ -254,19 +264,25 @@ function getSiteKind(context: IAppServiceWizardContext): string {

// storage container is needed for flex deployment, but it is not created automatically
async function tryCreateStorageContainer(site: Site, storageConnectionString: string): Promise<void> {
const blobClient = BlobServiceClient.fromConnectionString(storageConnectionString);
const containerUrl: string | undefined = site.functionAppConfig?.deployment?.storage?.value;
if (containerUrl) {
const containerName = containerUrl.split('/').pop();
if (containerName) {
const client = blobClient.getContainerClient(containerName);
if (!await client.exists()) {
await blobClient.createContainer(containerName);
} else {
ext.outputChannel.appendLog(localize('deploymentStorageExists', 'Deployment storage container "{0}" already exists.', containerName));
return;
try {
const blobClient = BlobServiceClient.fromConnectionString(storageConnectionString);
const containerUrl: string | undefined = site.functionAppConfig?.deployment?.storage?.value;
if (containerUrl) {
const containerName = containerUrl.split('/').pop();
if (containerName) {
const client = blobClient.getContainerClient(containerName);
if (!await client.exists()) {
await blobClient.createContainer(containerName);
} else {
ext.outputChannel.appendLog(localize('deploymentStorageExists', 'Deployment storage container "{0}" already exists.', containerName));
return;
}
}
}
} catch (error) {
// ignore error, we will show a warning in the output channel
const parsedError = parseError(error);
ext.outputChannel.appendLog(localize('failedToCreateDeploymentStorage', 'Failed to create deployment storage container. {0}', parsedError.message));
}

ext.outputChannel.appendLog(localize('noDeploymentStorage', 'No deployment storage specified in function app.'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, CustomLocationListStep, LogAnalyticsCreateStep, SiteNameStep, WebsiteOS, type IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice";
import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, type INewStorageAccountDefaults } from "@microsoft/vscode-azext-azureutils";
import { CommonRoleDefinitions, createRoleId, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, RoleAssignmentExecuteStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, UserAssignedIdentityCreateStep, UserAssignedIdentityListStep, type INewStorageAccountDefaults, type Role } from "@microsoft/vscode-azext-azureutils";
import { type AzureWizardExecuteStep, type AzureWizardPromptStep, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { FuncVersion, latestGAVersion, tryParseFuncVersion } from "../../FuncVersion";
import { funcVersionSetting } from "../../constants";
Expand Down Expand Up @@ -71,6 +71,7 @@ export async function createCreateFunctionAppComponents(context: ICreateFunction
executeSteps.push(new ResourceGroupCreateStep());
executeSteps.push(new StorageAccountCreateStep(storageAccountCreateOptions));
executeSteps.push(new AppInsightsCreateStep());
executeSteps.push(new UserAssignedIdentityCreateStep());
if (!context.dockerfilePath) {
executeSteps.push(new AppServicePlanCreateStep());
executeSteps.push(new LogAnalyticsCreateStep());
Expand All @@ -94,9 +95,18 @@ export async function createCreateFunctionAppComponents(context: ICreateFunction
}
));
promptSteps.push(new AppInsightsListStep());
promptSteps.push(new UserAssignedIdentityListStep());
}

executeSteps.push(new RoleAssignmentExecuteStep(() => {
const role: Role = {
scopeId: wizardContext?.storageAccount?.id,
roleDefinitionId: createRoleId(wizardContext?.subscriptionId, CommonRoleDefinitions.storageBlobDataOwner),
roleDefinitionName: CommonRoleDefinitions.storageBlobDataOwner.roleName
};

return [role];
}));
const storageProvider = 'Microsoft.Storage';
LocationListStep.addProviderForFiltering(wizardContext, storageProvider, 'storageAccounts');

Expand Down
29 changes: 29 additions & 0 deletions src/utils/managedIdentityUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type NameValuePair } from "@azure/arm-appservice";
import { type IFunctionAppWizardContext } from "../commands/createFunctionApp/IFunctionAppWizardContext";
import { ConnectionKey } from "../constants";

export function createAzureWebJobsStorageManagedIdentitySettings(context: IFunctionAppWizardContext): NameValuePair[] {
const appSettings: NameValuePair[] = [];
const storageAccountName = context.newStorageAccountName ?? context.storageAccount?.name;
if (context.managedIdentity) {
appSettings.push({
name: `${ConnectionKey.Storage}__blobServiceUri`,
value: `https://${storageAccountName}.blob.core.windows.net`
});
appSettings.push({
name: `${ConnectionKey.Storage}__queueServiceUri`,
value: `https://${storageAccountName}.queue.core.windows.net`
});
appSettings.push({
name: `${ConnectionKey.Storage}__tableServiceUri`,
value: `https://${storageAccountName}.table.core.windows.net`
});
}

return appSettings;
}