Skip to content

Commit 629f7a5

Browse files
committed
initial implementation
1 parent 65ca631 commit 629f7a5

12 files changed

+333
-14
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@
9191
}
9292
},
9393
"commands": [
94+
{
95+
"command": "azureFunctions.initializeProjectForSlashAzure",
96+
"title": "initializeProjectForSlashAzure",
97+
"category": "Azure Functions"
98+
},
9499
{
95100
"command": "azureFunctions.addBinding",
96101
"title": "%azureFunctions.addBinding%",
@@ -211,6 +216,12 @@
211216
"category": "Azure Functions",
212217
"enablement": "!virtualWorkspace"
213218
},
219+
{
220+
"command": "azureFunctions.testExternalRuntime",
221+
"title": "Test External Runtime Configuration",
222+
"category": "Azure Functions (Test)",
223+
"enablement": "!virtualWorkspace"
224+
},
214225
{
215226
"command": "azureFunctions.createSlot",
216227
"title": "%azureFunctions.createSlot%",
@@ -875,6 +886,10 @@
875886
}
876887
],
877888
"commandPalette": [
889+
{
890+
"command": "azureFunctions.initializeProjectForSlashAzure",
891+
"when": "never"
892+
},
878893
{
879894
"command": "azureFunctions.deployByFunctionAppId",
880895
"when": "never"

src/commands/createNewProject/IProjectWizardContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export interface IProjectWizardContext extends IActionContext {
3131
targetFramework?: string | string[];
3232

3333
containerizedProject?: boolean;
34+
35+
// External runtime configuration from Azure APIs
36+
externalRuntimeConfig?: {
37+
runtimeName: string;
38+
runtimeVersion: string;
39+
};
3440
}
3541

3642
export type OpenBehavior = 'AddToWorkspace' | 'OpenInNewWindow' | 'OpenInCurrentWindow' | 'AlreadyOpen' | 'DontOpen';

src/commands/createNewProject/NewProjectLanguageStep.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { type QuickPickOptions } from 'vscode';
88
import { ProjectLanguage, nodeDefaultModelVersion, nodeLearnMoreLink, nodeModels, pythonDefaultModelVersion, pythonLearnMoreLink, pythonModels } from '../../constants';
99
import { localize } from '../../localize';
1010
import { TemplateSchemaVersion } from '../../templates/TemplateProviderBase';
11+
import { getLanguageModelFromRuntime, getProjectLanguageFromRuntime } from '../../utils/externalRuntimeUtils';
1112
import { nonNullProp } from '../../utils/nonNull';
1213
import { openUrl } from '../../utils/openUrl';
1314
import { FunctionListStep } from '../createFunction/FunctionListStep';
@@ -38,6 +39,17 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizard
3839
}
3940

4041
public async prompt(context: IProjectWizardContext): Promise<void> {
42+
if (context.externalRuntimeConfig) {
43+
const projectLanguage = getProjectLanguageFromRuntime(context.externalRuntimeConfig.runtimeName);
44+
const languageModel = getLanguageModelFromRuntime(context.externalRuntimeConfig.runtimeName);
45+
46+
if (projectLanguage && languageModel) {
47+
context.language = projectLanguage as ProjectLanguage;
48+
context.languageModel = languageModel;
49+
return;
50+
}
51+
}
52+
4153
// Only display 'supported' languages that can be debugged in VS Code
4254
let languagePicks: IAzureQuickPickItem<{ language: ProjectLanguage, model?: number } | undefined>[] = [
4355
{ label: ProjectLanguage.JavaScript, data: { language: ProjectLanguage.JavaScript } },

src/commands/createNewProject/createNewProject.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ export async function createNewProjectFromCommand(
4747
});
4848
}
4949

50-
export async function createNewProjectInternal(context: IActionContext, options: api.ICreateFunctionOptions): Promise<void> {
50+
export async function createNewProjectInternal(context: IActionContext, options: api.ICreateFunctionOptions & {
51+
externalRuntimeConfig?: {
52+
runtimeName: string;
53+
runtimeVersion: string;
54+
};
55+
}): Promise<void> {
5156
addLocalFuncTelemetry(context, undefined);
5257

5358
const language: ProjectLanguage | undefined = <ProjectLanguage>options.language || getGlobalSetting(projectLanguageSetting);
@@ -59,7 +64,8 @@ export async function createNewProjectInternal(context: IActionContext, options:
5964
{
6065
language,
6166
version: tryParseFuncVersion(version),
62-
projectTemplateKey
67+
projectTemplateKey,
68+
externalRuntimeConfig: options.externalRuntimeConfig
6369
},
6470
await createActivityContext()
6571
);

src/commands/createNewProject/pythonSteps/IPythonVenvWizardContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ export interface IPythonVenvWizardContext extends IActionContext {
1414
useExistingVenv?: boolean;
1515
venvName?: string;
1616
suppressSkipVenv?: boolean;
17+
18+
// External runtime configuration from Azure APIs
19+
externalRuntimeConfig?: {
20+
runtimeName: string;
21+
runtimeVersion: string;
22+
};
1723
}

src/commands/createNewProject/pythonSteps/PythonAliasListStep.ts

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

66
import { AzureWizardPromptStep, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils";
7+
import { gt, satisfies } from "semver";
78
import { localize } from "../../../localize";
89
import { getGlobalSetting } from "../../../vsCodeConfig/settings";
910
import { EnterPythonAliasStep } from "./EnterPythonAliasStep";
@@ -14,6 +15,21 @@ export class PythonAliasListStep extends AzureWizardPromptStep<IPythonVenvWizard
1415
public hideStepCount: boolean = true;
1516

1617
public async prompt(context: IPythonVenvWizardContext): Promise<void> {
18+
19+
20+
if (context.externalRuntimeConfig) {
21+
const installedVersions = await getInstalledPythonVersions(context);
22+
const matchingVersion = findBestMatchingVersion(context.externalRuntimeConfig.runtimeVersion, installedVersions);
23+
24+
if (matchingVersion) {
25+
context.pythonAlias = matchingVersion.alias;
26+
context.telemetry.properties.pythonAliasBehavior = 'externalRuntimeConfigMatch';
27+
return;
28+
} else {
29+
return;
30+
}
31+
}
32+
1733
const placeHolder: string = localize('selectAlias', 'Select a Python interpreter to create a virtual environment');
1834
const result: string | boolean = (await context.ui.showQuickPick(getPicks(context), { placeHolder })).data;
1935
if (typeof result === 'string') {
@@ -26,6 +42,7 @@ export class PythonAliasListStep extends AzureWizardPromptStep<IPythonVenvWizard
2642
}
2743

2844
public shouldPrompt(context: IPythonVenvWizardContext): boolean {
45+
// Skip prompting if external runtime configuration is provided
2946
return !context.useExistingVenv && !context.pythonAlias;
3047
}
3148

@@ -55,7 +72,43 @@ async function getPicks(context: IPythonVenvWizardContext): Promise<IAzureQuickP
5572
}
5673

5774
const picks: IAzureQuickPickItem<string | boolean>[] = [];
58-
const versions: string[] = [];
75+
const pythonVersions = await getInstalledPythonVersions(context);
76+
pythonVersions.forEach(pv => picks.push({
77+
label: pv.alias,
78+
description: pv.version,
79+
data: pv.alias
80+
}));
81+
82+
picks.push({ label: localize('enterAlias', '$(keyboard) Manually enter Python interpreter or full path'), data: true });
83+
84+
if (!context.suppressSkipVenv) {
85+
picks.push({ label: localize('skipVenv', '$(circle-slash) Skip virtual environment'), data: false, suppressPersistence: true });
86+
}
87+
88+
return picks;
89+
}
90+
91+
interface InstalledPythonVersion {
92+
alias: string;
93+
version: string;
94+
}
95+
96+
// use this method to preselect python version if runtime version is provided externally
97+
98+
async function getInstalledPythonVersions(context: IPythonVenvWizardContext): Promise<InstalledPythonVersion[]> {
99+
const supportedVersions: string[] = await getSupportedPythonVersions(context, context.version);
100+
101+
const aliasesToTry: string[] = ['python', 'python3', 'py'];
102+
for (const version of supportedVersions) {
103+
aliasesToTry.push(`python${version}`, `py -${version}`);
104+
}
105+
106+
const globalPythonPathSetting: string | undefined = getGlobalSetting('pythonPath', 'python');
107+
if (globalPythonPathSetting) {
108+
aliasesToTry.unshift(globalPythonPathSetting);
109+
}
110+
111+
const versions: InstalledPythonVersion[] = [];
59112
for (const alias of aliasesToTry) {
60113
let version: string;
61114
try {
@@ -64,23 +117,27 @@ async function getPicks(context: IPythonVenvWizardContext): Promise<IAzureQuickP
64117
continue;
65118
}
66119

67-
if (isSupportedPythonVersion(supportedVersions, version) && !versions.some(v => v === version)) {
68-
picks.push({
69-
label: alias,
70-
description: version,
71-
data: alias
120+
if (isSupportedPythonVersion(supportedVersions, version) && !versions.some(v => v.version === version)) {
121+
versions.push({
122+
alias,
123+
version,
72124
});
73-
versions.push(version);
74125
}
75126
}
76127

77128
context.telemetry.properties.detectedPythonVersions = versions.join(',');
129+
return versions;
130+
}
78131

79-
picks.push({ label: localize('enterAlias', '$(keyboard) Manually enter Python interpreter or full path'), data: true });
132+
// use semver to find a matching python version
133+
function findMatchingVersion(requestedVersion: string, versions: InstalledPythonVersion[]): InstalledPythonVersion | undefined {
134+
return versions.find(v => satisfies(v.version, requestedVersion));
135+
}
80136

81-
if (!context.suppressSkipVenv) {
82-
picks.push({ label: localize('skipVenv', '$(circle-slash) Skip virtual environment'), data: false, suppressPersistence: true });
137+
function findBestMatchingVersion(requestedVersion: string, versions: InstalledPythonVersion[]): InstalledPythonVersion | undefined {
138+
let matchingVersion = findMatchingVersion(requestedVersion, versions);
139+
if (!matchingVersion) {
140+
matchingVersion = versions.reduce((prev, current) => (gt(current.version, prev.version) ? current : prev));
83141
}
84-
85-
return picks;
142+
return matchingVersion;
86143
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type IActionContext } from '@microsoft/vscode-azext-utils';
7+
import * as vscode from 'vscode';
8+
import { type FunctionAppStackValue } from './createFunctionApp/stacks/models/FunctionAppStackModel';
9+
import { initializeProjectFromApp } from './initializeProjectFromApp';
10+
11+
export type RuntimeName = FunctionAppStackValue | 'dotnet-isolated';
12+
13+
/**
14+
*
15+
* Needs to initialize a local project based on the given function app information.
16+
*
17+
* @param context
18+
* @param id id of the function app in Azure
19+
* @param runtimeName ex: 'node', 'python', 'dotnet', etc.
20+
* @param runtimeVersion ex: '18', '3.11', '8.0', etc.
21+
*/
22+
export async function initializeProjectForSlashAzure(
23+
context: IActionContext,
24+
): Promise<void> {
25+
26+
const { url: rawUrl }: { url: string } = await vscode.commands.executeCommand('vscode-dev-azurecloudshell.webOpener.getUrl');
27+
const url = new URL(rawUrl);
28+
29+
const params: FunctionsQueryParams = await getFunctionsQueryParams(url);
30+
31+
await initializeProjectFromApp(context, params);
32+
}
33+
34+
35+
export interface FunctionsQueryParams {
36+
functionAppResourceId: string;
37+
runtimeName: string;
38+
runtimeVersion: string;
39+
}
40+
41+
const functionAppResourceIdKey = 'functionAppResourceId';
42+
const runtimeNameKey = 'runtimeName';
43+
const runtimeVersionKey = 'runtimeVersion';
44+
45+
async function getFunctionsQueryParams(url: URL) {
46+
const params: FunctionsQueryParams = {
47+
functionAppResourceId: url.searchParams.get(functionAppResourceIdKey) || '',
48+
runtimeName: url.searchParams.get(runtimeNameKey) || '',
49+
runtimeVersion: url.searchParams.get(runtimeVersionKey) || ''
50+
};
51+
return params;
52+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type IActionContext } from '@microsoft/vscode-azext-utils';
7+
import { workspace } from 'vscode';
8+
import { updateWorkspaceSetting } from '../vsCodeConfig/settings';
9+
import { type FunctionAppStackValue } from './createFunctionApp/stacks/models/FunctionAppStackModel';
10+
import { createNewProjectInternal } from './createNewProject/createNewProject';
11+
import { type FunctionsQueryParams } from './initializeProjectForSlashAzure';
12+
13+
export type RuntimeName = FunctionAppStackValue | 'dotnet-isolated';
14+
15+
/**
16+
*
17+
* Needs to initialize a local project based on the given function app information.
18+
*
19+
* @param context
20+
* @param id id of the function app in Azure
21+
* @param runtimeName ex: 'node', 'python', 'dotnet', etc.
22+
* @param runtimeVersion ex: '18', '3.11', '8.0', etc.
23+
*/
24+
export async function initializeProjectFromApp(
25+
context: IActionContext,
26+
options: FunctionsQueryParams,
27+
): Promise<void> {
28+
const { functionAppResourceId, runtimeName, runtimeVersion } = options;
29+
30+
const workspaceFolder = workspace.workspaceFolders?.[0];
31+
32+
if (!workspaceFolder) {
33+
throw new Error('No workspace folder is open.');
34+
}
35+
36+
if (functionAppResourceId) {
37+
// set defaultFunctionAppToDeploy setting to the given function app id
38+
await updateWorkspaceSetting('defaultFunctionAppToDeploy', functionAppResourceId, workspaceFolder);
39+
}
40+
41+
context.telemetry.properties.externalRuntimeName = runtimeName;
42+
context.telemetry.properties.externalRuntimeVersion = runtimeVersion;
43+
44+
// todo set id in settings as the default app to deploy to
45+
console.log(functionAppResourceId);
46+
47+
// Initialize the project using the existing createNewProjectInternal with external runtime config
48+
await createNewProjectInternal(context, {
49+
folderPath: workspaceFolder.uri.fsPath,
50+
suppressOpenFolder: true, // Don't open folder since we're in current workspace
51+
externalRuntimeConfig: {
52+
runtimeName,
53+
runtimeVersion
54+
}
55+
});
56+
}

src/commands/registerCommands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { executeFunction } from './executeFunction/executeFunction';
6868
import { assignManagedIdentity } from './identity/assignManagedIdentity';
6969
import { enableSystemIdentity } from './identity/enableSystemIdentity';
7070
import { unassignManagedIdentity } from './identity/unassignManagedIdentity';
71+
import { initializeProjectForSlashAzure } from './initializeProjectForSlashAzure';
7172
import { initProjectForVSCode } from './initProjectForVSCode/initProjectForVSCode';
7273
import { startStreamingLogs } from './logstream/startStreamingLogs';
7374
import { stopStreamingLogs } from './logstream/stopStreamingLogs';
@@ -81,6 +82,7 @@ import { restartFunctionApp } from './restartFunctionApp';
8182
import { startFunctionApp } from './startFunctionApp';
8283
import { stopFunctionApp } from './stopFunctionApp';
8384
import { swapSlot } from './swapSlot';
85+
import { testExternalRuntimeCommand } from './testExternalRuntime';
8486
import { disableFunction, enableFunction } from './updateDisabledState';
8587
import { viewProperties } from './viewProperties';
8688

@@ -154,6 +156,10 @@ export function registerCommands(
154156
registerCommandWithTreeNodeUnwrapping('azureFunctions.enableFunction', enableFunction);
155157
registerCommandWithTreeNodeUnwrapping('azureFunctions.executeFunction', executeFunction);
156158
registerCommand('azureFunctions.initProjectForVSCode', initProjectForVSCode);
159+
registerCommand('azureFunctions.initializeProjectForSlashAzure', initializeProjectForSlashAzure);
160+
161+
// Test commands for external runtime initialization
162+
registerCommand('azureFunctions.testExternalRuntime', testExternalRuntimeCommand);
157163
registerCommandWithTreeNodeUnwrapping('azureFunctions.installOrUpdateFuncCoreTools', installOrUpdateFuncCoreTools);
158164
registerCommandWithTreeNodeUnwrapping('azureFunctions.openFile', openFile);
159165
registerCommandWithTreeNodeUnwrapping('azureFunctions.openInPortal', openDeploymentInPortal);

0 commit comments

Comments
 (0)