diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 08d4841af7..b5e8864ed3 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -553,20 +553,7 @@ ], "when": true } - ], - "agreement": { - "template": "%arc.control.plane.arc.data.controller.agreement%", - "links": [ - { - "text": "%microsoft.agreement.privacy.statement%", - "url": "https://go.microsoft.com/fwlink/?LinkId=853010" - }, - { - "text": "%arc.agreement.azdata.eula%", - "url": "https://aka.ms/eula-azdata-en" - } - ] - } + ] }, { "name": "arc.sql", @@ -577,18 +564,6 @@ "light": "./images/miaa.svg", "dark": "./images/miaa.svg" }, - "options": [ - { - "name": "resourceType", - "displayName": "%resource.type.picker.display.name%", - "values": [ - { - "name": "sql.managed.instance", - "displayName": "%sql.managed.instance.display.name%" - } - ] - } - ], "providers": [ { "dialog": { @@ -706,7 +681,7 @@ "version": "20.2.0" } ], - "when": "resourceType=sql.managed.instance" + "when": "true" } ], "agreement": { @@ -719,10 +694,6 @@ { "text": "%arc.agreement.sql.terms.conditions%", "url": "https://go.microsoft.com/fwlink/?linkid=2045708" - }, - { - "text": "%arc.agreement.azdata.eula%", - "url": "https://aka.ms/eula-azdata-en" } ] } @@ -736,18 +707,6 @@ "light": "./images/postgres.svg", "dark": "./images/postgres.svg" }, - "options": [ - { - "name": "resourceType", - "displayName": "%resource.type.picker.display.name%", - "values": [ - { - "name": "postgres", - "displayName": "%postgres.server.group.display.name%" - } - ] - } - ], "providers": [ { "dialog": { @@ -926,7 +885,7 @@ "version": "20.2.0" } ], - "when": "resourceType=postgres" + "when": "true" } ], "agreement": { @@ -939,10 +898,6 @@ { "text": "%arc.agreement.postgres.terms.conditions%", "url": "https://go.microsoft.com/fwlink/?linkid=2045708" - }, - { - "text": "%arc.agreement.azdata.eula%", - "url": "https://aka.ms/eula-azdata-en" } ] } diff --git a/extensions/arc/package.nls.json b/extensions/arc/package.nls.json index f172c3f4cb..319412eb46 100644 --- a/extensions/arc/package.nls.json +++ b/extensions/arc/package.nls.json @@ -65,7 +65,6 @@ "arc.control.plane.summary.location": "Location", "arc.control.plane.arc.data.controller.agreement": "I accept {0} and {1}.", "microsoft.agreement.privacy.statement":"Microsoft Privacy Statement", - "arc.agreement.azdata.eula":"azdata license terms", "deploy.arc.control.plane.action":"Script to notebook", @@ -73,12 +72,9 @@ "resource.type.arc.postgres.display.name": "PostgreSQL Hyperscale server groups - Azure Arc (preview)", "resource.type.arc.sql.description": "Managed SQL Instance service for app developers in a customer-managed environment", "resource.type.arc.postgres.description": "Deploy PostgreSQL server groups into an Azure Arc environment", - "resource.type.picker.display.name": "Resource Type", - "sql.managed.instance.display.name": "Azure SQL managed instance - Azure Arc", - "postgres.server.group.display.name": "PostgreSQL server groups - Azure Arc", - "arc.controller": "Target Azure Arc Controller", + "arc.sql.new.dialog.title": "Deploy Azure SQL managed instance - Azure Arc (preview)", "arc.sql.settings.section.title": "SQL Connection information", "arc.azure.section.title": "Azure information", @@ -130,7 +126,7 @@ "arc.postgres.server.group.cores.description": "Fractional cores are supported", "arc.postgres.server.group.memory.request": "Min memory MB (per node) to reserve", "arc.postgres.server.group.memory.limit": "Max memory MB (per node) to allow", - "arc.agreement": "I accept {0}, {1} and {2}.", + "arc.agreement": "I accept {0} and {1}.", "arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions", "arc.agreement.postgres.terms.conditions":"PostgreSQL server groups - Azure Arc terms and conditions", "arc.deploy.action":"Deploy" diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index b66b526230..01b60d0c51 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -24,9 +24,6 @@ const enum AzdataDeployOption { * Interface for an object to interact with the azdata tool installed on the box. */ export interface IAzdataTool extends azdataExt.IAzdataApi { - path: string, - cachedVersion: SemVer - /** * Executes azdata with the specified arguments (e.g. --version) and returns the result * @param args The args to pass to azdata @@ -39,9 +36,26 @@ export interface IAzdataTool extends azdataExt.IAzdataApi { * An object to interact with the azdata tool installed on the box. */ export class AzdataTool implements IAzdataTool { - public cachedVersion: SemVer; - constructor(public path: string, version: string) { - this.cachedVersion = new SemVer(version); + + private _semVersion: SemVer; + constructor(private _path: string, version: string) { + this._semVersion = new SemVer(version); + } + + /** + * The semVersion corresponding to this installation of azdata. version() method should have been run + * before fetching this value to ensure that correct value is returned. This is almost always correct unless + * Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed. + */ + public getSemVersion() { + return this._semVersion; + } + + /** + * gets the path where azdata tool is installed + */ + public getPath() { + return this._path; } public arc = { @@ -141,8 +155,8 @@ export class AzdataTool implements IAzdataTool { * It also updates the cachedVersion property based on the return value from the tool. */ public async version(): Promise> { - const output = await executeAzdataCommand(`"${this.path}"`, ['--version']); - this.cachedVersion = new SemVer(parseVersion(output.stdout)); + const output = await executeAzdataCommand(`"${this._path}"`, ['--version']); + this._semVersion = new SemVer(parseVersion(output.stdout)); return { logs: [], stdout: output.stdout.split(os.EOL), @@ -153,7 +167,7 @@ export class AzdataTool implements IAzdataTool { public async executeCommand(args: string[], additionalEnvVars?: { [key: string]: string }): Promise> { try { - const output = JSON.parse((await executeAzdataCommand(`"${this.path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout); + const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout); return { logs: output.log, stdout: output.stdout, @@ -172,7 +186,7 @@ export class AzdataTool implements IAzdataTool { // it means this was probably some other generic error (such as command not being found) // check if azdata still exists if it does then rethrow the original error if not then emit a new specific error. try { - await fs.promises.access(this.path); + await fs.promises.access(this._path); //this.path exists throw err; // rethrow the error } catch (e) { @@ -207,7 +221,7 @@ export async function findAzdata(): Promise { try { const azdata = await findSpecificAzdata(); await vscode.commands.executeCommand('setContext', azdataFound, true); // save a context key that azdata was found so that command for installing azdata is no longer available in commandPalette and that for updating it is. - Logger.log(loc.foundExistingAzdata(azdata.path, azdata.cachedVersion.raw)); + Logger.log(loc.foundExistingAzdata(azdata.getPath(), azdata.getSemVersion().raw)); return azdata; } catch (err) { Logger.log(loc.couldNotFindAzdata(err)); @@ -293,12 +307,12 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro */ export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise { if (currentAzdata !== undefined) { - const newVersion = await discoverLatestAvailableAzdataVersion(); - if (newVersion.compare(currentAzdata.cachedVersion) === 1) { - Logger.log(loc.foundAzdataVersionToUpdateTo(newVersion.raw, currentAzdata.cachedVersion.raw)); - return await promptToUpdateAzdata(newVersion.raw, userRequested); + const newSemVersion = await discoverLatestAvailableAzdataVersion(); + if (newSemVersion.compare(currentAzdata.getSemVersion()) === 1) { + Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentAzdata.getSemVersion().raw)); + return await promptToUpdateAzdata(newSemVersion.raw, userRequested); } else { - Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.cachedVersion.raw)); + Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.getSemVersion().raw)); } } else { Logger.log(loc.updateCheckSkipped); diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index 6a29f61ffb..b874061027 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -12,7 +12,6 @@ import * as loc from './localizedConstants'; let localAzdata: IAzdataTool | undefined = undefined; let eulaAccepted: boolean = false; - export async function activate(context: vscode.ExtensionContext): Promise { vscode.commands.registerCommand('azdata.acceptEula', async () => { eulaAccepted = await promptForEula(context.globalState, true /* userRequested */); @@ -59,26 +58,27 @@ export async function activate(context: vscode.ExtensionContext): Promise !!context.globalState.get(constants.eulaAccepted), azdata: { arc: { dc: { create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass); }, endpoint: { list: async () => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.dc.endpoint.list(); } }, config: { list: async () => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.dc.config.list(); }, show: async () => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.dc.config.show(); } } @@ -90,11 +90,11 @@ export async function activate(context: vscode.ExtensionContext): Promise { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.postgres.server.list(); }, show: async (name: string) => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.postgres.server.show(name); }, edit: async (args: { @@ -119,41 +119,53 @@ export async function activate(context: vscode.ExtensionContext): Promise { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.sql.mi.delete(name); }, list: async () => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.sql.mi.list(); }, show: async (name: string) => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.arc.sql.mi.show(name); } } } }, + getPath: () => { + throwIfNoAzdata(); + return localAzdata!.getPath(); + }, login: async (endpoint: string, username: string, password: string) => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdataOrEulaNotAccepted(); return localAzdata!.login(endpoint, username, password); }, + getSemVersion: () => { + throwIfNoAzdata(); + return localAzdata!.getSemVersion(); + }, version: async () => { - await throwIfNoAzdataOrEulaNotAccepted(); + throwIfNoAzdata(); return localAzdata!.version(); } } }; } -async function throwIfNoAzdataOrEulaNotAccepted(): Promise { - if (!localAzdata) { - Logger.log(loc.noAzdata); - throw new Error(loc.noAzdata); - } +function throwIfNoAzdataOrEulaNotAccepted(): void { + throwIfNoAzdata(); if (!eulaAccepted) { Logger.log(loc.eulaNotAccepted); throw new Error(loc.eulaNotAccepted); } } +function throwIfNoAzdata() { + if (!localAzdata) { + Logger.log(loc.noAzdata); + throw new Error(loc.noAzdata); + } +} + export function deactivate(): void { } diff --git a/extensions/azdata/src/localizedConstants.ts b/extensions/azdata/src/localizedConstants.ts index 9f052d14df..bec5ee3e9a 100644 --- a/extensions/azdata/src/localizedConstants.ts +++ b/extensions/azdata/src/localizedConstants.ts @@ -56,11 +56,11 @@ export const userRequestedInstall = localize('azdata.userRequestedInstall', "Use export const userRequestedUpdate = localize('azdata.userRequestedUpdate', "User requested to update Azure Data CLI using 'Azure Data CLI: Check for Update' command"); export const userRequestedAcceptEula = localize('azdata.acceptEula', "User requested to be prompted for accepting EULA by invoking 'Azure Data CLI: Accept EULA' command"); export const updateCheckSkipped = localize('azdata.updateCheckSkipped', "No check for new Azure Data CLI version availability performed as Azure Data CLI was not found to be installed"); -export const eulaNotAccepted = localize('azdata.eulaNotAccepted', "Microsoft Privacy statement and Azure Data CLI license terms have not been accepted, [accept the EULA](command:azdata.acceptEula) to use the features that require it."); +export const eulaNotAccepted = localize('azdata.eulaNotAccepted', "Microsoft Privacy statement and Azure Data CLI license terms have not been accepted. Execute the command: [Azure Data CLI: Accept EULA](command:azdata.acceptEula) to accept EULA to enable the features that requires Azure Data CLI."); export const installManually = (expectedVersion: string, instructionsUrl: string) => localize('azdata.installManually', "Azure Data CLI is not installed. Version: {0} needs to be installed or some features may not work. Please install it manually using these [instructions]({1}). Restart ADS when installation is done.", expectedVersion, instructionsUrl); export const installCorrectVersionManually = (currentVersion: string, expectedVersion: string, instructionsUrl: string) => localize('azdata.installCorrectVersionManually', "Azure Data CLI version: {0} is installed, version: {1} needs to be installed or some features may not work. Please uninstall the current version and then install the correct version manually using these [instructions]({2}). Restart ADS when installation is done.", currentVersion, expectedVersion, instructionsUrl); export const promptForEula = (privacyStatementUrl: string, eulaUrl: string) => localize('azdata.promptForEula', "It is required to accept the [Microsoft Privacy Statement]({0}) and the [Azure Data CLI license terms]({1}) to use this extension. Declining this will result in some features not working.", privacyStatementUrl, eulaUrl); export const promptForEulaLog = (privacyStatementUrl: string, eulaUrl: string) => promptLog(promptForEula(privacyStatementUrl, eulaUrl)); -export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to Eula prompt: {0}", response); -export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "eulaAccepted state on startup: {0}", eulaAccepted); -export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'eulaAccepted' state to: {0}", eulaAccepted); +export const userResponseToEulaPrompt = (response: string | undefined) => localize('azdata.promptForEulaResponse', "User response to EULA prompt: {0}", response); +export const eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted); +export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'EULA Accepted' state to: {0}", eulaAccepted); diff --git a/extensions/azdata/src/test/azdata.test.ts b/extensions/azdata/src/test/azdata.test.ts index c56ea4f2ac..d15414eec7 100644 --- a/extensions/azdata/src/test/azdata.test.ts +++ b/extensions/azdata/src/test/azdata.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { SemVer } from 'semver'; import * as should from 'should'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; @@ -12,10 +11,10 @@ import * as azdata from '../azdata'; import * as childProcess from '../common/childProcess'; import { HttpClient } from '../common/httpClient'; import * as utils from '../common/utils'; -import * as loc from '../localizedConstants'; import * as constants from '../constants'; +import * as loc from '../localizedConstants'; -const oldAzdataMock = { path: '/path/to/azdata', cachedVersion: new SemVer('0.0.0') }; +const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0'); const releaseJson = { win32: { 'version': '9999.999.999', diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index c25064880e..7fb577ee1d 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ declare module 'azdata-ext' { + import { SemVer } from 'semver'; + /** * Covers defining what the azdata extension exports to other extensions * @@ -80,7 +82,7 @@ declare module 'azdata-ext' { location: string, // "eastus2euap", resourceGroup: string, // "my-rg", subscription: string, // "a5082b29-8c6e-4bc5-8ddd-8ef39dfebc39" - }, + }, controller: { 'enableBilling': string, // "True" 'logs.rotation.days': string, // "7" @@ -254,12 +256,20 @@ declare module 'azdata-ext' { show(name: string): Promise> } } - } + }, + getPath(): string, login(endpoint: string, username: string, password: string): Promise>, + /** + * The semVersion corresponding to this installation of azdata. version() method should have been run + * before fetching this value to ensure that correct value is returned. This is almost always correct unless + * Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed. + */ + getSemVersion(): SemVer, version(): Promise> } export interface IExtension { azdata: IAzdataApi; + isEulaAccepted(): boolean; } } diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index ffc3ead921..100739168c 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -502,18 +502,20 @@ "dependencies": { "linux-release-info": "^2.0.0", "promisify-child-process": "^3.1.1", + "semver": "^7.3.2", "sudo-prompt": "9.1.1", "vscode-nls": "^4.0.0", "yamljs": "^0.3.0" }, "devDependencies": { "@types/mocha": "^5.2.5", + "@types/semver": "^7.3.1", "@types/yamljs": "0.2.30", "mocha": "^5.2.0", "mocha-junit-reporter": "^1.17.0", "mocha-multi-reporters": "^1.1.7", + "should": "^13.2.3", "typemoq": "^2.1.0", - "vscodetestcover": "^1.1.0", - "should": "^13.2.3" + "vscodetestcover": "^1.1.0" } } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 8b0225eacc..66e4a8cb3e 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -388,6 +388,7 @@ export interface ITool { finishInitialization(): Promise; install(): Promise; isSameOrNewerThan(version: string): boolean; + validateEula(): boolean; } export const enum BdcDeploymentType { diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index d739592394..0cbf9fc352 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -34,3 +34,4 @@ export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotD export const optionsNotObjectOrArray = localize('optionsNotObjectOrArray', "FieldInfo.options must be an object if it is not an array"); export const optionsTypeNotFound = localize('optionsTypeNotFound', "When FieldInfo.options is an object it must have 'optionsType' property"); export const optionsTypeRadioOrDropdown = localize('optionsTypeRadioOrDropdown', "When optionsType is not {0} then it must be {1}", OptionsType.Radio, OptionsType.Dropdown); +export const azdataEulaNotAccepted = localize('azdataEulaNotAccepted', "Deployment cannot continue. Azure Data CLI license terms have not been accepted. Execute the command: [Azure Data CLI: Accept EULA] to accept EULA to enable the features that requires Azure Data CLI."); diff --git a/extensions/resource-deployment/src/services/apiService.ts b/extensions/resource-deployment/src/services/apiService.ts index 1df81c4baa..fed993131b 100644 --- a/extensions/resource-deployment/src/services/apiService.ts +++ b/extensions/resource-deployment/src/services/apiService.ts @@ -3,18 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as arc from 'arc'; +import * as azdataExt from 'azdata-ext'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; -import * as arc from 'arc'; export interface IApiService { readonly azurecoreApi: azurecore.IExtension; + readonly azdataApi: azdataExt.IExtension; readonly arcApi: arc.IExtension; } class ApiService implements IApiService { constructor() { } public get azurecoreApi() { return vscode.extensions.getExtension(azurecore.extension.name)?.exports; } + public get azdataApi() { return vscode.extensions.getExtension(azdataExt.extension.name)?.exports; } public get arcApi() { return vscode.extensions.getExtension(arc.extension.name)?.exports; } } diff --git a/extensions/resource-deployment/src/services/tools/azdataTool.ts b/extensions/resource-deployment/src/services/tools/azdataTool.ts index 8806ccae29..307db0dbe9 100644 --- a/extensions/resource-deployment/src/services/tools/azdataTool.ts +++ b/extensions/resource-deployment/src/services/tools/azdataTool.ts @@ -8,10 +8,11 @@ import { SemVer } from 'semver'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { AzdataInstallLocationKey, DeploymentConfigurationKey } from '../../constants'; -import { Command, OsDistribution, ToolType } from '../../interfaces'; +import { Command, OsDistribution, ToolStatus, ToolType } from '../../interfaces'; +import { apiService } from '../apiService'; import { IPlatformService } from '../platformService'; import { dependencyType, ToolBase } from './toolBase'; -import { SemVerProxy } from './SemVerProxy'; +import * as loc from '../../localizedConstants'; const localize = nls.loadMessageBundle(); export const AzdataToolName = 'azdata'; @@ -44,25 +45,57 @@ export class AzdataTool extends ToolBase { return 'https://docs.microsoft.com/sql/big-data-cluster/deploy-install-azdata'; } + public validateEula(): boolean { + if (apiService.azdataApi.isEulaAccepted()) { + return true; + } else { + this.setStatusDescription(loc.azdataEulaNotAccepted); + return false; + } + } + + /* unused */ protected get versionCommand(): Command { return { - command: 'azdata -v' + command: '' }; } + /* unused */ protected get discoveryCommand(): Command { return { - command: this.discoveryCommandString('azdata') + command: '' }; } + /** + * updates the version and status for the tool. + */ + protected async updateVersionAndStatus(): Promise { + this.setStatusDescription(''); + await this.addInstallationSearchPathsToSystemPath(); + + const commandOutput = await apiService.azdataApi.azdata.version(); + this.version = apiService.azdataApi.azdata.getSemVersion(); + if (this.version) { + if (this.autoInstallSupported) { + // set the installationPath + this.setInstallationPathOrAdditionalInformation(apiService.azdataApi.azdata.getPath()); + } + this.setStatus(ToolStatus.Installed); + } + else { + this.setInstallationPathOrAdditionalInformation(localize('deployCluster.GetToolVersionErrorInformation', "Error retrieving version information. See output channel '{0}' for more details", this.outputChannelName)); + this.setStatusDescription(localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Invalid output received, get version command output: '{1}' ", EOL, commandOutput.stderr.join(EOL))); + this.setStatus(ToolStatus.NotInstalled); + } + } + protected getVersionFromOutput(output: string): SemVer | undefined { - let version: SemVer | undefined = undefined; - if (output && output.split(EOL).length > 0) { - version = new SemVerProxy(output.split(EOL)[0].replace(/ /g, '')); - } - return version; + return apiService.azdataApi.azdata.getSemVersion(); + } + protected async getSearchPaths(): Promise { switch (this.osDistribution) { case OsDistribution.win32: diff --git a/extensions/resource-deployment/src/services/tools/toolBase.ts b/extensions/resource-deployment/src/services/tools/toolBase.ts index 3f53a376b5..76080749ec 100644 --- a/extensions/resource-deployment/src/services/tools/toolBase.ts +++ b/extensions/resource-deployment/src/services/tools/toolBase.ts @@ -58,6 +58,8 @@ export abstract class ToolBase implements ITool { protected abstract readonly versionCommand: Command; + public validateEula(): boolean { return true; } + public get dependencyMessages(): string[] { return (this.dependenciesByOsType.get(this.osDistribution) || []).map((msgType: dependencyType) => messageByDependencyType.get(msgType)!); } @@ -126,10 +128,18 @@ export abstract class ToolBase implements ITool { return this._statusDescription; } + protected setStatusDescription(value: string | undefined): void { + this._statusDescription = value; + } + public get installationPathOrAdditionalInformation(): string | undefined { return this._installationPathOrAdditionalInformation; } + protected setInstallationPathOrAdditionalInformation(value: string | undefined) { + this._installationPathOrAdditionalInformation = value; + } + protected get installationCommands(): Command[] | undefined { return this.allInstallationCommands.get(this.osDistribution); } @@ -250,7 +260,7 @@ export abstract class ToolBase implements ITool { /** * updates the version and status for the tool. */ - private async updateVersionAndStatus(): Promise { + protected async updateVersionAndStatus(): Promise { this._statusDescription = ''; await this.addInstallationSearchPathsToSystemPath(); const commandOutput = await this.platformService.runCommand( @@ -306,7 +316,7 @@ export abstract class ToolBase implements ITool { } isSameOrNewerThan(version?: string): boolean { - return !version || (this._version ? SemVerCompare(this._version, version) >= 0 : false); + return !version || (this._version ? SemVerCompare(this._version.raw, version) >= 0 : false); } private _pendingVersionAndStatusUpdate!: Promise; diff --git a/extensions/resource-deployment/src/test/apiService.test.ts b/extensions/resource-deployment/src/test/apiService.test.ts index c1b7b8181f..4b3595189c 100644 --- a/extensions/resource-deployment/src/test/apiService.test.ts +++ b/extensions/resource-deployment/src/test/apiService.test.ts @@ -8,7 +8,6 @@ import assert = require('assert'); import { apiService } from '../services/apiService'; suite('API Service Tests', function (): void { - test('getAzurecoreApi returns azure api', () => { const api = apiService.azurecoreApi; assert(api !== undefined); diff --git a/extensions/resource-deployment/src/typings/ref.d.ts b/extensions/resource-deployment/src/typings/ref.d.ts index 8146e7bfb3..5f85967ab9 100644 --- a/extensions/resource-deployment/src/typings/ref.d.ts +++ b/extensions/resource-deployment/src/typings/ref.d.ts @@ -7,6 +7,7 @@ /// /// /// +/// /// /// /// diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 953655be14..6ea8176e1a 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -26,6 +26,7 @@ export class ResourceTypePickerDialog extends DialogBase { private _agreementContainer!: azdata.DivContainer; private _agreementCheckboxChecked: boolean = false; private _installToolButton: azdata.window.Button; + private _recheckEulaButton: azdata.window.Button; private _installationInProgress: boolean = false; private _tools: ITool[] = []; @@ -37,10 +38,15 @@ export class ResourceTypePickerDialog extends DialogBase { super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true); this._selectedResourceType = defaultResourceType; this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools")); + this._recheckEulaButton = azdata.window.createButton(localize('deploymentDialog.RecheckEulaButton', "Validate EULA")); this._toDispose.push(this._installToolButton.onClick(() => { this.installTools().catch(error => console.log(error)); })); - this._dialogObject.customButtons = [this._installToolButton]; + this._toDispose.push(this._recheckEulaButton.onClick(() => { + this._dialogObject.message = { text: '' }; // clear any previous message. + this._dialogObject.okButton.enabled = this.validateToolsEula(); // re-enable the okButton if validation succeeds. + })); + this._dialogObject.customButtons = [this._installToolButton, this._recheckEulaButton]; this._installToolButton.hidden = true; this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select"); this._dialogObject.okButton.enabled = false; // this is enabled after all tools are discovered. @@ -278,12 +284,12 @@ export class ResourceTypePickerDialog extends DialogBase { return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPathOrAdditionalInformation || '']; }); this._installToolButton.hidden = erroredOrFailedTool || minVersionCheckFailed || (toolsToAutoInstall.length === 0); - this._dialogObject.okButton.enabled = !erroredOrFailedTool && messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0); + this._dialogObject.okButton.enabled = !erroredOrFailedTool && messages.length === 0 && !minVersionCheckFailed && (toolsToAutoInstall.length === 0) && this.validateToolsEula(); if (messages.length !== 0) { if (messages.length > 1) { messages = messages.map(message => `• ${message}`); } - messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in 'Deployments' output channel")); + messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed manually to pick up the change. You may find additional details in 'Deployments' and 'azdata' output channels")); this._dialogObject.message = { level: azdata.window.MessageLevel.Error, text: messages.join(EOL) @@ -313,6 +319,21 @@ export class ResourceTypePickerDialog extends DialogBase { this._toolsLoadingComponent.loading = false; } + private validateToolsEula(): boolean { + const validationSucceeded = this._tools.every(tool => { + const eulaValidated = tool.validateEula(); + if (!eulaValidated) { + this._dialogObject.message = { + level: azdata.window.MessageLevel.Error, + text: tool.statusDescription! + }; + } + return eulaValidated; + }); + this._recheckEulaButton.hidden = validationSucceeded; + return validationSucceeded; + } + private get toolRequirements() { return this.getCurrentProvider().requiredTools; } diff --git a/extensions/resource-deployment/yarn.lock b/extensions/resource-deployment/yarn.lock index 811f06eb35..4e6d54ff71 100644 --- a/extensions/resource-deployment/yarn.lock +++ b/extensions/resource-deployment/yarn.lock @@ -194,6 +194,11 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== +"@types/semver@^7.3.1": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" + integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== + "@types/yamljs@0.2.30": version "0.2.30" resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382" @@ -694,6 +699,11 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + should-equal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"