diff --git a/package-lock.json b/package-lock.json index 8355bf499..d60589dd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,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", @@ -1149,9 +1149,9 @@ } }, "node_modules/@microsoft/vscode-azext-azureutils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-3.1.3.tgz", - "integrity": "sha512-wIdipdVFWh+tZ6pRQRL6CE4QuWmj1w9hAoi4KKWChk2/WQiX5urProOq6tBxYaVHmnS33a6bHq1YhByHsqv9zg==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-3.1.5.tgz", + "integrity": "sha512-OhqNLDYwwgDGlQsWqd6WfriJ2RnNpwnNOZ0IG+So5NhYIFQAYF0bZ6AbkM/cXVT9BWqS0yqBQgbBdmcjzZSZbQ==", "dependencies": { "@azure/arm-authorization": "^9.0.0", "@azure/arm-authorization-profile-2020-09-01-hybrid": "^2.1.0", @@ -1164,7 +1164,7 @@ "@azure/core-client": "^1.6.0", "@azure/core-rest-pipeline": "^1.9.0", "@azure/logger": "^1.0.4", - "@microsoft/vscode-azext-utils": "^2.5.7", + "@microsoft/vscode-azext-utils": "^2.6.2", "semver": "^7.3.7", "uuid": "^9.0.0" }, @@ -1393,10 +1393,9 @@ } }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.6.3.tgz", - "integrity": "sha512-eogbgZH1KQkGWstl9qb1Tq4DQH+JJCHLHZelIbnzIKE8JKxr8Et/byn3OuL5nL1qeITQQn3+AYVsbj2hbljM7w==", - "license": "MIT", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.6.4.tgz", + "integrity": "sha512-sM6Rmt2xG4vTfW7QggL+TZVIK5tJbalgH611ltSAWtSqQcXCuUrpESYsHmsGBs2BPaUkUyWQLjxH2PqI/Be7ig==", "dependencies": { "@microsoft/vscode-azureresources-api": "^2.3.1", "@vscode/extension-telemetry": "^0.9.6", diff --git a/package.json b/package.json index 73271cf82..8521eb854 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/commands/createFunctionApp/FunctionAppCreateStep.ts b/src/commands/createFunctionApp/FunctionAppCreateStep.ts index b7c9122ad..f38621aeb 100644 --- a/src/commands/createFunctionApp/FunctionAppCreateStep.ts +++ b/src/commands/createFunctionApp/FunctionAppCreateStep.ts @@ -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'; @@ -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'; @@ -56,7 +58,6 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { - 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); @@ -99,16 +92,7 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { - 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: { @@ -136,16 +120,42 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { + 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 { 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'); @@ -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 { - 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.')); diff --git a/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts b/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts index d600bba74..77e4f8b16 100644 --- a/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts +++ b/src/commands/createFunctionApp/createCreateFunctionAppComponents.ts @@ -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"; @@ -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()); @@ -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'); diff --git a/src/utils/managedIdentityUtils.ts b/src/utils/managedIdentityUtils.ts new file mode 100644 index 000000000..ea793959a --- /dev/null +++ b/src/utils/managedIdentityUtils.ts @@ -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; +}