diff --git a/src/commands/createFunctionApp/FunctionAppCreateStep.ts b/src/commands/createFunctionApp/FunctionAppCreateStep.ts index 9df272a20..f94176b29 100644 --- a/src/commands/createFunctionApp/FunctionAppCreateStep.ts +++ b/src/commands/createFunctionApp/FunctionAppCreateStep.ts @@ -22,22 +22,20 @@ import { getStorageConnectionString } from '../appSettings/connectionSettings/ge import { enableFileLogging } from '../logstream/enableFileLogging'; import { type FullFunctionAppStack, type IFunctionAppWizardContext } from './IFunctionAppWizardContext'; import { showSiteCreated } from './showSiteCreated'; -import { type FunctionAppRuntimeSettings } from './stacks/models/FunctionAppStackModel'; +import { type FunctionAppRuntimeSettings, type Sku } from './stacks/models/FunctionAppStackModel'; export class FunctionAppCreateStep extends AzureWizardExecuteStep { public priority: number = 140; public async execute(context: IFunctionAppWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { const os: WebsiteOS = nonNullProp(context, 'newSiteOS'); - const stack: FullFunctionAppStack | undefined = context.newSiteStack + const stack: FullFunctionAppStack = nonNullProp(context, 'newSiteStack'); - if (stack) { - context.telemetry.properties.newSiteOS = os; - context.telemetry.properties.newSiteStack = stack.stack.value; - context.telemetry.properties.newSiteMajorVersion = stack.majorVersion.value; - context.telemetry.properties.newSiteMinorVersion = stack.minorVersion.value; - context.telemetry.properties.planSkuTier = context.plan?.sku?.tier; - } + context.telemetry.properties.newSiteOS = os; + context.telemetry.properties.newSiteStack = stack.stack.value; + context.telemetry.properties.newSiteMajorVersion = stack.majorVersion.value; + context.telemetry.properties.newSiteMinorVersion = stack.minorVersion.value; + context.telemetry.properties.planSkuTier = context.plan?.sku?.tier; const message: string = localize('creatingNewApp', 'Creating new function app "{0}"...', context.newSiteName); ext.outputChannel.appendLog(message); @@ -45,9 +43,11 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { + private async getNewFlexSite(context: IFunctionAppWizardContext, sku: Sku): Promise { const location = await LocationListStep.getLocation(context, webProvider); const site: Site & { properties: FlexFunctionAppProperties } = { name: context.newSiteName, @@ -129,12 +129,12 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep im.isDefault)?.size || 2048, alwaysReady: [], triggers: null }, @@ -231,7 +231,7 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { + async createFlexFunctionApp(context: IFunctionAppWizardContext, rgName: string, siteName: string, sku: Sku): Promise { const headers = createHttpHeaders({ 'Content-Type': 'application/json', }); @@ -239,7 +239,7 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep { const placeHolder: string = localize('selectRuntimeStack', 'Select a runtime stack.'); const isFlex: boolean = context.newPlanSku?.tier === 'FlexConsumption'; - - // TODO: Since we aren't able to get the stacks for flex, we're using a simple object to represent the stack but we should when available - let result: FullFunctionAppStack | { runtime: string, version: string } | undefined; + let result: FullFunctionAppStack | undefined; while (true) { const options: AgentQuickPickOptions = { placeHolder, @@ -37,23 +35,18 @@ export class FunctionAppStackStep extends AzureWizardPromptStepcontext.newSiteStack.stack.preferredOs; - } + context.newSiteStack = result as FullFunctionAppStack; + if (!context.newSiteStack.minorVersion.stackSettings.linuxRuntimeSettings) { + context.newSiteOS = WebsiteOS.windows; + } else if (!context.newSiteStack.minorVersion.stackSettings.windowsRuntimeSettings) { + context.newSiteOS = WebsiteOS.linux; + } else if (!context.advancedCreation) { + context.newSiteOS = context.newSiteStack.stack.preferredOs; } } public shouldPrompt(context: IFunctionAppWizardContext): boolean { - return !context.newSiteStack && !context.newSiteStackFlex; + return !context.newSiteStack; } public async getSubWizard(context: IFunctionAppWizardContext): Promise> { @@ -69,76 +62,8 @@ export class FunctionAppStackStep extends AzureWizardPromptStep>[]> { - // TODO: hardcoding the runtime versions for now, but we should get this from the API when available - if (isFlex) { - return [ - { - label: '.NET 8 Isolated', - data: { - runtime: 'dotnet-isolated', - version: '8.0' - }, - group: '.NET', - agentMetadata: {} - }, - { - label: 'Java 17', - data: { - runtime: 'java', - version: '17' - }, - group: 'Java', - agentMetadata: {} - }, - { - label: 'Java 11', - data: { - runtime: 'java', - version: '11' - }, - group: 'Java', - agentMetadata: {} - }, - { - label: "Node.js 20 LTS", - data: { - runtime: 'node', - version: '20' - }, - group: 'Node.js', - agentMetadata: {} - }, - { - label: 'Python 3.11', - data: { - runtime: 'python', - version: '3.11' - }, - group: 'Python', - agentMetadata: {} - }, - { - label: 'Python 3.10', - data: { - runtime: 'python', - version: '3.10' - }, - group: 'Python', - agentMetadata: {} - }, - { - label: 'PowerShell 7.4', - data: { - runtime: 'powershell', - version: '7.4' - }, - group: 'PowerShell Core', - agentMetadata: {} - } - ]; - } - let picks: AgentQuickPickItem>[] = await getStackPicks(context); + private async getPicks(context: IFunctionAppWizardContext, isFlex: boolean): Promise>[]> { + let picks: AgentQuickPickItem>[] = await getStackPicks(context, isFlex); if (picks.filter(p => p.label !== noRuntimeStacksAvailableLabel).length === 0) { // if every runtime only has noRuntimeStackAvailable quickpick items, reset picks to [] picks = []; diff --git a/src/commands/createFunctionApp/stacks/getStackPicks.ts b/src/commands/createFunctionApp/stacks/getStackPicks.ts index 434da98f1..785a3f44a 100644 --- a/src/commands/createFunctionApp/stacks/getStackPicks.ts +++ b/src/commands/createFunctionApp/stacks/getStackPicks.ts @@ -5,7 +5,7 @@ import { type ServiceClient } from '@azure/core-client'; import { createPipelineRequest } from '@azure/core-rest-pipeline'; -import { createGenericClient, type AzExtPipelineResponse } from '@microsoft/vscode-azext-azureutils'; +import { createGenericClient, LocationListStep, type AzExtPipelineResponse } from '@microsoft/vscode-azext-azureutils'; import { openUrl, parseError, type AgentQuickPickItem, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; import { funcVersionLink } from '../../../FuncVersion'; import { hiddenStacksSetting, noRuntimeStacksAvailableLabel } from '../../../constants'; @@ -18,8 +18,10 @@ import { backupStacks } from './backupStacks'; import { type AppStackMinorVersion } from './models/AppStackModel'; import { type FunctionAppRuntimes, type FunctionAppStack } from './models/FunctionAppStackModel'; -export async function getStackPicks(context: IFunctionAppWizardContext): Promise>[]> { - const stacks: FunctionAppStack[] = (await getStacks(context)).filter(s => !context.stackFilter || context.stackFilter === s.value); +export async function getStackPicks(context: IFunctionAppWizardContext, isFlex: boolean): Promise>[]> { + const stacks: FunctionAppStack[] = isFlex ? + (await getFlexStacks(context)).filter(s => !context.stackFilter || context.stackFilter === s.value) : + (await getStacks(context)).filter(s => !context.stackFilter || context.stackFilter === s.value); const picks: AgentQuickPickItem>[] = []; let hasEndOfLife = false; let stackHasPicks: boolean; @@ -176,6 +178,46 @@ async function getStacks(context: IFunctionAppWizardContext & { _stacks?: Functi return context._stacks; } +async function getFlexStacks(context: IFunctionAppWizardContext & { _stacks?: FunctionAppStack[] }): Promise { + const client: ServiceClient = await createGenericClient(context, context); + const location = await LocationListStep.getLocation(context); + const flexFunctionAppStacks: FunctionAppStack[] = []; + const stacks = ['dotnet', 'java', 'node', 'powershell', 'python']; + if (!context._stacks) { + const getFlexStack = async (stack: string) => { + const result: AzExtPipelineResponse = await client.sendRequest(createPipelineRequest({ + method: 'GET', + url: requestUtils.createRequestUrl(`providers/Microsoft.Web/locations/${location.name}/functionAppStacks`, { + 'api-version': '2023-12-01', + stack, + removeDeprecatedStacks: String(!getWorkspaceSetting('showDeprecatedStacks')) + }), + })); + const stacksArmResponse = result.parsedBody; + for (const stack of stacksArmResponse.value) { + stack.properties.majorVersions = stack.properties.majorVersions.filter(mv => { + mv.minorVersions = mv.minorVersions.filter(minor => { + // Remove stacks that don't have a SKU + return minor.stackSettings.linuxRuntimeSettings && minor.stackSettings.linuxRuntimeSettings?.Sku !== null; + + }); + + return mv.minorVersions.length > 0; + }); + } + flexFunctionAppStacks.push(...stacksArmResponse.value.map(d => d.properties)); + } + + for (const stack of stacks) { + await getFlexStack(stack); + } + + context._stacks = flexFunctionAppStacks; + } + + return context._stacks; +} + // API is still showing certain deprecated stacks even when 'removeDeprecatedStacks' queryParameter is set to true. // We should filter them out manually just in case. function removeDeprecatedStacks(stacks: FunctionAppStack[]) { diff --git a/src/commands/createFunctionApp/stacks/models/FunctionAppStackModel.ts b/src/commands/createFunctionApp/stacks/models/FunctionAppStackModel.ts index 633909b4e..f6ad60f0e 100644 --- a/src/commands/createFunctionApp/stacks/models/FunctionAppStackModel.ts +++ b/src/commands/createFunctionApp/stacks/models/FunctionAppStackModel.ts @@ -40,4 +40,27 @@ export interface FunctionAppRuntimeSettings extends CommonSettings { appSettingsDictionary: AppSettingsDictionary; siteConfigPropertiesDictionary: SiteConfigPropertiesDictionary; supportedFunctionsExtensionVersions: FunctionsExtensionVersion[]; + // Sku property is only used for flex consumption plans + Sku: Sku[] | null; +} + +export interface Sku { + skuCode: string; + instanceMemoryMB: InstanceMemoryMB[]; + maximumInstanceCount: { + lowestMaximumInstanceCount: number; + highestMaximumInstanceCount: number; + defaultValue: number; + }, + functionAppConfigProperties: { + runtime: { + name: string, + version: string + } + } +} + +interface InstanceMemoryMB { + size: number; + isDefault: boolean; } diff --git a/src/tree/SubscriptionTreeItem.ts b/src/tree/SubscriptionTreeItem.ts index 8ccfe391f..e3da88967 100644 --- a/src/tree/SubscriptionTreeItem.ts +++ b/src/tree/SubscriptionTreeItem.ts @@ -141,7 +141,6 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase { } } else { promptSteps.push(new ResourceGroupListStep()); - CustomLocationListStep.addStep(wizardContext, promptSteps); promptSteps.push(new StorageAccountListStep( storageAccountCreateOptions, { @@ -237,6 +236,8 @@ async function createFunctionAppWizard(wizardContext: IFunctionAppWizardContext) if (wizardContext.advancedCreation) { promptSteps.push(new FunctionAppHostingPlanStep()); + // location is required to get flex runtimes, so prompt before stack step + CustomLocationListStep.addStep(wizardContext, promptSteps); } promptSteps.push(new FunctionAppStackStep());