diff --git a/extensions/sql-database-projects/src/models/deploy/deployService.ts b/extensions/sql-database-projects/src/models/deploy/deployService.ts index 4690502451..2aedc77eb7 100644 --- a/extensions/sql-database-projects/src/models/deploy/deployService.ts +++ b/extensions/sql-database-projects/src/models/deploy/deployService.ts @@ -15,6 +15,11 @@ import * as os from 'os'; import { ConnectionResult } from 'azdata'; import * as templates from '../../templates/templates'; +interface DockerImageSpec { + label: string; + containerName: string; + tag: string +} export class DeployService { constructor(private _outputChannel: vscode.OutputChannel) { @@ -94,6 +99,28 @@ export class DeployService { } } + public getDockerImageSpec(projectName: string, baseImage: string, imageUniqueId?: string): DockerImageSpec { + + imageUniqueId = imageUniqueId ?? UUID.generateUuid(); + // Remove unsupported characters + // + + // docker image name and tag can only include letters, digits, underscore, period and dash + const regexForDockerImageName = /[^a-zA-Z0-9_,\-]/g; + + let imageProjectName = projectName.replace(regexForDockerImageName, ''); + const tagMaxLength = 128; + const tag = baseImage.replace(':', '-').replace(constants.sqlServerDockerRegistry, '').replace(regexForDockerImageName, ''); + + // cut the name if it's too long + // + imageProjectName = imageProjectName.substring(0, tagMaxLength - (constants.dockerImageNamePrefix.length + tag.length + 2)); + const imageLabel = `${constants.dockerImageLabelPrefix}-${imageProjectName}`.toLocaleLowerCase(); + const imageTag = `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}`.toLocaleLowerCase(); + const dockerName = `${constants.dockerImageNamePrefix}-${imageProjectName}-${imageUniqueId}`.toLocaleLowerCase(); + return { label: imageLabel, tag: imageTag, containerName: dockerName }; + } + public async deploy(profile: IDeployProfile, project: Project): Promise { return await this.executeTask(constants.deployDbTaskName, async () => { if (!profile.localDbSetting) { @@ -102,9 +129,8 @@ export class DeployService { await this.verifyDocker(); - const projectName = project.projectFileName; - const imageLabel = `${constants.dockerImageLabelPrefix}_${projectName}`.toLocaleLowerCase(); - const imageName = `${constants.dockerImageNamePrefix}-${projectName}-${UUID.generateUuid()}`.toLocaleLowerCase(); + const imageSpec = this.getDockerImageSpec(project.projectFileName, profile.localDbSetting.dockerBaseImage); + const root = project.projectFolderPath; const mssqlFolderPath = path.join(root, constants.mssqlFolderName); const commandsFolderPath = path.join(mssqlFolderPath, constants.commandsFolderName); @@ -112,8 +138,8 @@ export class DeployService { const startFilePath = path.join(commandsFolderPath, constants.startCommandName); // Clean up existing docker image - const containerIds = await this.getCurrentDockerContainer(imageLabel); - if (containerIds && containerIds.length > 0) { + const containerIds = await this.getCurrentDockerContainer(imageSpec.label); + if (containerIds.length > 0) { const result = await vscode.window.showWarningMessage(constants.containerAlreadyExistForProject, constants.yesString, constants.noString); if (result === constants.yesString) { this.logToOutput(constants.cleaningDockerImagesMessage); @@ -125,19 +151,19 @@ export class DeployService { // Create commands // - await this.createCommands(mssqlFolderPath, commandsFolderPath, dockerFilePath, startFilePath, imageLabel, profile.localDbSetting.dockerBaseImage); + await this.createCommands(mssqlFolderPath, commandsFolderPath, dockerFilePath, startFilePath, imageSpec.label, profile.localDbSetting.dockerBaseImage); this.logToOutput(constants.runningDockerMessage); // Building the image and running the docker // - const createdDockerId: string | undefined = await this.buildAndRunDockerContainer(dockerFilePath, imageName, root, profile.localDbSetting, imageLabel); + const createdDockerId: string | undefined = await this.buildAndRunDockerContainer(dockerFilePath, imageSpec, root, profile.localDbSetting); 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); + return await utils.executeCommand(`docker ps -q -a --filter label=${imageSpec.label} -q`, this._outputChannel); }, (dockerId) => { return Promise.resolve({ validated: dockerId !== undefined, errorMessage: constants.dockerContainerNotRunningErrorMessage }); }, (dockerId) => { @@ -161,7 +187,7 @@ export class DeployService { }); } - private async buildAndRunDockerContainer(dockerFilePath: string, imageName: string, root: string, profile: ILocalDbSetting, imageLabel: string): Promise { + private async buildAndRunDockerContainer(dockerFilePath: string, dockerImageSpec: DockerImageSpec, root: string, profile: ILocalDbSetting): Promise { // Sensitive data to remove from output console const sensitiveData = [profile.password]; @@ -169,12 +195,12 @@ export class DeployService { // Running commands to build the docker image this.logToOutput('Building docker image ...'); 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 images --filter label=${imageLabel}`, this._outputChannel); + await utils.executeCommand(`docker build -f ${dockerFilePath} -t ${dockerImageSpec.tag} ${root}`, this._outputChannel); + await utils.executeCommand(`docker images --filter label=${dockerImageSpec.label}`, 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, sensitiveData); - return await utils.executeCommand(`docker ps -q -a --filter label=${imageLabel} -q`, this._outputChannel); + await utils.executeCommand(`docker run -p ${profile.port}:1433 -e "MSSQL_SA_PASSWORD=${profile.password}" -d --name ${dockerImageSpec.containerName} ${dockerImageSpec.tag}`, this._outputChannel, sensitiveData); + return await utils.executeCommand(`docker ps -q -a --filter label=${dockerImageSpec.label} -q`, this._outputChannel); } private async getConnectionString(connectionUri: string): Promise { @@ -345,7 +371,7 @@ export class DeployService { // await this.createFile(startFilePath, 'echo starting the container!'); if (os.platform() !== 'win32') { - await utils.executeCommand(`chmod +x ${startFilePath}`, this._outputChannel); + await utils.executeCommand(`chmod +x '${startFilePath}'`, this._outputChannel); } // Create the Dockerfile diff --git a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts index 8a03afd6f9..a420d1b54e 100644 --- a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts +++ b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts @@ -13,8 +13,9 @@ 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'); +import * as UUID from 'vscode-languageclient/lib/utils/uuid'; +import * as fse from 'fs-extra'; +import * as path from 'path'; import * as constants from '../../common/constants'; export interface TestContext { @@ -90,7 +91,7 @@ describe('deploy service', function (): void { }); - it('Should deploy fails if docker is not running', async function (): Promise { + it('Should fail the deploy if docker is not running', async function (): Promise { const testContext = createContext(); const deployProfile: IDeployProfile = { localDbSetting: { @@ -248,4 +249,47 @@ describe('deploy service', function (): void { await deployService.cleanDockerObjects(ids, ['docker stop', 'docker rm']); should(process.calledThrice); }); + + it('Should create docker image info correctly', () => { + const testContext = createContext(); + const deployService = new DeployService(testContext.outputChannel); + const id = UUID.generateUuid().toLocaleLowerCase(); + const baseImage = 'baseImage:latest'; + const tag = baseImage.replace(':', '-').replace(constants.sqlServerDockerRegistry, '').replace(/[^a-zA-Z0-9_,\-]/g, '').toLocaleLowerCase(); + + should(deployService.getDockerImageSpec('project-name123_test', baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-project-name123_test`, + containerName: `${constants.dockerImageNamePrefix}-project-name123_test-${id}`, + tag: `${constants.dockerImageNamePrefix}-project-name123_test-${tag}` + }); + should(deployService.getDockerImageSpec('project-name1', baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-project-name1`, + containerName: `${constants.dockerImageNamePrefix}-project-name1-${id}`, + tag: `${constants.dockerImageNamePrefix}-project-name1-${tag}` + }); + should(deployService.getDockerImageSpec('project-name2$#', baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-project-name2`, + containerName: `${constants.dockerImageNamePrefix}-project-name2-${id}`, + tag: `${constants.dockerImageNamePrefix}-project-name2-${tag}` + }); + should(deployService.getDockerImageSpec('project - name3', baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-project-name3`, + containerName: `${constants.dockerImageNamePrefix}-project-name3-${id}`, + tag: `${constants.dockerImageNamePrefix}-project-name3-${tag}` + }); + should(deployService.getDockerImageSpec('project_name4', baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-project_name4`, + containerName: `${constants.dockerImageNamePrefix}-project_name4-${id}`, + tag: `${constants.dockerImageNamePrefix}-project_name4-${tag}` + }); + + + const reallyLongName = new Array(128 + 1).join('a').replace(/[^a-zA-Z0-9_,\-]/g, ''); + const imageProjectName = reallyLongName.substring(0, 128 - (constants.dockerImageNamePrefix.length + tag.length + 2)); + should(deployService.getDockerImageSpec(reallyLongName, baseImage, id)).deepEqual({ + label: `${constants.dockerImageLabelPrefix}-${imageProjectName}`, + containerName: `${constants.dockerImageNamePrefix}-${imageProjectName}-${id}`, + tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}` + }); + }); });