Skip to content

Commit 5704a16

Browse files
authored
Basic implementation for managed identity for create function app (#4213)
* Basic implementation for managed identity for create function app * Remove unusued msi package * Get managed identity from the azure utils package * If/else conditional for adding mi * Add list step for advanced create * Use new azureutil changes * Small refactor
1 parent 3662838 commit 5704a16

File tree

5 files changed

+101
-47
lines changed

5 files changed

+101
-47
lines changed

package-lock.json

Lines changed: 8 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1346,7 +1346,7 @@
13461346
"@azure/storage-blob": "^12.5.0",
13471347
"@microsoft/vscode-azext-azureappservice": "^3.3.1",
13481348
"@microsoft/vscode-azext-azureappsettings": "^0.2.2",
1349-
"@microsoft/vscode-azext-azureutils": "^3.1.3",
1349+
"@microsoft/vscode-azext-azureutils": "^3.1.5",
13501350
"@microsoft/vscode-azext-serviceconnector": "^0.1.3",
13511351
"@microsoft/vscode-azext-utils": "^2.6.3",
13521352
"@microsoft/vscode-azureresources-api": "^2.0.4",

src/commands/createFunctionApp/FunctionAppCreateStep.ts

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { type NameValuePair, type Site, type SiteConfig, type WebSiteManagementClient } from '@azure/arm-appservice';
7+
import { type Identity } from '@azure/arm-resources';
78
import { BlobServiceClient } from '@azure/storage-blob';
89
import { ParsedSite, WebsiteOS, type CustomLocation, type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice';
910
import { LocationListStep } from '@microsoft/vscode-azext-azureutils';
@@ -16,6 +17,7 @@ import { ext } from '../../extensionVariables';
1617
import { localize } from '../../localize';
1718
import { createWebSiteClient } from '../../utils/azureClients';
1819
import { getRandomHexString } from '../../utils/fs';
20+
import { createAzureWebJobsStorageManagedIdentitySettings } from '../../utils/managedIdentityUtils';
1921
import { nonNullProp } from '../../utils/nonNull';
2022
import { getStorageConnectionString } from '../appSettings/connectionSettings/getLocalConnectionSetting';
2123
import { enableFileLogging } from '../logstream/enableFileLogging';
@@ -56,7 +58,6 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
5658
context.telemetry.properties.fileLoggingError = maskUserInfo(parseError(error).message, []);
5759
}
5860
}
59-
6061
showSiteCreated(site, context);
6162
}
6263

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

6768
private async getNewSite(context: IFunctionAppWizardContext, stack: FullFunctionAppStack): Promise<Site> {
68-
const location = await LocationListStep.getLocation(context, webProvider);
69-
const site: Site = {
70-
name: context.newSiteName,
71-
kind: getSiteKind(context),
72-
location: nonNullProp(location, 'name'),
73-
serverFarmId: context.plan?.id,
74-
clientAffinityEnabled: false,
75-
siteConfig: await this.getNewSiteConfig(context, stack),
76-
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.
77-
};
69+
const site: Site = await this.createNewSite(context, stack);
70+
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.
7871

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

10194
private async getNewFlexSite(context: IFlexFunctionAppWizardContext, sku: Sku): Promise<Site> {
102-
const location = await LocationListStep.getLocation(context, webProvider);
103-
const site: Site = {
104-
name: context.newSiteName,
105-
kind: getSiteKind(context),
106-
location: nonNullProp(location, 'name'),
107-
serverFarmId: context.plan?.id,
108-
clientAffinityEnabled: false,
109-
siteConfig: await this.getNewSiteConfig(context)
110-
};
111-
95+
const site: Site = await this.createNewSite(context);
11296
site.functionAppConfig = {
11397
deployment: {
11498
storage: {
@@ -136,16 +120,42 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
136120
return site;
137121
}
138122

123+
private async createNewSite(context: IFunctionAppWizardContext, stack?: FullFunctionAppStack): Promise<Site> {
124+
const location = await LocationListStep.getLocation(context, webProvider);
125+
let identity: Identity | undefined = undefined;
126+
if (context.managedIdentity) {
127+
const userAssignedIdentities = {};
128+
userAssignedIdentities[nonNullProp(context.managedIdentity, 'id')] =
129+
{ principalId: context.managedIdentity?.principalId, clientId: context.managedIdentity?.clientId };
130+
identity = { type: 'UserAssigned', userAssignedIdentities }
131+
}
132+
133+
return {
134+
name: context.newSiteName,
135+
kind: getSiteKind(context),
136+
location: nonNullProp(location, 'name'),
137+
serverFarmId: context.plan?.id,
138+
clientAffinityEnabled: false,
139+
siteConfig: await this.getNewSiteConfig(context, stack),
140+
identity
141+
};
142+
}
143+
139144
private async getNewSiteConfig(context: IFunctionAppWizardContext, stack?: FullFunctionAppStack): Promise<SiteConfig> {
140145
let newSiteConfig: SiteConfig = {};
141146

142147
const storageConnectionString: string = (await getStorageConnectionString(context)).connectionString;
143-
let appSettings: NameValuePair[] = [
144-
{
148+
149+
let appSettings: NameValuePair[] = [];
150+
if (context.managedIdentity) {
151+
appSettings.push(...createAzureWebJobsStorageManagedIdentitySettings(context));
152+
} else {
153+
appSettings.push({
145154
name: ConnectionKey.Storage,
146155
value: storageConnectionString
147-
}
148-
];
156+
});
157+
}
158+
149159

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

255265
// storage container is needed for flex deployment, but it is not created automatically
256266
async function tryCreateStorageContainer(site: Site, storageConnectionString: string): Promise<void> {
257-
const blobClient = BlobServiceClient.fromConnectionString(storageConnectionString);
258-
const containerUrl: string | undefined = site.functionAppConfig?.deployment?.storage?.value;
259-
if (containerUrl) {
260-
const containerName = containerUrl.split('/').pop();
261-
if (containerName) {
262-
const client = blobClient.getContainerClient(containerName);
263-
if (!await client.exists()) {
264-
await blobClient.createContainer(containerName);
265-
} else {
266-
ext.outputChannel.appendLog(localize('deploymentStorageExists', 'Deployment storage container "{0}" already exists.', containerName));
267-
return;
267+
try {
268+
const blobClient = BlobServiceClient.fromConnectionString(storageConnectionString);
269+
const containerUrl: string | undefined = site.functionAppConfig?.deployment?.storage?.value;
270+
if (containerUrl) {
271+
const containerName = containerUrl.split('/').pop();
272+
if (containerName) {
273+
const client = blobClient.getContainerClient(containerName);
274+
if (!await client.exists()) {
275+
await blobClient.createContainer(containerName);
276+
} else {
277+
ext.outputChannel.appendLog(localize('deploymentStorageExists', 'Deployment storage container "{0}" already exists.', containerName));
278+
return;
279+
}
268280
}
269281
}
282+
} catch (error) {
283+
// ignore error, we will show a warning in the output channel
284+
const parsedError = parseError(error);
285+
ext.outputChannel.appendLog(localize('failedToCreateDeploymentStorage', 'Failed to create deployment storage container. {0}', parsedError.message));
270286
}
271287

272288
ext.outputChannel.appendLog(localize('noDeploymentStorage', 'No deployment storage specified in function app.'));

src/commands/createFunctionApp/createCreateFunctionAppComponents.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, CustomLocationListStep, LogAnalyticsCreateStep, SiteNameStep, WebsiteOS, type IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice";
7-
import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, type INewStorageAccountDefaults } from "@microsoft/vscode-azext-azureutils";
7+
import { CommonRoleDefinitions, createRoleId, LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, RoleAssignmentExecuteStep, StorageAccountCreateStep, StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication, UserAssignedIdentityCreateStep, UserAssignedIdentityListStep, type INewStorageAccountDefaults, type Role } from "@microsoft/vscode-azext-azureutils";
88
import { type AzureWizardExecuteStep, type AzureWizardPromptStep, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
99
import { FuncVersion, latestGAVersion, tryParseFuncVersion } from "../../FuncVersion";
1010
import { funcVersionSetting } from "../../constants";
@@ -71,6 +71,7 @@ export async function createCreateFunctionAppComponents(context: ICreateFunction
7171
executeSteps.push(new ResourceGroupCreateStep());
7272
executeSteps.push(new StorageAccountCreateStep(storageAccountCreateOptions));
7373
executeSteps.push(new AppInsightsCreateStep());
74+
executeSteps.push(new UserAssignedIdentityCreateStep());
7475
if (!context.dockerfilePath) {
7576
executeSteps.push(new AppServicePlanCreateStep());
7677
executeSteps.push(new LogAnalyticsCreateStep());
@@ -94,9 +95,18 @@ export async function createCreateFunctionAppComponents(context: ICreateFunction
9495
}
9596
));
9697
promptSteps.push(new AppInsightsListStep());
98+
promptSteps.push(new UserAssignedIdentityListStep());
9799
}
98100

101+
executeSteps.push(new RoleAssignmentExecuteStep(() => {
102+
const role: Role = {
103+
scopeId: wizardContext?.storageAccount?.id,
104+
roleDefinitionId: createRoleId(wizardContext?.subscriptionId, CommonRoleDefinitions.storageBlobDataOwner),
105+
roleDefinitionName: CommonRoleDefinitions.storageBlobDataOwner.roleName
106+
};
99107

108+
return [role];
109+
}));
100110
const storageProvider = 'Microsoft.Storage';
101111
LocationListStep.addProviderForFiltering(wizardContext, storageProvider, 'storageAccounts');
102112

src/utils/managedIdentityUtils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type NameValuePair } from "@azure/arm-appservice";
7+
import { type IFunctionAppWizardContext } from "../commands/createFunctionApp/IFunctionAppWizardContext";
8+
import { ConnectionKey } from "../constants";
9+
10+
export function createAzureWebJobsStorageManagedIdentitySettings(context: IFunctionAppWizardContext): NameValuePair[] {
11+
const appSettings: NameValuePair[] = [];
12+
const storageAccountName = context.newStorageAccountName ?? context.storageAccount?.name;
13+
if (context.managedIdentity) {
14+
appSettings.push({
15+
name: `${ConnectionKey.Storage}__blobServiceUri`,
16+
value: `https://${storageAccountName}.blob.core.windows.net`
17+
});
18+
appSettings.push({
19+
name: `${ConnectionKey.Storage}__queueServiceUri`,
20+
value: `https://${storageAccountName}.queue.core.windows.net`
21+
});
22+
appSettings.push({
23+
name: `${ConnectionKey.Storage}__tableServiceUri`,
24+
value: `https://${storageAccountName}.table.core.windows.net`
25+
});
26+
}
27+
28+
return appSettings;
29+
}

0 commit comments

Comments
 (0)