Skip to content
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
30 changes: 30 additions & 0 deletions ext/vscode/src/commands/cmdUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,39 @@ import * as vscode from 'vscode';
import { createAzureDevCli } from '../utils/azureDevCli';
import { execAsync } from '../utils/execAsync';
import { fileExists } from '../utils/fileUtils';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';

const AzureYamlGlobPattern: vscode.GlobPattern = '**/[aA][zZ][uU][rR][eE].{[yY][aA][mM][lL],[yY][mM][lL]}';

/**
* Validates that a URI has a valid fsPath for file system operations.
* Virtual file systems or certain VS Code contexts may not provide a valid fsPath.
* @param context The action context
* @param selectedFile The URI to validate
* @param selectedItem The original selected item (for error message context)
* @param commandName The name of the command being executed (for error message)
* @throws Error if the URI doesn't have a valid fsPath
*/
export function validateFileSystemUri(
context: IActionContext,
selectedFile: vscode.Uri | undefined,
selectedItem: vscode.Uri | TreeViewModel | undefined,
commandName: string
): void {
if (selectedFile && selectedFile.fsPath === undefined) {
context.errorHandling.suppressReportIssue = true;
const itemType = isTreeViewModel(selectedItem) ? 'TreeViewModel' :
isAzureDevCliModel(selectedItem) ? 'AzureDevCliModel' :
selectedItem ? 'vscode.Uri' : 'undefined';
throw new Error(vscode.l10n.t(
"Unable to determine working folder for {0} command. The selected file has an unsupported URI scheme '{1}' (selectedItem type: {2}). Azure Developer CLI commands are not supported in virtual file systems. Please open a local folder or clone the repository locally.",
commandName,
selectedFile.scheme,
itemType
));
}
}

// If the command was invoked with a specific file context, use the file context as the working directory for running Azure developer CLI commands.
// Otherwise search the workspace for "azure.yaml" or "azure.yml" files. If only one is found, use it (i.e. its folder). If more than one is found, ask the user which one to use.
// If at this point we still do not have a working directory, prompt the user to select one.
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';

export async function deploy(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
let selectedModel: AzureDevCliModel | undefined;
Expand All @@ -26,6 +26,10 @@ export async function deploy(context: IActionContext, selectedItem?: vscode.Uri
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'deploy');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/down.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
import { getAzDevTerminalTitle, getWorkingFolder, } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri, } from './cmdUtil';

/**
* A tuple representing the arguments that must be passed to the `down` command when executed via {@link vscode.commands.executeCommand}
Expand All @@ -28,6 +28,10 @@ export async function down(context: IActionContext, selectedItem?: vscode.Uri |
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'down');

const workingFolder = await getWorkingFolder(context, selectedFile);

const confirmPrompt = vscode.l10n.t("Are you sure you want to delete all this application's Azure resources? You can soft-delete certain resources like Azure KeyVaults to preserve their data, or permanently delete and purge them.");
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
import { execAsync } from '../utils/execAsync';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
import { getWorkingFolder } from './cmdUtil';
import { getWorkingFolder, validateFileSystemUri } from './cmdUtil';

const MonitorChoices: IAzureQuickPickItem<string>[] = [
{
Expand Down Expand Up @@ -36,6 +36,10 @@ export async function monitor(context: IActionContext, selectedItem?: vscode.Uri
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'monitor');

const workingFolder = await getWorkingFolder(context, selectedFile);

const monitorChoices = await context.ui.showQuickPick(MonitorChoices, {
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/packageCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';

// `package` is a reserved identifier so `packageCli` had to be used instead
export async function packageCli(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
Expand All @@ -27,6 +27,10 @@ export async function packageCli(context: IActionContext, selectedItem?: vscode.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'package');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { IActionContext } from '@microsoft/vscode-azext-utils';
import { composeArgs, withArg } from '@microsoft/vscode-processutils';
import * as vscode from 'vscode';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
import { TelemetryId } from '../telemetry/telemetryId';
import { createAzureDevCli } from '../utils/azureDevCli';
import { executeAsTask } from '../utils/executeAsTask';
Expand All @@ -28,6 +28,10 @@ export async function pipelineConfig(context: IActionContext, selectedItem?: vsc
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'pipeline config');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';

export async function provision(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
let selectedFile: vscode.Uri | undefined;
Expand All @@ -21,6 +21,10 @@ export async function provision(context: IActionContext, selectedItem?: vscode.U
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'provision');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';

export async function restore(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
let selectedModel: AzureDevCliModel | undefined;
Expand All @@ -26,6 +26,10 @@ export async function restore(context: IActionContext, selectedItem?: vscode.Uri
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'restore');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
6 changes: 5 additions & 1 deletion ext/vscode/src/commands/up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
import { executeAsTask } from '../utils/executeAsTask';
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';

/**
* A tuple representing the arguments that must be passed to the `up` command when executed via {@link vscode.commands.executeCommand}
Expand All @@ -28,6 +28,10 @@ export async function up(context: IActionContext, selectedItem?: vscode.Uri | Tr
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
selectedFile = selectedItem!;
}

// Validate that selectedFile is valid for file system operations
validateFileSystemUri(context, selectedFile, selectedItem, 'up');

const workingFolder = await getWorkingFolder(context, selectedFile);

const azureCli = await createAzureDevCli(context);
Expand Down
58 changes: 58 additions & 0 deletions ext/vscode/src/test/suite/unit/provision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { expect } from 'chai';
import * as vscode from 'vscode';
import * as sinon from 'sinon';
import { provision } from '../../../commands/provision';
import { IActionContext } from '@microsoft/vscode-azext-utils';

suite('provision command', () => {
let sandbox: sinon.SinonSandbox;
let mockContext: IActionContext;

setup(() => {
sandbox = sinon.createSandbox();

mockContext = {
errorHandling: {
suppressReportIssue: false
},
telemetry: {
properties: {}
}
} as unknown as IActionContext;
});

teardown(() => {
sandbox.restore();
});

test('throws error when selectedFile has undefined fsPath (virtual file system)', async () => {
// Mock the URI to ensure fsPath is undefined - simulates virtual file system
const mockUri = {
scheme: 'virtual',
fsPath: undefined,
authority: '',
path: '',
query: '',
fragment: '',
with: () => mockUri,
toString: () => 'virtual:/test'
} as unknown as vscode.Uri;

try {
await provision(mockContext, mockUri);
expect.fail('Should have thrown an error');
} catch (error) {
expect(error).to.be.instanceOf(Error);
const errMessage = (error as Error).message;
expect(errMessage).to.include('Unable to determine working folder');
expect(errMessage).to.include('virtual');
expect(errMessage).to.include('vscode.Uri');
expect(errMessage).to.include('virtual file systems');
}

expect(mockContext.errorHandling.suppressReportIssue).to.equal(true, 'Should suppress automatic issue reporting for user errors');
});
});