Skip to content

Commit

Permalink
Create AI ARM 2.1 for Functions and App Service (#1200)
Browse files Browse the repository at this point in the history
* Workaround for checkNameAvailability bug for site and slots

* Create log analytics workspace to use for AI 2.1

* Update appservice/src/utils/azureClients.ts

Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com>

* Update appservice/src/createAppService/IAppServiceWizardContext.ts

Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com>

* Update appservice/src/createAppService/LogAnalyticsCreateStep.ts

Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com>

* Update appservice/src/createAppService/LogAnalyticsCreateStep.ts

Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com>

* Refactor out getAppInsightsSupprotedLocation and use it in LogAnalyticsCreateStep

* Bump version

* Minor fixes

* PR feedback

* Bump. Sorry for dismissing your review, Brandon

Co-authored-by: Brandon Waterloo [MSFT] <36966225+bwateratmsft@users.noreply.github.com>
  • Loading branch information
nturinski and bwateratmsft authored Jul 21, 2022
1 parent e878f6e commit 7df6b17
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 236 deletions.
435 changes: 288 additions & 147 deletions appservice/package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions appservice/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@microsoft/vscode-azext-azureappservice",
"author": "Microsoft Corporation",
"version": "0.7.1",
"version": "0.7.2",
"description": "Common tools for developing Azure App Service extensions for VS Code",
"tags": [
"azure",
Expand Down Expand Up @@ -33,16 +33,17 @@
},
"dependencies": {
"@azure/abort-controller": "^1.0.4",
"@azure/arm-appinsights": "^4.0.0",
"@azure/arm-appinsights": "^5.0.0-beta.4",
"@azure/arm-appservice": "^11.0.0",
"@azure/arm-operationalinsights": "^8.0.1",
"@azure/arm-resourcegraph": "^4.0.0",
"@azure/arm-resources": "^5.0.0",
"@azure/arm-subscriptions": "^5.0.0",
"@azure/ms-rest-azure-env": "^2.0.0",
"@azure/ms-rest-js": "^2.3.0",
"@azure/storage-blob": "^12.3.0",
"@microsoft/vscode-azext-azureutils": "^0.3.0",
"@microsoft/vscode-azext-utils": "^0.3.3",
"@microsoft/vscode-azext-azureutils": "^0.3.4",
"@microsoft/vscode-azext-utils": "^0.3.9",
"dayjs": "^1.11.2",
"fs-extra": "^10.0.0",
"glob-gitignore": "^1.0.14",
Expand Down
88 changes: 16 additions & 72 deletions appservice/src/createAppService/AppInsightsCreateStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@
*--------------------------------------------------------------------------------------------*/

import type { ApplicationInsightsManagementClient } from '@azure/arm-appinsights';
import type { Provider, ProviderResourceType, ResourceGroup, ResourceManagementClient } from '@azure/arm-resources';
import type { HttpOperationResponse, ServiceClient } from '@azure/ms-rest-js';
import { AzExtLocation, createGenericClient, LocationListStep } from '@microsoft/vscode-azext-azureutils';
import { AzureWizardExecuteStep, IActionContext, IParsedError, nonNullProp, parseError } from '@microsoft/vscode-azext-utils';
import type { ResourceGroup } from '@azure/arm-resources';
import { AzExtLocation, LocationListStep } from '@microsoft/vscode-azext-azureutils';
import { AzureWizardExecuteStep, IParsedError, nonNullProp, parseError } from '@microsoft/vscode-azext-utils';
import { MessageItem, Progress } from 'vscode';
import { ext } from '../extensionVariables';
import { localize } from '../localize';
import { createAppInsightsClient, createResourceClient } from '../utils/azureClients';
import { areLocationNamesEqual } from '../utils/azureUtils';
import { createAppInsightsClient } from '../utils/azureClients';
import { AppInsightsListStep } from './AppInsightsListStep';
import { getAppInsightsSupportedLocation } from './getAppInsightsSupportedLocation';
import { IAppServiceWizardContext } from './IAppServiceWizardContext';

export class AppInsightsCreateStep extends AzureWizardExecuteStep<IAppServiceWizardContext> {
public priority: number = 135;

public async execute(context: IAppServiceWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
const resourceLocation: AzExtLocation = await LocationListStep.getLocation(context);
const verifyingAppInsightsAvailable: string = localize('verifyingAppInsightsAvailable', 'Verifying that Application Insights is available for this location...');
ext.outputChannel.appendLog(verifyingAppInsightsAvailable);
const appInsightsLocation: string | undefined = await this.getSupportedLocation(context, resourceLocation);

const resourceLocation: AzExtLocation = await LocationListStep.getLocation(context);
const appInsightsLocation = await getAppInsightsSupportedLocation(context, resourceLocation);
if (appInsightsLocation) {
const client: ApplicationInsightsManagementClient = await createAppInsightsClient(context);
const rg: ResourceGroup = nonNullProp(context, 'resourceGroup');
Expand All @@ -42,7 +40,15 @@ export class AppInsightsCreateStep extends AzureWizardExecuteStep<IAppServiceWiz
ext.outputChannel.appendLog(creatingNewAppInsights);
progress.report({ message: creatingNewAppInsights });

context.appInsightsComponent = await client.components.createOrUpdate(rgName, aiName, { kind: 'web', applicationType: 'web', location: appInsightsLocation });
context.appInsightsComponent = await client.components.createOrUpdate(
rgName,
aiName,
{
kind: 'web',
applicationType: 'web',
location: appInsightsLocation,
workspaceResourceId: context.logAnalyticsWorkspace?.id
});
const createdNewAppInsights: string = localize('createdNewAppInsights', 'Successfully created Application Insights resource "{0}".', aiName);
ext.outputChannel.appendLog(createdNewAppInsights);
} else if (pError.errorType === 'AuthorizationFailed') {
Expand Down Expand Up @@ -82,66 +88,4 @@ export class AppInsightsCreateStep extends AzureWizardExecuteStep<IAppServiceWiz
public shouldExecute(context: IAppServiceWizardContext): boolean {
return !context.appInsightsComponent && !!context.newAppInsightsName;
}

// returns the supported location, a location in the region map, or undefined
private async getSupportedLocation(context: IAppServiceWizardContext, location: AzExtLocation): Promise<string | undefined> {
const locations: string[] = await this.getLocations(context) || [];
const locationName: string = nonNullProp(location, 'name');

if (locations.some((loc) => areLocationNamesEqual(loc, location.name))) {
context.telemetry.properties.aiLocationSupported = 'true';
return locationName;
} else {
// If there is no exact match, then query the regionMapping.json
const pairedRegions: string[] | undefined = await this.getPairedRegions(context, locationName);
if (pairedRegions.length > 0) {
// if there is at least one region listed, return the first
context.telemetry.properties.aiLocationSupported = 'pairedRegion';
return pairedRegions[0];
}

context.telemetry.properties.aiLocationSupported = 'false';
return undefined;
}
}

private async getPairedRegions(context: IActionContext, locationName: string): Promise<string[]> {
try {
const client: ServiceClient = await createGenericClient(context, undefined);
const response: HttpOperationResponse = await client.sendRequest({
method: 'GET',
url: 'https://appinsights.azureedge.net/portal/regionMapping.json'
});
const regionMappingJson: RegionMappingJsonResponse = <RegionMappingJsonResponse>response.parsedBody;

if (regionMappingJson.regions[locationName]) {
return regionMappingJson.regions[locationName].pairedRegions;
}
} catch (error) {
// ignore the error
}
return [];
}

private async getLocations(context: IAppServiceWizardContext): Promise<string[] | undefined> {
const resourceClient: ResourceManagementClient = await createResourceClient(context);
const supportedRegions: Provider = await resourceClient.providers.get('microsoft.insights');
const componentsResourceType: ProviderResourceType | undefined = supportedRegions.resourceTypes && supportedRegions.resourceTypes.find(aiRt => aiRt.resourceType === 'components');
if (!!componentsResourceType && !!componentsResourceType.locations) {
return componentsResourceType.locations;
} else {
return undefined;
}
}
}

type RegionMappingJsonResponse = {
regions: {
[key: string]: RegionMap
}
};

type RegionMap = {
geo: string,
pairedRegions: string[]
};
14 changes: 7 additions & 7 deletions appservice/src/createAppService/AppInsightsListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*--------------------------------------------------------------------------------------------*/

import type { ApplicationInsightsManagementClient } from "@azure/arm-appinsights";
// eslint-disable-next-line import/no-internal-modules
import type { ApplicationInsightsComponent, ApplicationInsightsComponentListResult } from "@azure/arm-appinsights/esm/models";
import { LocationListStep } from "@microsoft/vscode-azext-azureutils";
import type { ApplicationInsightsComponent } from "@azure/arm-appinsights";
import { LocationListStep, uiUtils } from "@microsoft/vscode-azext-azureutils";
import { AzureWizardPromptStep, IAzureNamingRules, IAzureQuickPickItem, IAzureQuickPickOptions, IWizardOptions, nonNullProp } from "@microsoft/vscode-azext-utils";
import { localize } from "../localize";
import { createAppInsightsClient } from "../utils/azureClients";
import { AppInsightsCreateStep } from "./AppInsightsCreateStep";
import { AppInsightsNameStep } from "./AppInsightsNameStep";
import { IAppServiceWizardContext } from "./IAppServiceWizardContext";
import { LogAnalyticsCreateStep } from "./LogAnalyticsCreateStep";

export const appInsightsNamingRules: IAzureNamingRules = {
minLength: 1,
Expand All @@ -30,10 +30,10 @@ export class AppInsightsListStep extends AzureWizardPromptStep<IAppServiceWizard
this._suppressCreate = suppressCreate;
}

public static async getAppInsightsComponents(context: IAppServiceWizardContext): Promise<ApplicationInsightsComponentListResult> {
public static async getAppInsightsComponents(context: IAppServiceWizardContext): Promise<ApplicationInsightsComponent[]> {
if (context.appInsightsTask === undefined) {
const client: ApplicationInsightsManagementClient = await createAppInsightsClient(context);
context.appInsightsTask = client.components.list();
context.appInsightsTask = uiUtils.listAllIterator(client.components.list());
}

return await context.appInsightsTask;
Expand Down Expand Up @@ -65,7 +65,7 @@ export class AppInsightsListStep extends AzureWizardPromptStep<IAppServiceWizard
LocationListStep.addStep(context, promptSteps);
return {
promptSteps: promptSteps,
executeSteps: [new AppInsightsCreateStep()]
executeSteps: [new LogAnalyticsCreateStep(), new AppInsightsCreateStep()]
};
}

Expand All @@ -84,7 +84,7 @@ export class AppInsightsListStep extends AzureWizardPromptStep<IAppServiceWizard
data: undefined
});

let components: ApplicationInsightsComponentListResult = await AppInsightsListStep.getAppInsightsComponents(context);
let components: ApplicationInsightsComponent[] = await AppInsightsListStep.getAppInsightsComponents(context);

// /~https://github.com/microsoft/vscode-azurefunctions/issues/1454
if (!Array.isArray(components)) {
Expand Down
5 changes: 2 additions & 3 deletions appservice/src/createAppService/AppInsightsNameStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

// eslint-disable-next-line import/no-internal-modules
import type { ApplicationInsightsComponent, ApplicationInsightsComponentListResult } from '@azure/arm-appinsights/esm/models';
import type { ApplicationInsightsComponent } from '@azure/arm-appinsights';
import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils';
import { localize } from '../localize';
import { AppInsightsListStep, appInsightsNamingRules } from './AppInsightsListStep';
Expand All @@ -13,7 +12,7 @@ import { IAppServiceWizardContext } from './IAppServiceWizardContext';
export class AppInsightsNameStep extends AzureWizardPromptStep<IAppServiceWizardContext> {

public async isNameAvailable(context: IAppServiceWizardContext, name: string): Promise<boolean> {
const appInsightsComponents: ApplicationInsightsComponentListResult = await AppInsightsListStep.getAppInsightsComponents(context);
const appInsightsComponents: ApplicationInsightsComponent[] = await AppInsightsListStep.getAppInsightsComponents(context);
return !appInsightsComponents.some((ai: ApplicationInsightsComponent) => ai.name !== undefined && ai.name.toLowerCase() === name.toLowerCase());
}

Expand Down
14 changes: 11 additions & 3 deletions appservice/src/createAppService/IAppServiceWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ApplicationInsightsManagementModels } from '@azure/arm-appinsights';
import type { ApplicationInsightsComponent } from '@azure/arm-appinsights';
import type { AppServicePlan, Site, SkuDescription } from '@azure/arm-appservice';
import type { Workspace } from '@azure/arm-operationalinsights';
import { IResourceGroupWizardContext, IStorageAccountWizardContext } from '@microsoft/vscode-azext-azureutils';
import { AppKind, WebsiteOS } from './AppKind';

Expand Down Expand Up @@ -69,13 +70,13 @@ export interface IAppServiceWizardContext extends IResourceGroupWizardContext, I
* App Insights components are necessary for Function apps log streaming. By default, we should instantiate
* one for the user if there is a data farm available within the same region as the web app
*/
appInsightsComponent?: ApplicationInsightsManagementModels.ApplicationInsightsComponent;
appInsightsComponent?: ApplicationInsightsComponent;

/**
* The task used to get existing App Insights components.
* By specifying this in the context, we can ensure that Azure is only queried once for the entire wizard
*/
appInsightsTask?: Promise<ApplicationInsightsManagementModels.ApplicationInsightsComponentListResult>;
appInsightsTask?: Promise<ApplicationInsightsComponent[]>;

/**
* Boolean indicating that the user opted out of creating an Application inisghts component.
Expand All @@ -89,6 +90,13 @@ export interface IAppServiceWizardContext extends IResourceGroupWizardContext, I
*/
newAppInsightsName?: string;

/**
* Log Anayltics Workspace component is neccessary for the new App Insights components. By default,
* it will look for a workspace within the same resource group and location as the App Insight
* component. If neither conditions are met, then it will automatically create a workspace
*/
logAnalyticsWorkspace?: Workspace;

/**
* Indicates advanced creation should be used
*/
Expand Down
54 changes: 54 additions & 0 deletions appservice/src/createAppService/LogAnalyticsCreateStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzExtLocation, getResourceGroupFromId, LocationListStep, uiUtils } from "@microsoft/vscode-azext-azureutils";
import { AzureWizardExecuteStep, nonNullProp, nonNullValueAndProp } from "@microsoft/vscode-azext-utils";
import { Progress } from "vscode";
import { ext } from "../extensionVariables";
import { localize } from "../localize";
import { createOperationalInsightsManagementClient } from "../utils/azureClients";
import { getAppInsightsSupportedLocation } from "./getAppInsightsSupportedLocation";
import { IAppServiceWizardContext } from "./IAppServiceWizardContext";

export class LogAnalyticsCreateStep extends AzureWizardExecuteStep<IAppServiceWizardContext> {
public priority: number = 134;

public async execute(context: IAppServiceWizardContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
const opClient = await createOperationalInsightsManagementClient(context);
const rgName = nonNullValueAndProp(context.resourceGroup, 'name');
const resourceLocation: AzExtLocation = await LocationListStep.getLocation(context);
const location = await getAppInsightsSupportedLocation(context, resourceLocation);

if (!location) {
// if there is no supported AI location, then skip this as AppInsightsCreateStep will be skipped
return;
}

const workspaces = await uiUtils.listAllIterator(opClient.workspaces.list());
const workspacesInSameLoc = workspaces.filter(ws => ws.location === location);
const workspacesInSameRg = workspacesInSameLoc.filter(ws => getResourceGroupFromId(nonNullProp(ws, 'id')) === rgName);

context.logAnalyticsWorkspace = workspacesInSameRg[0] ?? workspacesInSameLoc[0];

if (context.logAnalyticsWorkspace) {
const usingLaw: string = localize('usingLogAnalyticsWorkspace', 'Using existing Log Analytics workspace "{0}"', context.logAnalyticsWorkspace.name);
progress.report({ message: usingLaw });
ext.outputChannel.appendLog(usingLaw);
} else {
const creatingLaw: string = localize('creatingLogAnalyticsWorkspace', 'Creating new Log Analytics workspace...');
progress.report({ message: creatingLaw });
ext.outputChannel.appendLog(creatingLaw);
const workspaceName = `workspace-${context.newAppInsightsName}`
const createdLaw: string = localize('createdLogAnalyticWorkspace', 'Successfully created new Log Analytics workspace "{0}".', workspaceName);
ext.outputChannel.appendLog(createdLaw);
progress.report({ message: createdLaw });
context.logAnalyticsWorkspace = await opClient.workspaces.beginCreateOrUpdateAndWait(rgName, workspaceName, { location });
}
}

public shouldExecute(context: IAppServiceWizardContext): boolean {
return !context.logAnalyticsWorkspace;
}
}
Loading

0 comments on commit 7df6b17

Please sign in to comment.