Add getDockerImageSpec and cleanDockerObjects to API (#19900)

* Add getDockerImageSpec and cleanDockerObjects to API

* docs
This commit is contained in:
Charles Gagnon
2022-07-01 12:57:40 -07:00
committed by GitHub
parent c211fb981c
commit 60026a39f9
5 changed files with 94 additions and 49 deletions

View File

@@ -73,7 +73,7 @@ export class ProjectsController {
private buildHelper: BuildHelper; private buildHelper: BuildHelper;
private buildInfo: DashboardData[] = []; private buildInfo: DashboardData[] = [];
private publishInfo: PublishData[] = []; private publishInfo: PublishData[] = [];
private deployService: DeployService; public deployService: DeployService;
private connectionService: ConnectionService; private connectionService: ConnectionService;
private azureSqlClient: AzureSqlClient; private azureSqlClient: AzureSqlClient;
private autorestHelper: AutorestHelper; private autorestHelper: AutorestHelper;

View File

@@ -12,13 +12,8 @@ import * as vscode from 'vscode';
import { ShellExecutionHelper } from '../../tools/shellExecutionHelper'; import { ShellExecutionHelper } from '../../tools/shellExecutionHelper';
import { AzureSqlClient } from './azureSqlClient'; import { AzureSqlClient } from './azureSqlClient';
import { ConnectionService } from '../connections/connectionService'; 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 { export class DeployService {
constructor(private _azureSqlClient = new AzureSqlClient(), private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) { 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 * 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 * @param profile Azure Sql server settings
@@ -99,23 +72,14 @@ export class DeployService {
this.logToOutput(constants.dockerImageEulaMessage); this.logToOutput(constants.dockerImageEulaMessage);
this.logToOutput(profile.dockerSettings.dockerBaseImageEula); 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 name is not set use the docker name to have a unique name
if (!profile.dockerSettings.profileName) { if (!profile.dockerSettings.profileName) {
profile.dockerSettings.profileName = imageSpec.containerName; profile.dockerSettings.profileName = imageSpec.containerName;
} }
this.logToOutput(constants.cleaningDockerImagesMessage); await this.cleanDockerObjectsIfNeeded(imageSpec.label);
// 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']);
}
}
this.logToOutput(constants.creatingDeploymentSettingsMessage); this.logToOutput(constants.creatingDeploymentSettingsMessage);
// Create commands // Create commands
@@ -208,6 +172,23 @@ export class DeployService {
return currentIds ? currentIds.split(/\r?\n/) : []; 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<void> {
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<void> { public async cleanDockerObjects(ids: string[], commandsToClean: string[]): Promise<void> {
for (let index = 0; index < ids.length; index++) { for (let index = 0; index < ids.length; index++) {
const id = ids[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 };
}

View File

@@ -15,6 +15,7 @@ import { ProjectsController } from '../controllers/projectController';
import { Project } from '../models/project'; import { Project } from '../models/project';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem'; import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { getPublishToDockerSettings } from '../dialogs/deployDatabaseQuickpick'; import { getPublishToDockerSettings } from '../dialogs/deployDatabaseQuickpick';
import { getDockerImageSpec } from '../models/deploy/deployService';
export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider, sqldbproj.IExtension { export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvider, sqldbproj.IExtension {
constructor(private projectController: ProjectsController) { constructor(private projectController: ProjectsController) {
@@ -223,4 +224,12 @@ export class SqlDatabaseProjectProvider implements dataworkspace.IProjectProvide
public getPublishToDockerSettings(project: sqldbproj.ISqlProject): Promise<sqldbproj.IPublishToDockerSettings | undefined> { public getPublishToDockerSettings(project: sqldbproj.ISqlProject): Promise<sqldbproj.IPublishToDockerSettings | undefined> {
return getPublishToDockerSettings(project); return getPublishToDockerSettings(project);
} }
public getDockerImageSpec(projectName: string, baseImage: string, imageUniqueId?: string): sqldbproj.DockerImageSpec {
return getDockerImageSpec(projectName, baseImage, imageUniqueId);
}
public cleanDockerObjectsIfNeeded(imageLabel: string): Promise<void> {
return this.projectController.deployService.cleanDockerObjectsIfNeeded(imageLabel);
}
} }

View File

@@ -66,8 +66,25 @@ declare module 'sqldbproj' {
*/ */
addItemPrompt(project: ISqlProject, relativeFilePath: string, options?: AddItemOptions): Promise<void>; addItemPrompt(project: ISqlProject, relativeFilePath: string, options?: AddItemOptions): Promise<void>;
/**
* 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<IPublishToDockerSettings | undefined>; getPublishToDockerSettings(project: ISqlProject): Promise<IPublishToDockerSettings | undefined>;
/**
* 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<void>;
} }
export interface AddItemOptions { export interface AddItemOptions {
@@ -333,4 +350,22 @@ declare module 'sqldbproj' {
deploymentOptions?: DeploymentOptions; deploymentOptions?: DeploymentOptions;
profileUsed?: boolean; 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
}
} }

View File

@@ -7,7 +7,7 @@ import * as should from 'should';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as baselines from '../baselines/baselines'; import * as baselines from '../baselines/baselines';
import * as testUtils from '../testUtils'; import * as testUtils from '../testUtils';
import { DeployService } from '../../models/deploy/deployService'; import { DeployService, getDockerImageSpec } from '../../models/deploy/deployService';
import { Project } from '../../models/project'; import { Project } from '../../models/project';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as azdata from 'azdata'; import * as azdata from 'azdata';
@@ -175,33 +175,31 @@ describe('deploy service', function (): void {
}); });
it('Should create docker image info correctly', () => { 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 id = UUID.generateUuid().toLocaleLowerCase();
const baseImage = 'baseImage:latest'; const baseImage = 'baseImage:latest';
const tag = baseImage.replace(':', '-').replace(constants.sqlServerDockerRegistry, '').replace(/[^a-zA-Z0-9_,\-]/g, '').toLocaleLowerCase(); 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`, label: `${constants.dockerImageLabelPrefix}-project-name123_test`,
containerName: `${constants.dockerImageNamePrefix}-project-name123_test-${id}`, containerName: `${constants.dockerImageNamePrefix}-project-name123_test-${id}`,
tag: `${constants.dockerImageNamePrefix}-project-name123_test-${tag}` 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`, label: `${constants.dockerImageLabelPrefix}-project-name1`,
containerName: `${constants.dockerImageNamePrefix}-project-name1-${id}`, containerName: `${constants.dockerImageNamePrefix}-project-name1-${id}`,
tag: `${constants.dockerImageNamePrefix}-project-name1-${tag}` 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`, label: `${constants.dockerImageLabelPrefix}-project-name2`,
containerName: `${constants.dockerImageNamePrefix}-project-name2-${id}`, containerName: `${constants.dockerImageNamePrefix}-project-name2-${id}`,
tag: `${constants.dockerImageNamePrefix}-project-name2-${tag}` 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`, label: `${constants.dockerImageLabelPrefix}-project-name3`,
containerName: `${constants.dockerImageNamePrefix}-project-name3-${id}`, containerName: `${constants.dockerImageNamePrefix}-project-name3-${id}`,
tag: `${constants.dockerImageNamePrefix}-project-name3-${tag}` 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`, label: `${constants.dockerImageLabelPrefix}-project_name4`,
containerName: `${constants.dockerImageNamePrefix}-project_name4-${id}`, containerName: `${constants.dockerImageNamePrefix}-project_name4-${id}`,
tag: `${constants.dockerImageNamePrefix}-project_name4-${tag}` 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 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)); 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}`, label: `${constants.dockerImageLabelPrefix}-${imageProjectName}`,
containerName: `${constants.dockerImageNamePrefix}-${imageProjectName}-${id}`, containerName: `${constants.dockerImageNamePrefix}-${imageProjectName}-${id}`,
tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}` tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}`