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

Use Stacks API for Flex Consumption SKU #4104

Merged
merged 2 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions src/commands/createFunctionApp/FunctionAppCreateStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,32 @@ 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<IFunctionAppWizardContext> {
public priority: number = 140;

public async execute(context: IFunctionAppWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
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);
progress.report({ message });

const siteName: string = nonNullProp(context, 'newSiteName');
const rgName: string = nonNullProp(nonNullProp(context, 'resourceGroup'), 'name');
const flexSku: Sku | null | undefined = stack.minorVersion.stackSettings.linuxRuntimeSettings?.Sku && stack.minorVersion.stackSettings.linuxRuntimeSettings?.Sku[0];

// TODO: Because we don't have the stack API, assume no stack means it's a flex app
context.site = stack ? await this.createFunctionApp(context, rgName, siteName, stack) : await this.createFlexFunctionApp(context, rgName, siteName);
context.site = !flexSku ?
Copy link
Member

Choose a reason for hiding this comment

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

I prefer avoiding negating this and flipping the ternary around.

await this.createFunctionApp(context, rgName, siteName, stack) :
await this.createFlexFunctionApp(context, rgName, siteName, flexSku);
context.activityResult = context.site as AppResource;

const site = new ParsedSite(context.site, context);
Expand Down Expand Up @@ -101,7 +101,7 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
site.extendedLocation = { name: customLocation.id, type: 'customLocation' };
}

private async getNewFlexSite(context: IFunctionAppWizardContext): Promise<Site> {
private async getNewFlexSite(context: IFunctionAppWizardContext, sku: Sku): Promise<Site> {
const location = await LocationListStep.getLocation(context, webProvider);
const site: Site & { properties: FlexFunctionAppProperties } = {
name: context.newSiteName,
Expand Down Expand Up @@ -129,12 +129,12 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
}
},
runtime: {
name: context.newSiteStackFlex?.runtime,
version: context.newSiteStackFlex?.version
name: sku.functionAppConfigProperties.runtime.name,
version: sku.functionAppConfigProperties.runtime.version
},
scaleAndConcurrency: {
maximumInstanceCount: 100,
instanceMemoryMB: 2048,
maximumInstanceCount: sku.maximumInstanceCount.defaultValue,
instanceMemoryMB: sku.instanceMemoryMB.find(im => im.isDefault)?.size || 2048,
alwaysReady: [],
triggers: null
},
Expand Down Expand Up @@ -231,15 +231,15 @@ export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWi
return await client.webApps.beginCreateOrUpdateAndWait(rgName, siteName, await this.getNewSite(context, stack));
}

async createFlexFunctionApp(context: IFunctionAppWizardContext, rgName: string, siteName: string): Promise<Site> {
async createFlexFunctionApp(context: IFunctionAppWizardContext, rgName: string, siteName: string, sku: Sku): Promise<Site> {
const headers = createHttpHeaders({
'Content-Type': 'application/json',
});

const options: AzExtRequestPrepareOptions = {
url: `https://management.azure.com/subscriptions/${context.subscriptionId}/resourceGroups/${rgName}/providers/Microsoft.Web/sites/${siteName}?api-version=2023-12-01`,
method: 'PUT',
body: JSON.stringify(await this.getNewFlexSite(context)) as unknown as RequestBodyType,
body: JSON.stringify(await this.getNewFlexSite(context, sku)) as unknown as RequestBodyType,
Comment on lines 240 to +242
Copy link
Member

Choose a reason for hiding this comment

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

not for this PR, but this will eventually need to support sovereign clouds with different request urls

headers
};

Expand Down
2 changes: 0 additions & 2 deletions src/commands/createFunctionApp/IFunctionAppWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ export interface IFunctionAppWizardContext extends IAppServiceWizardContext, ICr
language: string | undefined;
stackFilter?: string;
newSiteStack?: FullFunctionAppStack;
newSiteStackFlex?: { runtime: string, version: string } /* While we're not using the stacks API for flex, it's easier to just hard-code these two values instead of the entire FullFunctionAppStack */

durableStorageType?: DurableBackendValues;

// Detected local connection string
Expand Down
97 changes: 11 additions & 86 deletions src/commands/createFunctionApp/stacks/FunctionAppStackStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
public async prompt(context: IFunctionAppWizardContext): Promise<void> {
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,
Expand All @@ -37,23 +35,18 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
break;
}
}
if (isFlex) {
context.newSiteStackFlex = result as { runtime: string, version: string };
} else {
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 = <WebsiteOS>context.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 = <WebsiteOS>context.newSiteStack.stack.preferredOs;
}
}

public shouldPrompt(context: IFunctionAppWizardContext): boolean {
return !context.newSiteStack && !context.newSiteStackFlex;
return !context.newSiteStack;
}

public async getSubWizard(context: IFunctionAppWizardContext): Promise<IWizardOptions<IFunctionAppWizardContext>> {
Expand All @@ -69,76 +62,8 @@ export class FunctionAppStackStep extends AzureWizardPromptStep<IFunctionAppWiza
return { promptSteps };
}

private async getPicks(context: IFunctionAppWizardContext, isFlex: boolean): Promise<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | { runtime: string, version: string } | undefined>>[]> {
// 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<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[] = await getStackPicks(context);
private async getPicks(context: IFunctionAppWizardContext, isFlex: boolean): Promise<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[]> {
let picks: AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[] = 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 = [];
Expand Down
48 changes: 45 additions & 3 deletions src/commands/createFunctionApp/stacks/getStackPicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[]> {
const stacks: FunctionAppStack[] = (await getStacks(context)).filter(s => !context.stackFilter || context.stackFilter === s.value);
export async function getStackPicks(context: IFunctionAppWizardContext, isFlex: boolean): Promise<AgentQuickPickItem<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[]> {
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<IAzureQuickPickItem<FullFunctionAppStack | undefined>>[] = [];
let hasEndOfLife = false;
let stackHasPicks: boolean;
Expand Down Expand Up @@ -176,6 +178,46 @@ async function getStacks(context: IFunctionAppWizardContext & { _stacks?: Functi
return context._stacks;
}

async function getFlexStacks(context: IFunctionAppWizardContext & { _stacks?: FunctionAppStack[] }): Promise<FunctionAppStack[]> {
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`, {
Copy link
Member

Choose a reason for hiding this comment

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

so flex stacks are location dependent? That's crazy

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I can see it being helpful for this team since they have to deploy skus to regions individually (it seems like).

'api-version': '2023-12-01',
stack,
removeDeprecatedStacks: String(!getWorkspaceSetting<boolean>('showDeprecatedStacks'))
}),
}));
const stacksArmResponse = <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[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 2 additions & 1 deletion src/tree/SubscriptionTreeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ export class SubscriptionTreeItem extends SubscriptionTreeItemBase {
}
} else {
promptSteps.push(new ResourceGroupListStep());
CustomLocationListStep.addStep(wizardContext, promptSteps);
promptSteps.push(new StorageAccountListStep(
storageAccountCreateOptions,
{
Expand Down Expand Up @@ -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());
Expand Down
Loading