Skip to content

Commit

Permalink
Fix 7752 don't allow non-param files to be selected (Azure#8521)
Browse files Browse the repository at this point in the history
* Fix 7752 don't allow non-param files to be selected

* only show param files

* show items in same folder first

Co-authored-by: Stephen Weatherford <Stephen.Weatherford.com>
  • Loading branch information
StephenWeatherford authored Oct 5, 2022
1 parent 0e9804e commit f416344
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 63 deletions.
12 changes: 5 additions & 7 deletions src/vscode-bicep/src/commands/commandManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { ExtensionContext, Uri } from "vscode";
import * as azureextensionui from "@microsoft/vscode-azext-utils";
import assert from "assert";
import * as fse from "fs-extra";
import { ExtensionContext, Uri } from "vscode";
import { Disposable } from "../utils/disposable";
import { Command } from "./types";
import * as azureextensionui from "@microsoft/vscode-azext-utils";
import assert from "assert";

export class CommandManager extends Disposable {
private _packageJson: IPackageJson | undefined;
Expand All @@ -31,7 +31,7 @@ export class CommandManager extends Disposable {
// when the extension is disposed.
azureextensionui.registerCommand(
command.id,
async (context: azureextensionui.IActionContext, ...args: any[]) => {
async (context: azureextensionui.IActionContext, ...args: unknown[]) => {
let documentUri: Uri | undefined = undefined;
let isFromWalkthrough = false;

Expand All @@ -45,10 +45,8 @@ export class CommandManager extends Disposable {
// a walkthrough), and the command contains a query string, the query string values
// will be in an object in the next argument.
if (
!!args[0] &&
args[0] instanceof Object &&
"walkthrough" in args[0] &&
args[0]["walkthrough"] === "true"
(<{ walkthrough?: string }>args[0])["walkthrough"] === "true"
) {
// Marked as a walkthrough via query string in markdow
isFromWalkthrough = true;
Expand Down
175 changes: 119 additions & 56 deletions src/vscode-bicep/src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as fse from "fs-extra";
import * as path from "path";
import vscode, { commands, Uri } from "vscode";
import { AccessToken } from "@azure/identity";
import { AzLoginTreeItem } from "../tree/AzLoginTreeItem";
import { AzManagementGroupTreeItem } from "../tree/AzManagementGroupTreeItem";
import { AzResourceGroupTreeItem } from "../tree/AzResourceGroupTreeItem";
import { Command } from "./types";
import { localize } from "../utils/localize";
import { LocationTreeItem } from "../tree/LocationTreeItem";
import { OutputChannelManager } from "../utils/OutputChannelManager";
import { TreeManager } from "../tree/TreeManager";
import {
AzExtTreeDataProvider,
IActionContext,
IAzureQuickPickItem,
ISubscriptionContext,
parseError,
} from "@microsoft/vscode-azext-utils";
import assert from "assert";
import * as fse from "fs-extra";
import * as path from "path";
import vscode, { commands, Uri } from "vscode";
import {
LanguageClient,
TextDocumentIdentifier,
} from "vscode-languageclient/node";
import {
BicepDeploymentParametersResponse,
BicepDeploymentScopeParams,
Expand All @@ -29,13 +26,17 @@ import {
BicepUpdatedDeploymentParameter,
ParametersFileUpdateOption,
} from "../language";
import {
LanguageClient,
TextDocumentIdentifier,
} from "vscode-languageclient/node";
import { findOrCreateActiveBicepFile } from "./findOrCreateActiveBicepFile";
import { setOutputChannelManagerAtTheStartOfDeployment } from "./deployHelper";
import { AzLoginTreeItem } from "../tree/AzLoginTreeItem";
import { AzManagementGroupTreeItem } from "../tree/AzManagementGroupTreeItem";
import { AzResourceGroupTreeItem } from "../tree/AzResourceGroupTreeItem";
import { LocationTreeItem } from "../tree/LocationTreeItem";
import { TreeManager } from "../tree/TreeManager";
import { compareStringsOrdinal } from "../utils/compareStringsOrdinal";
import { localize } from "../utils/localize";
import { OutputChannelManager } from "../utils/OutputChannelManager";
import { setOutputChannelManagerAtTheStartOfDeployment } from "./deployHelper";
import { findOrCreateActiveBicepFile } from "./findOrCreateActiveBicepFile";
import { Command } from "./types";

export class DeployCommand implements Command {
private _none = localize("none", "$(circle-slash) None");
Expand Down Expand Up @@ -440,46 +441,90 @@ export class DeployCommand implements Command {
}

private async selectParameterFile(
_context: IActionContext,
context: IActionContext,
sourceUri: Uri
): Promise<string | undefined> {
const quickPickItems: IAzureQuickPickItem<string>[] =
await this.createParameterFileQuickPickList();
const result: IAzureQuickPickItem<string> = await _context.ui.showQuickPick(
quickPickItems,
{
canPickMany: false,
placeHolder: `Select a parameter file`,
id: sourceUri.toString(),
}
);
// eslint-disable-next-line no-constant-condition
while (true) {
let parameterFilePath: string;

const quickPickItems: IAzureQuickPickItem<string>[] =
await this.createParameterFileQuickPickList(
path.dirname(sourceUri.fsPath)
);
const result: IAzureQuickPickItem<string> =
await context.ui.showQuickPick(quickPickItems, {
canPickMany: false,
placeHolder: `Select a parameter file`,
id: sourceUri.toString(),
});

if (result.label === this._browse) {
const paramsPaths: Uri[] | undefined = await vscode.window.showOpenDialog(
{
canSelectMany: false,
defaultUri: sourceUri,
openLabel: "Select Parameter File",
filters: { "JSON Files": ["json", "jsonc"] },
if (result.label === this._browse) {
const paramsPaths: Uri[] | undefined =
await vscode.window.showOpenDialog({
canSelectMany: false,
defaultUri: sourceUri,
openLabel: "Select Parameter File",
filters: { "JSON Files": ["json", "jsonc"] },
});
if (paramsPaths) {
assert(paramsPaths.length === 1, "Expected paramsPaths.length === 1");
parameterFilePath = paramsPaths[0].fsPath;
} else {
return undefined;
}
);
if (paramsPaths && paramsPaths.length === 1) {
const parameterFilePath = paramsPaths[0].fsPath;
} else if (result.label === this._none) {
return undefined;
} else {
parameterFilePath = result.data;
}

if (await this.validateIsValidParameterFile(parameterFilePath, true)) {
this.outputChannelManager.appendToOutputChannel(
`Parameter file used in deployment -> ${parameterFilePath}`
);

return parameterFilePath;
}
} else if (result.label === this._none) {
return undefined;
} else {
this.outputChannelManager.appendToOutputChannel(
`Parameter file used in deployment -> ${result.data}`
);
return result.data;
}
}

return undefined;
private async validateIsValidParameterFile(
path: string,
showErrorMessage: boolean
): Promise<boolean> {
const expectedSchema =
"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#";

let message: string | undefined;
let json: { $schema?: unknown } | undefined;
try {
json = fse.readJsonSync(path);
} catch (err) {
message = parseError(err).message;
}

if (json) {
const schema = json.$schema as string;
if (!schema) {
message = `The file has no $schema property. Expected $schema "${expectedSchema}"`;
} else if (!/deploymentparameters\.json/i.test(schema)) {
message = `Unexpected $schema found: ${schema}. Expected $schema "${expectedSchema}"`;
}
}

if (message) {
if (showErrorMessage) {
await vscode.window.showErrorMessage(
`The selected file is not a valid parameters file. ${message}`,
{ modal: true }
);
}

return false;
}

return true;
}

private async handleMissingAndDefaultParams(
Expand Down Expand Up @@ -633,9 +678,9 @@ export class DeployCommand implements Command {
return undefined;
}

private async createParameterFileQuickPickList(): Promise<
IAzureQuickPickItem<string>[]
> {
private async createParameterFileQuickPickList(
bicepFolder: string
): Promise<IAzureQuickPickItem<string>[]> {
const noneQuickPickItem: IAzureQuickPickItem<string> = {
label: this._none,
data: "",
Expand All @@ -647,25 +692,43 @@ export class DeployCommand implements Command {
let parameterFilesQuickPickList = [noneQuickPickItem].concat([
browseQuickPickItem,
]);
const jsonFilesInFolder = await this.getJsonFilesInFolder();

if (jsonFilesInFolder) {
parameterFilesQuickPickList =
parameterFilesQuickPickList.concat(jsonFilesInFolder);
}
const jsonFilesInWorkspace = await this.getParameterFilesInWorkspace(
bicepFolder
);
parameterFilesQuickPickList =
parameterFilesQuickPickList.concat(jsonFilesInWorkspace);

return parameterFilesQuickPickList;
}

private async getJsonFilesInFolder(): Promise<IAzureQuickPickItem<string>[]> {
private async getParameterFilesInWorkspace(
bicepFolder: string
): Promise<IAzureQuickPickItem<string>[]> {
const quickPickItems: IAzureQuickPickItem<string>[] = [];
const workspaceJsonFiles = (
await vscode.workspace.findFiles("**/*.{json,jsonc}", undefined)
).filter((f) => !!f.fsPath);

workspaceJsonFiles.sort((a, b) => compareStringsOrdinal(a.path, b.path));
workspaceJsonFiles.sort((a, b) => {
const aIsInBicepFolder = path.dirname(a.fsPath) === bicepFolder;
const bIsInBicepFolder = path.dirname(b.fsPath) === bicepFolder;

// Put those in the bicep file's folder first in the list
if (aIsInBicepFolder && !bIsInBicepFolder) {
return -1;
} else if (bIsInBicepFolder && !aIsInBicepFolder) {
return 1;
}

return compareStringsOrdinal(a.path, b.path);
});

for (const uri of workspaceJsonFiles) {
if (!(await this.validateIsValidParameterFile(uri.fsPath, false))) {
continue;
}

const workspaceRoot: string | undefined =
vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath;
const relativePath = workspaceRoot
Expand Down

0 comments on commit f416344

Please sign in to comment.