Skip to content
Draft
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
15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@
}
},
"commands": [
{
"command": "azureFunctions.initializeProjectForSlashAzure",
"title": "initializeProjectForSlashAzure",
"category": "Azure Functions"
},
{
"command": "azureFunctions.addBinding",
"title": "%azureFunctions.addBinding%",
Expand Down Expand Up @@ -211,6 +216,12 @@
"category": "Azure Functions",
"enablement": "!virtualWorkspace"
},
{
"command": "azureFunctions.testExternalRuntime",
"title": "Test External Runtime Configuration",
"category": "Azure Functions (Test)",
"enablement": "!virtualWorkspace"
},
{
"command": "azureFunctions.createSlot",
"title": "%azureFunctions.createSlot%",
Expand Down Expand Up @@ -875,6 +886,10 @@
}
],
"commandPalette": [
{
"command": "azureFunctions.initializeProjectForSlashAzure",
"when": "never"
},
{
"command": "azureFunctions.deployByFunctionAppId",
"when": "never"
Expand Down
6 changes: 6 additions & 0 deletions src/commands/createNewProject/IProjectWizardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export interface IProjectWizardContext extends IActionContext {
targetFramework?: string | string[];

containerizedProject?: boolean;

// External runtime configuration from Azure APIs
externalRuntimeConfig?: {
runtimeName: string;
runtimeVersion: string;
};
}

export type OpenBehavior = 'AddToWorkspace' | 'OpenInNewWindow' | 'OpenInCurrentWindow' | 'AlreadyOpen' | 'DontOpen';
12 changes: 12 additions & 0 deletions src/commands/createNewProject/NewProjectLanguageStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { type QuickPickOptions } from 'vscode';
import { ProjectLanguage, nodeDefaultModelVersion, nodeLearnMoreLink, nodeModels, pythonDefaultModelVersion, pythonLearnMoreLink, pythonModels } from '../../constants';
import { localize } from '../../localize';
import { TemplateSchemaVersion } from '../../templates/TemplateProviderBase';
import { getLanguageModelFromRuntime, getProjectLanguageFromRuntime } from '../../utils/externalRuntimeUtils';
import { nonNullProp } from '../../utils/nonNull';
import { openUrl } from '../../utils/openUrl';
import { FunctionListStep } from '../createFunction/FunctionListStep';
Expand Down Expand Up @@ -38,6 +39,17 @@ export class NewProjectLanguageStep extends AzureWizardPromptStep<IProjectWizard
}

public async prompt(context: IProjectWizardContext): Promise<void> {
if (context.externalRuntimeConfig) {
const projectLanguage = getProjectLanguageFromRuntime(context.externalRuntimeConfig.runtimeName);
const languageModel = getLanguageModelFromRuntime(context.externalRuntimeConfig.runtimeName);

if (projectLanguage && languageModel) {
context.language = projectLanguage as ProjectLanguage;
context.languageModel = languageModel;
return;
}
}

// Only display 'supported' languages that can be debugged in VS Code
let languagePicks: IAzureQuickPickItem<{ language: ProjectLanguage, model?: number } | undefined>[] = [
{ label: ProjectLanguage.JavaScript, data: { language: ProjectLanguage.JavaScript } },
Expand Down
10 changes: 8 additions & 2 deletions src/commands/createNewProject/createNewProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export async function createNewProjectFromCommand(
});
}

export async function createNewProjectInternal(context: IActionContext, options: api.ICreateFunctionOptions): Promise<void> {
export async function createNewProjectInternal(context: IActionContext, options: api.ICreateFunctionOptions & {
externalRuntimeConfig?: {
runtimeName: string;
runtimeVersion: string;
};
}): Promise<void> {
addLocalFuncTelemetry(context, undefined);

const language: ProjectLanguage | undefined = <ProjectLanguage>options.language || getGlobalSetting(projectLanguageSetting);
Expand All @@ -59,7 +64,8 @@ export async function createNewProjectInternal(context: IActionContext, options:
{
language,
version: tryParseFuncVersion(version),
projectTemplateKey
projectTemplateKey,
externalRuntimeConfig: options.externalRuntimeConfig
},
await createActivityContext()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ export interface IPythonVenvWizardContext extends IActionContext {
useExistingVenv?: boolean;
venvName?: string;
suppressSkipVenv?: boolean;

// External runtime configuration from Azure APIs
externalRuntimeConfig?: {
runtimeName: string;
runtimeVersion: string;
};
}
81 changes: 69 additions & 12 deletions src/commands/createNewProject/pythonSteps/PythonAliasListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

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

public async prompt(context: IPythonVenvWizardContext): Promise<void> {


if (context.externalRuntimeConfig) {
const installedVersions = await getInstalledPythonVersions(context);
const matchingVersion = findBestMatchingVersion(context.externalRuntimeConfig.runtimeVersion, installedVersions);

if (matchingVersion) {
context.pythonAlias = matchingVersion.alias;
context.telemetry.properties.pythonAliasBehavior = 'externalRuntimeConfigMatch';
return;
} else {
return;
}
}

const placeHolder: string = localize('selectAlias', 'Select a Python interpreter to create a virtual environment');
const result: string | boolean = (await context.ui.showQuickPick(getPicks(context), { placeHolder })).data;
if (typeof result === 'string') {
Expand All @@ -26,6 +42,7 @@ export class PythonAliasListStep extends AzureWizardPromptStep<IPythonVenvWizard
}

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

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

const picks: IAzureQuickPickItem<string | boolean>[] = [];
const versions: string[] = [];
const pythonVersions = await getInstalledPythonVersions(context);
pythonVersions.forEach(pv => picks.push({
label: pv.alias,
description: pv.version,
data: pv.alias
}));

picks.push({ label: localize('enterAlias', '$(keyboard) Manually enter Python interpreter or full path'), data: true });

if (!context.suppressSkipVenv) {
picks.push({ label: localize('skipVenv', '$(circle-slash) Skip virtual environment'), data: false, suppressPersistence: true });
}

return picks;
}

interface InstalledPythonVersion {
alias: string;
version: string;
}

// use this method to preselect python version if runtime version is provided externally

async function getInstalledPythonVersions(context: IPythonVenvWizardContext): Promise<InstalledPythonVersion[]> {
const supportedVersions: string[] = await getSupportedPythonVersions(context, context.version);

const aliasesToTry: string[] = ['python', 'python3', 'py'];
for (const version of supportedVersions) {
aliasesToTry.push(`python${version}`, `py -${version}`);
}

const globalPythonPathSetting: string | undefined = getGlobalSetting('pythonPath', 'python');
if (globalPythonPathSetting) {
aliasesToTry.unshift(globalPythonPathSetting);
}

const versions: InstalledPythonVersion[] = [];
for (const alias of aliasesToTry) {
let version: string;
try {
Expand All @@ -64,23 +117,27 @@ async function getPicks(context: IPythonVenvWizardContext): Promise<IAzureQuickP
continue;
}

if (isSupportedPythonVersion(supportedVersions, version) && !versions.some(v => v === version)) {
picks.push({
label: alias,
description: version,
data: alias
if (isSupportedPythonVersion(supportedVersions, version) && !versions.some(v => v.version === version)) {
versions.push({
alias,
version,
});
versions.push(version);
}
}

context.telemetry.properties.detectedPythonVersions = versions.join(',');
return versions;
}

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

if (!context.suppressSkipVenv) {
picks.push({ label: localize('skipVenv', '$(circle-slash) Skip virtual environment'), data: false, suppressPersistence: true });
function findBestMatchingVersion(requestedVersion: string, versions: InstalledPythonVersion[]): InstalledPythonVersion | undefined {
let matchingVersion = findMatchingVersion(requestedVersion, versions);
if (!matchingVersion) {
matchingVersion = versions.reduce((prev, current) => (gt(current.version, prev.version) ? current : prev));
}

return picks;
return matchingVersion;
}
52 changes: 52 additions & 0 deletions src/commands/initializeProjectForSlashAzure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import * as vscode from 'vscode';
import { type FunctionAppStackValue } from './createFunctionApp/stacks/models/FunctionAppStackModel';
import { initializeProjectFromApp } from './initializeProjectFromApp';

export type RuntimeName = FunctionAppStackValue | 'dotnet-isolated';

/**
*
* Needs to initialize a local project based on the given function app information.
*
* @param context
* @param id id of the function app in Azure
* @param runtimeName ex: 'node', 'python', 'dotnet', etc.
* @param runtimeVersion ex: '18', '3.11', '8.0', etc.
*/
export async function initializeProjectForSlashAzure(
context: IActionContext,
): Promise<void> {

const { url: rawUrl }: { url: string } = await vscode.commands.executeCommand('vscode-dev-azurecloudshell.webOpener.getUrl');
const url = new URL(rawUrl);

const params: FunctionsQueryParams = await getFunctionsQueryParams(url);

await initializeProjectFromApp(context, params);
}


export interface FunctionsQueryParams {
functionAppResourceId: string;
runtimeName: string;
runtimeVersion: string;
}

const functionAppResourceIdKey = 'functionAppResourceId';
const runtimeNameKey = 'runtimeName';
const runtimeVersionKey = 'runtimeVersion';

async function getFunctionsQueryParams(url: URL) {
const params: FunctionsQueryParams = {
functionAppResourceId: url.searchParams.get(functionAppResourceIdKey) || '',
runtimeName: url.searchParams.get(runtimeNameKey) || '',
runtimeVersion: url.searchParams.get(runtimeVersionKey) || ''
};
return params;
}
56 changes: 56 additions & 0 deletions src/commands/initializeProjectFromApp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type IActionContext } from '@microsoft/vscode-azext-utils';
import { workspace } from 'vscode';
import { updateWorkspaceSetting } from '../vsCodeConfig/settings';
import { type FunctionAppStackValue } from './createFunctionApp/stacks/models/FunctionAppStackModel';
import { createNewProjectInternal } from './createNewProject/createNewProject';
import { type FunctionsQueryParams } from './initializeProjectForSlashAzure';

export type RuntimeName = FunctionAppStackValue | 'dotnet-isolated';

/**
*
* Needs to initialize a local project based on the given function app information.
*
* @param context
* @param id id of the function app in Azure
* @param runtimeName ex: 'node', 'python', 'dotnet', etc.
* @param runtimeVersion ex: '18', '3.11', '8.0', etc.
*/
export async function initializeProjectFromApp(
context: IActionContext,
options: FunctionsQueryParams,
): Promise<void> {
const { functionAppResourceId, runtimeName, runtimeVersion } = options;

const workspaceFolder = workspace.workspaceFolders?.[0];

if (!workspaceFolder) {
throw new Error('No workspace folder is open.');
}

if (functionAppResourceId) {
// set defaultFunctionAppToDeploy setting to the given function app id
await updateWorkspaceSetting('defaultFunctionAppToDeploy', functionAppResourceId, workspaceFolder);
}

context.telemetry.properties.externalRuntimeName = runtimeName;
context.telemetry.properties.externalRuntimeVersion = runtimeVersion;

// todo set id in settings as the default app to deploy to
console.log(functionAppResourceId);

// Initialize the project using the existing createNewProjectInternal with external runtime config
await createNewProjectInternal(context, {
folderPath: workspaceFolder.uri.fsPath,
suppressOpenFolder: true, // Don't open folder since we're in current workspace
externalRuntimeConfig: {
runtimeName,
runtimeVersion
}
});
}
6 changes: 6 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { executeFunction } from './executeFunction/executeFunction';
import { assignManagedIdentity } from './identity/assignManagedIdentity';
import { enableSystemIdentity } from './identity/enableSystemIdentity';
import { unassignManagedIdentity } from './identity/unassignManagedIdentity';
import { initializeProjectForSlashAzure } from './initializeProjectForSlashAzure';
import { initProjectForVSCode } from './initProjectForVSCode/initProjectForVSCode';
import { startStreamingLogs } from './logstream/startStreamingLogs';
import { stopStreamingLogs } from './logstream/stopStreamingLogs';
Expand All @@ -81,6 +82,7 @@ import { restartFunctionApp } from './restartFunctionApp';
import { startFunctionApp } from './startFunctionApp';
import { stopFunctionApp } from './stopFunctionApp';
import { swapSlot } from './swapSlot';
import { testExternalRuntimeCommand } from './testExternalRuntime';
import { disableFunction, enableFunction } from './updateDisabledState';
import { viewProperties } from './viewProperties';

Expand Down Expand Up @@ -154,6 +156,10 @@ export function registerCommands(
registerCommandWithTreeNodeUnwrapping('azureFunctions.enableFunction', enableFunction);
registerCommandWithTreeNodeUnwrapping('azureFunctions.executeFunction', executeFunction);
registerCommand('azureFunctions.initProjectForVSCode', initProjectForVSCode);
registerCommand('azureFunctions.initializeProjectForSlashAzure', initializeProjectForSlashAzure);

// Test commands for external runtime initialization
registerCommand('azureFunctions.testExternalRuntime', testExternalRuntimeCommand);
registerCommandWithTreeNodeUnwrapping('azureFunctions.installOrUpdateFuncCoreTools', installOrUpdateFuncCoreTools);
registerCommandWithTreeNodeUnwrapping('azureFunctions.openFile', openFile);
registerCommandWithTreeNodeUnwrapping('azureFunctions.openInPortal', openDeploymentInPortal);
Expand Down
Loading