mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
sql proj - publish to docker improvements (#17124)
This commit is contained in:
@@ -131,18 +131,23 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
|
|||||||
// Deploy
|
// Deploy
|
||||||
export const selectPublishOption = localize('selectPublishOption', "Select where to publish the project to");
|
export const selectPublishOption = localize('selectPublishOption', "Select where to publish the project to");
|
||||||
export const publishToExistingServer = localize('publishToExistingServer', "Publish to existing server");
|
export const publishToExistingServer = localize('publishToExistingServer', "Publish to existing server");
|
||||||
export const publishToDockerContainer = localize('publishToDockerContainer', "Publish to docker container");
|
export const publishToDockerContainer = localize('publishToDockerContainer', "Publish to new server in a container");
|
||||||
export const enterPortNumber = localize('enterPortNumber', "Enter port number or press enter to use the default value");
|
export const enterPortNumber = localize('enterPortNumber', "Enter SQL server port number or press enter to use the default value");
|
||||||
export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name");
|
export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name");
|
||||||
export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template");
|
export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template");
|
||||||
export const enterPassword = localize('enterPassword', "Enter password");
|
export const enterPassword = localize('enterPassword', "Enter SQL Server admin password");
|
||||||
export const enterBaseImage = localize('enterBaseImage', "Enter the base SQL Server docker image or press enter to use the default value");
|
export const confirmPassword = localize('confirmPassword', "Confirm SQL server admin password");
|
||||||
|
export const selectBaseImage = localize('selectBaseImage', "Select the base SQL Server docker image");
|
||||||
|
export const invalidSQLPasswordMessage = localize('invalidSQLPassword', "SQL Server password doesn't meet the password complexity requirement. For more information see https://docs.microsoft.com/sql/relational-databases/security/password-policy");
|
||||||
|
export const passwordNotMatch = localize('passwordNotMatch', "SQL Server password doesn't match the confirmation password");
|
||||||
export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number");
|
export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number");
|
||||||
export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty");
|
export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty");
|
||||||
export const dockerImageLabelPrefix = 'source=sqldbproject';
|
export const dockerImageLabelPrefix = 'source=sqldbproject';
|
||||||
export const dockerImageNamePrefix = 'sqldbproject';
|
export const dockerImageNamePrefix = 'sqldbproject';
|
||||||
export const connectionNamePrefix = 'SQLDbProject';
|
export const connectionNamePrefix = 'SQLDbProject';
|
||||||
export const defaultDockerBaseImage = 'mcr.microsoft.com/mssql/server:2019-latest';
|
export const sqlServerDockerRegistry = 'mcr.microsoft.com';
|
||||||
|
export const sqlServerDockerRepository = 'mssql/server';
|
||||||
|
export const azureSqlEdgeDockerRepository = 'azure-sql-edge';
|
||||||
export const commandsFolderName = 'commands';
|
export const commandsFolderName = 'commands';
|
||||||
export const mssqlFolderName = '.mssql';
|
export const mssqlFolderName = '.mssql';
|
||||||
export const dockerFileName = 'Dockerfile';
|
export const dockerFileName = 'Dockerfile';
|
||||||
@@ -159,12 +164,13 @@ export const deployProjectSucceed = localize('deployProjectSucceed', "Database p
|
|||||||
export const cleaningDockerImagesMessage = localize('cleaningDockerImagesMessage', "Cleaning existing deployments...");
|
export const cleaningDockerImagesMessage = localize('cleaningDockerImagesMessage', "Cleaning existing deployments...");
|
||||||
export const creatingDeploymentSettingsMessage = localize('creatingDeploymentSettingsMessage', "Creating deployment settings ...");
|
export const creatingDeploymentSettingsMessage = localize('creatingDeploymentSettingsMessage', "Creating deployment settings ...");
|
||||||
export const runningDockerMessage = localize('runningDockerMessage', "Building and running the docker container ...");
|
export const runningDockerMessage = localize('runningDockerMessage', "Building and running the docker container ...");
|
||||||
|
export function dockerNotRunningError(error: string) { return localize('dockerNotRunningError', "Failed to verify docker. Please make sure docker is installed and running. Error: '{0}'", error || ''); }
|
||||||
export const dockerContainerNotRunningErrorMessage = localize('dockerContainerNotRunningErrorMessage', "Docker container is not running");
|
export const dockerContainerNotRunningErrorMessage = localize('dockerContainerNotRunningErrorMessage', "Docker container is not running");
|
||||||
export const dockerContainerFailedToRunErrorMessage = localize('dockerContainerFailedToRunErrorMessage', "Failed to run the docker container");
|
export const dockerContainerFailedToRunErrorMessage = localize('dockerContainerFailedToRunErrorMessage', "Failed to run the docker container");
|
||||||
export const connectingToSqlServerOnDockerMessage = localize('connectingToSqlServerOnDockerMessage', "Connecting to SQL Server on Docker");
|
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 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 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 publishToContainerFailed(errorMessage: string) { return localize('publishToContainerFailed', "Failed to publish to container. 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 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 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 connectionFailedError(error: string) { return localize('connectionFailedError', "Connection failed error: '{0}'", error); }
|
||||||
|
|||||||
@@ -422,10 +422,16 @@ export async function createFolderIfNotExist(folderPath: string): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeCommand(cmd: string, outputChannel: vscode.OutputChannel, timeout: number = 5 * 60 * 1000): Promise<string> {
|
export async function executeCommand(cmd: string, outputChannel: vscode.OutputChannel, sensitiveData: string[] = [], timeout: number = 5 * 60 * 1000): Promise<string> {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
if (outputChannel) {
|
if (outputChannel) {
|
||||||
outputChannel.appendLine(` > ${cmd}`);
|
let cmdOutputMessage = cmd;
|
||||||
|
|
||||||
|
sensitiveData.forEach(element => {
|
||||||
|
cmdOutputMessage = cmdOutputMessage.replace(element, '***');
|
||||||
|
});
|
||||||
|
|
||||||
|
outputChannel.appendLine(` > ${cmdOutputMessage}`);
|
||||||
}
|
}
|
||||||
let child = childProcess.exec(cmd, {
|
let child = childProcess.exec(cmd, {
|
||||||
timeout: timeout
|
timeout: timeout
|
||||||
@@ -535,3 +541,14 @@ export function validateSqlServerPortNumber(port: string | undefined): boolean {
|
|||||||
export function isEmptyString(password: string | undefined): boolean {
|
export function isEmptyString(password: string | undefined): boolean {
|
||||||
return password === undefined || password === '';
|
return password === undefined || password === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidSQLPassword(password: string, userName: string = 'sa'): boolean {
|
||||||
|
// Validate SQL Server password
|
||||||
|
const containsUserName = password && userName !== undefined && password.toUpperCase().includes(userName.toUpperCase());
|
||||||
|
// Instead of using one RegEx, I am separating it to make it more readable.
|
||||||
|
const hasUpperCase = /[A-Z]/.test(password) ? 1 : 0;
|
||||||
|
const hasLowerCase = /[a-z]/.test(password) ? 1 : 0;
|
||||||
|
const hasNumbers = /\d/.test(password) ? 1 : 0;
|
||||||
|
const hasNonAlphas = /\W/.test(password) ? 1 : 0;
|
||||||
|
return !containsUserName && password.length >= 8 && password.length <= 128 && (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphas >= 3);
|
||||||
|
}
|
||||||
|
|||||||
@@ -283,14 +283,14 @@ export class ProjectsController {
|
|||||||
}
|
}
|
||||||
void vscode.window.showInformationMessage(constants.deployProjectSucceed);
|
void vscode.window.showInformationMessage(constants.deployProjectSucceed);
|
||||||
} else {
|
} else {
|
||||||
void vscode.window.showErrorMessage(constants.deployProjectFailed(publishResult?.errorMessage || ''));
|
void vscode.window.showErrorMessage(constants.publishToContainerFailed(publishResult?.errorMessage || ''));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
void vscode.window.showErrorMessage(constants.deployProjectFailed(constants.deployProjectFailedMessage));
|
void vscode.window.showErrorMessage(constants.publishToContainerFailed(constants.deployProjectFailedMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
void vscode.window.showErrorMessage(constants.deployProjectFailed(utils.getErrorMessage(error)));
|
void vscode.window.showErrorMessage(constants.publishToContainerFailed(utils.getErrorMessage(error)));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
|
|||||||
title: constants.enterPassword,
|
title: constants.enterPassword,
|
||||||
ignoreFocusOut: true,
|
ignoreFocusOut: true,
|
||||||
value: password,
|
value: password,
|
||||||
validateInput: input => utils.isEmptyString(input) ? constants.valueCannotBeEmpty : undefined,
|
validateInput: input => !utils.isValidSQLPassword(input) ? constants.invalidSQLPasswordMessage : undefined,
|
||||||
password: true
|
password: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -104,15 +104,29 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let baseImage: string | undefined = '';
|
let confirmPassword: string | undefined = '';
|
||||||
baseImage = await vscode.window.showInputBox({
|
confirmPassword = await vscode.window.showInputBox({
|
||||||
title: constants.enterBaseImage,
|
title: constants.confirmPassword,
|
||||||
ignoreFocusOut: true,
|
ignoreFocusOut: true,
|
||||||
value: constants.defaultDockerBaseImage,
|
value: confirmPassword,
|
||||||
validateInput: input => utils.isEmptyString(input) ? constants.valueCannotBeEmpty : undefined
|
validateInput: input => input !== password ? constants.passwordNotMatch : undefined,
|
||||||
|
password: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Return when user hits escape
|
||||||
|
if (!confirmPassword) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseImage = await vscode.window.showQuickPick(
|
||||||
|
[
|
||||||
|
`${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`,
|
||||||
|
`${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`,
|
||||||
|
`${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`
|
||||||
|
],
|
||||||
|
{ title: constants.selectBaseImage, ignoreFocusOut: true });
|
||||||
|
|
||||||
// Return when user hits escape
|
// Return when user hits escape
|
||||||
if (!baseImage) {
|
if (!baseImage) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -134,9 +148,11 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localDbSetting && deploySettings) {
|
// Server name should be set to localhost
|
||||||
deploySettings.serverName = localDbSetting.serverName;
|
deploySettings.serverName = localDbSetting.serverName;
|
||||||
}
|
|
||||||
|
// Get the database name from deploy settings
|
||||||
|
localDbSetting.dbName = deploySettings.databaseName;
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -85,11 +85,23 @@ export class DeployService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async verifyDocker(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await utils.executeCommand(`docker version --format {{.Server.APIVersion}}`, this._outputChannel);
|
||||||
|
// TODO verify min version
|
||||||
|
} catch (error) {
|
||||||
|
throw Error(constants.dockerNotRunningError(utils.getErrorMessage(error)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async deploy(profile: IDeployProfile, project: Project): Promise<string | undefined> {
|
public async deploy(profile: IDeployProfile, project: Project): Promise<string | undefined> {
|
||||||
return await this.executeTask(constants.deployDbTaskName, async () => {
|
return await this.executeTask(constants.deployDbTaskName, async () => {
|
||||||
if (!profile.localDbSetting) {
|
if (!profile.localDbSetting) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.verifyDocker();
|
||||||
|
|
||||||
const projectName = project.projectFileName;
|
const projectName = project.projectFileName;
|
||||||
const imageLabel = `${constants.dockerImageLabelPrefix}_${projectName}`.toLocaleLowerCase();
|
const imageLabel = `${constants.dockerImageLabelPrefix}_${projectName}`.toLocaleLowerCase();
|
||||||
const imageName = `${constants.dockerImageNamePrefix}-${projectName}-${UUID.generateUuid()}`.toLocaleLowerCase();
|
const imageName = `${constants.dockerImageNamePrefix}-${projectName}-${UUID.generateUuid()}`.toLocaleLowerCase();
|
||||||
@@ -146,13 +158,18 @@ export class DeployService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildAndRunDockerContainer(dockerFilePath: string, imageName: string, root: string, profile: ILocalDbSetting, imageLabel: string): Promise<string | undefined> {
|
private async buildAndRunDockerContainer(dockerFilePath: string, imageName: string, root: string, profile: ILocalDbSetting, imageLabel: string): Promise<string | undefined> {
|
||||||
|
|
||||||
|
// Sensitive data to remove from output console
|
||||||
|
const sensitiveData = [profile.password];
|
||||||
|
|
||||||
|
// Running commands to build the docker image
|
||||||
this.logToOutput('Building docker image ...');
|
this.logToOutput('Building docker image ...');
|
||||||
await utils.executeCommand(`docker pull ${profile.dockerBaseImage}`, this._outputChannel);
|
await utils.executeCommand(`docker pull ${profile.dockerBaseImage}`, this._outputChannel);
|
||||||
await utils.executeCommand(`docker build -f ${dockerFilePath} -t ${imageName} ${root}`, this._outputChannel);
|
await utils.executeCommand(`docker build -f ${dockerFilePath} -t ${imageName} ${root}`, this._outputChannel);
|
||||||
await utils.executeCommand(`docker images --filter label=${imageLabel}`, this._outputChannel);
|
await utils.executeCommand(`docker images --filter label=${imageLabel}`, this._outputChannel);
|
||||||
|
|
||||||
this.logToOutput('Running docker container ...');
|
this.logToOutput('Running docker container ...');
|
||||||
await utils.executeCommand(`docker run -p ${profile.port}:1433 -e "MSSQL_SA_PASSWORD=${profile.password}" -d ${imageName}`, this._outputChannel);
|
await utils.executeCommand(`docker run -p ${profile.port}:1433 -e "MSSQL_SA_PASSWORD=${profile.password}" -d ${imageName}`, this._outputChannel, sensitiveData);
|
||||||
return await utils.executeCommand(`docker ps -q -a --filter label=${imageLabel} -q`, this._outputChannel);
|
return await utils.executeCommand(`docker ps -q -a --filter label=${imageLabel} -q`, this._outputChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +201,7 @@ export class DeployService {
|
|||||||
providerName: 'MSSQL',
|
providerName: 'MSSQL',
|
||||||
saveProfile: false,
|
saveProfile: false,
|
||||||
id: '',
|
id: '',
|
||||||
connectionName: `${constants.connectionNamePrefix} ${profile.dbName}`,
|
connectionName: `${constants.connectionNamePrefix} ${database}`,
|
||||||
options: [],
|
options: [],
|
||||||
authenticationType: 'SqlLogin'
|
authenticationType: 'SqlLogin'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,6 +88,27 @@ describe('deploy service', function (): void {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should deploy fails if docker is not running', async function (): Promise<void> {
|
||||||
|
const testContext = createContext();
|
||||||
|
const deployProfile: IDeployProfile = {
|
||||||
|
localDbSetting: {
|
||||||
|
dbName: 'test',
|
||||||
|
password: 'PLACEHOLDER',
|
||||||
|
port: 1433,
|
||||||
|
serverName: 'localhost',
|
||||||
|
userName: 'sa',
|
||||||
|
dockerBaseImage: 'image',
|
||||||
|
connectionRetryTimeout: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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.tasks, 'startBackgroundOperation').callThrough();
|
||||||
|
sandbox.stub(childProcess, 'exec').throws('error');
|
||||||
|
await should(deployService.deploy(deployProfile, project1)).rejected();
|
||||||
|
});
|
||||||
|
|
||||||
it('Should retry connecting to the server', async function (): Promise<void> {
|
it('Should retry connecting to the server', async function (): Promise<void> {
|
||||||
const testContext = createContext();
|
const testContext = createContext();
|
||||||
const localDbSettings = {
|
const localDbSettings = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as should from 'should';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { createDummyFileStructure } from './testUtils';
|
import { createDummyFileStructure } from './testUtils';
|
||||||
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation } from '../common/utils';
|
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword } from '../common/utils';
|
||||||
import { Uri } from 'vscode';
|
import { Uri } from 'vscode';
|
||||||
|
|
||||||
describe('Tests to verify utils functions', function (): void {
|
describe('Tests to verify utils functions', function (): void {
|
||||||
@@ -110,5 +110,18 @@ describe('Tests to verify utils functions', function (): void {
|
|||||||
should(await detectCommandInstallation('node')).equal(true, '"node" should have been detected.');
|
should(await detectCommandInstallation('node')).equal(true, '"node" should have been detected.');
|
||||||
should(await detectCommandInstallation('bogusFakeCommand')).equal(false, '"bogusFakeCommand" should have been detected.');
|
should(await detectCommandInstallation('bogusFakeCommand')).equal(false, '"bogusFakeCommand" should have been detected.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should validate SQL server password correctly', () => {
|
||||||
|
should(isValidSQLPassword('invalid')).equals(false, 'string with chars only is invalid password');
|
||||||
|
should(isValidSQLPassword('')).equals(false, 'empty string is invalid password');
|
||||||
|
should(isValidSQLPassword('65536')).equals(false, 'string with numbers only is invalid password');
|
||||||
|
should(isValidSQLPassword('dFGj')).equals(false, 'string with lowercase and uppercase char only is invalid password');
|
||||||
|
should(isValidSQLPassword('dj$')).equals(false, 'string with char and symbols only is invalid password');
|
||||||
|
should(isValidSQLPassword('dF65530')).equals(false, 'string with char and numbers only is invalid password');
|
||||||
|
should(isValidSQLPassword('dF6$30')).equals(false, 'dF6$30 is invalid password');
|
||||||
|
should(isValidSQLPassword('dF65$530')).equals(true, 'dF65$530 is valid password');
|
||||||
|
should(isValidSQLPassword('dFdf65$530')).equals(true, 'dF65$530 is valid password');
|
||||||
|
should(isValidSQLPassword('av1fgh533@')).equals(true, 'dF65$530 is valid password');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user