diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 4056366093..3c8fb885c0 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -70,6 +70,7 @@ export const dataSourcesNodeName = localize('dataSourcesNodeName', "Data Sources export const databaseReferencesNodeName = localize('databaseReferencesNodeName', "Database References"); export const sqlConnectionStringFriendly = localize('sqlConnectionStringFriendly', "SQL connection string"); export const yesString = localize('yesString', "Yes"); +export const openEulaString = localize('openEulaString', "Open License Agreement"); export const noString = localize('noString', "No"); export const noStringDefault = localize('noStringDefault', "No (default)"); export const okString = localize('okString', "Ok"); @@ -149,6 +150,14 @@ export const portMustBeNumber = localize('portMustNotBeNumber', "Port must a be export const valueCannotBeEmpty = localize('valueCannotBeEmpty', "Value cannot be empty"); export const dockerImageLabelPrefix = 'source=sqldbproject'; export const dockerImageNamePrefix = 'sqldbproject'; + +// +export const eulaAgreementTemplate = localize({ key: 'eulaAgreementTemplate', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}."); +export function eulaAgreementText(name: string) { return localize({ key: 'eulaAgreementText', comment: ['The placeholders are contents of the line and should not be translated.'] }, "I accept the {0}.", name); } +export const eulaAgreementTitle = localize('eulaAgreementTitle', "Microsoft SQL Server License Agreement"); +export const edgeEulaAgreementTitle = localize('edgeEulaAgreementTitle', "Microsoft Azure SQL Edge License Agreement"); +export const sqlServerEulaLink = 'https://go.microsoft.com/fwlink/?linkid=857698'; +export const sqlServerEdgeEulaLink = 'https://go.microsoft.com/fwlink/?linkid=2139274'; export const connectionNamePrefix = 'SQLDbProject'; export const sqlServerDockerRegistry = 'mcr.microsoft.com'; export const sqlServerDockerRepository = 'mssql/server'; @@ -170,6 +179,8 @@ export const deployDbTaskName = localize('deployDbTaskName', "Deploying SQL Db P export const publishProjectSucceed = localize('publishProjectSucceed', "Database project published successfully"); export const publishingProjectMessage = localize('publishingProjectMessage', "Publishing project in a container..."); export const cleaningDockerImagesMessage = localize('cleaningDockerImagesMessage', "Cleaning existing deployments..."); +export const dockerImageMessage = localize('dockerImageMessage', "Docker Image:"); +export const dockerImageEulaMessage = localize('dockerImageEulaMessage', "License Agreement:"); export const creatingDeploymentSettingsMessage = localize('creatingDeploymentSettingsMessage', "Creating deployment settings ..."); export const runningDockerMessage = localize('runningDockerMessage', "Building and running the docker container ..."); export function dockerNotRunningError(error: string) { return localize('dockerNotRunningError', "Failed to verify docker. Please make sure docker is installed and running. Error: '{0}'", error || ''); } diff --git a/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts index f8e9fa081b..eb81126935 100644 --- a/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts +++ b/extensions/sql-database-projects/src/dialogs/deployDatabaseQuickpick.ts @@ -70,6 +70,50 @@ export async function launchDeployAppIntegrationQuickpick(project: Project): Pro }; } +async function launchEulaQuickPick(baseImage: string): Promise { + let eulaAccepted: boolean = false; + const baseImages = uiUtils.getDockerBaseImages(); + const imageInfo = baseImages.find(x => x.name === baseImage); + const agreementInfo = imageInfo?.agreementInfo; + if (agreementInfo) { + const openEulaButton: vscode.QuickInputButton = { + iconPath: new vscode.ThemeIcon('link-external'), + tooltip: constants.openEulaString + }; + const quickPick = vscode.window.createQuickPick(); + quickPick.items = [{ label: constants.yesString }, + { label: constants.noString }]; + quickPick.title = uiUtils.getAgreementDisplayText(agreementInfo); + quickPick.ignoreFocusOut = true; + quickPick.buttons = [openEulaButton]; + const disposables: vscode.Disposable[] = []; + try { + const eulaAcceptedPromise = new Promise((resolve) => { + disposables.push( + quickPick.onDidHide(() => { + resolve(false); + }), + quickPick.onDidTriggerButton(async () => { + await vscode.env.openExternal(vscode.Uri.parse(agreementInfo.link.url)); + }), + quickPick.onDidChangeSelection((item) => { + resolve(item[0].label === constants.yesString); + })); + }); + + quickPick.show(); + eulaAccepted = await eulaAcceptedPromise; + quickPick.hide(); + } + finally { + disposables.forEach(d => d.dispose()); + } + + return eulaAccepted; + } + return false; +} + /** * Create flow for publishing a database to docker container using only VS Code-native APIs such as QuickPick */ @@ -120,8 +164,9 @@ export async function launchPublishToDockerContainerQuickpick(project: Project): return undefined; } + const baseImages = uiUtils.getDockerBaseImages(); const baseImage = await vscode.window.showQuickPick( - uiUtils.getDockerBaseImages(), + baseImages.map(x => x.name), { title: constants.selectBaseImage, ignoreFocusOut: true }); // Return when user hits escape @@ -129,13 +174,21 @@ export async function launchPublishToDockerContainerQuickpick(project: Project): return undefined; } + const eulaAccepted = await launchEulaQuickPick(baseImage); + if (!eulaAccepted) { + return undefined; + } + + const imageInfo = baseImages.find(x => x.name === baseImage); + localDbSetting = { serverName: constants.defaultLocalServerName, userName: constants.defaultLocalServerAdminName, dbName: project.projectFileName, password: password, port: +portNumber, - dockerBaseImage: baseImage + dockerBaseImage: baseImage, + dockerBaseImageEula: imageInfo?.agreementInfo?.link?.url || '' }; let deploySettings = await getPublishDatabaseSettings(project, false); diff --git a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts index 09d4d4e335..3f69b3c866 100644 --- a/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts +++ b/extensions/sql-database-projects/src/dialogs/publishDatabaseDialog.ts @@ -14,7 +14,7 @@ import { IDeploySettings } from '../models/IDeploySettings'; import { DeploymentOptions } from '../../../mssql/src/mssql'; import { IconPathHelper } from '../common/iconHelper'; import { cssStyles } from '../common/uiConstants'; -import { getConnectionName, getDockerBaseImages } from './utils'; +import { getAgreementDisplayText, getConnectionName, getDockerBaseImages } from './utils'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { IDeployProfile } from '../models/deploy/deployProfile'; import { Deferred } from '../common/promise'; @@ -35,6 +35,7 @@ export class PublishDatabaseDialog { private connectionsRadioButton: azdataType.RadioButtonComponent | undefined; private existingServerRadioButton: azdataType.RadioButtonComponent | undefined; private dockerServerRadioButton: azdataType.RadioButtonComponent | undefined; + private eulaCheckBox: azdataType.CheckBoxComponent | undefined; private dataSourcesRadioButton: azdataType.RadioButtonComponent | undefined; private sqlCmdVariablesTable: azdataType.DeclarativeTableComponent | undefined; private sqlCmdVariablesFormComponentGroup: azdataType.FormComponentGroup | undefined; @@ -230,10 +231,14 @@ export class PublishDatabaseDialog { utils.getAzdataApi()!.window.closeDialog(this.dialog); await this.publish!(this.project, settings); } else { + const dockerBaseImage = this.getBaseDockerImageName(); + const baseImages = getDockerBaseImages(); + const imageInfo = baseImages.find(x => x.name === dockerBaseImage); const settings: IDeployProfile = { localDbSetting: { dbName: this.targetDatabaseName, - dockerBaseImage: this.getBaseDockerImageName(), + dockerBaseImage: dockerBaseImage, + dockerBaseImageEula: imageInfo?.agreementInfo?.link?.url || '', password: this.serverAdminPasswordTextBox?.value || '', port: +(this.serverPortTextBox?.value || constants.defaultPortNumber), serverName: constants.defaultLocalServerName, @@ -570,21 +575,49 @@ export class PublishDatabaseDialog { }); this.serverConfigAdminPasswordTextBox.onTextChanged(() => { this.tryEnableGenerateScriptAndOkButtons(); - }); const serverConfirmPasswordRow = this.createFormRow(view, constants.confirmServerPassword, this.serverConfigAdminPasswordTextBox); + const baseImages = getDockerBaseImages(); this.baseDockerImageDropDown = view.modelBuilder.dropDown().withProps({ - values: getDockerBaseImages(), + values: baseImages.map(x => x.name), ariaLabel: constants.baseDockerImage, width: cssStyles.publishDialogTextboxWidth, enabled: true }).component(); + const agreementInfo = baseImages[0].agreementInfo; const dropDownRow = this.createFormRow(view, constants.baseDockerImage, this.baseDockerImageDropDown); + this.eulaCheckBox = view.modelBuilder.checkBox().withProps({ + ariaLabel: getAgreementDisplayText(agreementInfo), + required: true + }).component(); + this.eulaCheckBox.onChanged(() => { + this.tryEnableGenerateScriptAndOkButtons(); + }); + + const eulaRow = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); this.localDbSection = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, dropDownRow]); + this.localDbSection.addItems([serverPortRow, serverPasswordRow, serverConfirmPasswordRow, dropDownRow, eulaRow]); + this.baseDockerImageDropDown.onValueChanged(() => { + if (this.eulaCheckBox) { + this.eulaCheckBox.checked = false; + } + const baseImage = getDockerBaseImages().find(x => x.name === this.baseDockerImageDropDown?.value); + if (baseImage?.agreementInfo.link) { + const text = view.modelBuilder.text().withProps({ + value: constants.eulaAgreementTemplate, + links: [baseImage.agreementInfo.link], + requiredIndicator: true + }).component(); + + if (eulaRow && this.eulaCheckBox) { + eulaRow?.clearItems(); + eulaRow?.addItems([this.eulaCheckBox, text], { flex: '0 0 auto', CSSStyles: { 'margin-right': '10px' } }); + } + } + }); return this.localDbSection; } @@ -835,7 +868,8 @@ export class PublishDatabaseDialog { } else if (utils.validateSqlServerPortNumber(this.serverPortTextBox?.value) && !utils.isEmptyString(this.serverAdminPasswordTextBox?.value) && utils.isValidSQLPassword(this.serverAdminPasswordTextBox?.value || '', constants.defaultLocalServerAdminName) && - this.serverAdminPasswordTextBox?.value === this.serverConfigAdminPasswordTextBox?.value) { + this.serverAdminPasswordTextBox?.value === this.serverConfigAdminPasswordTextBox?.value + && this.eulaCheckBox?.checked) { publishEnabled = true; // only publish is supported for container } diff --git a/extensions/sql-database-projects/src/dialogs/utils.ts b/extensions/sql-database-projects/src/dialogs/utils.ts index 6d9d80cca1..d8668ce8d7 100644 --- a/extensions/sql-database-projects/src/dialogs/utils.ts +++ b/extensions/sql-database-projects/src/dialogs/utils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as constants from '../common/constants'; +import { AgreementInfo, DockerImageInfo } from '../models/deploy/deployProfile'; /** * Gets connection name from connection object if there is one, @@ -25,11 +26,38 @@ export function getConnectionName(connection: any): string { return connectionName; } +export function getAgreementDisplayText(agreementInfo: AgreementInfo): string { + return constants.eulaAgreementText(agreementInfo.link!.text); +} -export function getDockerBaseImages(): string[] { +export function getDockerBaseImages(): DockerImageInfo[] { return [ - `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`, - `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`, - `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest` + { + name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2017-latest`, + agreementInfo: { + link: { + text: constants.eulaAgreementTitle, + url: constants.sqlServerEulaLink, + } + } + }, + { + name: `${constants.sqlServerDockerRegistry}/${constants.sqlServerDockerRepository}:2019-latest`, + agreementInfo: { + link: { + text: constants.eulaAgreementTitle, + url: constants.sqlServerEulaLink, + } + } + }, + { + name: `${constants.sqlServerDockerRegistry}/${constants.azureSqlEdgeDockerRepository}:latest`, + agreementInfo: { + link: { + text: constants.edgeEulaAgreementTitle, + url: constants.sqlServerEdgeEulaLink, + } + } + }, ]; } diff --git a/extensions/sql-database-projects/src/models/deploy/deployProfile.ts b/extensions/sql-database-projects/src/models/deploy/deployProfile.ts index 6e239275dd..2a2ee31374 100644 --- a/extensions/sql-database-projects/src/models/deploy/deployProfile.ts +++ b/extensions/sql-database-projects/src/models/deploy/deployProfile.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDeploySettings } from '../IDeploySettings'; +import type * as azdataType from 'azdata'; export enum AppSettingType { None, @@ -27,6 +28,15 @@ export interface ILocalDbSetting { password: string, dbName: string, dockerBaseImage: string, + dockerBaseImageEula: string, connectionRetryTimeout?: number, profileName?: string } + +export interface DockerImageInfo { + name: string, + agreementInfo: AgreementInfo +} +export interface AgreementInfo { + link: azdataType.LinkArea; +} diff --git a/extensions/sql-database-projects/src/models/deploy/deployService.ts b/extensions/sql-database-projects/src/models/deploy/deployService.ts index f6039e83ea..b84e536635 100644 --- a/extensions/sql-database-projects/src/models/deploy/deployService.ts +++ b/extensions/sql-database-projects/src/models/deploy/deployService.ts @@ -131,6 +131,11 @@ export class DeployService { } await this.verifyDocker(); + this.logToOutput(constants.dockerImageMessage); + this.logToOutput(profile.localDbSetting.dockerBaseImage); + + this.logToOutput(constants.dockerImageEulaMessage); + this.logToOutput(profile.localDbSetting.dockerBaseImageEula); const imageSpec = this.getDockerImageSpec(project.projectFileName, profile.localDbSetting.dockerBaseImage); diff --git a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts index 86710f14b5..fa6452de81 100644 --- a/extensions/sql-database-projects/src/test/deploy/deployService.test.ts +++ b/extensions/sql-database-projects/src/test/deploy/deployService.test.ts @@ -76,7 +76,8 @@ describe('deploy service', function (): void { serverName: 'localhost', userName: 'sa', dockerBaseImage: 'image', - connectionRetryTimeout: 1 + connectionRetryTimeout: 1, + dockerBaseImageEula: '' } }; const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); @@ -105,7 +106,8 @@ describe('deploy service', function (): void { serverName: 'localhost', userName: 'sa', dockerBaseImage: 'image', - connectionRetryTimeout: 1 + connectionRetryTimeout: 1, + dockerBaseImageEula: '' } }; const projFilePath = await testUtils.createTestSqlProjFile(baselines.newProjectFileBaseline); @@ -128,7 +130,8 @@ describe('deploy service', function (): void { serverName: 'localhost', userName: 'sa', dockerBaseImage: 'image', - connectionRetryTimeout: 1 + connectionRetryTimeout: 1, + dockerBaseImageEula: '' }; const shellExecutionHelper = TypeMoq.Mock.ofType(ShellExecutionHelper); @@ -177,7 +180,8 @@ describe('deploy service', function (): void { port: 1433, serverName: 'localhost', userName: 'sa', - dockerBaseImage: 'image' + dockerBaseImage: 'image', + dockerBaseImageEula: '' } }; diff --git a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts index 914c0f2fa6..34ea8a3ce0 100644 --- a/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts +++ b/extensions/sql-database-projects/src/test/dialogs/publishDatabaseDialog.test.ts @@ -117,7 +117,8 @@ describe('Publish Database Dialog', () => { password: '', port: 1433, serverName: 'localhost', - userName: 'sa' + userName: 'sa', + dockerBaseImageEula: '' }, deploySettings: {