Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for domain name label scopes #2703

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
17 changes: 15 additions & 2 deletions src/WebAppResolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Site } from "@azure/arm-appservice";
import { uiUtils } from "@microsoft/vscode-azext-azureutils";
import { callWithTelemetryAndErrorHandling, nonNullProp, nonNullValue, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { callWithTelemetryAndErrorHandling, nonNullProp, nonNullValue, nonNullValueAndProp, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils";
import { type AppResource, type AppResourceResolver } from "@microsoft/vscode-azext-utils/hostapi";
import { ResolvedWebAppResource } from "./tree/ResolvedWebAppResource";
import { createWebSiteClient } from "./utils/azureClients";
Expand All @@ -9,6 +9,7 @@ export class WebAppResolver implements AppResourceResolver {

private siteCacheLastUpdated = 0;
private siteCache: Map<string, Site> = new Map<string, Site>();
private siteNameCounter: Map<string, number> = new Map<string, number>();
private listWebAppsTask: Promise<void> | undefined;

public async resolveResource(subContext: ISubscriptionContext, resource: AppResource): Promise<ResolvedWebAppResource | undefined> {
Expand All @@ -17,10 +18,17 @@ export class WebAppResolver implements AppResourceResolver {

if (this.siteCacheLastUpdated < Date.now() - 1000 * 3) {
this.siteCacheLastUpdated = Date.now();

this.listWebAppsTask = new Promise((resolve, reject) => {
this.siteCache.clear();
this.siteNameCounter.clear();

uiUtils.listAllIterator(client.webApps.list()).then((sites) => {
for (const site of sites) {
const siteName: string = nonNullProp(site, 'name');
Copy link
Contributor Author

@MicroFish91 MicroFish91 Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a lookahead that checks for duplicate site names and adds a location description in order to help differentiate them, example:

image

const count: number = (this.siteNameCounter.get(siteName) ?? 0) + 1;

this.siteNameCounter.set(siteName, count);
this.siteCache.set(nonNullProp(site, 'id').toLowerCase(), site);
}
resolve();
Expand All @@ -33,7 +41,12 @@ export class WebAppResolver implements AppResourceResolver {

await this.listWebAppsTask;
const site = this.siteCache.get(nonNullProp(resource, 'id').toLowerCase());
return new ResolvedWebAppResource(subContext, nonNullValue(site));

return new ResolvedWebAppResource(subContext, nonNullValue(site), {
// Multiple sites with the same name could be displayed as long as they are in different locations
// To help distinguish these apps for our users, lookahead and determine if the location should be provided for duplicated site names
showLocationAsTreeItemDescription: (this.siteNameCounter.get(nonNullValueAndProp(site, 'name')) ?? 1) > 1,
});
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/commands/createWebApp/IWebAppWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { type IAppServiceWizardContext } from '@microsoft/vscode-azext-azureappservice';
import { type ExecuteActivityContext } from '@microsoft/vscode-azext-utils';
import { type ExecuteActivityContext, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
import { type AppStackMajorVersion, type AppStackMinorVersion } from './stacks/models/AppStackModel';
import { type JavaContainers, type WebAppRuntimes, type WebAppStack, type WebAppStackValue } from './stacks/models/WebAppStackModel';

Expand All @@ -20,7 +20,7 @@ export type FullJavaStack = {
minorVersion: AppStackMinorVersion<JavaContainers>;
};

export interface IWebAppWizardContext extends IAppServiceWizardContext, ExecuteActivityContext {
export interface IWebAppWizardContext extends ISubscriptionActionContext, IAppServiceWizardContext, ExecuteActivityContext {
newSiteRuntime?: string;

usingBackupStacks?: boolean;
Expand Down
22 changes: 16 additions & 6 deletions src/commands/createWebApp/createWebApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, AppServicePlanSkuStep, CustomLocationListStep, LogAnalyticsCreateStep, ParsedSite, setLocationsTask, SiteNameStep } from "@microsoft/vscode-azext-azureappservice";
import { AppInsightsCreateStep, AppInsightsListStep, AppKind, AppServicePlanCreateStep, AppServicePlanListStep, AppServicePlanSkuStep, CustomLocationListStep, DomainNameLabelScope, LogAnalyticsCreateStep, ParsedSite, setLocationsTask, SiteDomainNameLabelScopeStep, SiteNameStep } from "@microsoft/vscode-azext-azureappservice";
import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep, SubscriptionTreeItemBase, VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils";
import { AzureWizard, maskUserInfo, nonNullProp, parseError, type AzExtParentTreeItem, type AzureWizardExecuteStep, type AzureWizardPromptStep, type IActionContext, type ICreateChildImplContext } from "@microsoft/vscode-azext-utils";
import { webProvider } from "../../constants";
import { ext } from "../../extensionVariables";
import { localize } from "../../localize";
import { SiteTreeItem } from "../../tree/SiteTreeItem";
import { createActivityContext } from "../../utils/activityUtils";
import { WebAppWithDomainLabelScopeCreateStep } from "./domainLabelScope/WebAppWithDomainLabelScopeCreateStep";
import { type IWebAppWizardContext } from "./IWebAppWizardContext";
import { SetPostPromptDefaultsStep } from "./SetPostPromptDefaultsStep";
import { setPrePromptDefaults } from "./setPrePromptDefaults";
Expand Down Expand Up @@ -43,19 +44,25 @@

const promptSteps: AzureWizardPromptStep<IWebAppWizardContext>[] = [];
const executeSteps: AzureWizardExecuteStep<IWebAppWizardContext>[] = [];

// Add these steps to the front because we need this information for checking site name availability
LocationListStep.addStep(wizardContext, promptSteps);
promptSteps.push(new SiteDomainNameLabelScopeStep());

Check failure on line 50 in src/commands/createWebApp/createWebApp.ts

View workflow job for this annotation

GitHub Actions / Build / Build

Unsafe argument of type `any` assigned to a parameter of type `AzureWizardPromptStep<IWebAppWizardContext>`

Check failure on line 50 in src/commands/createWebApp/createWebApp.ts

View workflow job for this annotation

GitHub Actions / Build / Build

Unsafe construction of an any type value
if (context.advancedCreation) {
promptSteps.push(new ResourceGroupListStep());
}

const siteStep: SiteNameStep = new SiteNameStep();
promptSteps.push(siteStep);

if (context.advancedCreation) {
promptSteps.push(new ResourceGroupListStep());
promptSteps.push(new WebAppStackStep());
CustomLocationListStep.addStep(wizardContext, promptSteps);
promptSteps.push(new AppServicePlanListStep());
promptSteps.push(new AppInsightsListStep());
} else {
promptSteps.push(new WebAppStackStep());
promptSteps.push(new AppServicePlanSkuStep());
LocationListStep.addStep(wizardContext, promptSteps);
executeSteps.push(new ResourceGroupCreateStep());
executeSteps.push(new AppServicePlanCreateStep());
executeSteps.push(new AppInsightsCreateStep());
Expand All @@ -64,7 +71,6 @@

executeSteps.push(new VerifyProvidersStep([webProvider, 'Microsoft.Insights']));
executeSteps.push(new LogAnalyticsCreateStep());
executeSteps.push(new WebAppCreateStep());

if (wizardContext.newSiteOS !== undefined) {
await setLocationsTask(wizardContext);
Expand All @@ -76,14 +82,18 @@
await wizard.prompt();

const newSiteName = nonNullProp(wizardContext, 'newSiteName');

wizardContext.activityTitle = localize('createWebApp', 'Create Web App "{0}"', newSiteName);

if (wizardContext.newSiteDomainNameLabelScope === DomainNameLabelScope.Global) {

Check failure on line 87 in src/commands/createWebApp/createWebApp.ts

View workflow job for this annotation

GitHub Actions / Build / Build

Unsafe member access .Global on an `any` value
executeSteps.push(new WebAppCreateStep());
} else {
executeSteps.push(new WebAppWithDomainLabelScopeCreateStep());
}

await wizard.execute();
await ext.rgApi.appResourceTree.refresh(context);

const rawSite = nonNullProp(wizardContext, 'site');
// site is set as a result of SiteCreateStep.execute()
const site = new ParsedSite(rawSite, wizardContext);
ext.outputChannel.appendLog(getCreatedWebAppMessage(site));

Expand Down
4 changes: 4 additions & 0 deletions src/commands/createWebApp/domainLabelScope/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## Domain Label Scope

The files in this folder are intended to be temporary because the App Service SDK does not currently support the required API version we need. Consequently, we needed to implement a custom call with custom types. In the future, we expect to be able to remove this folder and consolidate everything into a single web app creation file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* 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, type SiteConfig, type WebSiteManagementClient } from '@azure/arm-appservice';
import { createHttpHeaders, createPipelineRequest } from '@azure/core-rest-pipeline';
import { createWebSiteClient, WebsiteOS, type CustomLocation } from '@microsoft/vscode-azext-azureappservice';
import { createGenericClient, LocationListStep, type AzExtPipelineResponse, type AzExtRequestPrepareOptions } from '@microsoft/vscode-azext-azureutils';
import { AzureWizardExecuteStep } from '@microsoft/vscode-azext-utils';
import { type AppResource } from '@microsoft/vscode-azext-utils/hostapi';
import { type Progress } from 'vscode';
import * as constants from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { nonNullProp } from '../../../utils/nonNull';
import { type FullJavaStack, type FullWebAppStack, type IWebAppWizardContext } from '../IWebAppWizardContext';
import { getJavaLinuxRuntime } from '../stacks/getJavaLinuxRuntime';
import { type WebAppStackValue, type WindowsJavaContainerSettings } from '../stacks/models/WebAppStackModel';
import { type SitePayload } from './types';

export class WebAppWithDomainLabelScopeCreateStep extends AzureWizardExecuteStep<IWebAppWizardContext> {
public priority: number = 140;

public async execute(context: IWebAppWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this file is a straight carryover of the WebAppCreateStep file, but I had to make a call manually without the SDK which means I added a GenericClient call to the API endpoint, and I had to slightly amend the site payload shape because it's different than when updating using the SDK

context.telemetry.properties.newSiteOS = context.newSiteOS;
context.telemetry.properties.newSiteStack = context.newSiteStack?.stack.value;
context.telemetry.properties.newSiteMajorVersion = context.newSiteStack?.majorVersion.value;
context.telemetry.properties.newSiteMinorVersion = context.newSiteStack?.minorVersion.value;
if (context.newSiteJavaStack) {
context.telemetry.properties.newSiteJavaStack = context.newSiteJavaStack.stack.value;
context.telemetry.properties.newSiteJavaMajorVersion = context.newSiteJavaStack.majorVersion.value;
context.telemetry.properties.newSiteJavaMinorVersion = context.newSiteJavaStack.minorVersion.value;
}
context.telemetry.properties.planSkuTier = context.plan && context.plan.sku && context.plan.sku.tier;

const message: string = localize('creatingNewApp', 'Creating new web app "{0}"...', context.newSiteName);
ext.outputChannel.appendLog(message);
progress.report({ message });

const siteName: string = nonNullProp(context, 'newSiteName');
const rgName: string = nonNullProp(nonNullProp(context, 'resourceGroup'), 'name');

// The SDK does not currently support this updated api version, so we should make the call to the endpoint manually until the SDK gets updated
const authToken = (await context.credentials.getToken() as { token?: string }).token;
const options: AzExtRequestPrepareOptions = {
url: `${context.environment.resourceManagerEndpointUrl}subscriptions/${context.subscriptionId}/resourceGroups/${rgName}/providers/Microsoft.Web/sites/${siteName}?api-version=2024-04-01`,
method: 'PUT',
headers: createHttpHeaders({
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
}),
body: JSON.stringify(await this.getNewSite(context)),
};

const client = await createGenericClient(context, undefined);
// We don't care about storing the response here because the manual response returned is different from the SDK formatting that our code expects.
// The stored site should come from the SDK instead.
await client.sendRequest(createPipelineRequest(options)) as AzExtPipelineResponse;

const sdkClient: WebSiteManagementClient = await createWebSiteClient(context);
context.site = await sdkClient.webApps.get(rgName, siteName);
context.activityResult = context.site as AppResource;
}

public shouldExecute(context: IWebAppWizardContext): boolean {
return !context.site;
}

private async getNewSite(context: IWebAppWizardContext): Promise<SitePayload> {
const location = await LocationListStep.getLocation(context, constants.webProvider);
const newSiteConfig: SiteConfig = this.getSiteConfig(context);

const site: SitePayload = {
name: context.newSiteName,
kind: this.getKind(context),
location: nonNullProp(location, 'name'),
properties: {
autoGeneratedDomainNameLabelScope: context.newSiteDomainNameLabelScope,

Check failure on line 79 in src/commands/createWebApp/domainLabelScope/WebAppWithDomainLabelScopeCreateStep.ts

View workflow job for this annotation

GitHub Actions / Build / Build

Unsafe assignment of an `any` value
clientAffinityEnabled: true,
serverFarmId: context.plan && context.plan.id,
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.
siteConfig: newSiteConfig,
},
};

if (context.customLocation) {
this.addCustomLocationProperties(site, context.customLocation);
}

return site;
}

private getKind(context: IWebAppWizardContext): string {
let kind: string = context.newSiteKind;
if (context.newSiteOS === 'linux') {
kind += ',linux';
}
if (context.customLocation) {
kind += ',kubernetes';
}
return kind;
}

private addCustomLocationProperties(site: SitePayload, customLocation: CustomLocation): void {
site.extendedLocation = { name: customLocation.id, type: 'customLocation' };
}

private getSiteConfig(context: IWebAppWizardContext): SiteConfig {
const newSiteConfig: SiteConfig = {};

newSiteConfig.appSettings = this.getAppSettings(context);

const stack: FullWebAppStack = nonNullProp(context, 'newSiteStack');
if (context.newSiteOS === WebsiteOS.linux) {
newSiteConfig.linuxFxVersion = stack.stack.value === 'java' ?
getJavaLinuxRuntime(stack.majorVersion.value, nonNullProp(context, 'newSiteJavaStack').minorVersion) :
nonNullProp(stack.minorVersion.stackSettings, 'linuxRuntimeSettings').runtimeVersion;
} else {
const runtimeVersion: string = nonNullProp(stack.minorVersion.stackSettings, 'windowsRuntimeSettings').runtimeVersion;
switch (stack.stack.value) {
case 'dotnet':
if (!/core/i.test(stack.minorVersion.displayText)) { // Filter out .NET _Core_ stacks because this is a .NET _Framework_ property
newSiteConfig.netFrameworkVersion = runtimeVersion;
}
break;
case 'php':
newSiteConfig.phpVersion = runtimeVersion;
break;
case 'node':
newSiteConfig.nodeVersion = runtimeVersion;
newSiteConfig.appSettings.push({
name: 'WEBSITE_NODE_DEFAULT_VERSION',
value: runtimeVersion
});
break;
case 'java':
newSiteConfig.javaVersion = runtimeVersion;
const javaStack: FullJavaStack = nonNullProp(context, 'newSiteJavaStack');
const windowsStackSettings: WindowsJavaContainerSettings = nonNullProp(javaStack.minorVersion.stackSettings, 'windowsContainerSettings');
newSiteConfig.javaContainer = windowsStackSettings.javaContainer;
newSiteConfig.javaContainerVersion = windowsStackSettings.javaContainerVersion;
break;
case 'python':
newSiteConfig.pythonVersion = runtimeVersion;
break;
default:
}
}
return newSiteConfig;
}

private getAppSettings(context: IWebAppWizardContext): NameValuePair[] {
const appSettings: NameValuePair[] = [];
const disabled: string = 'disabled';
const trueString: string = 'true';

const runtime: WebAppStackValue = nonNullProp(context, 'newSiteStack').stack.value;
if (context.newSiteOS === WebsiteOS.linux && (runtime === 'node' || runtime === 'python')) {
appSettings.push({
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT',
value: trueString
});
}
if (context.appInsightsComponent) {
appSettings.push({
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING',
value: context.appInsightsComponent.connectionString
});

appSettings.push({
name: 'ApplicationInsightsAgent_EXTENSION_VERSION',
value: context.newSiteOS === WebsiteOS.windows ? '~2' : '~3' // ~2 is for Windows, ~3 is for Linux
});

// all these settings are set on the portal if AI is enabled for Windows apps
if (context.newSiteOS === WebsiteOS.windows) {
appSettings.push(
{
name: 'APPINSIGHTS_PROFILERFEATURE_VERSION',
value: disabled
},
{
name: 'APPINSIGHTS_SNAPSHOTFEATURE_VERSION',
value: disabled
},
{
name: 'DiagnosticServices_EXTENSION_VERSION',
value: disabled
},
{
name: 'InstrumentationEngine_EXTENSION_VERSION',
value: disabled
},
{
name: 'SnapshotDebugger_EXTENSION_VERSION',
value: disabled
},
{
name: 'XDT_MicrosoftApplicationInsights_BaseExtensions',
value: disabled
},
{
name: 'XDT_MicrosoftApplicationInsights_Mode',
value: 'default'
});
} else {
appSettings.push({
name: 'APPLICATIONINSIGHTSAGENT_EXTENSION_ENABLED',
value: trueString
});
}
}

return appSettings;
}
}
Loading
Loading