diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index c8419885a5..188511e4a7 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -73,7 +73,7 @@ export class ProjectsController { private buildHelper: BuildHelper; private buildInfo: DashboardData[] = []; private publishInfo: PublishData[] = []; - private deployService: DeployService; + public deployService: DeployService; private connectionService: ConnectionService; private azureSqlClient: AzureSqlClient; private autorestHelper: AutorestHelper; diff --git a/extensions/sql-database-projects/src/models/deploy/deployService.ts b/extensions/sql-database-projects/src/models/deploy/deployService.ts index 2ec42b5ffd..5787898d79 100644 --- a/extensions/sql-database-projects/src/models/deploy/deployService.ts +++ b/extensions/sql-database-projects/src/models/deploy/deployService.ts @@ -12,13 +12,8 @@ import * as vscode from 'vscode'; import { ShellExecutionHelper } from '../../tools/shellExecutionHelper'; import { AzureSqlClient } from './azureSqlClient'; import { ConnectionService } from '../connections/connectionService'; -import { IDockerSettings, IPublishToDockerSettings } from 'sqldbproj'; +import { DockerImageSpec, IDockerSettings, IPublishToDockerSettings } from 'sqldbproj'; -interface DockerImageSpec { - label: string; - containerName: string; - tag: string -} export class DeployService { constructor(private _azureSqlClient = new AzureSqlClient(), private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) { @@ -38,28 +33,6 @@ 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 }; - } - /** * Creates a new Azure Sql server and tries to connect to the new server. If connection fails because of firewall rule, it prompts user to add firewall rule settings * @param profile Azure Sql server settings @@ -99,23 +72,14 @@ export class DeployService { this.logToOutput(constants.dockerImageEulaMessage); this.logToOutput(profile.dockerSettings.dockerBaseImageEula); - const imageSpec = this.getDockerImageSpec(project.projectFileName, profile.dockerSettings.dockerBaseImage); + const imageSpec = getDockerImageSpec(project.projectFileName, profile.dockerSettings.dockerBaseImage); // If profile name is not set use the docker name to have a unique name if (!profile.dockerSettings.profileName) { profile.dockerSettings.profileName = imageSpec.containerName; } - this.logToOutput(constants.cleaningDockerImagesMessage); - // Clean up existing docker image - 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); - await this.cleanDockerObjects(containerIds, ['docker stop', 'docker rm']); - } - } + await this.cleanDockerObjectsIfNeeded(imageSpec.label); this.logToOutput(constants.creatingDeploymentSettingsMessage); // Create commands @@ -208,6 +172,23 @@ export class DeployService { return currentIds ? currentIds.split(/\r?\n/) : []; } + /** + * Checks if any containers with the specified label already exist, and if they do prompt the user whether they want to clean them up + * @param imageLabel The label of the container to search for + */ + public async cleanDockerObjectsIfNeeded(imageLabel: string): Promise { + this.logToOutput(constants.cleaningDockerImagesMessage); + // Clean up existing docker image + const containerIds = await this.getCurrentDockerContainer(imageLabel); + if (containerIds.length > 0) { + const result = await vscode.window.showWarningMessage(constants.containerAlreadyExistForProject, constants.yesString, constants.noString); + if (result === constants.yesString) { + this.logToOutput(constants.cleaningDockerImagesMessage); + await this.cleanDockerObjects(containerIds, ['docker stop', 'docker rm']); + } + } + } + public async cleanDockerObjects(ids: string[], commandsToClean: string[]): Promise { for (let index = 0; index < ids.length; index++) { const id = ids[index]; @@ -220,3 +201,25 @@ export class DeployService { } } } + +export function 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 }; +} diff --git a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts index 141f27b4b6..eb3f7e2da5 100644 --- a/extensions/sql-database-projects/src/projectProvider/projectProvider.ts +++ b/extensions/sql-database-projects/src/projectProvider/projectProvider.ts @@ -15,6 +15,7 @@ import { ProjectsController } from '../controllers/projectController'; import { Project } from '../models/project'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { getPublishToDockerSettings } from '../dialogs/deployDatabaseQuickpick'; +import { getDockerImageSpec } from '../models/deploy/deployService'; export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider, sqldbproj.IExtension { constructor(private projectController: ProjectsController) { @@ -223,4 +224,12 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide public getPublishToDockerSettings(project: sqldbproj.ISqlProject): Promise { return getPublishToDockerSettings(project); } + + public getDockerImageSpec(projectName: string, baseImage: string, imageUniqueId?: string): sqldbproj.DockerImageSpec { + return getDockerImageSpec(projectName, baseImage, imageUniqueId); + } + + public cleanDockerObjectsIfNeeded(imageLabel: string): Promise { + return this.projectController.deployService.cleanDockerObjectsIfNeeded(imageLabel); + } } diff --git a/extensions/sql-database-projects/src/sqldbproj.d.ts b/extensions/sql-database-projects/src/sqldbproj.d.ts index 3e9a870618..993e1bbe46 100644 --- a/extensions/sql-database-projects/src/sqldbproj.d.ts +++ b/extensions/sql-database-projects/src/sqldbproj.d.ts @@ -66,8 +66,25 @@ declare module 'sqldbproj' { */ addItemPrompt(project: ISqlProject, relativeFilePath: string, options?: AddItemOptions): Promise; + /** + * Gathers information required for publishing a project to a docker container, prompting the user as necessary + * @param project The Project being published + */ getPublishToDockerSettings(project: ISqlProject): Promise; + /** + * Gets the information required to start a docker container for publishing to + * @param projectName The name of the project being published + * @param baseImage The base docker image being deployed + * @param imageUniqueId The unique ID to use in the name, default is a random GUID + */ + getDockerImageSpec(projectName: string, baseImage: string, imageUniqueId?: string): DockerImageSpec; + + /** + * Checks if any containers with the specified label already exist, and if they do prompt the user whether they want to clean them up + * @param imageLabel The label of the container to search for + */ + cleanDockerObjectsIfNeeded(imageLabel: string): Promise; } export interface AddItemOptions { @@ -333,4 +350,22 @@ declare module 'sqldbproj' { deploymentOptions?: DeploymentOptions; profileUsed?: boolean; } + + /** + * Information for deploying a new docker container + */ + interface DockerImageSpec { + /** + * The label to apply to the container + */ + label: string; + /** + * The full name to give the container + */ + containerName: string; + /** + * The tag to apply to the container + */ + tag: string + } } 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 ff8efe6c32..2a6ee0da76 100644 --- a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts +++ b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts @@ -7,7 +7,7 @@ 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 { DeployService, getDockerImageSpec } from '../../models/deploy/deployService'; import { Project } from '../../models/project'; import * as vscode from 'vscode'; import * as azdata from 'azdata'; @@ -175,33 +175,31 @@ describe('deploy service', function (): void { }); it('Should create docker image info correctly', () => { - const testContext = createContext(); - const deployService = new DeployService(testContext.azureSqlClient.object, 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({ + should(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({ + should(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({ + should(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({ + should(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({ + should(getDockerImageSpec('project_name4', baseImage, id)).deepEqual({ label: `${constants.dockerImageLabelPrefix}-project_name4`, containerName: `${constants.dockerImageNamePrefix}-project_name4-${id}`, tag: `${constants.dockerImageNamePrefix}-project_name4-${tag}` @@ -210,7 +208,7 @@ describe('deploy service', function (): void { 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({ + should(getDockerImageSpec(reallyLongName, baseImage, id)).deepEqual({ label: `${constants.dockerImageLabelPrefix}-${imageProjectName}`, containerName: `${constants.dockerImageNamePrefix}-${imageProjectName}-${id}`, tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}`