[Port] //Build features for VSCode database projects extension release only (#19422)

* add SDK option to create project from db quickpick (#19100)

* Add SDK option to create project from db quickpick

* cleanup

* New UI for deploying SQL project to a new Azure server (#18833)

* SQL Project Deploy to docker container - Adding a UI for user to select docker image tag (#19297)

* add docker image with telemetry for publish to container (#19360)

* add docker info image to telemetry for publish to container

* change name

* merge issue

* version bump

Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com>
Co-authored-by: Leila Lali <llali@microsoft.com>
This commit is contained in:
Benjin Dubishar
2022-05-18 20:11:41 -07:00
committed by GitHub
parent e56e1d931b
commit b77aa3dc77
21 changed files with 1807 additions and 401 deletions

View File

@@ -2,7 +2,7 @@
"name": "sql-database-projects",
"displayName": "SQL Database Projects",
"description": "Enables users to develop and publish database schemas for MSSQL Databases",
"version": "0.16.1",
"version": "0.16.2",
"publisher": "Microsoft",
"preview": true,
"engines": {
@@ -456,6 +456,7 @@
}
},
"dependencies": {
"axios": "^0.27.2",
"@microsoft/ads-extension-telemetry": "^1.1.5",
"fast-glob": "^3.2.7",
"fs-extra": "^5.0.0",
@@ -464,7 +465,7 @@
"vscode-languageclient": "^5.3.0-next.1",
"vscode-nls": "^4.1.2",
"which": "^2.0.2",
"xml-formatter": "^2.1.0",
"xml-formatter": "2.1.0",
"xmldom": "^0.6.0"
},
"devDependencies": {

View File

@@ -149,9 +149,22 @@ export const nameMustNotBeEmpty = localize('nameMustNotBeEmpty', "Name must not
// Deploy
export const SqlServerName = 'SQL server';
export const AzureSqlServerName = 'Azure SQL server';
export const SqlServerDockerImageName = 'Microsoft SQL Server';
export const AzureSqlDbFullDockerImageName = 'Azure SQL Database emulator Full';
export const AzureSqlDbLiteDockerImageName = 'Azure SQL Database emulator Lite';
export const AzureSqlLogicalServerName = 'Azure SQL logical server';
export const selectPublishOption = localize('selectPublishOption', "Select where to publish the project to");
export const defaultQuickPickItem = localize('defaultQuickPickItem', "Default - image defined as default in the container registry");
export function dockerImagesPlaceHolder(name: string) { return localize('dockerImagesPlaceHolder', 'Use {0} on local arm64/Apple Silicon', name); }
export function publishToExistingServer(name: string) { return localize('publishToExistingServer', "Publish to an existing {0}", name); }
export function publishToDockerContainer(name: string) { return localize('publishToDockerContainer', "Publish to new {0} local development container", name); }
export const publishToAzureEmulator = localize('publishToAzureEmulator', "Publish to new Azure SQL Database emulator");
export const publishToNewAzureServer = localize('publishToNewAzureServer', "Publish to new Azure SQL logical server");
export const azureServerName = localize('azureServerName', "Azure SQL server name");
export const azureSubscription = localize('azureSubscription', "Azure subscription");
export const resourceGroup = localize('resourceGroup', "Resource group");
export const azureLocation = localize('location', "Location");
export const azureAccounts = localize('azureAccounts', "Azure accounts");
export function enterPortNumber(name: string) { return localize('enterPortNumber', "Enter {0} port number or press enter to use the default value", name); }
export function serverPortNumber(name: string) { return localize('serverPortNumber', "{0} port number", name); }
export function serverPassword(name: string) { return localize('serverPassword', "{0} admin password", name); }
@@ -160,15 +173,18 @@ export function baseDockerImage(name: string) { return localize('baseDockerImage
export const publishTo = localize('publishTo', "Publish Target");
export const enterConnectionStringEnvName = localize('enterConnectionStringEnvName', "Enter connection string environment variable name");
export const enterConnectionStringTemplate = localize('enterConnectionStringTemplate', "Enter connection string template");
export function enterUser(name: string) { return localize('enterUser', "Enter {0} admin user name", name); }
export function enterPassword(name: string) { return localize('enterPassword', "Enter {0} admin password", name); }
export function confirmPassword(name: string) { return localize('confirmPassword', "Confirm {0} admin password", name); }
export function selectBaseImage(name: string) { return localize('selectBaseImage', "Select the base {0} docker image", name); }
export function selectImageTag(name: string) { return localize('selectImageTag', "Select the image tag or press enter to use the default value", name); }
export function invalidSQLPasswordMessage(name: string) { return localize('invalidSQLPassword', "{0} password doesn't meet the password complexity requirement. For more information see https://docs.microsoft.com/sql/relational-databases/security/password-policy", name); }
export function passwordNotMatch(name: string) { return localize('passwordNotMatch', "{0} password doesn't match the confirmation password", name); }
export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be number");
export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty");
export const dockerImageLabelPrefix = 'source=sqldbproject';
export const dockerImageNamePrefix = 'sqldbproject';
export const dockerImageDefaultTag = 'latest';
// Publish to Container
export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.");
@@ -205,12 +221,16 @@ export const runningDockerMessage = localize('runningDockerMessage', "Running th
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 dockerContainerFailedToRunErrorMessage = localize('dockerContainerFailedToRunErrorMessage', "Failed to run the docker container");
export const connectingToSqlServerMessage = localize('connectingToSqlServerOnDockerMessage', "Connecting to SQL Server");
export const connectingToSqlServerMessage = localize('connectingToSqlServerMessage', "Connecting to SQL Server");
export const serverCreated = localize('serverCreated', "Server created");
export const deployProjectFailedMessage = localize('deployProjectFailedMessage', "Failed to open a connection to the deployed database'");
export const containerAlreadyExistForProject = localize('containerAlreadyExistForProject', "Containers already exist for this project. Do you want to delete them before deploying a new one?");
export const checkoutOutputMessage = localize('checkoutOutputMessage', "Check output pane for more details");
export function creatingAzureSqlServer(name: string): string { return localize('creatingAzureSqlServer', "Creating Azure SQL Server '{0}' ...", name); }
export function azureSqlServerCreated(name: string): string { return localize('azureSqlServerCreated', "Azure SQL Server '{0}' created", name); }
export function taskFailedError(taskName: string, err: string): string { return localize('taskFailedError.error', "Failed to complete task '{0}'. Error: {1}", taskName, err); }
export function publishToContainerFailed(errorMessage: string) { return localize('publishToContainerFailed', "Failed to publish to container. {0}", errorMessage); }
export function publishToNewAzureServerFailed(errorMessage: string) { return localize('publishToNewAzureServerFailed', "Failed to publish to new Azure SQL server. {0}", errorMessage); }
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 connectionFailedError(error: string) { return localize('connectionFailedError', "Connection failed error: '{0}'", error); }
@@ -220,7 +240,7 @@ export function retryWaitMessage(numberOfSeconds: number, name: string) { return
export function retryRunMessage(attemptNumber: number, numberOfAttempts: number, name: string) { return localize('retryRunMessage', "Running operation '{2}' Attempt {0} of {1}", attemptNumber, numberOfAttempts, name); }
export function retrySucceedMessage(name: string, result: string) { return localize('retrySucceedMessage', "Operation '{0}' completed successfully. Result: {1}", name, result); }
export function retryFailedMessage(name: string, result: string, error: string) { return localize('retryFailedMessage', "Operation '{0}' failed. Re-trying... Current Result: {1}. Error: '{2}'", name, result, error); }
export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}'", name, error || ''); }
export function retryMessage(name: string, error: string) { return localize('retryMessage', "Operation '{0}' failed. Re-trying... Error: '{1}' ", name, error); }
// Add Database Reference dialog strings
@@ -269,6 +289,8 @@ export const WorkspaceFileExtension = '.code-workspace';
export const browseEllipsisWithIcon = `$(folder) ${localize('browseEllipsis', "Browse...")}`;
export const selectProjectLocation = localize('selectProjectLocation', "Select project location");
export const sdkStyleProject = localize('sdkStyleProject', 'SDK-style project (Preview)');
export const YesRecommended = localize('yesRecommended', "Yes (Recommended)");
export const SdkLearnMorePlaceholder = localize('sdkLearnMorePlaceholder', "Click \"Learn More\" button for more information about SDK-style projects");
export const ProjectParentDirectoryNotExistError = (location: string): string => { return localize('dataworkspace.projectParentDirectoryNotExistError', "The selected project location '{0}' does not exist or is not a directory.", location); };
export const ProjectDirectoryAlreadyExistError = (projectName: string, location: string): string => { return localize('dataworkspace.projectDirectoryAlreadyExistError', "There is already a directory named '{0}' in the selected location: '{1}'.", projectName, location); };
@@ -480,6 +502,8 @@ export const integratedAuth = 'Integrated';
export const azureMfaAuth = 'AzureMFA';
export const sqlAuth = 'SqlAuth';
export const azureAddAccount = localize('azureAddAccount', "Add an Account...");
// Tree item types
export enum DatabaseProjectItemType {
project = 'databaseProject.itemType.project',
@@ -548,4 +572,5 @@ export function getTargetPlatformFromVersion(version: string): string {
export enum PublishTargetType {
existingServer = 'existingServer',
docker = 'docker',
newAzureServer = 'newAzureServer'
}

View File

@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import axios, { AxiosRequestConfig } from 'axios';
/**
* Class includes method for making http request
*/
export class HttpClient {
private static cache: Map<string, any> = new Map();
/**
* Makes http GET request to the given url. If useCache is set to true, returns the result from cache if exists
* @param url url to make http GET request against
* @param useCache if true and result is already cached the cached value will be returned
* @returns result of http GET request
*/
public static async getRequest(url: string, useCache = false): Promise<any> {
if (useCache) {
if (HttpClient.cache.has(url)) {
return HttpClient.cache.get(url);
}
}
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json'
},
validateStatus: () => true // Never throw
};
const response = await axios.get(url, config);
if (response.status !== 200) {
let errorMessage: string[] = [];
errorMessage.push(response.status.toString());
errorMessage.push(response.statusText);
if (response.data?.error) {
errorMessage.push(`${response.data?.error?.code} : ${response.data?.error?.message}`);
}
throw new Error(errorMessage.join(os.EOL));
}
if (useCache) {
HttpClient.cache.set(url, response.data);
}
return response.data;
}
}

View File

@@ -38,5 +38,7 @@ export enum TelemetryActions {
finishAddSqlBinding = 'finishAddSqlBinding',
createProjectFromDatabase = 'createProjectFromDatabase',
updateProjectFromDatabase = 'updateProjectFromDatabase',
publishToContainer = 'publishToContainer'
publishToContainer = 'publishToContainer',
publishToNewAzureServer = 'publishToNewAzureServer',
generateProjectFromOpenApiSpec = 'generateProjectFromOpenApiSpec'
}

View File

@@ -307,6 +307,18 @@ export async function getVscodeMssqlApi(): Promise<vscodeMssql.IExtension> {
return ext.activate();
}
export type AzureResourceServiceFactory = () => Promise<vscodeMssql.IAzureResourceService>;
export async function defaultAzureResourceServiceFactory(): Promise<vscodeMssql.IAzureResourceService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureResourceService;
}
export type AzureAccountServiceFactory = () => Promise<vscodeMssql.IAzureAccountService>;
export async function defaultAzureAccountServiceFactory(): Promise<vscodeMssql.IAzureAccountService> {
const vscodeMssqlApi = await getVscodeMssqlApi();
return vscodeMssqlApi.azureAccountService;
}
/*
* Returns the default deployment options from DacFx, filtered to appropriate options for the given project.
*/
@@ -441,7 +453,7 @@ export async function retry<T>(
}
} catch (err) {
outputChannel.appendLine(constants.retryMessage(name, err));
outputChannel.appendLine(constants.retryMessage(name, getErrorMessage(err)));
}
}
@@ -599,3 +611,47 @@ export function getFoldersAlongPath(startFolder: string, endFolder: string): str
return folders;
}
/**
* Returns SQL version number from docker image name which is in the beginning of the image name
* @param imageName docker image name
* @returns SQL server version
*/
export function findSqlVersionInImageName(imageName: string): number | undefined {
// Regex to find the version in the beginning of the image name
// e.g. 2017-CU16-ubuntu, 2019-latest
const regex = new RegExp('^([0-9]+)[-].+$');
if (regex.test(imageName)) {
const finds = regex.exec(imageName);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}
/**
* Returns SQL version number from target platform name
* @param targetPlatform target platform
* @returns SQL server version
*/
export function findSqlVersionInTargetPlatform(targetPlatform: string): number | undefined {
// Regex to find the version in target platform
// e.g. SQL Server 2019
const regex = new RegExp('([0-9]+)$');
if (regex.test(targetPlatform)) {
const finds = regex.exec(targetPlatform);
if (finds) {
// 0 is the full match and 1 is the number with pattern inside the first ()
return +finds[1];
}
}
return undefined;
}

View File

@@ -38,16 +38,17 @@ import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/t
import { IconPathHelper } from '../common/iconHelper';
import { DashboardData, PublishData, Status } from '../models/dashboardData/dashboardData';
import { getPublishDatabaseSettings, launchPublishTargetOption } from '../dialogs/publishDatabaseQuickpick';
import { launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick';
import { launchCreateAzureServerQuickPick, launchPublishToDockerContainerQuickpick } from '../dialogs/deployDatabaseQuickpick';
import { DeployService } from '../models/deploy/deployService';
import { GenerateProjectFromOpenApiSpecOptions, SqlTargetPlatform } from 'sqldbproj';
import { AutorestHelper } from '../tools/autorestHelper';
import { createNewProjectFromDatabaseWithQuickpick } from '../dialogs/createProjectFromDatabaseQuickpick';
import { addDatabaseReferenceQuickpick } from '../dialogs/addDatabaseReferenceQuickpick';
import { IDeployProfile } from '../models/deploy/deployProfile';
import { ILocalDbDeployProfile, ISqlDbDeployProfile } from '../models/deploy/deployProfile';
import { EntryType, FileProjectEntry, IDatabaseReferenceProjectEntry, SqlProjectReferenceProjectEntry } from '../models/projectEntry';
import { UpdateProjectAction, UpdateProjectDataModel } from '../models/api/updateProject';
import { targetPlatformToAssets } from '../projectProvider/projectAssets';
import { AzureSqlClient } from '../models/deploy/azureSqlClient';
const maxTableLength = 10;
@@ -75,6 +76,7 @@ export class ProjectsController {
private buildInfo: DashboardData[] = [];
private publishInfo: PublishData[] = [];
private deployService: DeployService;
private azureSqlClient: AzureSqlClient;
private autorestHelper: AutorestHelper;
projFileWatchers = new Map<string, vscode.FileSystemWatcher>();
@@ -82,7 +84,8 @@ export class ProjectsController {
constructor(private _outputChannel: vscode.OutputChannel) {
this.netCoreTool = new NetCoreTool(this._outputChannel);
this.buildHelper = new BuildHelper();
this.deployService = new DeployService(this._outputChannel);
this.azureSqlClient = new AzureSqlClient();
this.deployService = new DeployService(this.azureSqlClient, this._outputChannel);
this.autorestHelper = new AutorestHelper(this._outputChannel);
}
@@ -285,21 +288,58 @@ export class ProjectsController {
}
}
/**
* Publishes a project to a new Azure server
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project or the Project itself
* @param deployProfile deploy profile
*/
public async publishToNewAzureServer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: ISqlDbDeployProfile): Promise<void> {
try {
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToNewAzureServer);
const project: Project = this.getProjectFromContext(context);
if (deployProfile?.deploySettings && deployProfile?.sqlDbSetting) {
void utils.showInfoMessageWithOutputChannel(constants.creatingAzureSqlServer(deployProfile?.sqlDbSetting?.serverName), this._outputChannel);
const connectionUri = await this.deployService.createNewAzureSqlServer(deployProfile);
if (connectionUri) {
deployProfile.deploySettings.connectionUri = connectionUri;
const publishResult = await this.publishOrScriptProject(project, deployProfile.deploySettings, true);
if (publishResult && publishResult.success) {
if (deployProfile.sqlDbSetting) {
// Connecting to the deployed db to add the profile to connection viewlet
await this.deployService.getConnection(deployProfile.sqlDbSetting, true, deployProfile.sqlDbSetting.dbName);
}
void vscode.window.showInformationMessage(constants.publishProjectSucceed);
} else {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, publishResult?.errorMessage || '', this._outputChannel);
}
} else {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, constants.deployProjectFailedMessage, this._outputChannel);
}
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, error, this._outputChannel);
TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToNewAzureServer);
}
}
/**
* Publishes a project to docker container
* @param context a treeItem in a project's hierarchy, to be used to obtain a Project or the Project itself
* @param deployProfile
*/
public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: IDeployProfile): Promise<void> {
public async publishToDockerContainer(context: Project | dataworkspace.WorkspaceTreeItem, deployProfile: ILocalDbDeployProfile): Promise<void> {
const project: Project = this.getProjectFromContext(context);
try {
TelemetryReporter.sendActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer);
TelemetryReporter.createActionEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer)
.withAdditionalProperties({ dockerBaseImage: deployProfile.localDbSetting!.dockerBaseImage })
.send();
if (deployProfile && deployProfile.deploySettings) {
let connectionUri: string | undefined;
if (deployProfile.localDbSetting) {
void utils.showInfoMessageWithOutputChannel(constants.publishingProjectMessage, this._outputChannel);
connectionUri = await this.deployService.deploy(deployProfile, project);
connectionUri = await this.deployService.deployToContainer(deployProfile, project);
if (connectionUri) {
deployProfile.deploySettings.connectionUri = connectionUri;
}
@@ -321,7 +361,9 @@ export class ProjectsController {
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToContainerFailed, error, this._outputChannel);
TelemetryReporter.sendErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer);
TelemetryReporter.createErrorEvent(TelemetryViews.ProjectController, TelemetryActions.publishToContainer)
.withAdditionalProperties({ dockerBaseImage: deployProfile.localDbSetting!.dockerBaseImage })
.send();
}
return;
}
@@ -367,9 +409,19 @@ export class ProjectsController {
if (publishTarget === constants.PublishTargetType.docker) {
const deployProfile = await launchPublishToDockerContainerQuickpick(project);
if (deployProfile?.deploySettings) {
if (deployProfile?.deploySettings && deployProfile?.localDbSetting) {
await this.publishToDockerContainer(project, deployProfile);
}
} else if (publishTarget === constants.PublishTargetType.newAzureServer) {
try {
const settings = await launchCreateAzureServerQuickPick(project, this.azureSqlClient);
if (settings?.deploySettings && settings?.sqlDbSetting) {
await this.publishToNewAzureServer(project, settings);
}
} catch (error) {
void utils.showErrorMessageWithOutputChannel(constants.publishToNewAzureServerFailed, error, this._outputChannel);
}
} else {
let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project);

View File

@@ -133,6 +133,48 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?:
return undefined;
}
// 5. SDK-style project or not
let sdkStyle;
const sdkLearnMoreButton: vscode.QuickInputButton = {
iconPath: new vscode.ThemeIcon('link-external'),
tooltip: constants.learnMore
};
const quickPick = vscode.window.createQuickPick();
quickPick.items = [{ label: constants.YesRecommended }, { label: constants.noString }];
quickPick.title = constants.sdkStyleProject;
quickPick.ignoreFocusOut = true;
const disposables: vscode.Disposable[] = [];
try {
quickPick.buttons = [sdkLearnMoreButton];
quickPick.placeholder = constants.SdkLearnMorePlaceholder;
const sdkStylePromise = new Promise<boolean | undefined>((resolve) => {
disposables.push(
quickPick.onDidHide(() => {
resolve(undefined);
}),
quickPick.onDidChangeSelection((item) => {
resolve(item[0].label === constants.YesRecommended);
}));
disposables.push(quickPick.onDidTriggerButton(async () => {
await vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!));
}));
});
quickPick.show();
sdkStyle = await sdkStylePromise;
quickPick.hide();
} finally {
disposables.forEach(d => d.dispose());
}
if (sdkStyle === undefined) {
// User cancelled
return;
}
return {
connectionUri: connectionUri,
database: selectedDatabase,
@@ -140,6 +182,6 @@ export async function createNewProjectFromDatabaseWithQuickpick(connectionInfo?:
filePath: projectLocation,
version: '1.0.0.0',
extractTarget: mapExtractTargetEnum(folderStructure),
sdkStyle: false // todo: add sdkstyle option to quickpick
sdkStyle: sdkStyle
};
}

View File

@@ -7,11 +7,14 @@ import * as vscode from 'vscode';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import * as uiUtils from './utils';
import { AppSettingType, DockerImageInfo, IDeployAppIntegrationProfile, IDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile';
import { AppSettingType, DockerImageInfo, IDeployAppIntegrationProfile, ISqlDbDeployProfile, ILocalDbDeployProfile, ILocalDbSetting } from '../models/deploy/deployProfile';
import { Project } from '../models/project';
import { getPublishDatabaseSettings } from './publishDatabaseQuickpick';
import * as path from 'path';
import * as fse from 'fs-extra';
import { AzureSqlClient } from '../models/deploy/azureSqlClient';
import { IDeploySettings } from '../models/IDeploySettings';
import { IAccount } from 'vscode-mssql';
/**
* Create flow for Deploying a database using only VS Code-native APIs such as QuickPick
@@ -112,12 +115,168 @@ async function launchEulaQuickPick(imageInfo: DockerImageInfo | undefined): Prom
return false;
}
export async function launchCreateAzureServerQuickPick(project: Project, azureSqlClient: AzureSqlClient): Promise<ISqlDbDeployProfile | undefined> {
const name = uiUtils.getPublishServerName(project.getProjectTargetVersion());
const accounts = await azureSqlClient.getAccounts();
const accountOptions = accounts.map(x => x.displayInfo?.displayName || '');
accountOptions.unshift(constants.azureAddAccount);
let account: IAccount | undefined;
let accountOption = await vscode.window.showQuickPick(
accountOptions,
{ title: constants.azureAccounts, ignoreFocusOut: true });
// Return when user hits escape
if (!accountOption) {
return undefined;
}
if (accountOption === constants.azureAddAccount) {
account = await azureSqlClient.getAccount();
} else {
account = accounts.find(x => x.displayInfo.displayName === accountOption);
}
if (!account) {
return undefined;
}
const sessions = await azureSqlClient.getSessions(account);
const subscriptionName = await vscode.window.showQuickPick(
sessions.map(x => x.subscription.displayName || ''),
{ title: constants.azureSubscription, ignoreFocusOut: true });
// Return when user hits escape
if (!subscriptionName) {
return undefined;
}
const session = sessions.find(x => x.subscription.displayName === subscriptionName);
if (!session?.subscription?.subscriptionId) {
return undefined;
}
const resourceGroups = await azureSqlClient.getResourceGroups(session);
const resourceGroupName = await vscode.window.showQuickPick(
resourceGroups.map(x => x.name || ''),
{ title: constants.resourceGroup, ignoreFocusOut: true });
// Return when user hits escape
if (!resourceGroupName) {
return undefined;
}
const resourceGroup = resourceGroups.find(x => x.name === resourceGroupName);
// Return resource group is invalid
if (!resourceGroup) {
return undefined;
}
let locations = await azureSqlClient.getLocations(session);
if (resourceGroup.location) {
const defaultLocation = locations.find(x => x.name === resourceGroup.location);
if (defaultLocation) {
locations = locations.filter(x => x.name !== defaultLocation.name);
locations.unshift(defaultLocation);
}
}
let locationName = await vscode.window.showQuickPick(
locations.map(x => x.name || ''),
{ title: constants.azureLocation, ignoreFocusOut: true, placeHolder: resourceGroup?.location });
// Return when user hits escape
if (!locationName) {
return undefined;
}
let serverName: string | undefined = '';
serverName = await vscode.window.showInputBox({
title: constants.azureServerName,
ignoreFocusOut: true,
value: serverName,
password: false
}
);
// Return when user hits escape
if (!serverName) {
return undefined;
}
let user: string | undefined = '';
user = await vscode.window.showInputBox({
title: constants.enterUser(name),
ignoreFocusOut: true,
value: user,
password: false
}
);
// Return when user hits escape
if (!user) {
return undefined;
}
let password: string | undefined = '';
password = await vscode.window.showInputBox({
title: constants.enterPassword(name),
ignoreFocusOut: true,
value: password,
validateInput: input => !utils.isValidSQLPassword(input) ? constants.invalidSQLPasswordMessage(name) : undefined,
password: true
}
);
// Return when user hits escape
if (!password) {
return undefined;
}
let confirmPassword: string | undefined = '';
confirmPassword = await vscode.window.showInputBox({
title: constants.confirmPassword(name),
ignoreFocusOut: true,
value: confirmPassword,
validateInput: input => input !== password ? constants.passwordNotMatch(name) : undefined,
password: true
}
);
// Return when user hits escape
if (!confirmPassword) {
return undefined;
}
let settings: IDeploySettings | undefined = await getPublishDatabaseSettings(project, false);
return {
// TODO add tenant
deploySettings: settings, sqlDbSetting: {
tenantId: session.tenantId,
accountId: session.account.key.id,
serverName: serverName,
userName: user,
password: password,
port: 1433,
dbName: '',
session: session,
resourceGroupName: resourceGroup.name || '',
location: locationName
}
};
}
/**
* Create flow for publishing a database to docker container using only VS Code-native APIs such as QuickPick
*/
export async function launchPublishToDockerContainerQuickpick(project: Project): Promise<IDeployProfile | undefined> {
const name = uiUtils.getPublishServerName(project.getProjectTargetVersion());
export async function launchPublishToDockerContainerQuickpick(project: Project): Promise<ILocalDbDeployProfile | undefined> {
const target = project.getProjectTargetVersion();
const name = uiUtils.getPublishServerName(target);
let localDbSetting: ILocalDbSetting | undefined;
// Deploy to docker selected
let portNumber = await vscode.window.showInputBox({
@@ -163,10 +322,10 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
return undefined;
}
const baseImages = uiUtils.getDockerBaseImages(project.getProjectTargetVersion());
const baseImages = uiUtils.getDockerBaseImages(target);
const baseImage = await vscode.window.showQuickPick(
baseImages.map(x => x.displayName),
{ title: constants.selectBaseImage(name), ignoreFocusOut: true });
{ title: constants.selectBaseImage(name), ignoreFocusOut: true, placeHolder: uiUtils.getDockerImagePlaceHolder(target) });
// Return when user hits escape
if (!baseImage) {
@@ -174,19 +333,50 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
}
const imageInfo = baseImages.find(x => x.displayName === baseImage);
if (!imageInfo) {
return undefined;
}
const eulaAccepted = await launchEulaQuickPick(imageInfo);
if (!eulaAccepted) {
return undefined;
}
let imageTags = await uiUtils.getImageTags(imageInfo, target);
let imageTagsItems: vscode.QuickPickItem[] = imageTags.map(tag => { return { label: tag }; });
if (imageInfo.defaultTag) {
// move the default to be the first one in the list
const defaultIndex = imageTagsItems.findIndex(i => i.label === imageInfo.defaultTag);
if (defaultIndex > -1) {
imageTagsItems.splice(defaultIndex, 1);
}
// add default next to the default value
imageTagsItems.unshift({ label: imageInfo.defaultTag, description: constants.defaultQuickPickItem });
}
const imageTag = await vscode.window.showQuickPick(
imageTagsItems,
{ title: constants.selectImageTag(name), ignoreFocusOut: true });
if (!imageTag) {
return undefined;
}
// Add the image tag if it's not the latest
let imageName = imageInfo.name;
if (imageTag && imageTag.label !== constants.dockerImageDefaultTag) {
imageName = `${imageName}:${imageTag.label}`;
}
localDbSetting = {
serverName: constants.defaultLocalServerName,
userName: constants.defaultLocalServerAdminName,
dbName: project.projectFileName,
password: password,
port: +portNumber,
dockerBaseImage: imageInfo?.name || '',
dockerBaseImageEula: imageInfo?.agreementInfo?.link?.url || ''
dockerBaseImage: imageName,
dockerBaseImageEula: imageInfo.agreementInfo.link.url
};
let deploySettings = await getPublishDatabaseSettings(project, false);
@@ -202,7 +392,6 @@ export async function launchPublishToDockerContainerQuickpick(project: Project):
// Get the database name from deploy settings
localDbSetting.dbName = deploySettings.databaseName;
return {
localDbSetting: localDbSetting,
deploySettings: deploySettings,

View File

@@ -16,7 +16,7 @@ import { IconPathHelper } from '../common/iconHelper';
import { cssStyles } from '../common/uiConstants';
import { getAgreementDisplayText, getConnectionName, getDockerBaseImages, getPublishServerName } from './utils';
import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry';
import { IDeployProfile } from '../models/deploy/deployProfile';
import { ILocalDbDeployProfile } from '../models/deploy/deployProfile';
import { Deferred } from '../common/promise';
interface DataSourceDropdownValue extends azdataType.CategoryValue {
@@ -62,7 +62,7 @@ export class PublishDatabaseDialog {
private toDispose: vscode.Disposable[] = [];
public publish: ((proj: Project, profile: IDeploySettings) => any) | undefined;
public publishToContainer: ((proj: Project, profile: IDeployProfile) => any) | undefined;
public publishToContainer: ((proj: Project, profile: ILocalDbDeployProfile) => any) | undefined;
public generateScript: ((proj: Project, profile: IDeploySettings) => any) | undefined;
public readPublishProfile: ((profileUri: vscode.Uri) => any) | undefined;
@@ -232,7 +232,7 @@ export class PublishDatabaseDialog {
const dockerBaseImage = this.getBaseDockerImageName();
const baseImages = getDockerBaseImages(this.project.getProjectTargetVersion());
const imageInfo = baseImages.find(x => x.name === dockerBaseImage);
const settings: IDeployProfile = {
const settings: ILocalDbDeployProfile = {
localDbSetting: {
dbName: this.targetDatabaseName,
dockerBaseImage: dockerBaseImage,

View File

@@ -12,6 +12,7 @@ import { getDefaultPublishDeploymentOptions, getVscodeMssqlApi } from '../common
import { IConnectionInfo } from 'vscode-mssql';
import { IDeploySettings } from '../models/IDeploySettings';
import { getPublishServerName } from './utils';
import { SqlTargetPlatform } from 'sqldbproj';
/**
* Create flow for Publishing a database using only VS Code-native APIs such as QuickPick
@@ -209,9 +210,18 @@ export async function getPublishDatabaseSettings(project: Project, promptForConn
export async function launchPublishTargetOption(project: Project): Promise<constants.PublishTargetType | undefined> {
// Show options to user for deploy to existing server or docker
const name = getPublishServerName(project.getProjectTargetVersion());
const target = project.getProjectTargetVersion();
const name = getPublishServerName(target);
const logicalServerName = target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ? constants.AzureSqlLogicalServerName : constants.SqlServerName;
// Options list based on target
const options = target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ?
[constants.publishToAzureEmulator, constants.publishToNewAzureServer, constants.publishToExistingServer(logicalServerName)] :
[constants.publishToDockerContainer(name), constants.publishToExistingServer(logicalServerName)];
// Show the options to the user
const publishOption = await vscode.window.showQuickPick(
[constants.publishToExistingServer(name), constants.publishToDockerContainer(name)],
options,
{ title: constants.selectPublishOption, ignoreFocusOut: true });
// Return when user hits escape
@@ -219,11 +229,16 @@ export async function launchPublishTargetOption(project: Project): Promise<const
return undefined;
}
// Map the title to the publish option type
switch (publishOption) {
case constants.publishToExistingServer(name):
return constants.PublishTargetType.existingServer;
case constants.publishToDockerContainer(name):
return constants.PublishTargetType.docker;
case constants.publishToAzureEmulator:
return constants.PublishTargetType.docker;
case constants.publishToNewAzureServer:
return constants.PublishTargetType.newAzureServer;
default:
return constants.PublishTargetType.existingServer;
}

View File

@@ -5,6 +5,8 @@
import { SqlTargetPlatform } from 'sqldbproj';
import * as constants from '../common/constants';
import * as utils from '../common/utils';
import { HttpClient } from '../common/httpClient';
import { AgreementInfo, DockerImageInfo } from '../models/deploy/deployProfile';
/**
@@ -38,58 +40,117 @@ export function getPublishServerName(target: string): string {
return target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ? constants.AzureSqlServerName : constants.SqlServerName;
}
/**
* Returns the docker image place holder based on the target version
*/
export function getDockerImagePlaceHolder(target: string): string {
return target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure) ?
constants.dockerImagesPlaceHolder(constants.AzureSqlDbLiteDockerImageName) :
constants.dockerImagesPlaceHolder(SqlTargetPlatform.sqlEdge);
}
/**
* Returns the list of image tags for given target
* @param imageInfo docker image info
* @param target project target version
* @returns image tags
*/
export async function getImageTags(imageInfo: DockerImageInfo, target: string): Promise<string[]> {
let imageTags: string[] | undefined = [];
const versionToImageTags: Map<number, string[]> = new Map<number, string[]>();
try {
const imageTagsFromUrl = await HttpClient.getRequest(imageInfo?.tagsUrl, true);
if (imageTagsFromUrl?.tags) {
// Create a map for version and tags and find the max version in the list
let defaultVersion: number = 0;
let maxVersionNumber: number = defaultVersion;
(imageTagsFromUrl.tags as string[]).forEach(imageTag => {
const version = utils.findSqlVersionInImageName(imageTag) || defaultVersion;
let tags = versionToImageTags.has(version) ? versionToImageTags.get(version) : [];
tags = tags ?? [];
tags = tags?.concat(imageTag);
versionToImageTags.set(version, tags);
maxVersionNumber = version && version > maxVersionNumber ? version : maxVersionNumber;
});
// Find the version maps to the target framework and default to max version in the tags
const targetVersion = utils.findSqlVersionInTargetPlatform(constants.getTargetPlatformFromVersion(target)) || maxVersionNumber;
// Get the image tags with no version of the one that matches project platform
versionToImageTags.forEach((tags: string[], version: number) => {
if (version === defaultVersion || version >= targetVersion) {
imageTags = imageTags?.concat(tags);
}
});
imageTags = imageTags ?? [];
imageTags = imageTags.sort((a, b) => a.indexOf(constants.dockerImageDefaultTag) > 0 ? -1 : a.localeCompare(b));
}
} catch (err) {
// Ignore the error. If http request fails, we just use the default tag
console.debug(`Failed to get docker image tags ${err}`);
}
return imageTags;
}
/**
* Returns the list of base images for given target version
* @param target
* @returns list of image info
*/
export function getDockerBaseImages(target: string): DockerImageInfo[] {
if (target === constants.targetPlatformToVersion.get(SqlTargetPlatform.sqlAzure)) {
return [{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`,
displayName: SqlTargetPlatform.sqlServer2019,
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}`,
displayName: constants.AzureSqlDbFullDockerImageName,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.sqlServerDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
}, {
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`,
displayName: SqlTargetPlatform.sqlEdge,
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}`,
displayName: constants.AzureSqlDbLiteDockerImageName,
agreementInfo: {
link: {
text: constants.edgeEulaAgreementTitle,
url: constants.sqlServerEdgeEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.azureSqlEdgeDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
}];
} else {
return [
{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`,
displayName: SqlTargetPlatform.sqlServer2017,
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}`,
displayName: constants.SqlServerDockerImageName,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.sqlServerDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
},
{
name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`,
displayName: SqlTargetPlatform.sqlServer2019,
agreementInfo: {
link: {
text: constants.eulaAgreementTitle,
url: constants.sqlServerEulaLink,
}
}
},
{
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`,
name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}`,
displayName: SqlTargetPlatform.sqlEdge,
agreementInfo: {
link: {
text: constants.edgeEulaAgreementTitle,
url: constants.sqlServerEdgeEulaLink,
}
}
},
tagsUrl: `https://${constants.sqlServerDockerRegistry}/v2/${constants.azureSqlEdgeDockerRepository}/tags/list`,
defaultTag: constants.dockerImageDefaultTag
},
];
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as utils from '../../common/utils';
import { IAccount, IAzureAccountSession, azure } from 'vscode-mssql';
/**
* Client module to call Azure APIs for getting or creating resources
*/
export class AzureSqlClient {
constructor(
private _azureAccountServiceFactory: utils.AzureAccountServiceFactory = utils.defaultAzureAccountServiceFactory,
private _azureResourceServiceFactory: utils.AzureResourceServiceFactory = utils.defaultAzureResourceServiceFactory
) { }
/**
* Returns existing Azure accounts
*/
public async getAccounts(): Promise<IAccount[]> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.getAccounts();
}
/**
* Prompt user to login to Azure and returns the account
* @returns Azure account that user logged in to
*/
public async getAccount(): Promise<IAccount> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.addAccount();
}
/**
* Returns Azure locations for given subscription
*/
public async getLocations(session: IAzureAccountSession): Promise<azure.subscription.Location[]> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.getLocations(session);
}
/**
* Returns Azure sessions with subscription, tenant and token for given account
*/
public async getSessions(account: IAccount): Promise<IAzureAccountSession[]> {
const azureAccountService = await this._azureAccountServiceFactory();
return await azureAccountService.getAccountSessions(account);
}
/**
* Creates a new Azure SQL server for given subscription, resource group and location
*/
public async createOrUpdateServer(session: IAzureAccountSession, resourceGroupName: string, serverName: string, parameters: azure.sql.Server): Promise<string | undefined> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.createOrUpdateServer(session, resourceGroupName, serverName, parameters);
}
/**
* Returns Azure resource groups for given subscription
*/
public async getResourceGroups(session: IAzureAccountSession): Promise<Array<azure.resources.ResourceGroup> | []> {
const azureResourceService = await this._azureResourceServiceFactory();
return await azureResourceService.getResourceGroups(session);
}
}

View File

@@ -5,39 +5,59 @@
import { IDeploySettings } from '../IDeploySettings';
import type * as azdataType from 'azdata';
import { IAzureAccountSession } from 'vscode-mssql';
export enum AppSettingType {
None,
AzureFunction
}
export interface IDeployProfile {
export interface ILocalDbDeployProfile {
localDbSetting?: ILocalDbSetting;
deploySettings?: IDeploySettings;
}
export interface ISqlDbDeployProfile {
sqlDbSetting?: ISqlDbSetting;
deploySettings?: IDeploySettings;
}
export interface IDeployAppIntegrationProfile {
envVariableName?: string;
appSettingFile?: string;
appSettingType: AppSettingType;
}
export interface ILocalDbSetting {
serverName: string,
port: number,
userName: string,
password: string,
dbName: string,
export interface ISqlDbSetting extends ISqlConnectionProperties {
session: IAzureAccountSession
resourceGroupName: string,
location: string
}
export interface ILocalDbSetting extends ISqlConnectionProperties {
dockerBaseImage: string,
dockerBaseImageEula: string,
connectionRetryTimeout?: number,
profileName?: string
}
export interface ISqlConnectionProperties {
tenantId?: string,
accountId?: string
serverName: string,
userName: string,
password: string,
port: number,
dbName: string,
profileName?: string,
connectionRetryTimeout?: number
}
export interface DockerImageInfo {
name: string,
displayName: string,
agreementInfo: AgreementInfo
agreementInfo: AgreementInfo,
tagsUrl: string,
defaultTag: string
}
export interface AgreementInfo {
link: azdataType.LinkArea;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AppSettingType, IDeployAppIntegrationProfile, IDeployProfile, ILocalDbSetting } from './deployProfile';
import { AppSettingType, IDeployAppIntegrationProfile, ILocalDbDeployProfile, ILocalDbSetting, ISqlConnectionProperties, ISqlDbDeployProfile } from './deployProfile';
import * as UUID from 'vscode-languageclient/lib/utils/uuid';
import { Project } from '../project';
import * as constants from '../../common/constants';
@@ -13,6 +13,8 @@ import * as vscode from 'vscode';
import { ConnectionResult } from 'azdata';
import * as templates from '../../templates/templates';
import { ShellExecutionHelper } from '../../tools/shellExecutionHelper';
import { AzureSqlClient } from './azureSqlClient';
import { IFireWallRuleError } from 'vscode-mssql';
interface DockerImageSpec {
label: string;
@@ -21,7 +23,7 @@ interface DockerImageSpec {
}
export class DeployService {
constructor(private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) {
constructor(private _azureSqlClient = new AzureSqlClient(), private _outputChannel: vscode.OutputChannel, shellExecutionHelper: ShellExecutionHelper | undefined = undefined) {
this._shellExecutionHelper = shellExecutionHelper ?? new ShellExecutionHelper(this._outputChannel);
}
@@ -50,7 +52,7 @@ export class DeployService {
return undefined;
}
public async updateAppSettings(profile: IDeployAppIntegrationProfile, deployProfile: IDeployProfile | undefined): Promise<void> {
public async updateAppSettings(profile: IDeployAppIntegrationProfile, deployProfile: ILocalDbDeployProfile | undefined): Promise<void> {
// Update app settings
//
if (!profile.appSettingFile) {
@@ -122,7 +124,37 @@ export class DeployService {
return { label: imageLabel, tag: imageTag, containerName: dockerName };
}
public async deploy(profile: IDeployProfile, project: Project): Promise<string | undefined> {
/**
* 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
* @returns connection url for the new server
*/
public async createNewAzureSqlServer(profile: ISqlDbDeployProfile | undefined): Promise<string | undefined> {
if (!profile?.sqlDbSetting) {
return undefined;
}
this.logToOutput(constants.creatingAzureSqlServer(profile?.sqlDbSetting?.serverName));
// Create the server
const server = await this._azureSqlClient.createOrUpdateServer(profile.sqlDbSetting.session, profile?.sqlDbSetting.resourceGroupName, profile?.sqlDbSetting.serverName, {
location: profile?.sqlDbSetting?.location,
administratorLogin: profile?.sqlDbSetting.userName,
administratorLoginPassword: profile?.sqlDbSetting.password
});
if (server) {
this._outputChannel.appendLine(constants.serverCreated);
profile.sqlDbSetting.serverName = server;
this.logToOutput(constants.azureSqlServerCreated(profile?.sqlDbSetting?.serverName));
// Connect to the server
return await this.getConnection(profile.sqlDbSetting, false, constants.master);
}
return undefined;
}
public async deployToContainer(profile: ILocalDbDeployProfile, project: Project): Promise<string | undefined> {
return await this.executeTask(constants.deployDbTaskName, async () => {
if (!profile.localDbSetting) {
return undefined;
@@ -218,7 +250,7 @@ export class DeployService {
}
// Connects to a database
private async connectToDatabase(profile: ILocalDbSetting, saveConnectionAndPassword: boolean, database: string): Promise<ConnectionResult | string | undefined> {
private async connectToDatabase(profile: ISqlConnectionProperties, saveConnectionAndPassword: boolean, database: string): Promise<ConnectionResult | string | undefined> {
const getAzdataApi = await utils.getAzdataApi();
const vscodeMssqlApi = getAzdataApi ? undefined : await utils.getVscodeMssqlApi();
if (getAzdataApi) {
@@ -248,7 +280,7 @@ export class DeployService {
encrypt: false,
connectTimeout: 30,
applicationName: 'SQL Database Project',
accountId: undefined,
accountId: profile.accountId,
azureAccountToken: undefined,
applicationIntent: undefined,
attachDbFilename: undefined,
@@ -272,9 +304,19 @@ export class DeployService {
workstationId: undefined,
profileName: profile.profileName,
expiresOn: undefined,
tenantId: undefined
tenantId: profile.tenantId
};
let connectionUrl = await vscodeMssqlApi.connect(connectionProfile, saveConnectionAndPassword);
let connectionUrl = '';
try {
connectionUrl = await vscodeMssqlApi.connect(connectionProfile, saveConnectionAndPassword);
} catch (err) {
const firewallRuleError = <IFireWallRuleError>err;
if (firewallRuleError?.connectionUri) {
await vscodeMssqlApi.promptForFirewallRule(err.connectionUri, connectionProfile);
} else {
throw err;
}
}
return connectionUrl;
} else {
return undefined;
@@ -307,7 +349,7 @@ export class DeployService {
return connectionResult ? connectionResult.connectionId : <string>connection;
}
public async getConnection(profile: ILocalDbSetting, saveConnectionAndPassword: boolean, database: string): Promise<string | undefined> {
public async getConnection(profile: ISqlConnectionProperties, saveConnectionAndPassword: boolean, database: string): Promise<string | undefined> {
const getAzdataApi = await utils.getAzdataApi();
let connection = await utils.retry(
constants.connectingToSqlServerMessage,

View File

@@ -0,0 +1,109 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import { AzureSqlClient } from '../../models/deploy/azureSqlClient';
import { IAccount, IAzureAccountService, IAzureAccountSession, IAzureResourceService, azure } from 'vscode-mssql';
export interface TestContext {
azureAccountService: IAzureAccountService;
azureResourceService: IAzureResourceService;
accounts: IAccount[];
session: IAzureAccountSession;
subscriptions: azure.subscription.Subscription[];
locations: azure.subscription.Location[];
groups: azure.resources.ResourceGroup[];
}
export function createContext(): TestContext {
const accounts = [{
key: undefined!,
displayInfo: undefined!,
properties: {
tenants: [{
id: '',
displayName: ''
}]
},
isStale: false,
isSignedIn: true
}];
const subscriptions: azure.subscription.Subscription[] = [{ subscriptionId: 'id1' }, { subscriptionId: 'id2' }];
const locations: azure.subscription.Location[] = [{ id: 'id1' }, { id: 'id2' }];
const groups: azure.resources.ResourceGroup[] = [{ id: 'id1', location: 'l1' }, { id: 'id2', location: 'l2' }];
const session: IAzureAccountSession = {
account: accounts[0],
subscription: subscriptions[0],
tenantId: 'tenantId',
token: {
key: '',
token: '',
tokenType: '',
}
};
return {
groups: groups,
locations: locations,
subscriptions: subscriptions,
session: session,
accounts: accounts,
azureAccountService: {
addAccount: () => Promise.resolve(accounts[0]),
getAccounts: () => Promise.resolve(accounts),
getAccountSecurityToken: () => Promise.resolve({
key: '',
token: '',
tokenType: ''
}),
getAccountSessions: () => Promise.resolve([session])
},
azureResourceService: {
getLocations: () => Promise.resolve(locations),
getResourceGroups: () => Promise.resolve(groups),
createOrUpdateServer: () => Promise.resolve('new_server')
}
};
}
describe('Azure SQL client', function (): void {
it('Should return accounts successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const accounts = await azureSqlClient.getAccounts();
should(accounts.length).equal(testContext.accounts.length);
});
it('Should create and return new account successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const account = await azureSqlClient.getAccount();
should(account.key).equal(testContext.accounts[0].key);
});
it('Should return subscriptions successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService));
const result = await azureSqlClient.getSessions(testContext.accounts[0]);
should(result[0].subscription.id).deepEqual(testContext.subscriptions[0].id);
});
it('Should return locations successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService), () => Promise.resolve(testContext.azureResourceService));
const result = await azureSqlClient.getLocations(testContext.session);
should(result.length).deepEqual(testContext.locations.length);
});
it('Should return resource groups successfully', async function (): Promise<void> {
const testContext = createContext();
const azureSqlClient = new AzureSqlClient(() => Promise.resolve(testContext.azureAccountService), () => Promise.resolve(testContext.azureResourceService));
const result = await azureSqlClient.getResourceGroups(testContext.session);
should(result.length).deepEqual(testContext.groups.length);
should(result[0].location).deepEqual(testContext.groups[0].location);
});
});

View File

@@ -11,16 +11,18 @@ import { DeployService } from '../../models/deploy/deployService';
import { Project } from '../../models/project';
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import { AppSettingType, IDeployProfile } from '../../models/deploy/deployProfile';
import { AppSettingType, ILocalDbDeployProfile, ISqlDbDeployProfile } from '../../models/deploy/deployProfile';
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';
import { ShellExecutionHelper } from '../../tools/shellExecutionHelper';
import * as TypeMoq from 'typemoq';
import { AzureSqlClient } from '../../models/deploy/azureSqlClient';
export interface TestContext {
outputChannel: vscode.OutputChannel;
azureSqlClient: TypeMoq.IMock<AzureSqlClient>;
}
export const mockConnectionResult: azdata.ConnectionResult = {
@@ -47,7 +49,8 @@ export function createContext(): TestContext {
show: () => { },
hide: () => { },
dispose: () => { }
}
},
azureSqlClient: TypeMoq.Mock.ofType(AzureSqlClient)
};
}
@@ -68,7 +71,7 @@ describe('deploy service', function (): void {
it('Should deploy a database to docker container successfully', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -84,21 +87,21 @@ describe('deploy service', function (): void {
const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath);
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
sandbox.stub(azdata.connection, 'connect').returns(Promise.resolve(mockConnectionResult));
sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection'));
sandbox.stub(vscode.window, 'showWarningMessage').returns(<any>Promise.resolve(constants.yesString));
sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough();
let connection = await deployService.deploy(deployProfile, project1);
let connection = await deployService.deployToContainer(deployProfile, project1);
should(connection).equals('connection');
});
it('Should fail the deploy if docker is not running', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -114,11 +117,11 @@ describe('deploy service', function (): void {
const project1 = await Project.openProject(vscode.Uri.file(projFilePath).fsPath);
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('error'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.reject('error'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
sandbox.stub(azdata.tasks, 'startBackgroundOperation').callThrough();
await should(deployService.deploy(deployProfile, project1)).rejected();
await should(deployService.deployToContainer(deployProfile, project1)).rejected();
});
it('Should retry connecting to the server', async function (): Promise<void> {
@@ -136,8 +139,8 @@ describe('deploy service', function (): void {
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connectionStub = sandbox.stub(azdata.connection, 'connect');
connectionStub.onFirstCall().returns(Promise.resolve(mockFailedConnectionResult));
connectionStub.onSecondCall().returns(Promise.resolve(mockConnectionResult));
@@ -173,7 +176,7 @@ describe('deploy service', function (): void {
const filePath = path.join(project1.projectFolderPath, 'local.settings.json');
await fse.writeFile(filePath, settingContent);
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
@@ -194,8 +197,8 @@ describe('deploy service', function (): void {
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
await deployService.updateAppSettings(appInteg, deployProfile);
let newContent = JSON.parse(fse.readFileSync(filePath, 'utf8'));
@@ -228,7 +231,7 @@ describe('deploy service', function (): void {
const filePath = path.join(project1.projectFolderPath, 'local.settings.json');
await fse.writeFile(filePath, settingContent);
const deployProfile: IDeployProfile = {
const deployProfile: ILocalDbDeployProfile = {
deploySettings: {
connectionUri: 'connection',
@@ -245,8 +248,8 @@ describe('deploy service', function (): void {
};
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connection = new azdata.connection.ConnectionProfile();
sandbox.stub(azdata.connection, 'getConnection').returns(Promise.resolve(connection));
@@ -260,10 +263,10 @@ describe('deploy service', function (): void {
const testContext = createContext();
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(`id
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(`id
id2
id3`));
const deployService = new DeployService(testContext.outputChannel, shellExecutionHelper.object);
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
const ids = await deployService.getCurrentDockerContainer('label');
await deployService.cleanDockerObjects(ids, ['docker stop', 'docker rm']);
shellExecutionHelper.verify(x => x.runStreamedCommand(TypeMoq.It.isAny(), undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.exactly(7));
@@ -271,7 +274,7 @@ describe('deploy service', function (): void {
it('Should create docker image info correctly', () => {
const testContext = createContext();
const deployService = new DeployService(testContext.outputChannel);
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();
@@ -311,4 +314,53 @@ describe('deploy service', function (): void {
tag: `${constants.dockerImageNamePrefix}-${imageProjectName}-${tag}`
});
});
it('Should create a new Azure SQL server successfully', async function (): Promise<void> {
const testContext = createContext();
const deployProfile: ISqlDbDeployProfile = {
sqlDbSetting: {
dbName: 'test',
password: 'PLACEHOLDER',
port: 1433,
serverName: 'localhost',
userName: 'sa',
connectionRetryTimeout: 1,
resourceGroupName: 'resourceGroups',
session: {
subscription: {
subscriptionId: 'subscriptionId',
},token: {
key: '',
token: '',
tokenType: '',
},
tenantId: '',
account: undefined!
},
location: 'location'
}
};
const fullyQualifiedDomainName = 'servername';
const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper);
shellExecutionHelper.setup(x => x.runStreamedCommand(TypeMoq.It.isAny(),
undefined, TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('id'));
const session = deployProfile?.sqlDbSetting?.session;
if (deployProfile?.sqlDbSetting?.session && session) {
testContext.azureSqlClient.setup(x => x.createOrUpdateServer(
session,
deployProfile.sqlDbSetting?.resourceGroupName || '',
deployProfile.sqlDbSetting?.serverName || '',
{
location: deployProfile?.sqlDbSetting?.location || '',
administratorLogin: deployProfile?.sqlDbSetting?.userName,
administratorLoginPassword: deployProfile?.sqlDbSetting?.password
})).returns(() => Promise.resolve(fullyQualifiedDomainName));
}
sandbox.stub(azdata.connection, 'connect').returns(Promise.resolve(mockConnectionResult));
sandbox.stub(azdata.connection, 'getUriForConnection').returns(Promise.resolve('connection'));
const deployService = new DeployService(testContext.azureSqlClient.object, testContext.outputChannel, shellExecutionHelper.object);
let connection = await deployService.createNewAzureSqlServer(deployProfile);
should(deployProfile.sqlDbSetting?.serverName).equal(fullyQualifiedDomainName);
should(connection).equals('connection');
});
});

View File

@@ -18,7 +18,7 @@ import { ProjectsController } from '../../controllers/projectController';
import { IDeploySettings } from '../../models/IDeploySettings';
import { emptySqlDatabaseProjectTypeId } from '../../common/constants';
import { createContext, mockDacFxOptionsResult, TestContext } from '../testContext';
import { IDeployProfile } from '../../models/deploy/deployProfile';
import { ILocalDbDeployProfile } from '../../models/deploy/deployProfile';
let testContext: TestContext;
describe('Publish Database Dialog', () => {
@@ -112,7 +112,7 @@ describe('Publish Database Dialog', () => {
should(profile).deepEqual(expectedGenScript);
const expectedContainerPublishProfile: IDeployProfile = {
const expectedContainerPublishProfile: ILocalDbDeployProfile = {
localDbSetting: {
dbName: 'MockDatabaseName',
dockerBaseImage: '',
@@ -136,7 +136,7 @@ describe('Publish Database Dialog', () => {
}
};
dialog.object.publishToExistingServer = false;
let deployProfile: IDeployProfile | undefined;
let deployProfile: ILocalDbDeployProfile | undefined;
dialog.object.publishToContainer = (_, prof) => { deployProfile = prof; };
await dialog.object.publishClick();

View File

@@ -7,7 +7,7 @@ import * as should from 'should';
import * as path from 'path';
import * as os from 'os';
import { createDummyFileStructure } from './testUtils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword } from '../common/utils';
import { exists, trimUri, removeSqlCmdVariableFormatting, formatSqlCmdVariable, isValidSqlCmdVariableName, timeConversion, validateSqlServerPortNumber, isEmptyString, detectCommandInstallation, isValidSQLPassword, findSqlVersionInImageName, findSqlVersionInTargetPlatform } from '../common/utils';
import { Uri } from 'vscode';
describe('Tests to verify utils functions', function (): void {
@@ -123,5 +123,20 @@ describe('Tests to verify utils functions', function (): void {
should(isValidSQLPassword('dFdf65$530')).equals(true, 'dF65$530 is valid password');
should(isValidSQLPassword('av1fgh533@')).equals(true, 'dF65$530 is valid password');
});
it('findSqlVersionInImageName should return the version correctly', () => {
should(findSqlVersionInImageName('2017-CU1-ubuntu')).equals(2017, 'invalid number returned for 2017-CU1-ubuntu');
should(findSqlVersionInImageName('2019-latest')).equals(2019, 'invalid number returned for 2019-latest');
should(findSqlVersionInImageName('latest')).equals(undefined, 'invalid number returned for latest');
should(findSqlVersionInImageName('latest-ubuntu')).equals(undefined, 'invalid number returned for latest-ubuntu');
should(findSqlVersionInImageName('2017-CU20-ubuntu-16.04')).equals(2017, 'invalid number returned for 2017-CU20-ubuntu-16.04');
});
it('findSqlVersionInTargetPlatform should return the version correctly', () => {
should(findSqlVersionInTargetPlatform('SQL Server 2012')).equals(2012, 'invalid number returned for SQL Server 2012');
should(findSqlVersionInTargetPlatform('SQL Server 2019')).equals(2019, 'invalid number returned for SQL Server 2019');
should(findSqlVersionInTargetPlatform('Azure SQL Database')).equals(undefined, 'invalid number returned for Azure SQL Database');
should(findSqlVersionInTargetPlatform('Azure Synapse Dedicated SQL Pool')).equals(undefined, 'invalid number returned for Azure Synapse Dedicated SQL Pool');
});
});

File diff suppressed because it is too large Load Diff