SQL Database Project - Deploy db to docker (#16406)

Added a new command to deploy the project to docker
This commit is contained in:
Leila Lali
2021-08-12 13:24:16 -07:00
committed by GitHub
parent 57e11c7b5c
commit 32a71a2de6
18 changed files with 1111 additions and 92 deletions

View File

@@ -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");

View File

@@ -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<void> {
try {
await fse.mkdir(folderPath);
} catch {
// Ignore if failed
}
}
export async function executeCommand(cmd: string, outputChannel: vscode.OutputChannel, timeout: number = 5 * 60 * 1000): Promise<string> {
return new Promise<string>((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<T>(
name: string,
attempt: () => Promise<T>,
verify: (result: T) => Promise<ValidationResult>,
formatResult: (result: T) => Promise<string>,
outputChannel: vscode.OutputChannel,
numberOfAttempts: number = 10,
waitInSeconds: number = 2
): Promise<T | undefined> {
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;
}

View File

@@ -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); });

View File

@@ -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<string, vscode.FileSystemWatcher>();
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<void> {
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

View File

@@ -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<IDeployProfile | undefined> {
// 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
};
}

View File

@@ -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<void> {
export async function getPublishDatabaseSettings(project: Project, promptForConnection: boolean = true): Promise<IDeploySettings | undefined> {
// 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<void> {
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);
}
}

View File

@@ -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,
}

View File

@@ -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 <string>appSettingContent?.Values['FUNCTIONS_WORKER_RUNTIME'];
default:
}
return undefined;
}
public async updateAppSettings(profile: IDeployProfile): Promise<void> {
// 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<string, string> = {
'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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
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<ConnectionResult | string | undefined> {
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<utils.ValidationResult> {
const getAzdataApi = await utils.getAzdataApi();
if (!connection) {
return { validated: false, errorMessage: constants.connectionFailedError('No result returned') };
} else if (getAzdataApi) {
const connectionResult = <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<string> {
const getAzdataApi = await utils.getAzdataApi();
const connectionResult = connection !== undefined && getAzdataApi ? <ConnectionResult>connection : undefined;
return connectionResult ? connectionResult.connectionId : <string>connection;
}
public async getConnection(profile: ILocalDbSetting, savePassword: boolean, database: string, timeoutInSeconds: number = 5): Promise<string | undefined> {
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 = <ConnectionResult>connection;
if (getAzdataApi) {
return await getAzdataApi.connection.getUriForConnection(connectionResult.connectionId);
} else {
return <string>connection;
}
}
return undefined;
}
private async executeTask<T>(taskName: string, task: () => Promise<T>): Promise<T> {
const getAzdataApi = await utils.getAzdataApi();
if (getAzdataApi) {
return new Promise<T>((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<void> {
// 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<void> {
this.logToOutput(`Creating file ${filePath}, content:${content}`);
await fse.writeFile(filePath, content);
}
public async cleanDockerObjects(commandToGetObjects: string, commandsToClean: string[]): Promise<void> {
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);
}
}
}
}
}
}

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
});
});

View File

@@ -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<void> {
await templates.loadTemplates(path.join(__dirname, '..', '..', '..', 'resources', 'templates'));
await baselines.loadBaselines();
testContext = createContext();
});
it('Should open dialog successfully ', async function (): Promise<void> {
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<void> {
const projController = new ProjectsController();
const projController = new ProjectsController(testContext.outputChannel);
const projFolder = `TestProject_${new Date().getTime()}`;
const projFileDir = path.join(os.tmpdir(), projFolder);

View File

@@ -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<void> {
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<void> {
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<void> {
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');

View File

@@ -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<void> {
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<void> {
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<void> {
// 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(<any>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(<FolderNode>projTreeRoot.children.find(x => x.friendlyName === 'UpperFolder')!.children[0]) /* LowerFolder */);
await projController.exclude(createWorkspaceTreeItem(<FileNode>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(<FileNode>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<void> {
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<void> {
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<void> {
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<void> {
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');

View File

@@ -12,6 +12,7 @@ import * as mssql from '../../../mssql/src/mssql';
export interface TestContext {
context: vscode.ExtensionContext;
dacFxService: TypeMoq.IMock<mssql.IDacFxService>;
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: () => { }
}
};
}

View File

@@ -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<void> {
let result;
if (this.netCoreInstallState === netCoreInstallState.netCoreNotPresent) {
@@ -183,7 +185,7 @@ export class NetCoreTool {
public async runDotnetCommand(options: DotNetCommandOptions): Promise<string> {
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;
}
}

View File

@@ -46,7 +46,7 @@ declare module 'vscode-mssql' {
* @param connectionInfo The connection info
* @returns The URI associated with this connection
*/
connect(connectionInfo: IConnectionInfo): Promise<string>;
connect(connectionInfo: IConnectionInfo): Promise<string>;
/**
* 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 {