Read publish profile using DacFX (#16110)

* Read publish profile using DacFX in VS Code

* Fixes

* complete promise on hide

* comment
This commit is contained in:
Charles Gagnon
2021-07-15 08:53:43 -07:00
committed by GitHub
parent 06da33bb3b
commit d03fbbc066
9 changed files with 126 additions and 82 deletions

View File

@@ -5,6 +5,7 @@
import * as nls from 'vscode-nls';
import { SqlTargetPlatform } from 'sqldbproj';
import * as utils from '../common/utils';
const localize = nls.loadMessageBundle();
@@ -101,7 +102,7 @@ export const connectionRadioButtonLabel = localize('connectionRadioButtonLabel',
export const dataSourceDropdownTitle = localize('dataSourceDropdownTitle', "Data source");
export const noDataSourcesText = localize('noDataSourcesText', "No data sources in this project");
export const loadProfilePlaceholderText = localize('loadProfilePlaceholderText', "Load profile...");
export const profileReadError = localize('profileReadError', "Could not load the profile file.");
export const profileReadError = (err: any) => localize('profileReadError', "Error loading the publish profile. {0}", utils.getErrorMessage(err));
export const sqlCmdTableLabel = localize('sqlCmdTableLabel', "SQLCMD Variables");
export const sqlCmdVariableColumn = localize('sqlCmdVariableColumn', "Name");
export const sqlCmdValueColumn = localize('sqlCmdValueColumn', "Value");
@@ -110,7 +111,8 @@ export const profile = localize('profile', "Profile");
export const selectConnection = localize('selectConnection', "Select connection");
export const server = localize('server', "Server");
export const defaultUser = localize('default', "default");
export const selectProfile = localize('selectProfile', "Select publish profile to load");
export const selectProfileToUse = localize('selectProfileToUse', "Select publish profile to load");
export const selectProfile = localize('selectProfile', "Select Profile");
export const dontUseProfile = localize('dontUseProfile', "Don't use profile");
export const browseForProfile = localize('browseForProfile', "Browse for profile");
export const chooseAction = localize('chooseAction', "Choose action");

View File

@@ -11,6 +11,7 @@ import * as path from 'path';
import * as glob from 'fast-glob';
import * as dataworkspace from 'dataworkspace';
import * as mssql from '../../../mssql';
import * as vscodeMssql from 'vscode-mssql';
import { promises as fs } from 'fs';
/**
@@ -251,6 +252,20 @@ export function getDataWorkspaceExtensionApi(): dataworkspace.IExtension {
return extension.exports;
}
export type IDacFxService = mssql.IDacFxService | vscodeMssql.IDacFxService;
export async function getDacFxService(): Promise<IDacFxService> {
if (getAzdataApi()) {
let ext = vscode.extensions.getExtension(mssql.extension.name) as vscode.Extension<mssql.IExtension>;
const api = await ext.activate();
return api.dacFx;
} else {
let ext = vscode.extensions.getExtension(vscodeMssql.extension.name) as vscode.Extension<vscodeMssql.IExtension>;
const api = await ext.activate();
return api.dacFx;
}
}
/*
* Returns the default deployment options from DacFx
*/

View File

@@ -26,7 +26,7 @@ import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { ImportDataModel } from '../models/api/import';
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
import { BuildHelper } from '../tools/buildHelper';
import { PublishProfile, load } from '../models/publishProfile/publishProfile';
import { readPublishProfile } from '../models/publishProfile/publishProfile';
import { AddDatabaseReferenceDialog } from '../dialogs/addDatabaseReferenceDialog';
import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectReferenceSettings } from '../models/IDatabaseReferenceSettings';
import { DatabaseReferenceTreeItem } from '../models/tree/databaseReferencesTreeItem';
@@ -39,8 +39,6 @@ import { launchPublishDatabaseQuickpick } from '../dialogs/publishDatabaseQuickp
const maxTableLength = 10;
export type IDacFxService = mssql.IDacFxService | mssqlVscode.IDacFxService;
/**
* Controller for managing lifecycle of projects
*/
@@ -242,9 +240,9 @@ export class ProjectsController {
if (utils.getAzdataApi()) {
let publishDatabaseDialog = this.getPublishDialog(project);
publishDatabaseDialog.publish = async (proj, prof) => await this.publishProjectCallback(proj, prof);
publishDatabaseDialog.generateScript = async (proj, prof) => await this.publishProjectCallback(proj, prof);
publishDatabaseDialog.readPublishProfile = async (profileUri) => await this.readPublishProfileCallback(profileUri);
publishDatabaseDialog.publish = async (proj, prof) => this.publishProjectCallback(proj, prof);
publishDatabaseDialog.generateScript = async (proj, prof) => this.publishProjectCallback(proj, prof);
publishDatabaseDialog.readPublishProfile = async (profileUri) => readPublishProfile(profileUri);
publishDatabaseDialog.openDialog();
@@ -279,7 +277,7 @@ export class ProjectsController {
const tempPath = path.join(os.tmpdir(), `${path.parse(dacpacPath).name}_${new Date().getTime()}${constants.sqlprojExtension}`);
await fs.copyFile(dacpacPath, tempPath);
const dacFxService = await this.getDaxFxService();
const dacFxService = await utils.getDacFxService();
let result: mssql.DacFxResult;
telemetryProps.profileUsed = (settings.profileUsed ?? false).toString();
@@ -349,17 +347,6 @@ export class ProjectsController {
return result;
}
public async readPublishProfileCallback(profileUri: vscode.Uri): Promise<PublishProfile> {
try {
const dacFxService = await this.getDaxFxService();
const profile = await load(profileUri, dacFxService);
return profile;
} catch (e) {
vscode.window.showErrorMessage(constants.profileReadError);
throw e;
}
}
public async schemaCompare(treeNode: dataworkspace.WorkspaceTreeItem): Promise<void> {
try {
// check if schema compare extension is installed
@@ -740,7 +727,7 @@ export class ProjectsController {
const streamingJobDefinition: string = (await fs.readFile(node.element.fileSystemUri.fsPath)).toString();
const dacFxService = await this.getDaxFxService();
const dacFxService = await utils.getDacFxService();
const actionStartTime = new Date().getTime();
const result: mssql.ValidateStreamingJobResult = await dacFxService.validateStreamingJob(dacpacPath, streamingJobDefinition);
@@ -831,21 +818,6 @@ export class ProjectsController {
}
}
public async getDaxFxService(): Promise<IDacFxService> {
if (utils.getAzdataApi()) {
const ext: vscode.Extension<mssql.IExtension> = vscode.extensions.getExtension(mssql.extension.name)!;
const extensionApi = await ext.activate();
return extensionApi.dacFx;
} else {
const ext: vscode.Extension<mssqlVscode.IExtension> = vscode.extensions.getExtension(mssql.extension.name)!;
const extensionApi = await ext.activate();
return extensionApi.dacFx;
}
}
private async promptForNewObjectName(itemType: templates.ProjectScriptType, _project: Project, folderPath: string, fileExtension?: string): Promise<string | undefined> {
const suggestedName = itemType.friendlyName.replace(/\s+/g, '');
let counter: number = 0;

View File

@@ -622,6 +622,7 @@ export class PublishDatabaseDialog {
export function promptForPublishProfile(defaultPath: string): Thenable<vscode.Uri[] | undefined> {
return vscode.window.showOpenDialog(
{
title: constants.selectProfile,
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: false,

View File

@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import * as constants from '../common/constants';
import { IGenerateScriptSettings, IPublishSettings } from '../models/IPublishSettings';
import { Project } from '../models/project';
import { PublishProfile, readPublishProfile } from '../models/publishProfile/publishProfile';
import { promptForPublishProfile } from './publishDatabaseDialog';
/**
@@ -15,21 +16,57 @@ import { promptForPublishProfile } from './publishDatabaseDialog';
export async function launchPublishDatabaseQuickpick(project: Project): Promise<void> {
// 1. Select publish settings file (optional)
// TODO@chgagnon: Hook up to dacfx service
const browseProfileOption = await vscode.window.showQuickPick(
[constants.dontUseProfile, constants.browseForProfile],
{ title: constants.selectProfile, ignoreFocusOut: true });
if (!browseProfileOption) {
return;
}
// Create custom quickpick so we can control stuff like displaying the loading indicator
const quickPick = vscode.window.createQuickPick();
quickPick.items = [{ label: constants.dontUseProfile }, { label: constants.browseForProfile }];
quickPick.ignoreFocusOut = true;
quickPick.title = constants.selectProfileToUse;
const profilePicked = new Promise<PublishProfile | undefined>((resolve, reject) => {
quickPick.onDidHide(() => {
// If the quickpick is hidden that means the user cancelled or another quickpick came up - so we reject
// here to be able to complete the promise being waited on below
reject();
});
quickPick.onDidChangeSelection(async items => {
if (items[0].label === constants.browseForProfile) {
const locations = await promptForPublishProfile(project.projectFolderPath);
if (!locations) {
// Clear items so that this event will trigger again if they select the same item
quickPick.selectedItems = [];
quickPick.activeItems = [];
// If the user cancels out of the file picker then just return and let them choose another option
return;
}
let publishProfileUri = locations[0];
try {
// Show loading state while reading profile
quickPick.busy = true;
quickPick.enabled = false;
const profile = await readPublishProfile(publishProfileUri);
resolve(profile);
} catch (err) {
// readPublishProfile will handle displaying an error if one occurs
// Clear items so that this event will trigger again if they select the same item
quickPick.selectedItems = [];
quickPick.activeItems = [];
quickPick.busy = false;
quickPick.enabled = true;
// let publishSettingsFile: vscode.Uri | undefined;
if (browseProfileOption === constants.browseForProfile) {
const locations = await promptForPublishProfile(project.projectFolderPath);
if (!locations) {
return;
}
// publishSettingsFile = locations[0];
}
} else {
// Selected no profile so just continue on
resolve(undefined);
}
});
});
quickPick.show();
let publishProfile: PublishProfile | undefined = undefined;
try {
publishProfile = await profilePicked;
} catch (err) {
// User cancelled or another quickpick came up and hid the current one
// so exit the flow.
return;
}
// 2. Select connection
@@ -83,8 +120,9 @@ export async function launchPublishDatabaseQuickpick(project: Project): Promise<
// 4. Modify sqlcmd vars
// TODO@chgagnon: Concat ones from publish profile
let sqlCmdVariables = Object.assign({}, project.sqlCmdVariables);
// If a publish profile is provided then the values from there will overwrite the ones in the
// project file (if they exist)
let sqlCmdVariables = Object.assign({}, project.sqlCmdVariables, publishProfile?.sqlCmdVariables);
if (Object.keys(sqlCmdVariables).length > 0) {
// Continually loop here, allowing the user to modify SQLCMD variables one
@@ -123,7 +161,7 @@ export async function launchPublishDatabaseQuickpick(project: Project): Promise<
sqlCmdVariables[sqlCmd.key] = newValue;
}
} else if (sqlCmd.isResetAllVars) {
sqlCmdVariables = Object.assign({}, project.sqlCmdVariables);
sqlCmdVariables = Object.assign({}, project.sqlCmdVariables, publishProfile?.sqlCmdVariables);
} else if (sqlCmd.isDone) {
break;
}

View File

@@ -8,11 +8,10 @@ import * as constants from '../../common/constants';
import * as utils from '../../common/utils';
import * as mssql from '../../../../mssql';
import * as vscodeMssql from 'vscode-mssql';
import * as vscode from 'vscode';
import { promises as fs } from 'fs';
import { Uri } from 'vscode';
import { SqlConnectionDataSource } from '../dataSources/sqlConnectionStringSource';
import { IDacFxService } from '../../controllers/projectController';
// only reading db name, connection string, and SQLCMD vars from profile for now
export interface PublishProfile {
@@ -24,10 +23,21 @@ export interface PublishProfile {
options?: mssql.DeploymentOptions | vscodeMssql.DeploymentOptions;
}
export async function readPublishProfile(profileUri: vscode.Uri): Promise<PublishProfile> {
try {
const dacFxService = await utils.getDacFxService();
const profile = await load(profileUri, dacFxService);
return profile;
} catch (e) {
vscode.window.showErrorMessage(constants.profileReadError(e));
throw e;
}
}
/**
* parses the specified file to load publish settings
*/
export async function load(profileUri: Uri, dacfxService: IDacFxService): Promise<PublishProfile> {
export async function load(profileUri: vscode.Uri, dacfxService: utils.IDacFxService): Promise<PublishProfile> {
const profileText = await fs.readFile(profileUri.fsPath);
const profileXmlDoc = new xmldom.DOMParser().parseFromString(profileText.toString());
@@ -67,17 +77,26 @@ async function readConnectionString(xmlDoc: any): Promise<{ connectionId: string
const connectionProfile = dataSource.getConnectionProfile();
try {
const azdataApi = utils.getAzdataApi();
if (dataSource.integratedSecurity) {
const connection = await utils.getAzdataApi()!.connection.connect(connectionProfile, false, false);
connId = connection.connectionId;
if (azdataApi) {
const connection = await utils.getAzdataApi()!.connection.connect(connectionProfile, false, false);
connId = connection.connectionId;
} else {
// TODO@chgagnon - hook up VS Code MSSQL
}
server = dataSource.server;
username = constants.defaultUser;
}
else {
const connection = await utils.getAzdataApi()!.connection.openConnectionDialog(undefined, connectionProfile);
connId = connection.connectionId;
server = connection.options['server'];
username = connection.options['user'];
if (azdataApi) {
const connection = await utils.getAzdataApi()!.connection.openConnectionDialog(undefined, connectionProfile);
connId = connection.connectionId;
server = connection.options['server'];
username = connection.options['user'];
} else {
// TODO@chgagnon - hook up VS Code MSSQL
}
}
targetConnection = `${server} (${username})`;

View File

@@ -15,6 +15,7 @@ import * as templates from '../templates/templates';
import * as testUtils from './testUtils';
import * as constants from '../common/constants';
import * as mssql from '../../../mssql';
import * as utils from '../common/utils';
import { SqlDatabaseProjectTreeViewProvider } from '../controllers/databaseProjectTreeViewProvider';
import { ProjectsController } from '../controllers/projectController';
@@ -23,7 +24,6 @@ import { createContext, TestContext, mockDacFxResult, mockConnectionProfile } fr
import { Project, reservedProjectFolders, SystemDatabase, FileProjectEntry, SystemDatabaseReferenceProjectEntry } from '../models/project';
import { PublishDatabaseDialog } from '../dialogs/publishDatabaseDialog';
import { IPublishSettings, IGenerateScriptSettings } from '../models/IPublishSettings';
import { exists } from '../common/utils';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { FolderNode, FileNode } from '../models/tree/fileFolderTreeItem';
import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
@@ -198,10 +198,10 @@ describe('ProjectsController', function (): void {
should(proj.postDeployScripts.length).equal(0);
should(proj.noneDeployScripts.length).equal(0);
should(await exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted');
should(await exists(preDeployEntry.fsUri.fsPath)).equal(false, 'pre-deployment script is supposed to be deleted');
should(await exists(postDeployEntry.fsUri.fsPath)).equal(false, 'post-deployment script is supposed to be deleted');
should(await exists(noneEntry.fsUri.fsPath)).equal(false, 'none entry pre-deployment script is supposed to be deleted');
should(await utils.exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted');
should(await utils.exists(preDeployEntry.fsUri.fsPath)).equal(false, 'pre-deployment script is supposed to be deleted');
should(await utils.exists(postDeployEntry.fsUri.fsPath)).equal(false, 'post-deployment script is supposed to be deleted');
should(await utils.exists(noneEntry.fsUri.fsPath)).equal(false, 'none entry pre-deployment script is supposed to be deleted');
});
it('Should delete database references', async function (): Promise<void> {
@@ -259,10 +259,10 @@ describe('ProjectsController', function (): void {
should(proj.postDeployScripts.length).equal(0);
should(proj.noneDeployScripts.length).equal(0);
should(await exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk');
should(await exists(preDeployEntry.fsUri.fsPath)).equal(true, 'pre-deployment script is supposed to still exist on disk');
should(await exists(postDeployEntry.fsUri.fsPath)).equal(true, 'post-deployment script is supposed to still exist on disk');
should(await exists(noneEntry.fsUri.fsPath)).equal(true, 'none entry pre-deployment script is supposed to still exist on disk');
should(await utils.exists(scriptEntry.fsUri.fsPath)).equal(true, 'script is supposed to still exist on disk');
should(await utils.exists(preDeployEntry.fsUri.fsPath)).equal(true, 'pre-deployment script is supposed to still exist on disk');
should(await utils.exists(postDeployEntry.fsUri.fsPath)).equal(true, 'post-deployment script is supposed to still exist on disk');
should(await utils.exists(noneEntry.fsUri.fsPath)).equal(true, 'none entry pre-deployment script is supposed to still exist on disk');
});
it('Should delete folders with excluded items', async function (): Promise<void> {
@@ -287,9 +287,9 @@ describe('ProjectsController', function (): void {
// Confirm result
should(proj.files.some(x => x.relativePath === 'UpperFolder')).equal(false, 'UpperFolder should not be part of proj file any more');
should(await exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted from disk');
should(await exists(lowerFolder.projectUri.fsPath)).equal(false, 'LowerFolder is supposed to be deleted from disk');
should(await exists(upperFolder.projectUri.fsPath)).equal(false, 'UpperFolder is supposed to be deleted from disk');
should(await utils.exists(scriptEntry.fsUri.fsPath)).equal(false, 'script is supposed to be deleted from disk');
should(await utils.exists(lowerFolder.projectUri.fsPath)).equal(false, 'LowerFolder is supposed to be deleted from disk');
should(await utils.exists(upperFolder.projectUri.fsPath)).equal(false, 'UpperFolder is supposed to be deleted from disk');
});
it('Should reload correctly after changing sqlproj file', async function (): Promise<void> {
@@ -426,8 +426,7 @@ describe('ProjectsController', function (): void {
builtDacpacPath = await testUtils.createTestFile(fakeDacpacContents, 'output.dacpac');
return builtDacpacPath;
});
projController.setup(x => x.getDaxFxService()).returns(() => Promise.resolve(testContext.dacFxService.object));
sinon.stub(utils, 'getDacFxService').resolves(testContext.dacFxService.object);
const proj = await testUtils.createTestProject(baselines.openProjectFileBaseline);

View File

@@ -11,9 +11,8 @@ import * as TypeMoq from 'typemoq';
import * as baselines from './baselines/baselines';
import * as testUtils from './testUtils';
import * as constants from '../common/constants';
import { ProjectsController } from '../controllers/projectController';
import { TestContext, createContext, mockDacFxOptionsResult } from './testContext';
import { load } from '../models/publishProfile/publishProfile';
import { load, readPublishProfile } from '../models/publishProfile/publishProfile';
let testContext: TestContext;
@@ -81,10 +80,9 @@ describe('Publish profile tests', function (): void {
it('Should throw error when connecting does not work', async function (): Promise<void> {
await baselines.loadBaselines();
let profilePath = await testUtils.createTestFile(baselines.publishProfileIntegratedSecurityBaseline, 'publishProfile.publish.xml');
const projController = new ProjectsController();
sinon.stub(azdata.connection, 'connect').throws(new Error('Could not connect'));
await testUtils.shouldThrowSpecificError(async () => await projController.readPublishProfileCallback(vscode.Uri.file(profilePath)), constants.unableToCreatePublishConnection('Could not connect'));
await testUtils.shouldThrowSpecificError(async () => await readPublishProfile(vscode.Uri.file(profilePath)), constants.unableToCreatePublishConnection('Could not connect'));
});
});

View File

@@ -13,7 +13,7 @@ declare module 'vscode-mssql' {
export const enum extension {
name = 'Microsoft.mssql'
name = 'ms-mssql.mssql'
}
/**
@@ -45,7 +45,7 @@ declare module 'vscode-mssql' {
validateStreamingJob(packageFilePath: string, createStreamingJobTsql: string): Thenable<ValidateStreamingJobResult>;
}
export enum TaskExecutionMode {
export const enum TaskExecutionMode {
execute = 0,
script = 1,
executeAndScript = 2