From 32a71a2de612a1177c65263ed61be4223227f718 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Thu, 12 Aug 2021 13:24:16 -0700 Subject: [PATCH] SQL Database Project - Deploy db to docker (#16406) Added a new command to deploy the project to docker --- extensions/sql-database-projects/package.json | 21 +- .../sql-database-projects/package.nls.json | 1 + .../src/common/constants.ts | 47 +++ .../sql-database-projects/src/common/utils.ts | 83 ++++ .../src/controllers/mainController.ts | 7 +- .../src/controllers/projectController.ts | 48 ++- .../src/dialogs/deployDatabaseQuickpick.ts | 142 +++++++ .../src/dialogs/publishDatabaseQuickpick.ts | 70 ++-- .../src/models/deploy/deployProfile.ts | 21 + .../src/models/deploy/deployService.ts | 361 ++++++++++++++++++ .../src/test/deploy/deployService.test.ts | 222 +++++++++++ .../dialogs/publishDatabaseDialog.test.ts | 8 +- .../src/test/netCoreTool.test.ts | 13 +- .../src/test/projectController.test.ts | 38 +- .../src/test/testContext.ts | 12 +- .../src/tools/netcoreTool.ts | 14 +- .../src/typings/vscode-mssql.d.ts | 52 +-- extensions/sql-database-projects/yarn.lock | 43 +++ 18 files changed, 1111 insertions(+), 92 deletions(-) create mode 100644 extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts create mode 100644 extensions/sql-database-projects/src/models/deploy/deployProfile.ts create mode 100644 extensions/sql-database-projects/src/models/deploy/deployService.ts create mode 100644 extensions/sql-database-projects/src/test/deploy/deployService.test.ts diff --git a/extensions/sql-database-projects/package.json b/extensions/sql-database-projects/package.json index 913a497191..881bb9daa2 100644 --- a/extensions/sql-database-projects/package.json +++ b/extensions/sql-database-projects/package.json @@ -113,6 +113,11 @@ "title": "%sqlDatabaseProjects.publish%", "category": "%sqlDatabaseProjects.displayName%" }, + { + "command": "sqlDatabaseProjects.deployLocal", + "title": "%sqlDatabaseProjects.deployLocal%", + "category": "%sqlDatabaseProjects.displayName%" + }, { "command": "sqlDatabaseProjects.properties", "title": "%sqlDatabaseProjects.properties%", @@ -216,6 +221,10 @@ "command": "sqlDatabaseProjects.publish", "when": "false" }, + { + "command": "sqlDatabaseProjects.deployLocal", + "when": "false" + }, { "command": "sqlDatabaseProjects.properties", "when": "false" @@ -267,6 +276,11 @@ "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project", "group": "1_dbProjectsFirst@2" }, + { + "command": "sqlDatabaseProjects.deployLocal", + "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project && !azdataAvailable", + "group": "1_dbProjectsFirst@2" + }, { "command": "sqlDatabaseProjects.schemaCompare", "when": "view == dataworkspace.views.main && viewItem == databaseProject.itemType.project && azdataAvailable", @@ -388,7 +402,9 @@ "vscode-languageclient": "^5.3.0-next.1", "vscode-nls": "^4.1.2", "xml-formatter": "^2.1.0", - "xmldom": "^0.3.0" + "xmldom": "^0.3.0", + "generate-password": "^1.6.0", + "fs-extra": "^5.0.0" }, "devDependencies": { "@types/mocha": "^5.2.5", @@ -402,7 +418,8 @@ "tslint": "^5.8.0", "typemoq": "^2.1.0", "typescript": "^2.6.1", - "vscodetestcover": "^1.1.0" + "vscodetestcover": "^1.1.0", + "@types/fs-extra": "^5.0.0" }, "__metadata": { "id": "70", diff --git a/extensions/sql-database-projects/package.nls.json b/extensions/sql-database-projects/package.nls.json index 32be34be63..7e30c02627 100644 --- a/extensions/sql-database-projects/package.nls.json +++ b/extensions/sql-database-projects/package.nls.json @@ -8,6 +8,7 @@ "sqlDatabaseProjects.close": "Close Database Project", "sqlDatabaseProjects.build": "Build", "sqlDatabaseProjects.publish": "Publish", + "sqlDatabaseProjects.deployLocal": "Deploy", "sqlDatabaseProjects.createProjectFromDatabase": "Create Project From Database", "sqlDatabaseProjects.properties": "Properties", "sqlDatabaseProjects.schemaCompare": "Schema Compare", diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 115d1208c9..49fafd23a3 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -126,6 +126,53 @@ export const selectDatabase = localize('selectDatabase', "Select database"); export const done = localize('done', "Done"); export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not be empty"); +// Deploy +export const selectDeployOption = localize('selectDeployOption', "Select where to deploy the project to"); +export const deployToExistingServer = localize('deployToExistingServer', "Deploy to existing server"); +export const deployToDockerContainer = localize('deployToDockerContainer', "Deploy to docker container"); +export const enterPortNumber = localize('enterPortNumber', "Enter port number or press enter to use the default value"); +export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name"); +export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template"); +export const enterPassword = localize('enterPassword', "Enter password or press enter to use the generated password"); +export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number"); +export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty"); +export const dockerImageLabelPrefix = 'source=sqldbproject'; +export const dockerImageNamePrefix = 'sqldbproject'; +export const connectionNamePrefix = 'SQLDbProject'; +export const dockerBaseImage = 'mcr.microsoft.com/azure-sql-edge:latest'; +export const commandsFolderName = 'commands'; +export const mssqlFolderName = '.mssql'; +export const dockerFileName = 'Dockerfile'; +export const startCommandName = 'start.sh'; +export const defaultPortNumber = '1433'; +export const defaultConnectionStringEnvVarName = 'SQLConnectionString'; +export const defaultConnectionStringTemplate = 'Data Source=@@SERVER@@,@@PORT@@;Initial Catalog=@@DATABASE@@;User id=@@USER@@;Password=@@SA_PASSWORD@@;'; +export const azureFunctionLocalSettingsFileName = 'local.settings.json'; +export const enterConnStringTemplateDescription = localize('enterConnStringTemplateDescription', "Enter a template for SQL connection string"); +export const appSettingPrompt = localize('appSettingPrompt', "Would you like to update Azure Function local.settings.json with the new connection string?"); +export const enterConnectionStringEnvNameDescription = localize('enterConnectionStringEnvNameDescription', "Enter environment variable for SQL connection string"); +export const deployDbTaskName = localize('deployDbTaskName', "Deploying SQL Db Project Locally"); +export const deployProjectSucceed = localize('deployProjectSucceed', "Database project deployed successfully"); +export const cleaningDockerImagesMessage = localize('cleaningDockerImagesMessage', "Cleaning existing deployments..."); +export const creatingDeploymentSettingsMessage = localize('creatingDeploymentSettingsMessage', "Creating deployment settings ..."); +export const runningDockerMessage = localize('runningDockerMessage', "Building and running the docker container ..."); +export const dockerContainerNotRunningErrorMessage = localize('dockerContainerNotRunningErrorMessage', "Docker container is not running"); +export const dockerContainerFailedToRunErrorMessage = localize('dockerContainerFailedToRunErrorMessage', "Failed to run the docker container"); +export const connectingToSqlServerOnDockerMessage = localize('connectingToSqlServerOnDockerMessage', "Connecting to SQL Server on Docker"); +export const deployProjectFailedMessage = localize('deployProjectFailedMessage', "Failed to open a connection to the deployed database'"); +export function taskFailedError(taskName: string, err: string): string { return localize('taskFailedError.error', "Failed to complete task '{0}'. Error: {1}", taskName, err); } +export function deployProjectFailed(errorMessage: string) { return localize('deployProjectFailed', "Failed to deploy project. Check output pane for more details. {0}", errorMessage); } +export function deployAppSettingUpdateFailed(appSetting: string) { return localize('deployAppSettingUpdateFailed', "Failed to update app setting '{0}'", appSetting); } +export function deployAppSettingUpdating(appSetting: string) { return localize('deployAppSettingUpdating', "Updating app setting: '{0}'", appSetting); } +export function connectionFailedError(error: string) { return localize('connectionFailedError', "Connection failed error: '{0}'", error); } +export function dockerContainerCreatedMessage(id: string) { return localize('dockerContainerCreatedMessage', "Docker created id: '{0}'", id); } +export function dockerLogMessage(log: string) { return localize('dockerLogMessage', "Docker logs: '{0}'", log); } +export function retryWaitMessage(numberOfSeconds: number, name: string) { return localize('retryWaitMessage', "Waiting for {0} seconds before another attempt for operation '{1}'", numberOfSeconds, name); } +export function retryRunMessage(attemptNumber: number, numberOfAttempts: number, name: string) { return localize('retryRunMessage', "Running operation '{2}' Attempt {0} of {1}", attemptNumber, numberOfAttempts, name); } +export function retrySucceedMessage(name: string, result: string) { return localize('retrySucceedMessage', "Operation '{0}' completed successfully. Result: {1}", name, result); } +export function retryFailedMessage(name: string, result: string, error: string) { return localize('retryFailedMessage', "Operation '{0}' failed. Re-trying... Current Result: {1}. Error: '{2}'", name, result, error); } +export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}'", name, error || ''); } + // Add Database Reference dialog strings export const addDatabaseReferenceDialogName = localize('addDatabaseReferencedialogName', "Add database reference"); diff --git a/extensions/sql-database-projects/src/common/utils.ts b/extensions/sql-database-projects/src/common/utils.ts index 5e7b95fd5c..bf9be842be 100644 --- a/extensions/sql-database-projects/src/common/utils.ts +++ b/extensions/sql-database-projects/src/common/utils.ts @@ -14,6 +14,13 @@ import * as mssql from '../../../mssql'; import * as vscodeMssql from 'vscode-mssql'; import { promises as fs } from 'fs'; import { Project } from '../models/project'; +import * as childProcess from 'child_process'; +import * as fse from 'fs-extra'; + +export interface ValidationResult { + errorMessage: string; + validated: boolean +} /** * Consolidates on the error message string @@ -395,3 +402,79 @@ try { export function getAzdataApi(): typeof azdataType | undefined { return azdataApi; } + +export async function createFolderIfNotExist(folderPath: string): Promise { + try { + await fse.mkdir(folderPath); + } catch { + // Ignore if failed + } +} + +export async function executeCommand(cmd: string, outputChannel: vscode.OutputChannel, timeout: number = 5 * 60 * 1000): Promise { + return new Promise((resolve, reject) => { + if (outputChannel) { + outputChannel.appendLine(` > ${cmd}`); + } + let child = childProcess.exec(cmd, { + timeout: timeout + }, (err, stdout) => { + if (err) { + reject(err); + } else { + resolve(stdout); + } + }); + + // Add listeners to print stdout and stderr if an output channel was provided + + if (child?.stdout) { + child.stdout.on('data', data => { outputDataChunk(outputChannel, data, ' stdout: '); }); + } + if (child?.stderr) { + child.stderr.on('data', data => { outputDataChunk(outputChannel, data, ' stderr: '); }); + } + }); +} + +export function outputDataChunk(outputChannel: vscode.OutputChannel, data: string | Buffer, header: string): void { + data.toString().split(/\r?\n/) + .forEach(line => { + if (outputChannel) { + outputChannel.appendLine(header + line); + } + }); +} + +export async function retry( + name: string, + attempt: () => Promise, + verify: (result: T) => Promise, + formatResult: (result: T) => Promise, + outputChannel: vscode.OutputChannel, + numberOfAttempts: number = 10, + waitInSeconds: number = 2 +): Promise { + for (let count = 0; count < numberOfAttempts; count++) { + outputChannel.appendLine(constants.retryWaitMessage(waitInSeconds, name)); + await new Promise(c => setTimeout(c, waitInSeconds * 1000)); + outputChannel.appendLine(constants.retryRunMessage(count, numberOfAttempts, name)); + + try { + let result = await attempt(); + const validationResult = await verify(result); + const formattedResult = await formatResult(result); + if (validationResult.validated) { + outputChannel.appendLine(constants.retrySucceedMessage(name, formattedResult)); + return result; + } else { + outputChannel.appendLine(constants.retryFailedMessage(name, formattedResult, validationResult.errorMessage)); + } + + } catch (err) { + outputChannel.appendLine(constants.retryMessage(name, err)); + } + } + + return undefined; +} diff --git a/extensions/sql-database-projects/src/controllers/mainController.ts b/extensions/sql-database-projects/src/controllers/mainController.ts index 1640c7b151..d9e8ae7467 100644 --- a/extensions/sql-database-projects/src/controllers/mainController.ts +++ b/extensions/sql-database-projects/src/controllers/mainController.ts @@ -13,6 +13,7 @@ import { ProjectsController } from './projectController'; import { NetCoreTool } from '../tools/netcoreTool'; import { IconPathHelper } from '../common/iconHelper'; import { WorkspaceTreeItem } from 'dataworkspace'; +import * as constants from '../common/constants'; import { SqlDatabaseProjectProvider } from '../projectProvider/projectProvider'; import { launchAddSqlBindingQuickpick } from '../dialogs/addSqlBindingQuickpick'; @@ -22,10 +23,11 @@ import { launchAddSqlBindingQuickpick } from '../dialogs/addSqlBindingQuickpick' export default class MainController implements vscode.Disposable { protected projectsController: ProjectsController; protected netcoreTool: NetCoreTool; + private _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(constants.projectsOutputChannel); public constructor(private context: vscode.ExtensionContext) { - this.projectsController = new ProjectsController(); - this.netcoreTool = new NetCoreTool(); + this.projectsController = new ProjectsController(this._outputChannel); + this.netcoreTool = new NetCoreTool(this._outputChannel); } public get extensionContext(): vscode.ExtensionContext { @@ -50,6 +52,7 @@ export default class MainController implements vscode.Disposable { vscode.commands.registerCommand('sqlDatabaseProjects.build', async (node: WorkspaceTreeItem) => { await this.projectsController.buildProject(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.publish', async (node: WorkspaceTreeItem) => { this.projectsController.publishProject(node); }); + vscode.commands.registerCommand('sqlDatabaseProjects.deployLocal', async (node: WorkspaceTreeItem) => { this.projectsController.deployProject(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.schemaCompare', async (node: WorkspaceTreeItem) => { await this.projectsController.schemaCompare(node); }); vscode.commands.registerCommand('sqlDatabaseProjects.createProjectFromDatabase', async (context: azdataType.IConnectionProfile | vscodeMssql.ITreeNodeInfo | undefined) => { await this.projectsController.createProjectFromDatabase(context); }); diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index 2aea958aa6..87a4441476 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -35,6 +35,8 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t import { IconPathHelper } from '../common/iconHelper'; import { DashboardData, PublishData, Status } from '../models/dashboardData/dashboardData'; import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickpick'; +import { launchDeployDatabaseQuickpick } from '../dialogs/deployDatabaseQuickpick'; +import { DeployService } from '../models/deploy/deployService'; import { SqlTargetPlatform } from 'sqldbproj'; import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick'; import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick'; @@ -64,12 +66,14 @@ export class ProjectsController { private buildHelper: BuildHelper; private buildInfo: DashboardData[] = []; private publishInfo: PublishData[] = []; + private deployService: DeployService; projFileWatchers = new Map(); - constructor() { - this.netCoreTool = new NetCoreTool(); + constructor(outputChannel: vscode.OutputChannel) { + this.netCoreTool = new NetCoreTool(outputChannel); this.buildHelper = new BuildHelper(); + this.deployService = new DeployService(outputChannel); } public getDashboardPublishData(projectFile: string): (string | dataworkspace.IconCellValue)[][] { @@ -251,6 +255,46 @@ export class ProjectsController { } } + /** + * Deploys a project + * @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project + */ + public async deployProject(context: Project | dataworkspace.WorkspaceTreeItem): Promise { + const project: Project = this.getProjectFromContext(context); + try { + let deployProfile = await launchDeployDatabaseQuickpick(project); + if (deployProfile && deployProfile.deploySettings) { + let connectionUri: string | undefined; + if (deployProfile.localDbSetting) { + connectionUri = await this.deployService.deploy(deployProfile, project); + if (connectionUri) { + deployProfile.deploySettings.connectionUri = connectionUri; + } + } + if (deployProfile.deploySettings.connectionUri) { + const publishResult = await this.publishOrScriptProject(project, deployProfile.deploySettings, true); + if (publishResult && publishResult.success) { + + // Update app settings if requested by user + // + await this.deployService.updateAppSettings(deployProfile); + if (deployProfile.localDbSetting) { + await this.deployService.getConnection(deployProfile.localDbSetting, true, deployProfile.localDbSetting.dbName); + } + vscode.window.showInformationMessage(constants.deployProjectSucceed); + } else { + vscode.window.showErrorMessage(constants.deployProjectFailed(publishResult?.errorMessage || '')); + } + } else { + vscode.window.showErrorMessage(constants.deployProjectFailed(constants.deployProjectFailedMessage)); + } + } + } catch (error) { + vscode.window.showErrorMessage(constants.deployProjectFailed(utils.getErrorMessage(error))); + } + return; + } + /** * Builds and publishes a project * @param treeNode a treeItem in a project's hierarchy, to be used to obtain a Project diff --git a/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts new file mode 100644 index 0000000000..572e2b581a --- /dev/null +++ b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as constants from '../common/constants'; +import { AppSettingType, IDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile'; +import { Project } from '../models/project'; +import * as generator from 'generate-password'; +import { getPublishDatabaseSettings } from './publishDatabaseQuickpick'; +import * as path from 'path'; +import * as fse from 'fs-extra'; + +/** + * Create flow for Deploying a database using only VS Code-native APIs such as QuickPick + */ +export async function launchDeployDatabaseQuickpick(project: Project): Promise { + + // Show options to user for deploy to existing server or docker + + const deployOption = await vscode.window.showQuickPick( + [constants.deployToExistingServer, constants.deployToDockerContainer], + { title: constants.selectDeployOption, ignoreFocusOut: true }); + + // Return when user hits escape + if (!deployOption) { + return undefined; + } + + let localDbSetting: ILocalDbSetting | undefined; + // Deploy to docker selected + if (deployOption === constants.deployToDockerContainer) { + let portNumber = await vscode.window.showInputBox({ + title: constants.enterPortNumber, + ignoreFocusOut: true, + value: constants.defaultPortNumber, + validateInput: input => isNaN(+input) ? constants.portMustBeNumber : undefined + } + ); + + // Return when user hits escape + if (!portNumber) { + return undefined; + } + + let password: string | undefined = generator.generate({ + length: 10, + numbers: true, + symbols: true, + lowercase: true, + uppercase: true, + exclude: '`"\'' // Exclude the chars that cannot be included in the password. Some chars can make the command fail in the terminal + }); + password = await vscode.window.showInputBox({ + title: constants.enterPassword, + ignoreFocusOut: true, + value: password, + password: true + } + ); + + // Return when user hits escape + if (!password) { + return undefined; + } + + localDbSetting = { + serverName: 'localhost', + userName: 'sa', + dbName: project.projectFileName, + password: password, + port: +portNumber, + }; + } + let deploySettings = await getPublishDatabaseSettings(project, deployOption !== constants.deployToDockerContainer); + + // Return when user hits escape + if (!deploySettings) { + return undefined; + } + + // TODO: Ask for SQL CMD Variables or profile + + let envVarName: string | undefined = ''; + const integrateWithAzureFunctions: boolean = true; //TODO: get value from settings or quickpick + + //TODO: find a better way to find if AF or local settings is in the project + // + const localSettings = path.join(project.projectFolderPath, constants.azureFunctionLocalSettingsFileName); + const settingExist: boolean = await fse.pathExists(localSettings); + if (integrateWithAzureFunctions && settingExist) { + + // Ask user to update app settings or not + // + let choices: { [id: string]: boolean } = {}; + let options = { + placeHolder: constants.appSettingPrompt + }; + choices[constants.yesString] = true; + choices[constants.noString] = false; + let result = await vscode.window.showQuickPick(Object.keys(choices).map(c => { + return { + label: c + }; + }), options); + + // Return when user hits escape + if (!result) { + return undefined; + } + + if (result !== undefined && choices[result.label] || false) { + envVarName = await vscode.window.showInputBox( + { + title: constants.enterConnectionStringEnvName, + ignoreFocusOut: true, + value: constants.defaultConnectionStringEnvVarName, + validateInput: input => input === '' ? constants.valueCannotBeEmpty : undefined, + placeHolder: constants.enterConnectionStringEnvNameDescription + } + ); + + // Return when user hits escape + if (!envVarName) { + return undefined; + } + } + } + + if (localDbSetting && deploySettings) { + deploySettings.serverName = localDbSetting.serverName; + } + + return { + localDbSetting: localDbSetting, + envVariableName: envVarName, + appSettingFile: settingExist ? localSettings : undefined, + deploySettings: deploySettings, + appSettingType: settingExist ? AppSettingType.AzureFunction : AppSettingType.None + }; +} diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts index 9e680d87a7..7752f7f617 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseQuickpick.ts @@ -16,7 +16,7 @@ import { IDeploySettings } from '../models/IDeploySettings'; /** * Create flow for Publishing a database using only VS Code-native APIs such as QuickPick */ -export async function launchPublishDatabaseQuickpick(project: Project, projectController: ProjectsController): Promise { +export async function getPublishDatabaseSettings(project: Project, promptForConnection: boolean = true): Promise { // 1. Select publish settings file (optional) // Create custom quickpick so we can control stuff like displaying the loading indicator @@ -74,23 +74,26 @@ export async function launchPublishDatabaseQuickpick(project: Project, projectCo quickPick.hide(); // Hide the quickpick immediately so it isn't showing while the API loads // 2. Select connection - const vscodeMssqlApi = await getVscodeMssqlApi(); + let connectionProfile: IConnectionInfo | undefined = undefined; - let connectionUri: string = ''; - let dbs: string[] | undefined = undefined; - while (!dbs) { - connectionProfile = await vscodeMssqlApi.promptForConnection(true); - if (!connectionProfile) { - // User cancelled - return; - } - // Get the list of databases now to validate that the connection is valid and re-prompt them if it isn't - try { - connectionUri = await vscodeMssqlApi.connect(connectionProfile); - dbs = await vscodeMssqlApi.listDatabases(connectionUri); - } catch (err) { - // no-op, the mssql extension handles showing the error to the user. We'll just go - // back and prompt the user for a connection again + let dbs: string[] = []; + let connectionUri: string | undefined; + if (promptForConnection) { + const vscodeMssqlApi = await getVscodeMssqlApi(); + while (!dbs) { + connectionProfile = await vscodeMssqlApi.promptForConnection(true); + if (!connectionProfile) { + // User cancelled + return; + } + // Get the list of databases now to validate that the connection is valid and re-prompt them if it isn't + try { + connectionUri = await vscodeMssqlApi.connect(connectionProfile); + dbs = await vscodeMssqlApi.listDatabases(connectionUri); + } catch (err) { + // no-op, the mssql extension handles showing the error to the user. We'll just go + // back and prompt the user for a connection again + } } } @@ -184,22 +187,33 @@ export async function launchPublishDatabaseQuickpick(project: Project, projectCo } } - // 5. Select action to take - const action = await vscode.window.showQuickPick( - [constants.generateScriptButtonText, constants.publish], - { title: constants.chooseAction, ignoreFocusOut: true }); - if (!action) { - return; - } - // 6. Generate script/publish let settings: IDeploySettings = { databaseName: databaseName, - serverName: connectionProfile!.server, - connectionUri: connectionUri, + serverName: connectionProfile?.server || '', + connectionUri: connectionUri || '', sqlCmdVariables: sqlCmdVariables, deploymentOptions: await getDefaultPublishDeploymentOptions(project), profileUsed: !!publishProfile }; - await projectController.publishOrScriptProject(project, settings, action === constants.publish); + return settings; } + +/** +* Create flow for Publishing a database using only VS Code-native APIs such as QuickPick +*/ +export async function launchPublishDatabaseQuickpick(project: Project, projectController: ProjectsController): Promise { + let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project); + + if (settings) { + // 5. Select action to take + const action = await vscode.window.showQuickPick( + [constants.generateScriptButtonText, constants.publish], + { title: constants.chooseAction, ignoreFocusOut: true }); + if (!action) { + return; + } + await projectController.publishOrScriptProject(project, settings, action === constants.publish); + } +} + diff --git a/extensions/sql-database-projects/src/models/deploy/deployProfile.ts b/extensions/sql-database-projects/src/models/deploy/deployProfile.ts new file mode 100644 index 0000000000..2f6d5c41a9 --- /dev/null +++ b/extensions/sql-database-projects/src/models/deploy/deployProfile.ts @@ -0,0 +1,21 @@ +import { IDeploySettings } from '../IDeploySettings'; + +export enum AppSettingType { + None, + AzureFunction +} +export interface IDeployProfile { + localDbSetting?: ILocalDbSetting; + deploySettings?: IDeploySettings; + envVariableName?: string; + appSettingFile?: string; + appSettingType: AppSettingType; +} + +export interface ILocalDbSetting { + serverName: string, + port: number, + userName: string, + password: string, + dbName: string, +} diff --git a/extensions/sql-database-projects/src/models/deploy/deployService.ts b/extensions/sql-database-projects/src/models/deploy/deployService.ts new file mode 100644 index 0000000000..8161abe2c6 --- /dev/null +++ b/extensions/sql-database-projects/src/models/deploy/deployService.ts @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppSettingType, IDeployProfile, ILocalDbSetting } from './deployProfile'; +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import { Project } from '../project'; +import * as constants from '../../common/constants'; +import * as utils from '../../common/utils'; +import * as fse from 'fs-extra'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as os from 'os'; +import { ConnectionResult } from 'azdata'; +import * as templates from '../../templates/templates'; + +export class DeployService { + + constructor(private _outputChannel: vscode.OutputChannel) { + } + + private createConnectionStringTemplate(runtime: string | undefined): string { + switch (runtime?.toLocaleLowerCase()) { + case 'dotnet': + return constants.defaultConnectionStringTemplate; + break; + // TODO: add connection strings for other languages + default: + break; + } + return ''; + } + + private findAppRuntime(profile: IDeployProfile, appSettingContent: any): string | undefined { + switch (profile.appSettingType) { + case AppSettingType.AzureFunction: + return appSettingContent?.Values['FUNCTIONS_WORKER_RUNTIME']; + default: + } + return undefined; + } + + public async updateAppSettings(profile: IDeployProfile): Promise { + // Update app settings + // + if (!profile.appSettingFile) { + return; + } + this.logToOutput(constants.deployAppSettingUpdating(profile.appSettingFile)); + + // TODO: handle parsing errors + let content = JSON.parse(fse.readFileSync(profile.appSettingFile, 'utf8')); + if (content && content.Values) { + let connectionString: string | undefined = ''; + if (profile.localDbSetting) { + // Find the runtime and generate the connection string for the runtime + // + const runtime = this.findAppRuntime(profile, content); + let connectionStringTemplate = this.createConnectionStringTemplate(runtime); + const macroDict: Record = { + 'SERVER': profile?.localDbSetting?.serverName || '', + 'PORT': profile?.localDbSetting?.port?.toString() || '', + 'USER': profile?.localDbSetting?.userName || '', + 'SA_PASSWORD': profile?.localDbSetting?.password || '', + 'DATABASE': profile?.localDbSetting?.dbName || '', + }; + + connectionString = templates.macroExpansion(connectionStringTemplate, macroDict); + } else if (profile.deploySettings?.connectionUri) { + connectionString = await this.getConnectionString(profile.deploySettings?.connectionUri); + } + + if (connectionString && profile.envVariableName) { + content.Values[profile.envVariableName] = connectionString; + await fse.writeFileSync(profile.appSettingFile, JSON.stringify(content, undefined, 4)); + this.logToOutput(`app setting '${profile.appSettingFile}' has been updated. env variable name: ${profile.envVariableName} connection String: ${connectionString}`); + + } else { + this.logToOutput(constants.deployAppSettingUpdateFailed(profile.appSettingFile)); + } + } + } + + public async deploy(profile: IDeployProfile, project: Project): Promise { + return await this.executeTask(constants.deployDbTaskName, async () => { + if (!profile.localDbSetting) { + return undefined; + } + const projectName = project.projectFileName; + const imageLabel = `${constants.dockerImageLabelPrefix}_${projectName}`; + const imageName = `${constants.dockerImageNamePrefix}-${projectName}-${UUID.generateUuid().toLowerCase()}`; + const root = project.projectFolderPath; + const mssqlFolderPath = path.join(root, constants.mssqlFolderName); + const commandsFolderPath = path.join(mssqlFolderPath, constants.commandsFolderName); + const dockerFilePath = path.join(mssqlFolderPath, constants.dockerFileName); + const startFilePath = path.join(commandsFolderPath, constants.startCommandName); + + this.logToOutput(constants.cleaningDockerImagesMessage); + // Clean up existing docker image + + await this.cleanDockerObjects(`docker ps -q -a --filter label=${imageLabel}`, ['docker stop', 'docker rm']); + await this.cleanDockerObjects(`docker images -f label=${imageLabel} -q`, [`docker rmi -f `]); + + this.logToOutput(constants.creatingDeploymentSettingsMessage); + // Create commands + // + + await this.createCommands(mssqlFolderPath, commandsFolderPath, dockerFilePath, startFilePath, imageLabel); + + this.logToOutput(constants.runningDockerMessage); + // Building the image and running the docker + // + const createdDockerId: string | undefined = await this.buildAndRunDockerContainer(dockerFilePath, imageName, root, profile.localDbSetting, imageLabel); + this.logToOutput(`Docker container created. Id: ${createdDockerId}`); + + + // Waiting a bit to make sure docker container doesn't crash + // + const runningDockerId = await utils.retry('Validating the docker container', async () => { + return await utils.executeCommand(`docker ps -q -a --filter label=${imageLabel} -q`, this._outputChannel); + }, (dockerId) => { + return Promise.resolve({ validated: dockerId !== undefined, errorMessage: constants.dockerContainerNotRunningErrorMessage }); + }, (dockerId) => { + return Promise.resolve(dockerId); + }, this._outputChannel + ); + + if (runningDockerId) { + this.logToOutput(constants.dockerContainerCreatedMessage(runningDockerId)); + return await this.getConnection(profile.localDbSetting, false, 'master'); + + } else { + this.logToOutput(constants.dockerContainerFailedToRunErrorMessage); + if (createdDockerId) { + // Get the docker logs if docker was created but crashed + await utils.executeCommand(constants.dockerLogMessage(createdDockerId), this._outputChannel); + } + } + + return undefined; + }); + } + + private async buildAndRunDockerContainer(dockerFilePath: string, imageName: string, root: string, profile: ILocalDbSetting, imageLabel: string): Promise { + this.logToOutput('Building docker image ...'); + await utils.executeCommand(`docker pull ${constants.dockerBaseImage}`, this._outputChannel); + await utils.executeCommand(`docker build -f ${dockerFilePath} -t ${imageName} ${root}`, this._outputChannel); + await utils.executeCommand(`docker images --filter label=${imageLabel}`, this._outputChannel); + + this.logToOutput('Running docker container ...'); + await utils.executeCommand(`docker run -p ${profile.port}:1433 -e "MSSQL_SA_PASSWORD=${profile.password}" -d ${imageName}`, this._outputChannel); + return await utils.executeCommand(`docker ps -q -a --filter label=${imageLabel} -q`, this._outputChannel); + } + + private async getConnectionString(connectionUri: string): Promise { + const getAzdataApi = await utils.getAzdataApi(); + if (getAzdataApi) { + const connection = await getAzdataApi.connection.getConnection(connectionUri); + if (connection) { + return await getAzdataApi.connection.getConnectionString(connection.connectionId, true); + } + } + // TODO: vscode connections string + + return undefined; + + } + + // Connects to a database + private async connectToDatabase(profile: ILocalDbSetting, savePassword: boolean, database: string): Promise { + const getAzdataApi = await utils.getAzdataApi(); + const vscodeMssqlApi = getAzdataApi ? undefined : await utils.getVscodeMssqlApi(); + if (getAzdataApi) { + const connectionProfile = { + password: profile.password, + serverName: `${profile.serverName},${profile.port}`, + database: database, + savePassword: savePassword, + userName: profile.userName, + providerName: 'MSSQL', + saveProfile: false, + id: '', + connectionName: `${constants.connectionNamePrefix} ${profile.dbName}`, + options: [], + authenticationType: 'SqlLogin' + }; + return await getAzdataApi.connection.connect(connectionProfile, false, false); + } else if (vscodeMssqlApi) { + const connectionProfile = { + password: profile.password, + server: `${profile.serverName}`, + port: profile.port, + database: database, + savePassword: savePassword, + user: profile.userName, + authenticationType: 'SqlLogin', + encrypt: false, + connectTimeout: 30, + applicationName: 'SQL Database Project', + accountId: undefined, + azureAccountToken: undefined, + applicationIntent: undefined, + attachDbFilename: undefined, + connectRetryCount: undefined, + connectRetryInterval: undefined, + connectionString: undefined, + currentLanguage: undefined, + email: undefined, + failoverPartner: undefined, + loadBalanceTimeout: undefined, + maxPoolSize: undefined, + minPoolSize: undefined, + multiSubnetFailover: undefined, + multipleActiveResultSets: undefined, + packetSize: undefined, + persistSecurityInfo: undefined, + pooling: undefined, + replication: undefined, + trustServerCertificate: undefined, + typeSystemVersion: undefined, + workstationId: undefined + }; + let connectionUrl = await vscodeMssqlApi.connect(connectionProfile); + return connectionUrl; + } else { + return undefined; + } + } + + // Validates the connection result. If using azdata API, verifies connection was successful and connection id is returns + // If using vscode API, verifies the connection url is returns + private async validateConnection(connection: ConnectionResult | string | undefined): Promise { + const getAzdataApi = await utils.getAzdataApi(); + if (!connection) { + return { validated: false, errorMessage: constants.connectionFailedError('No result returned') }; + } else if (getAzdataApi) { + const connectionResult = connection; + if (connectionResult) { + const connected = connectionResult !== undefined && connectionResult.connected && connectionResult.connectionId !== undefined; + return { validated: connected, errorMessage: connected ? '' : constants.connectionFailedError(connectionResult?.errorMessage) }; + } else { + return { validated: false, errorMessage: constants.connectionFailedError('') }; + } + } else { + return { validated: connection !== undefined, errorMessage: constants.connectionFailedError('') }; + } + } + + // Formats connection result to string to be able to add to log + private async formatConnectionResult(connection: ConnectionResult | string | undefined): Promise { + const getAzdataApi = await utils.getAzdataApi(); + const connectionResult = connection !== undefined && getAzdataApi ? connection : undefined; + return connectionResult ? connectionResult.connectionId : connection; + } + + public async getConnection(profile: ILocalDbSetting, savePassword: boolean, database: string, timeoutInSeconds: number = 5): Promise { + const getAzdataApi = await utils.getAzdataApi(); + let connection = await utils.retry( + constants.connectingToSqlServerOnDockerMessage, + async () => { + return await this.connectToDatabase(profile, savePassword, database); + }, + this.validateConnection, + this.formatConnectionResult, + this._outputChannel, + 5, timeoutInSeconds); + + if (connection) { + const connectionResult = connection; + if (getAzdataApi) { + return await getAzdataApi.connection.getUriForConnection(connectionResult.connectionId); + } else { + return connection; + } + } + + return undefined; + } + + private async executeTask(taskName: string, task: () => Promise): Promise { + const getAzdataApi = await utils.getAzdataApi(); + if (getAzdataApi) { + return new Promise((resolve, reject) => { + let msgTaskName = taskName; + getAzdataApi!.tasks.startBackgroundOperation({ + displayName: msgTaskName, + description: msgTaskName, + isCancelable: false, + operation: async op => { + try { + let result: T = await task(); + + op.updateStatus(getAzdataApi!.TaskStatus.Succeeded); + resolve(result); + } catch (error) { + let errorMsg = constants.taskFailedError(taskName, error ? error.message : ''); + op.updateStatus(getAzdataApi!.TaskStatus.Failed, errorMsg); + reject(errorMsg); + } + } + }); + }); + } else { + return await task(); + } + } + + private logToOutput(message: string): void { + this._outputChannel.appendLine(message); + } + + // Creates command file and docker file needed for deploy operation + private async createCommands(mssqlFolderPath: string, commandsFolderPath: string, dockerFilePath: string, startFilePath: string, imageLabel: string): Promise { + // Create mssql folders if doesn't exist + // + await utils.createFolderIfNotExist(mssqlFolderPath); + await utils.createFolderIfNotExist(commandsFolderPath); + + // Start command + // + await this.createFile(startFilePath, 'echo starting the container!'); + if (os.platform() !== 'win32') { + await utils.executeCommand(`chmod +x ${startFilePath}`, this._outputChannel); + } + + // Create the Dockerfile + // + await this.createFile(dockerFilePath, + ` +FROM ${constants.dockerBaseImage} +ENV ACCEPT_EULA=Y +ENV MSSQL_PID=Developer +LABEL ${imageLabel} +RUN mkdir -p /opt/sqlproject +COPY ${constants.mssqlFolderName}/${constants.commandsFolderName}/ /opt/commands +RUN ["/bin/bash", "/opt/commands/start.sh"] +`); + } + + private async createFile(filePath: string, content: string): Promise { + this.logToOutput(`Creating file ${filePath}, content:${content}`); + await fse.writeFile(filePath, content); + } + + public async cleanDockerObjects(commandToGetObjects: string, commandsToClean: string[]): Promise { + const currentIds = await utils.executeCommand(commandToGetObjects, this._outputChannel); + if (currentIds) { + const ids = currentIds.split(/\r?\n/); + for (let index = 0; index < ids.length; index++) { + const id = ids[index]; + if (id) { + for (let commandId = 0; commandId < commandsToClean.length; commandId++) { + const command = commandsToClean[commandId]; + await utils.executeCommand(`${command} ${id}`, this._outputChannel); + } + } + } + } + } +} diff --git a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts new file mode 100644 index 0000000000..3ae12f8fc8 --- /dev/null +++ b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as sinon from 'sinon'; +import * as baselines from '../baselines/baselines'; +import * as testUtils from '../testUtils'; +import { DeployService } from '../../models/deploy/deployService'; +import { Project } from '../../models/project'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as childProcess from 'child_process'; +import { AppSettingType, IDeployProfile } from '../../models/deploy/deployProfile'; +let fse = require('fs-extra'); +let path = require('path'); + +export interface TestContext { + outputChannel: vscode.OutputChannel; +} + +export const mockConnectionResult: azdata.ConnectionResult = { + connected: true, + connectionId: 'id', + errorMessage: '', + errorCode: 0 +}; + +export const mockFailedConnectionResult: azdata.ConnectionResult = { + connected: false, + connectionId: 'id', + errorMessage: 'Failed to connect', + errorCode: 0 +}; + +export function createContext(): TestContext { + return { + outputChannel: { + name: '', + append: () => { }, + appendLine: () => { }, + clear: () => { }, + show: () => { }, + hide: () => { }, + dispose: () => { } + } + }; +} + +let sandbox: sinon.SinonSandbox; + +describe('deploy service', function (): void { + before(async function (): Promise { + await baselines.loadBaselines(); + }); + afterEach(function () { + sandbox.restore(); + sinon.restore(); + }); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + it('Should deploy a database to docker container successfully', async function (): Promise { + const testContext = createContext(); + const deployProfile: IDeployProfile = { + appSettingType: AppSettingType.AzureFunction, + appSettingFile: '', + deploySettings: undefined, + envVariableName: '', + localDbSetting: { + dbName: 'test', + password: 'PLACEHOLDER', + port: 1433, + serverName: 'localhost', + userName: 'sa' + } + }; + const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); + const deployService = new DeployService(testContext.outputChannel); + sandbox.stub(azdata.connection, 'connect').returns(Promise.resolve(mockConnectionResult)); + sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection')); + sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough(); + sandbox.stub(childProcess, 'exec').yields(undefined, 'id'); + let connection = await deployService.deploy(deployProfile, project1); + should(connection).equals('connection'); + + }); + + it('Should retry connecting to the server', async function (): Promise { + const testContext = createContext(); + const localDbSettings = { + dbName: 'test', + password: 'PLACEHOLDER', + port: 1433, + serverName: 'localhost', + userName: 'sa' + }; + + const deployService = new DeployService(testContext.outputChannel); + let connectionStub = sandbox.stub(azdata.connection, 'connect'); + connectionStub.onFirstCall().returns(Promise.resolve(mockFailedConnectionResult)); + connectionStub.onSecondCall().returns(Promise.resolve(mockConnectionResult)); + sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection')); + sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough(); + sandbox.stub(childProcess, 'exec').yields(undefined, 'id'); + let connection = await deployService.getConnection(localDbSettings, false, 'master', 2); + should(connection).equals('connection'); + }); + + it('Should update app settings successfully', async function (): Promise { + const testContext = createContext(); + const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); + const jsondData = + { + IsEncrypted: false, + Values: { + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + FUNCTIONS_WORKER_RUNTIME: 'dotnet' + } + }; + let settingContent = JSON.stringify(jsondData, undefined, 4); + const expected = + { + IsEncrypted: false, + Values: { + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + FUNCTIONS_WORKER_RUNTIME: 'dotnet', + SQLConnectionString: 'Data Source=localhost,1433;Initial Catalog=test;User id=sa;Password=PLACEHOLDER;' + } + }; + const filePath = path.join(project1.projectFolderPath, 'local.settings.json'); + await fse.writeFile(filePath, settingContent); + + const deployProfile: IDeployProfile = { + appSettingType: AppSettingType.AzureFunction, + appSettingFile: filePath, + deploySettings: undefined, + envVariableName: 'SQLConnectionString', + localDbSetting: { + dbName: 'test', + password: 'PLACEHOLDER', + port: 1433, + serverName: 'localhost', + userName: 'sa' + } + }; + + const deployService = new DeployService(testContext.outputChannel); + sandbox.stub(childProcess, 'exec').yields(undefined, 'id'); + await deployService.updateAppSettings(deployProfile); + let newContent = JSON.parse(fse.readFileSync(filePath, 'utf8')); + should(newContent).deepEqual(expected); + + }); + + it('Should update app settings using connection uri if there are no local settings', async function (): Promise { + const testContext = createContext(); + const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); + const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); + const jsondData = + { + IsEncrypted: false, + Values: { + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + FUNCTIONS_WORKER_RUNTIME: 'dotnet' + } + }; + let settingContent = JSON.stringify(jsondData, undefined, 4); + const expected = + { + IsEncrypted: false, + Values: { + AzureWebJobsStorage: 'UseDevelopmentStorage=true', + FUNCTIONS_WORKER_RUNTIME: 'dotnet', + SQLConnectionString: 'connectionString' + } + }; + const filePath = path.join(project1.projectFolderPath, 'local.settings.json'); + await fse.writeFile(filePath, settingContent); + + const deployProfile: IDeployProfile = { + appSettingType: AppSettingType.AzureFunction, + appSettingFile: filePath, + deploySettings: { + connectionUri: 'connection', + databaseName: 'test', + serverName: 'test' + }, + envVariableName: 'SQLConnectionString', + localDbSetting: undefined + }; + + const deployService = new DeployService(testContext.outputChannel); + let connection = new azdata.connection.ConnectionProfile(); + sandbox.stub(azdata.connection, 'getConnection').returns(Promise.resolve(connection)); + sandbox.stub(childProcess, 'exec').yields(undefined, 'id'); + sandbox.stub(azdata.connection, 'getConnectionString').returns(Promise.resolve('connectionString')); + await deployService.updateAppSettings(deployProfile); + let newContent = JSON.parse(fse.readFileSync(filePath, 'utf8')); + should(newContent).deepEqual(expected); + + }); + + it('Should clean a list of docker images successfully', async function (): Promise { + const testContext = createContext(); + const deployService = new DeployService(testContext.outputChannel); + + let process = sandbox.stub(childProcess, 'exec').yields(undefined, ` + id + id2 + id3`); + + await deployService.cleanDockerObjects(`docker ps -q -a --filter label=test`, ['docker stop', 'docker rm']); + should(process.calledThrice); + }); +}); + diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index a1848aa58c..f323333beb 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -17,16 +17,18 @@ import { Project } from '../../models/project'; import { ProjectsController } from '../../controllers/projectController'; import { IDeploySettings } from '../../models/IDeploySettings'; import { emptySqlDatabaseProjectTypeId } from '../../common/constants'; -import { mockDacFxOptionsResult } from '../testContext'; +import { createContext, mockDacFxOptionsResult, TestContext } from '../testContext'; +let testContext: TestContext; describe('Publish Database Dialog', () => { before(async function (): Promise { await templates.loadTemplates(path.join(__dirname, '..', '..', '..', 'resources', 'templates')); await baselines.loadBaselines(); + testContext = createContext(); }); it('Should open dialog successfully ', async function (): Promise { - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); const projFilePath = await projController.createNewProject({ @@ -43,7 +45,7 @@ describe('Publish Database Dialog', () => { }); it('Should create default database name correctly ', async function (): Promise { - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const projFolder = `TestProject_${new Date().getTime()}`; const projFileDir = path.join(os.tmpdir(), projFolder); diff --git a/extensions/sql-database-projects/src/test/netCoreTool.test.ts b/extensions/sql-database-projects/src/test/netCoreTool.test.ts index c0ee32bc15..7d1d957ca9 100644 --- a/extensions/sql-database-projects/src/test/netCoreTool.test.ts +++ b/extensions/sql-database-projects/src/test/netCoreTool.test.ts @@ -13,17 +13,24 @@ import { NetCoreTool, DBProjectConfigurationKey, NetCoreInstallLocationKey, NetC import { getQuotedPath } from '../common/utils'; import { isNullOrUndefined } from 'util'; import { generateTestFolderPath } from './testUtils'; +import { createContext, TestContext } from './testContext'; + +let testContext: TestContext; describe('NetCoreTool: Net core tests', function (): void { afterEach(function (): void { sinon.restore(); }); + beforeEach(function (): void { + testContext = createContext(); + }); + it('Should override dotnet default value with settings', async function (): Promise { try { // update settings and validate await vscode.workspace.getConfiguration(DBProjectConfigurationKey).update(NetCoreInstallLocationKey, 'test value path', true); - const netcoreTool = new NetCoreTool(); + const netcoreTool = new NetCoreTool(testContext.outputChannel); sinon.stub(netcoreTool, 'showInstallDialog').returns(Promise.resolve()); should(netcoreTool.netcoreInstallLocation).equal('test value path'); // the path in settings should be taken should(await netcoreTool.findOrInstallNetCore()).equal(false); // dotnet can not be present at dummy path in settings @@ -35,7 +42,7 @@ describe('NetCoreTool: Net core tests', function (): void { }); it('Should find right dotnet default paths', async function (): Promise { - const netcoreTool = new NetCoreTool(); + const netcoreTool = new NetCoreTool(testContext.outputChannel); sinon.stub(netcoreTool, 'showInstallDialog').returns(Promise.resolve()); await netcoreTool.findOrInstallNetCore(); @@ -53,7 +60,7 @@ describe('NetCoreTool: Net core tests', function (): void { }); it('should run a command successfully', async function (): Promise { - const netcoreTool = new NetCoreTool(); + const netcoreTool = new NetCoreTool(testContext.outputChannel); const dummyFile = path.join(await generateTestFolderPath(), 'dummy.dacpac'); const outputChannel = vscode.window.createOutputChannel('db project test'); diff --git a/extensions/sql-database-projects/src/test/projectController.test.ts b/extensions/sql-database-projects/src/test/projectController.test.ts index 93a81eeb45..e7fce92cd1 100644 --- a/extensions/sql-database-projects/src/test/projectController.test.ts +++ b/extensions/sql-database-projects/src/test/projectController.test.ts @@ -51,7 +51,7 @@ describe('ProjectsController', function (): void { describe('project controller operations', function (): void { describe('Project file operations and prompting', function (): void { it('Should create new sqlproj file with correct values', async function (): Promise { - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); const projFilePath = await projController.createNewProject({ @@ -67,7 +67,7 @@ describe('ProjectsController', function (): void { }); it('Should create new sqlproj file with correct specified target platform', async function (): Promise { - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const projFileDir = path.join(os.tmpdir(), `TestProject_${new Date().getTime()}`); const projTargetPlatform = SqlTargetPlatform.sqlAzure; // default is SQL Server 2019 @@ -89,7 +89,7 @@ describe('ProjectsController', function (): void { for (const name of ['', ' ', undefined]) { const showInputBoxStub = sinon.stub(vscode.window, 'showInputBox').resolves(name); const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = new Project('FakePath'); should(project.files.length).equal(0); @@ -105,7 +105,7 @@ describe('ProjectsController', function (): void { const tableName = 'table1'; sinon.stub(vscode.window, 'showInputBox').resolves(tableName); const spy = sinon.spy(vscode.window, 'showErrorMessage'); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); should(project.files.length).equal(0, 'There should be no files'); @@ -121,7 +121,7 @@ describe('ProjectsController', function (): void { const folderName = 'folder1'; const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); const projectRoot = new ProjectRootTreeItem(project); @@ -141,7 +141,7 @@ describe('ProjectsController', function (): void { const folderName = 'folder1'; const stub = sinon.stub(vscode.window, 'showInputBox').resolves(folderName); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = await testUtils.createTestProject(baselines.openProjectFileBaseline); const projectRoot = new ProjectRootTreeItem(project); @@ -180,7 +180,7 @@ describe('ProjectsController', function (): void { const setupResult = await setupDeleteExcludeTest(proj); const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0]) /* LowerFolder */); await projController.delete(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!)); @@ -206,7 +206,7 @@ describe('ProjectsController', function (): void { it('Should delete database references', async function (): Promise { // setup - openProject baseline has a system db reference to master const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); sinon.stub(vscode.window, 'showWarningMessage').returns(Promise.resolve(constants.yesString)); // add dacpac reference @@ -241,7 +241,7 @@ describe('ProjectsController', function (): void { const setupResult = await setupDeleteExcludeTest(proj); const scriptEntry = setupResult[0], projTreeRoot = setupResult[1], preDeployEntry = setupResult[2], postDeployEntry = setupResult[3], noneEntry = setupResult[4]; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); await projController.exclude(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0]) /* LowerFolder */); await projController.exclude(createWorkspaceTreeItem(projTreeRoot.children.find(x => x.friendlyName === 'anotherScript.sql')!)); @@ -272,7 +272,7 @@ describe('ProjectsController', function (): void { const upperFolder = projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!; const lowerFolder = upperFolder.children.find(x => x.friendlyName === 'LowerFolder')!; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); // Exclude files under LowerFolder await projController.exclude(createWorkspaceTreeItem(lowerFolder.children.find(x => x.friendlyName === 'someScript.sql')!)); @@ -296,7 +296,7 @@ describe('ProjectsController', function (): void { const folderPath = await testUtils.generateTestFolderPath(); const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline, folderPath); const treeProvider = new SqlDatabaseProjectTreeViewProvider(); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = await Project.openProject(vscode.Uri.file(sqlProjPath).fsPath); treeProvider.load([project]); @@ -319,7 +319,7 @@ describe('ProjectsController', function (): void { const preDeployScriptName = 'PreDeployScript1.sql'; const postDeployScriptName = 'PostDeployScript1.sql'; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project = await testUtils.createTestProject(baselines.newProjectFileBaseline); sinon.stub(vscode.window, 'showInputBox').resolves(preDeployScriptName); @@ -337,7 +337,7 @@ describe('ProjectsController', function (): void { it('Should change target platform', async function (): Promise { sinon.stub(vscode.window, 'showQuickPick').resolves({ label: SqlTargetPlatform.sqlAzure }); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); const project = await Project.openProject(sqlProjPath); should(project.getProjectTargetVersion()).equal(constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlServer2019)); @@ -447,7 +447,7 @@ describe('ProjectsController', function (): void { it('Should create list of all files and folders correctly', async function (): Promise { const testFolderPath = await testUtils.createDummyFileStructure(); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const fileList = await projController.generateList(testFolderPath); should(fileList.length).equal(15); // Parent folder + 2 files under parent folder + 2 directories with 5 files each @@ -459,7 +459,7 @@ describe('ProjectsController', function (): void { let testFolderPath = await testUtils.generateTestFolderPath(); testFolderPath += '_nonexistentFolder'; // Modify folder path to point to a nonexistent location - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); await projController.generateList(testFolderPath); should(spy.calledOnce).be.true('showErrorMessage should have been called'); @@ -521,7 +521,7 @@ describe('ProjectsController', function (): void { let importPath; let model: ImportDataModel = { connectionUri: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['file'] }; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); projController.setFilePath(model); importPath = model.filePath; @@ -534,7 +534,7 @@ describe('ProjectsController', function (): void { let importPath; let model: ImportDataModel = { connectionUri: 'My Id', database: 'My Database', projName: projectName, filePath: folderPath, version: '1.0.0.0', extractTarget: mssql.ExtractTarget['schemaObjectType'] }; - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); projController.setFilePath(model); importPath = model.filePath; @@ -591,7 +591,7 @@ describe('ProjectsController', function (): void { it('Should not allow adding circular project references', async function (): Promise { const projPath1 = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); const projPath2 = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project1 = await Project.openProject(vscode.Uri.file(projPath1).fsPath); const project2 = await Project.openProject(vscode.Uri.file(projPath2).fsPath); @@ -623,7 +623,7 @@ describe('ProjectsController', function (): void { it('Should add dacpac references as relative paths', async function (): Promise { const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); - const projController = new ProjectsController(); + const projController = new ProjectsController(testContext.outputChannel); const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath); const showErrorMessageSpy = sinon.spy(vscode.window, 'showErrorMessage'); diff --git a/extensions/sql-database-projects/src/test/testContext.ts b/extensions/sql-database-projects/src/test/testContext.ts index b747bde718..1e42a695be 100644 --- a/extensions/sql-database-projects/src/test/testContext.ts +++ b/extensions/sql-database-projects/src/test/testContext.ts @@ -12,6 +12,7 @@ import * as mssql from '../../../mssql/src/mssql'; export interface TestContext { context: vscode.ExtensionContext; dacFxService: TypeMoq.IMock; + outputChannel: vscode.OutputChannel; } export const mockDacFxResult = { @@ -147,7 +148,16 @@ export function createContext(): TestContext { secrets: undefined as any, extension: undefined as any }, - dacFxService: TypeMoq.Mock.ofType(MockDacFxService) + dacFxService: TypeMoq.Mock.ofType(MockDacFxService), + outputChannel: { + name: '', + append: () => { }, + appendLine: () => { }, + clear: () => { }, + show: () => { }, + hide: () => { }, + dispose: () => { } + } }; } diff --git a/extensions/sql-database-projects/src/tools/netcoreTool.ts b/extensions/sql-database-projects/src/tools/netcoreTool.ts index 00888b8471..d8b78c8ff4 100644 --- a/extensions/sql-database-projects/src/tools/netcoreTool.ts +++ b/extensions/sql-database-projects/src/tools/netcoreTool.ts @@ -12,7 +12,7 @@ import * as semver from 'semver'; import { isNullOrUndefined } from 'util'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DoNotAskAgain, InstallNetCore, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, projectsOutputChannel, UpdateNetCoreLocation } from '../common/constants'; +import { DoNotAskAgain, InstallNetCore, NetCoreInstallationConfirmation, NetCoreSupportedVersionInstallationConfirmation, UpdateNetCoreLocation } from '../common/constants'; import * as utils from '../common/utils'; const localize = nls.loadMessageBundle(); @@ -39,10 +39,8 @@ export interface DotNetCommandOptions { commandTitle?: string; argument?: string; } - export class NetCoreTool { - private static _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(projectsOutputChannel); private osPlatform: string = os.platform(); private netCoreSdkInstalledVersion: string | undefined; private netCoreInstallState: netCoreInstallState = netCoreInstallState.netCoreVersionSupported; @@ -63,6 +61,10 @@ export class NetCoreTool { return true; } + + constructor(private _outputChannel: vscode.OutputChannel) { + } + public async showInstallDialog(): Promise { let result; if (this.netCoreInstallState === netCoreInstallState.netCoreNotPresent) { @@ -183,7 +185,7 @@ export class NetCoreTool { public async runDotnetCommand(options: DotNetCommandOptions): Promise { if (options && options.commandTitle !== undefined && options.commandTitle !== null) { - NetCoreTool._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`); + this._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`); } if (!(await this.findOrInstallNetCore())) { @@ -198,9 +200,9 @@ export class NetCoreTool { const command = dotnetPath + ' ' + options.argument; try { - return await this.runStreamedCommand(command, NetCoreTool._outputChannel, options); + return await this.runStreamedCommand(command, this._outputChannel, options); } catch (error) { - NetCoreTool._outputChannel.append(localize('sqlDatabaseProject.RunCommand.ErroredOut', "\t>>> {0} … errored out: {1}", command, utils.getErrorMessage(error))); //errors are localized in our code where emitted, other errors are pass through from external components that are not easily localized + this._outputChannel.append(localize('sqlDatabaseProject.RunCommand.ErroredOut', "\t>>> {0} … errored out: {1}", command, utils.getErrorMessage(error))); //errors are localized in our code where emitted, other errors are pass through from external components that are not easily localized throw error; } } diff --git a/extensions/sql-database-projects/src/typings/vscode-mssql.d.ts b/extensions/sql-database-projects/src/typings/vscode-mssql.d.ts index 4d7c826581..96c73ec415 100644 --- a/extensions/sql-database-projects/src/typings/vscode-mssql.d.ts +++ b/extensions/sql-database-projects/src/typings/vscode-mssql.d.ts @@ -46,7 +46,7 @@ declare module 'vscode-mssql' { * @param connectionInfo The connection info * @returns The URI associated with this connection */ - connect(connectionInfo: IConnectionInfo): Promise; + connect(connectionInfo: IConnectionInfo): Promise; /** * Lists the databases for a given connection. Must be given an already-opened connection to succeed. @@ -68,7 +68,7 @@ declare module 'vscode-mssql' { /** * Information about a database connection */ - export interface IConnectionInfo { + export interface IConnectionInfo { /** * server name */ @@ -92,12 +92,12 @@ declare module 'vscode-mssql' { /** * email */ - email: string; + email: string | undefined; /** * accountId */ - accountId: string; + accountId: string | undefined; /** * The port number to connect to. @@ -112,7 +112,7 @@ declare module 'vscode-mssql' { /** * Gets or sets the azure account token to use. */ - azureAccountToken: string; + azureAccountToken: string | undefined; /** * Gets or sets a Boolean value that indicates whether SQL Server uses SSL encryption for all data sent between the client and server if @@ -123,109 +123,109 @@ declare module 'vscode-mssql' { /** * Gets or sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust. */ - trustServerCertificate: boolean; + trustServerCertificate: boolean | undefined; /** * Gets or sets a Boolean value that indicates if security-sensitive information, such as the password, is not returned as part of the connection * if the connection is open or has ever been in an open state. */ - persistSecurityInfo: boolean; + persistSecurityInfo: boolean | undefined; /** * Gets or sets the length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error. */ - connectTimeout: number; + connectTimeout: number | undefined; /** * The number of reconnections attempted after identifying that there was an idle connection failure. */ - connectRetryCount: number; + connectRetryCount: number | undefined; /** * Amount of time (in seconds) between each reconnection attempt after identifying that there was an idle connection failure. */ - connectRetryInterval: number; + connectRetryInterval: number | undefined; /** * Gets or sets the name of the application associated with the connection string. */ - applicationName: string; + applicationName: string | undefined; /** * Gets or sets the name of the workstation connecting to SQL Server. */ - workstationId: string; + workstationId: string | undefined; /** * Declares the application workload type when connecting to a database in an SQL Server Availability Group. */ - applicationIntent: string; + applicationIntent: string | undefined; /** * Gets or sets the SQL Server Language record name. */ - currentLanguage: string; + currentLanguage: string | undefined; /** * Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested. */ - pooling: boolean; + pooling: boolean | undefined; /** * Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string. */ - maxPoolSize: number; + maxPoolSize: number | undefined; /** * Gets or sets the minimum number of connections allowed in the connection pool for this specific connection string. */ - minPoolSize: number; + minPoolSize: number | undefined; /** * Gets or sets the minimum time, in seconds, for the connection to live in the connection pool before being destroyed. */ - loadBalanceTimeout: number; + loadBalanceTimeout: number | undefined; /** * Gets or sets a Boolean value that indicates whether replication is supported using the connection. */ - replication: boolean; + replication: boolean | undefined; /** * Gets or sets a string that contains the name of the primary data file. This includes the full path name of an attachable database. */ - attachDbFilename: string; + attachDbFilename: string | undefined; /** * Gets or sets the name or address of the partner server to connect to if the primary server is down. */ - failoverPartner: string; + failoverPartner: string | undefined; /** * If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting MultiSubnetFailover=true * provides faster detection of and connection to the (currently) active server. */ - multiSubnetFailover: boolean; + multiSubnetFailover: boolean | undefined; /** * When true, an application can maintain multiple active result sets (MARS). */ - multipleActiveResultSets: boolean; + multipleActiveResultSets: boolean | undefined; /** * Gets or sets the size in bytes of the network packets used to communicate with an instance of SQL Server. */ - packetSize: number; + packetSize: number | undefined; /** * Gets or sets a string value that indicates the type system the application expects. */ - typeSystemVersion: string; + typeSystemVersion: string | undefined; /** * Gets or sets the connection string to use for this connection. */ - connectionString: string; + connectionString: string | undefined; } export const enum ExtractTarget { diff --git a/extensions/sql-database-projects/yarn.lock b/extensions/sql-database-projects/yarn.lock index 3ebcf69f3b..b1220e4d08 100644 --- a/extensions/sql-database-projects/yarn.lock +++ b/extensions/sql-database-projects/yarn.lock @@ -269,11 +269,23 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@types/fs-extra@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-5.1.0.tgz#2a325ef97901504a3828718c390d34b8426a10a1" + integrity sha512-AInn5+UBFIK9FK5xc9yP5e3TQSPNNgjHByqYcj9g5elVBnDQcQL7PlO1CIRy2gWlbwK7UPYqi7vRvFA44dCmYQ== + dependencies: + "@types/node" "*" + "@types/mocha@^5.2.5": version "5.2.7" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/node@*": + version "16.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.0.tgz#0d5685f85066f94e97f19e8a67fe003c5fadacc4" + integrity sha512-OyiZPohMMjZEYqcVo/UJ04GyAxXOJEZO/FpzyXxcH4r/ArrVoXHf4MbUrkLp0Tz7/p1mMKpo5zJ6ZHl8XBNthQ== + "@types/sinon@^9.0.4": version "9.0.4" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" @@ -574,11 +586,25 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fs-extra@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-5.0.0.tgz#414d0110cdd06705734d055652c5411260c31abd" + integrity sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +generate-password@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/generate-password/-/generate-password-1.6.0.tgz#9dff4fcca7f831e6832d464509910fab333d0323" + integrity sha512-YUJTQkApkLT/fru0QdYWP0lVZdPKhF5kXCP24sgI4gR/vFMJFopCj5t1+9FAKIYcML/nxzx2PMkA1ymO1FC+tQ== + gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -620,6 +646,11 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== +graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -768,6 +799,13 @@ json5@^2.1.2: dependencies: minimist "^1.2.5" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + just-extend@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" @@ -1175,6 +1213,11 @@ typescript@^2.6.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + vscode-extension-telemetry@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.6.tgz#048b70c93243413036a8315cda493b8e7342980c"