Skip to content

Commit 0dc04aa

Browse files
authored
Use ARM Graph to list resources (#4548)
* WIP to use ARM graph for listing * Build fixes * Update version for testing * Make changes to get site asynchronously * Simplfy the resolver code
1 parent 78a0e42 commit 0dc04aa

37 files changed

+299
-183
lines changed

package-lock.json

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

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "vscode-azurefunctions",
33
"displayName": "Azure Functions",
44
"description": "%azureFunctions.description%",
5-
"version": "1.17.3",
5+
"version": "1.17.4-alpha",
66
"publisher": "ms-azuretools",
77
"icon": "resources/azure-functions.png",
88
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
@@ -1473,6 +1473,7 @@
14731473
"@azure/arm-appservice": "^15.0.0",
14741474
"@azure/arm-cosmosdb": "^15.0.0",
14751475
"@azure/arm-eventhub": "^5.1.0",
1476+
"@azure/arm-resourcegraph": "^5.0.0-beta.3",
14761477
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
14771478
"@azure/arm-servicebus": "^5.0.0",
14781479
"@azure/arm-sql": "^9.1.0",

src/FunctionAppResolver.ts

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,102 @@
1-
import { type Site } from "@azure/arm-appservice";
2-
import { getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureutils";
3-
import { callWithTelemetryAndErrorHandling, nonNullProp, nonNullValue, nonNullValueAndProp, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
1+
import { type ResourceGraphClient } from "@azure/arm-resourcegraph";
2+
import { createWebSiteClient } from "@microsoft/vscode-azext-azureappservice";
3+
import { callWithTelemetryAndErrorHandling, nonNullProp, nonNullValueAndProp, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
44
import { type AppResource, type AppResourceResolver } from "@microsoft/vscode-azext-utils/hostapi";
55
import { ResolvedFunctionAppResource } from "./tree/ResolvedFunctionAppResource";
66
import { ResolvedContainerizedFunctionAppResource } from "./tree/containerizedFunctionApp/ResolvedContainerizedFunctionAppResource";
7-
import { createWebSiteClient } from "./utils/azureClients";
7+
import { createResourceGraphClient } from "./utils/azureClients";
88

9-
// TODO: this is temporary until the new SDK with api-version=2023-12-01 is available
10-
type Site20231201 = Site & { isFlex?: boolean };
9+
export type FunctionAppModel = {
10+
pricingTier: string,
11+
id: string,
12+
kind: string,
13+
name: string,
14+
resourceGroup: string,
15+
status: string,
16+
location: string
17+
}
18+
19+
type FunctionQueryModel = {
20+
properties: {
21+
sku: string,
22+
state: string
23+
},
24+
location: string,
25+
id: string,
26+
kind: string,
27+
name: string,
28+
resourceGroup: string
29+
}
1130
export class FunctionAppResolver implements AppResourceResolver {
31+
private loaded: boolean = false;
1232
private siteCacheLastUpdated = 0;
13-
private siteCache: Map<string, Site20231201> = new Map<string, Site20231201>();
33+
private siteCache: Map<string, FunctionAppModel> = new Map<string, FunctionAppModel>();
1434
private listFunctionAppsTask: Promise<void> | undefined;
1535

1636
public async resolveResource(subContext: ISubscriptionContext, resource: AppResource): Promise<ResolvedFunctionAppResource | ResolvedContainerizedFunctionAppResource | undefined> {
1737
return await callWithTelemetryAndErrorHandling('resolveResource', async (context: IActionContext) => {
18-
const client = await createWebSiteClient({ ...context, ...subContext });
19-
2038
if (this.siteCacheLastUpdated < Date.now() - 1000 * 3) {
2139
this.siteCacheLastUpdated = Date.now();
22-
this.listFunctionAppsTask = new Promise((resolve, reject) => {
23-
this.siteCache.clear();
24-
uiUtils.listAllIterator(client.webApps.list()).then((sites) => {
25-
for (const site of sites) {
26-
this.siteCache.set(nonNullProp(site, 'id').toLowerCase(), site);
27-
}
28-
resolve();
29-
})
30-
.catch((reason) => {
31-
reject(reason);
40+
const graphClient = await createResourceGraphClient({ ...context, ...subContext });
41+
async function fetchAllApps(graphClient: ResourceGraphClient, subContext: ISubscriptionContext, resolver: FunctionAppResolver): Promise<void> {
42+
resolver.loaded = false;
43+
resolver.siteCache.clear(); // clear the cache before fetching new data
44+
const query = `resources | where type == 'microsoft.web/sites' and kind contains 'functionapp' and kind !contains 'workflowapp'`;
45+
46+
async function fetchApps(skipToken?: string): Promise<void> {
47+
const response = await graphClient.resources({
48+
query,
49+
subscriptions: [subContext.subscriptionId],
50+
options: {
51+
skipToken,
52+
}
53+
});
54+
55+
const record = response.data as Record<string, FunctionQueryModel>;
56+
Object.values(record).forEach(data => {
57+
const dataModel: FunctionAppModel = {
58+
pricingTier: data.properties.sku,
59+
id: data.id,
60+
kind: data.kind,
61+
name: data.name,
62+
resourceGroup: data.resourceGroup,
63+
status: data.properties.state,
64+
location: data.location
65+
}
66+
resolver.siteCache.set(dataModel.id.toLowerCase(), dataModel);
3267
});
33-
});
68+
69+
const nextSkipToken = response?.skipToken;
70+
if (nextSkipToken) {
71+
await fetchApps(nextSkipToken); // recurse to next page
72+
} else {
73+
resolver.loaded = true; // mark as loaded when all pages are fetched
74+
return;
75+
}
76+
}
77+
78+
return await fetchApps(); // start with no skipToken
79+
}
80+
81+
this.listFunctionAppsTask = fetchAllApps(graphClient, subContext, this);
3482
}
35-
await this.listFunctionAppsTask;
36-
37-
let site = this.siteCache.get(nonNullProp(resource, 'id').toLowerCase());
38-
// check for required properties that sometime don't exist in the LIST operation
39-
if (!site || !site.defaultHostName) {
40-
// if this required property doesn't exist, try getting the full site payload
41-
site = await client.webApps.get(getResourceGroupFromId(resource.id), resource.name);
42-
this.siteCache.set(resource.id.toLowerCase(), site);
83+
84+
while (!this.loaded) {
85+
// wait for the data to be loaded
86+
await this.listFunctionAppsTask;
4387
}
4488

89+
const site = this.siteCache.get(nonNullProp(resource, 'id').toLowerCase());
4590
if (nonNullValueAndProp(site, 'kind') === 'functionapp,linux,container,azurecontainerapps') {
91+
const client = await createWebSiteClient({ ...context, ...subContext });
4692
const fullSite = await client.webApps.get(nonNullValueAndProp(site, 'resourceGroup'), nonNullValueAndProp(site, 'name'));
4793
return ResolvedContainerizedFunctionAppResource.createResolvedFunctionAppResource(context, subContext, fullSite);
4894
}
95+
if (site) {
96+
return new ResolvedFunctionAppResource(subContext, site);
97+
}
4998

50-
return ResolvedFunctionAppResource.createResolvedFunctionAppResource(context, subContext, nonNullValue(site));
99+
return undefined;
51100
});
52101
}
53102

src/commands/browseWebsite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ export async function browseWebsite(context: IActionContext, node?: SlotTreeItem
1414
node = await pickAppResource(context);
1515
}
1616

17-
await openUrl(nonNullValueAndProp(node.site, 'defaultHostUrl'));
17+
await openUrl(nonNullValueAndProp(await node.getSite(context), 'defaultHostUrl'));
1818
}

src/commands/configureDeploymentSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export async function configureDeploymentSource(context: IActionContext, node?:
1313
node = await pickFunctionApp(context);
1414
}
1515

16-
const updatedScmType: string | undefined = await editScmType(context, node.site, node.subscription);
16+
const updatedScmType: string | undefined = await editScmType(context, (await node.getSite(context)), node.subscription);
1717
if (updatedScmType !== undefined) {
1818
context.telemetry.properties.updatedScmType = updatedScmType;
1919
}

src/commands/createFunctionApp/stacks/getStackPicks.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,10 @@ export async function getEolWarningMessages(context: ISubscriptionActionContext,
369369

370370
export async function showEolWarningIfNecessary(context: ISubscriptionActionContext, parent: AzExtParentTreeItem, client?: IAppSettingsClient) {
371371
if (isResolvedFunctionApp(parent)) {
372-
client = client ?? await parent.site.createClient(context);
372+
const site = await parent.getSite(context);
373+
client = client ?? await site.createClient(context);
373374
const eolWarningMessage = await getEolWarningMessages(context, {
374-
site: parent.site.rawSite,
375+
site: site.rawSite,
375376
isLinux: client.isLinux,
376377
isFlex: parent.isFlex,
377378
client

src/commands/deploy/deploy.ts

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

66
import { type Site, type SiteConfigResource, type StringDictionary } from '@azure/arm-appservice';
7-
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths } from '@microsoft/vscode-azext-azureappservice';
7+
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths, type ParsedSite } from '@microsoft/vscode-azext-azureappservice';
88
import { DialogResponses, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
99
import { type AzureSubscription } from '@microsoft/vscode-azureresources-api';
1010
import type * as vscode from 'vscode';
@@ -83,6 +83,8 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
8383
return await getOrCreateFunctionApp(context)
8484
});
8585

86+
const site = await node.resolved.getSite(context);
87+
8688
if (node.contextValue.includes('container')) {
8789
const learnMoreLink: string = 'https://aka.ms/deployContainerApps'
8890
await context.ui.showWarningMessage(localize('containerFunctionAppError', 'Deploy is not currently supported for containerized function apps within the Azure Functions extension. Please read here to learn how to deploy your project.'), { learnMoreLink });
@@ -98,19 +100,19 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
98100
context.telemetry.properties.projectRuntime = version;
99101
context.telemetry.properties.languageModel = String(languageModel);
100102

101-
if (language === ProjectLanguage.Python && !node.site.isLinux) {
103+
if (language === ProjectLanguage.Python && !site.isLinux) {
102104
context.errorHandling.suppressReportIssue = true;
103105
throw new Error(localize('pythonNotAvailableOnWindows', 'Python projects are not supported on Windows Function Apps. Deploy to a Linux Function App instead.'));
104106
}
105107

106-
void showCoreToolsWarning(context, version, node.site.fullName);
108+
void showCoreToolsWarning(context, version, site.fullName);
107109

108-
const client = await node.site.createClient(actionContext);
110+
const client = await site.createClient(actionContext);
109111
const siteConfig: SiteConfigResource = await client.getSiteConfig();
110112
const isConsumption: boolean = await client.getIsConsumption(actionContext);
111113
let isZipDeploy: boolean = siteConfig.scmType !== ScmType.LocalGit && siteConfig.scmType !== ScmType.GitHub;
112-
if (!isZipDeploy && node.site.isLinux && isConsumption) {
113-
ext.outputChannel.appendLog(localize('linuxConsZipOnly', 'WARNING: Using zip deploy because scm type "{0}" is not supported on Linux consumption', siteConfig.scmType), { resourceName: node.site.fullName });
114+
if (!isZipDeploy && site.isLinux && isConsumption) {
115+
ext.outputChannel.appendLog(localize('linuxConsZipOnly', 'WARNING: Using zip deploy because scm type "{0}" is not supported on Linux consumption', siteConfig.scmType), { resourceName: site.fullName });
114116
isZipDeploy = true;
115117
context.deployMethod = 'zip';
116118
}
@@ -121,10 +123,10 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
121123
const doRemoteBuild: boolean | undefined = getWorkspaceSetting<boolean>(remoteBuildSetting, deployPaths.effectiveDeployFsPath) && !isFlexConsumption;
122124
actionContext.telemetry.properties.scmDoBuildDuringDeployment = String(doRemoteBuild);
123125
if (doRemoteBuild) {
124-
await validateRemoteBuild(context, node.site, context.workspaceFolder, language);
126+
await validateRemoteBuild(context, site, context.workspaceFolder, language);
125127
}
126128

127-
if (isZipDeploy && node.site.isLinux && isConsumption && !doRemoteBuild) {
129+
if (isZipDeploy && site.isLinux && isConsumption && !doRemoteBuild) {
128130
context.deployMethod = 'storage';
129131
} else if (isFlexConsumption) {
130132
context.deployMethod = 'flexconsumption';
@@ -161,7 +163,7 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
161163
} as unknown as ISubscriptionActionContext;
162164

163165
const eolWarningMessage = await getEolWarningMessages(subContext, {
164-
site: node.site.rawSite,
166+
site: site.rawSite,
165167
isLinux: client.isLinux,
166168
isFlex: isFlexConsumption,
167169
client
@@ -175,7 +177,7 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
175177
deploymentWarningMessages.length > 0) {
176178
// if there is a warning message, we want to show the deploy confirmation regardless of the setting
177179
const deployCommandId = 'azureFunctions.deploy';
178-
await showDeployConfirmation(context, node.site, deployCommandId, deploymentWarningMessages,
180+
await showDeployConfirmation(context, site, deployCommandId, deploymentWarningMessages,
179181
eolWarningMessage ? stackUpgradeLearnMoreLink : undefined);
180182
}
181183

@@ -185,8 +187,8 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
185187
void validateGlobSettings(context, context.effectiveDeployFsPath);
186188
}
187189

188-
if (language === ProjectLanguage.CSharp && !node.site.isLinux || durableStorageType) {
189-
await updateWorkerProcessTo64BitIfRequired(context, siteConfig, node, language, durableStorageType);
190+
if (language === ProjectLanguage.CSharp && !site.isLinux || durableStorageType) {
191+
await updateWorkerProcessTo64BitIfRequired(context, siteConfig, site, language, durableStorageType);
190192
}
191193

192194
// app settings shouldn't be checked with flex consumption plans
@@ -222,15 +224,15 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
222224
}
223225
const deployContext = Object.assign(context, await createActivityContext());
224226
deployContext.activityChildren = [];
225-
await innerDeploy(node.site, deployFsPath, deployContext);
227+
await innerDeploy(site, deployFsPath, deployContext);
226228
}
227229
);
228230

229231
await notifyDeployComplete(context, node, context.workspaceFolder, isFlexConsumption);
230232
}
231233

232-
async function updateWorkerProcessTo64BitIfRequired(context: IDeployContext, siteConfig: SiteConfigResource, node: SlotTreeItem, language: ProjectLanguage, durableStorageType: DurableBackendValues | undefined): Promise<void> {
233-
const client = await node.site.createClient(context);
234+
async function updateWorkerProcessTo64BitIfRequired(context: IDeployContext, siteConfig: SiteConfigResource, site: ParsedSite, language: ProjectLanguage, durableStorageType: DurableBackendValues | undefined): Promise<void> {
235+
const client = await site.createClient(context);
234236
const config: SiteConfigResource = {
235237
use32BitWorkerProcess: false
236238
};

src/commands/deploy/getWarningsForConnectionSettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ export async function getWarningsForConnectionSettings(context: IActionContext,
3232

3333
const localConnectionSettings = await getConnectionSettings(localSettings.Values ?? {});
3434
const remoteConnectionSettings = await getConnectionSettings(options.appSettings?.properties ?? {});
35+
const site = await options.node.getSite(context);
3536

3637
if (localConnectionSettings.some(setting => setting.type === 'ManagedIdentity')) {
37-
if (!options.node.site.rawSite.identity ||
38-
options.node.site.rawSite.identity.type === 'None') {
38+
if (!site.rawSite.identity ||
39+
site.rawSite.identity.type === 'None') {
3940
// if they have nothing in remote, warn them to connect a managed identity
4041
return localize('configureManagedIdentityWarning',
4142
'Your app is not connected to a managed identity. To ensure access, please configure a managed identity. Without it, your application may encounter authorization issues.');

0 commit comments

Comments
 (0)