mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
sqlproj - publish to container - fix the image name, tag and added validation (#17295)
This commit is contained in:
@@ -15,6 +15,11 @@ import * as os from 'os';
|
|||||||
import { ConnectionResult } from 'azdata';
|
import { ConnectionResult } from 'azdata';
|
||||||
import * as templates from '../../templates/templates';
|
import * as templates from '../../templates/templates';
|
||||||
|
|
||||||
|
interface DockerImageSpec {
|
||||||
|
label: string;
|
||||||
|
containerName: string;
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
export class DeployService {
|
export class DeployService {
|
||||||
|
|
||||||
constructor(private _outputChannel: vscode.OutputChannel) {
|
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<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) {
|
||||||
@@ -102,9 +129,8 @@ export class DeployService {
|
|||||||
|
|
||||||
await this.verifyDocker();
|
await this.verifyDocker();
|
||||||
|
|
||||||
const projectName = project.projectFileName;
|
const imageSpec = this.getDockerImageSpec(project.projectFileName, profile.localDbSetting.dockerBaseImage);
|
||||||
const imageLabel = `${constants.dockerImageLabelPrefix}_${projectName}`.toLocaleLowerCase();
|
|
||||||
const imageName = `${constants.dockerImageNamePrefix}-${projectName}-${UUID.generateUuid()}`.toLocaleLowerCase();
|
|
||||||
const root = project.projectFolderPath;
|
const root = project.projectFolderPath;
|
||||||
const mssqlFolderPath = path.join(root, constants.mssqlFolderName);
|
const mssqlFolderPath = path.join(root, constants.mssqlFolderName);
|
||||||
const commandsFolderPath = path.join(mssqlFolderPath, constants.commandsFolderName);
|
const commandsFolderPath = path.join(mssqlFolderPath, constants.commandsFolderName);
|
||||||
@@ -112,8 +138,8 @@ export class DeployService {
|
|||||||
const startFilePath = path.join(commandsFolderPath, constants.startCommandName);
|
const startFilePath = path.join(commandsFolderPath, constants.startCommandName);
|
||||||
|
|
||||||
// Clean up existing docker image
|
// Clean up existing docker image
|
||||||
const containerIds = await this.getCurrentDockerContainer(imageLabel);
|
const containerIds = await this.getCurrentDockerContainer(imageSpec.label);
|
||||||
if (containerIds && containerIds.length > 0) {
|
if (containerIds.length > 0) {
|
||||||
const result = await vscode.window.showWarningMessage(constants.containerAlreadyExistForProject, constants.yesString, constants.noString);
|
const result = await vscode.window.showWarningMessage(constants.containerAlreadyExistForProject, constants.yesString, constants.noString);
|
||||||
if (result === constants.yesString) {
|
if (result === constants.yesString) {
|
||||||
this.logToOutput(constants.cleaningDockerImagesMessage);
|
this.logToOutput(constants.cleaningDockerImagesMessage);
|
||||||
@@ -125,19 +151,19 @@ export class DeployService {
|
|||||||
// Create commands
|
// 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);
|
this.logToOutput(constants.runningDockerMessage);
|
||||||
// Building the image and running the docker
|
// 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}`);
|
this.logToOutput(`Docker container created. Id: ${createdDockerId}`);
|
||||||
|
|
||||||
|
|
||||||
// Waiting a bit to make sure docker container doesn't crash
|
// Waiting a bit to make sure docker container doesn't crash
|
||||||
//
|
//
|
||||||
const runningDockerId = await utils.retry('Validating the docker container', async () => {
|
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) => {
|
}, (dockerId) => {
|
||||||
return Promise.resolve({ validated: dockerId !== undefined, errorMessage: constants.dockerContainerNotRunningErrorMessage });
|
return Promise.resolve({ validated: dockerId !== undefined, errorMessage: constants.dockerContainerNotRunningErrorMessage });
|
||||||
}, (dockerId) => {
|
}, (dockerId) => {
|
||||||
@@ -161,7 +187,7 @@ export class DeployService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildAndRunDockerContainer(dockerFilePath: string, imageName: string, root: string, profile: ILocalDbSetting, imageLabel: string): Promise<string | undefined> {
|
private async buildAndRunDockerContainer(dockerFilePath: string, dockerImageSpec: DockerImageSpec, root: string, profile: ILocalDbSetting): Promise<string | undefined> {
|
||||||
|
|
||||||
// Sensitive data to remove from output console
|
// Sensitive data to remove from output console
|
||||||
const sensitiveData = [profile.password];
|
const sensitiveData = [profile.password];
|
||||||
@@ -169,12 +195,12 @@ export class DeployService {
|
|||||||
// Running commands to build the docker image
|
// 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 ${dockerImageSpec.tag} ${root}`, this._outputChannel);
|
||||||
await utils.executeCommand(`docker images --filter label=${imageLabel}`, this._outputChannel);
|
await utils.executeCommand(`docker images --filter label=${dockerImageSpec.label}`, 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, sensitiveData);
|
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=${imageLabel} -q`, this._outputChannel);
|
return await utils.executeCommand(`docker ps -q -a --filter label=${dockerImageSpec.label} -q`, this._outputChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getConnectionString(connectionUri: string): Promise<string | undefined> {
|
private async getConnectionString(connectionUri: string): Promise<string | undefined> {
|
||||||
@@ -345,7 +371,7 @@ export class DeployService {
|
|||||||
//
|
//
|
||||||
await this.createFile(startFilePath, 'echo starting the container!');
|
await this.createFile(startFilePath, 'echo starting the container!');
|
||||||
if (os.platform() !== 'win32') {
|
if (os.platform() !== 'win32') {
|
||||||
await utils.executeCommand(`chmod +x ${startFilePath}`, this._outputChannel);
|
await utils.executeCommand(`chmod +x '${startFilePath}'`, this._outputChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the Dockerfile
|
// Create the Dockerfile
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import * as vscode from 'vscode';
|
|||||||
import * as azdata from 'azdata';
|
import * as azdata from 'azdata';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import { AppSettingType, IDeployProfile } from '../../models/deploy/deployProfile';
|
import { AppSettingType, IDeployProfile } from '../../models/deploy/deployProfile';
|
||||||
let fse = require('fs-extra');
|
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
|
||||||
let path = require('path');
|
import * as fse from 'fs-extra';
|
||||||
|
import * as path from 'path';
|
||||||
import * as constants from '../../common/constants';
|
import * as constants from '../../common/constants';
|
||||||
|
|
||||||
export interface TestContext {
|
export interface TestContext {
|
||||||
@@ -90,7 +91,7 @@ describe('deploy service', function (): void {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should deploy fails if docker is not running', async function (): Promise<void> {
|
it('Should fail the deploy if docker is not running', async function (): Promise<void> {
|
||||||
const testContext = createContext();
|
const testContext = createContext();
|
||||||
const deployProfile: IDeployProfile = {
|
const deployProfile: IDeployProfile = {
|
||||||
localDbSetting: {
|
localDbSetting: {
|
||||||
@@ -248,4 +249,47 @@ describe('deploy service', function (): void {
|
|||||||
await deployService.cleanDockerObjects(ids, ['docker stop', 'docker rm']);
|
await deployService.cleanDockerObjects(ids, ['docker stop', 'docker rm']);
|
||||||
should(process.calledThrice);
|
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}`
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user