Remove azdata eula acceptance from arc deployments (#12292)

* saving to switch tasks

* activate to exports in extApi

* working version - cleanup pending

* improve messages

* apply pr feedback from a different review

* remove unneeded strings

* redo apiService

* remove async from getVersionFromOutput

* remove _ prefix from protected fields

* error message fix

* throw specif errors from azdata extension

* arrow methods to regular methods

* pr feedback

* expand azdata extension api

* pr feedback

* remove unused var

* pr feedback
This commit is contained in:
Arvind Ranasaria
2020-09-17 11:20:32 -07:00
committed by GitHub
parent 945e04ed92
commit ba44a2f02e
17 changed files with 181 additions and 114 deletions

View File

@@ -553,20 +553,7 @@
], ],
"when": true "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", "name": "arc.sql",
@@ -577,18 +564,6 @@
"light": "./images/miaa.svg", "light": "./images/miaa.svg",
"dark": "./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": [ "providers": [
{ {
"dialog": { "dialog": {
@@ -706,7 +681,7 @@
"version": "20.2.0" "version": "20.2.0"
} }
], ],
"when": "resourceType=sql.managed.instance" "when": "true"
} }
], ],
"agreement": { "agreement": {
@@ -719,10 +694,6 @@
{ {
"text": "%arc.agreement.sql.terms.conditions%", "text": "%arc.agreement.sql.terms.conditions%",
"url": "https://go.microsoft.com/fwlink/?linkid=2045708" "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", "light": "./images/postgres.svg",
"dark": "./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": [ "providers": [
{ {
"dialog": { "dialog": {
@@ -926,7 +885,7 @@
"version": "20.2.0" "version": "20.2.0"
} }
], ],
"when": "resourceType=postgres" "when": "true"
} }
], ],
"agreement": { "agreement": {
@@ -939,10 +898,6 @@
{ {
"text": "%arc.agreement.postgres.terms.conditions%", "text": "%arc.agreement.postgres.terms.conditions%",
"url": "https://go.microsoft.com/fwlink/?linkid=2045708" "url": "https://go.microsoft.com/fwlink/?linkid=2045708"
},
{
"text": "%arc.agreement.azdata.eula%",
"url": "https://aka.ms/eula-azdata-en"
} }
] ]
} }

View File

@@ -65,7 +65,6 @@
"arc.control.plane.summary.location": "Location", "arc.control.plane.summary.location": "Location",
"arc.control.plane.arc.data.controller.agreement": "I accept {0} and {1}.", "arc.control.plane.arc.data.controller.agreement": "I accept {0} and {1}.",
"microsoft.agreement.privacy.statement":"Microsoft Privacy Statement", "microsoft.agreement.privacy.statement":"Microsoft Privacy Statement",
"arc.agreement.azdata.eula":"azdata license terms",
"deploy.arc.control.plane.action":"Script to notebook", "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.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.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.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.controller": "Target Azure Arc Controller",
"arc.sql.new.dialog.title": "Deploy Azure SQL managed instance - Azure Arc (preview)", "arc.sql.new.dialog.title": "Deploy Azure SQL managed instance - Azure Arc (preview)",
"arc.sql.settings.section.title": "SQL Connection information", "arc.sql.settings.section.title": "SQL Connection information",
"arc.azure.section.title": "Azure 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.cores.description": "Fractional cores are supported",
"arc.postgres.server.group.memory.request": "Min memory MB (per node) to reserve", "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.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.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.agreement.postgres.terms.conditions":"PostgreSQL server groups - Azure Arc terms and conditions",
"arc.deploy.action":"Deploy" "arc.deploy.action":"Deploy"

View File

@@ -24,9 +24,6 @@ const enum AzdataDeployOption {
* Interface for an object to interact with the azdata tool installed on the box. * Interface for an object to interact with the azdata tool installed on the box.
*/ */
export interface IAzdataTool extends azdataExt.IAzdataApi { export interface IAzdataTool extends azdataExt.IAzdataApi {
path: string,
cachedVersion: SemVer
/** /**
* Executes azdata with the specified arguments (e.g. --version) and returns the result * Executes azdata with the specified arguments (e.g. --version) and returns the result
* @param args The args to pass to azdata * @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. * An object to interact with the azdata tool installed on the box.
*/ */
export class AzdataTool implements IAzdataTool { export class AzdataTool implements IAzdataTool {
public cachedVersion: SemVer;
constructor(public path: string, version: string) { private _semVersion: SemVer;
this.cachedVersion = new SemVer(version); 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 = { 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. * It also updates the cachedVersion property based on the return value from the tool.
*/ */
public async version(): Promise<azdataExt.AzdataOutput<string>> { public async version(): Promise<azdataExt.AzdataOutput<string>> {
const output = await executeAzdataCommand(`"${this.path}"`, ['--version']); const output = await executeAzdataCommand(`"${this._path}"`, ['--version']);
this.cachedVersion = new SemVer(parseVersion(output.stdout)); this._semVersion = new SemVer(parseVersion(output.stdout));
return { return {
logs: [], logs: [],
stdout: output.stdout.split(os.EOL), stdout: output.stdout.split(os.EOL),
@@ -153,7 +167,7 @@ export class AzdataTool implements IAzdataTool {
public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> { public async executeCommand<R>(args: string[], additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<R>> {
try { 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 { return {
logs: <string[]>output.log, logs: <string[]>output.log,
stdout: <string[]>output.stdout, stdout: <string[]>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) // 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. // check if azdata still exists if it does then rethrow the original error if not then emit a new specific error.
try { try {
await fs.promises.access(this.path); await fs.promises.access(this._path);
//this.path exists //this.path exists
throw err; // rethrow the error throw err; // rethrow the error
} catch (e) { } catch (e) {
@@ -207,7 +221,7 @@ export async function findAzdata(): Promise<IAzdataTool> {
try { try {
const azdata = await findSpecificAzdata(); 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. 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; return azdata;
} catch (err) { } catch (err) {
Logger.log(loc.couldNotFindAzdata(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<boolean> { export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
if (currentAzdata !== undefined) { if (currentAzdata !== undefined) {
const newVersion = await discoverLatestAvailableAzdataVersion(); const newSemVersion = await discoverLatestAvailableAzdataVersion();
if (newVersion.compare(currentAzdata.cachedVersion) === 1) { if (newSemVersion.compare(currentAzdata.getSemVersion()) === 1) {
Logger.log(loc.foundAzdataVersionToUpdateTo(newVersion.raw, currentAzdata.cachedVersion.raw)); Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentAzdata.getSemVersion().raw));
return await promptToUpdateAzdata(newVersion.raw, userRequested); return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
} else { } else {
Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.cachedVersion.raw)); Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.getSemVersion().raw));
} }
} else { } else {
Logger.log(loc.updateCheckSkipped); Logger.log(loc.updateCheckSkipped);

View File

@@ -12,7 +12,6 @@ import * as loc from './localizedConstants';
let localAzdata: IAzdataTool | undefined = undefined; let localAzdata: IAzdataTool | undefined = undefined;
let eulaAccepted: boolean = false; let eulaAccepted: boolean = false;
export async function activate(context: vscode.ExtensionContext): Promise<azdataExt.IExtension> { export async function activate(context: vscode.ExtensionContext): Promise<azdataExt.IExtension> {
vscode.commands.registerCommand('azdata.acceptEula', async () => { vscode.commands.registerCommand('azdata.acceptEula', async () => {
eulaAccepted = await promptForEula(context.globalState, true /* userRequested */); eulaAccepted = await promptForEula(context.globalState, true /* userRequested */);
@@ -59,26 +58,27 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
}); });
return { return {
isEulaAccepted: () => !!context.globalState.get<boolean>(constants.eulaAccepted),
azdata: { azdata: {
arc: { arc: {
dc: { dc: {
create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => { 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); return localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
}, },
endpoint: { endpoint: {
list: async () => { list: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.dc.endpoint.list(); return localAzdata!.arc.dc.endpoint.list();
} }
}, },
config: { config: {
list: async () => { list: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.dc.config.list(); return localAzdata!.arc.dc.config.list();
}, },
show: async () => { show: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.dc.config.show(); return localAzdata!.arc.dc.config.show();
} }
} }
@@ -90,11 +90,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
return localAzdata!.arc.postgres.server.delete(name); return localAzdata!.arc.postgres.server.delete(name);
}, },
list: async () => { list: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.postgres.server.list(); return localAzdata!.arc.postgres.server.list();
}, },
show: async (name: string) => { show: async (name: string) => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.postgres.server.show(name); return localAzdata!.arc.postgres.server.show(name);
}, },
edit: async (args: { edit: async (args: {
@@ -119,41 +119,53 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
sql: { sql: {
mi: { mi: {
delete: async (name: string) => { delete: async (name: string) => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.sql.mi.delete(name); return localAzdata!.arc.sql.mi.delete(name);
}, },
list: async () => { list: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.sql.mi.list(); return localAzdata!.arc.sql.mi.list();
}, },
show: async (name: string) => { show: async (name: string) => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.arc.sql.mi.show(name); return localAzdata!.arc.sql.mi.show(name);
} }
} }
} }
}, },
getPath: () => {
throwIfNoAzdata();
return localAzdata!.getPath();
},
login: async (endpoint: string, username: string, password: string) => { login: async (endpoint: string, username: string, password: string) => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdataOrEulaNotAccepted();
return localAzdata!.login(endpoint, username, password); return localAzdata!.login(endpoint, username, password);
}, },
getSemVersion: () => {
throwIfNoAzdata();
return localAzdata!.getSemVersion();
},
version: async () => { version: async () => {
await throwIfNoAzdataOrEulaNotAccepted(); throwIfNoAzdata();
return localAzdata!.version(); return localAzdata!.version();
} }
} }
}; };
} }
async function throwIfNoAzdataOrEulaNotAccepted(): Promise<void> { function throwIfNoAzdataOrEulaNotAccepted(): void {
if (!localAzdata) { throwIfNoAzdata();
Logger.log(loc.noAzdata);
throw new Error(loc.noAzdata);
}
if (!eulaAccepted) { if (!eulaAccepted) {
Logger.log(loc.eulaNotAccepted); Logger.log(loc.eulaNotAccepted);
throw new Error(loc.eulaNotAccepted); throw new Error(loc.eulaNotAccepted);
} }
} }
function throwIfNoAzdata() {
if (!localAzdata) {
Logger.log(loc.noAzdata);
throw new Error(loc.noAzdata);
}
}
export function deactivate(): void { } export function deactivate(): void { }

View File

@@ -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 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 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 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 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 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 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 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 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 eulaAcceptedStateOnStartup = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateOnStartup', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'eulaAccepted' state to: {0}", eulaAccepted); export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'EULA Accepted' state to: {0}", eulaAccepted);

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
import * as path from 'path'; import * as path from 'path';
import { SemVer } from 'semver';
import * as should from 'should'; import * as should from 'should';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
@@ -12,10 +11,10 @@ import * as azdata from '../azdata';
import * as childProcess from '../common/childProcess'; import * as childProcess from '../common/childProcess';
import { HttpClient } from '../common/httpClient'; import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils'; import * as utils from '../common/utils';
import * as loc from '../localizedConstants';
import * as constants from '../constants'; import * as constants from '../constants';
import * as loc from '../localizedConstants';
const oldAzdataMock = <azdata.AzdataTool>{ path: '/path/to/azdata', cachedVersion: new SemVer('0.0.0') }; const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
const releaseJson = { const releaseJson = {
win32: { win32: {
'version': '9999.999.999', 'version': '9999.999.999',

View File

@@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
declare module 'azdata-ext' { declare module 'azdata-ext' {
import { SemVer } from 'semver';
/** /**
* Covers defining what the azdata extension exports to other extensions * Covers defining what the azdata extension exports to other extensions
* *
@@ -254,12 +256,20 @@ declare module 'azdata-ext' {
show(name: string): Promise<AzdataOutput<SqlMiShowResult>> show(name: string): Promise<AzdataOutput<SqlMiShowResult>>
} }
} }
} },
getPath(): string,
login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>, login(endpoint: string, username: string, password: string): Promise<AzdataOutput<any>>,
/**
* 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<AzdataOutput<string>> version(): Promise<AzdataOutput<string>>
} }
export interface IExtension { export interface IExtension {
azdata: IAzdataApi; azdata: IAzdataApi;
isEulaAccepted(): boolean;
} }
} }

View File

@@ -502,18 +502,20 @@
"dependencies": { "dependencies": {
"linux-release-info": "^2.0.0", "linux-release-info": "^2.0.0",
"promisify-child-process": "^3.1.1", "promisify-child-process": "^3.1.1",
"semver": "^7.3.2",
"sudo-prompt": "9.1.1", "sudo-prompt": "9.1.1",
"vscode-nls": "^4.0.0", "vscode-nls": "^4.0.0",
"yamljs": "^0.3.0" "yamljs": "^0.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^5.2.5", "@types/mocha": "^5.2.5",
"@types/semver": "^7.3.1",
"@types/yamljs": "0.2.30", "@types/yamljs": "0.2.30",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mocha-junit-reporter": "^1.17.0", "mocha-junit-reporter": "^1.17.0",
"mocha-multi-reporters": "^1.1.7", "mocha-multi-reporters": "^1.1.7",
"should": "^13.2.3",
"typemoq": "^2.1.0", "typemoq": "^2.1.0",
"vscodetestcover": "^1.1.0", "vscodetestcover": "^1.1.0"
"should": "^13.2.3"
} }
} }

View File

@@ -388,6 +388,7 @@ export interface ITool {
finishInitialization(): Promise<void>; finishInitialization(): Promise<void>;
install(): Promise<void>; install(): Promise<void>;
isSameOrNewerThan(version: string): boolean; isSameOrNewerThan(version: string): boolean;
validateEula(): boolean;
} }
export const enum BdcDeploymentType { export const enum BdcDeploymentType {

View File

@@ -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 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 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 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.");

View File

@@ -3,18 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 azurecore from 'azurecore';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as arc from 'arc';
export interface IApiService { export interface IApiService {
readonly azurecoreApi: azurecore.IExtension; readonly azurecoreApi: azurecore.IExtension;
readonly azdataApi: azdataExt.IExtension;
readonly arcApi: arc.IExtension; readonly arcApi: arc.IExtension;
} }
class ApiService implements IApiService { class ApiService implements IApiService {
constructor() { } constructor() { }
public get azurecoreApi() { return vscode.extensions.getExtension(azurecore.extension.name)?.exports; } 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; } public get arcApi() { return vscode.extensions.getExtension(arc.extension.name)?.exports; }
} }

View File

@@ -8,10 +8,11 @@ import { SemVer } from 'semver';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as nls from 'vscode-nls'; import * as nls from 'vscode-nls';
import { AzdataInstallLocationKey, DeploymentConfigurationKey } from '../../constants'; 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 { IPlatformService } from '../platformService';
import { dependencyType, ToolBase } from './toolBase'; import { dependencyType, ToolBase } from './toolBase';
import { SemVerProxy } from './SemVerProxy'; import * as loc from '../../localizedConstants';
const localize = nls.loadMessageBundle(); const localize = nls.loadMessageBundle();
export const AzdataToolName = 'azdata'; 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'; 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 { protected get versionCommand(): Command {
return { return {
command: 'azdata -v' command: ''
}; };
} }
/* unused */
protected get discoveryCommand(): Command { protected get discoveryCommand(): Command {
return { return {
command: this.discoveryCommandString('azdata') command: ''
}; };
} }
/**
* updates the version and status for the tool.
*/
protected async updateVersionAndStatus(): Promise<void> {
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 { protected getVersionFromOutput(output: string): SemVer | undefined {
let version: SemVer | undefined = undefined; return apiService.azdataApi.azdata.getSemVersion();
if (output && output.split(EOL).length > 0) {
version = new SemVerProxy(output.split(EOL)[0].replace(/ /g, ''));
}
return version;
} }
protected async getSearchPaths(): Promise<string[]> { protected async getSearchPaths(): Promise<string[]> {
switch (this.osDistribution) { switch (this.osDistribution) {
case OsDistribution.win32: case OsDistribution.win32:

View File

@@ -58,6 +58,8 @@ export abstract class ToolBase implements ITool {
protected abstract readonly versionCommand: Command; protected abstract readonly versionCommand: Command;
public validateEula(): boolean { return true; }
public get dependencyMessages(): string[] { public get dependencyMessages(): string[] {
return (this.dependenciesByOsType.get(this.osDistribution) || []).map((msgType: dependencyType) => messageByDependencyType.get(msgType)!); 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; return this._statusDescription;
} }
protected setStatusDescription(value: string | undefined): void {
this._statusDescription = value;
}
public get installationPathOrAdditionalInformation(): string | undefined { public get installationPathOrAdditionalInformation(): string | undefined {
return this._installationPathOrAdditionalInformation; return this._installationPathOrAdditionalInformation;
} }
protected setInstallationPathOrAdditionalInformation(value: string | undefined) {
this._installationPathOrAdditionalInformation = value;
}
protected get installationCommands(): Command[] | undefined { protected get installationCommands(): Command[] | undefined {
return this.allInstallationCommands.get(this.osDistribution); return this.allInstallationCommands.get(this.osDistribution);
} }
@@ -250,7 +260,7 @@ export abstract class ToolBase implements ITool {
/** /**
* updates the version and status for the tool. * updates the version and status for the tool.
*/ */
private async updateVersionAndStatus(): Promise<void> { protected async updateVersionAndStatus(): Promise<void> {
this._statusDescription = ''; this._statusDescription = '';
await this.addInstallationSearchPathsToSystemPath(); await this.addInstallationSearchPathsToSystemPath();
const commandOutput = await this.platformService.runCommand( const commandOutput = await this.platformService.runCommand(
@@ -306,7 +316,7 @@ export abstract class ToolBase implements ITool {
} }
isSameOrNewerThan(version?: string): boolean { 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<void>; private _pendingVersionAndStatusUpdate!: Promise<void>;

View File

@@ -8,7 +8,6 @@ import assert = require('assert');
import { apiService } from '../services/apiService'; import { apiService } from '../services/apiService';
suite('API Service Tests', function (): void { suite('API Service Tests', function (): void {
test('getAzurecoreApi returns azure api', () => { test('getAzurecoreApi returns azure api', () => {
const api = apiService.azurecoreApi; const api = apiService.azurecoreApi;
assert(api !== undefined); assert(api !== undefined);

View File

@@ -7,6 +7,7 @@
/// <reference path='../../../../src/sql/azdata.d.ts'/> /// <reference path='../../../../src/sql/azdata.d.ts'/>
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/> /// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
/// <reference path='../../../arc/src/typings/arc.d.ts'/> /// <reference path='../../../arc/src/typings/arc.d.ts'/>
/// <reference path='../../../azdata/src/typings/azdata-ext.d.ts'/>
/// <reference path='../../../azurecore/src/azurecore.d.ts'/> /// <reference path='../../../azurecore/src/azurecore.d.ts'/>
/// <reference path='../../../azurecore/src/azureResource/azure-resource.d.ts'/> /// <reference path='../../../azurecore/src/azureResource/azure-resource.d.ts'/>
/// <reference types='@types/node'/> /// <reference types='@types/node'/>

View File

@@ -26,6 +26,7 @@ export class ResourceTypePickerDialog extends DialogBase {
private _agreementContainer!: azdata.DivContainer; private _agreementContainer!: azdata.DivContainer;
private _agreementCheckboxChecked: boolean = false; private _agreementCheckboxChecked: boolean = false;
private _installToolButton: azdata.window.Button; private _installToolButton: azdata.window.Button;
private _recheckEulaButton: azdata.window.Button;
private _installationInProgress: boolean = false; private _installationInProgress: boolean = false;
private _tools: ITool[] = []; private _tools: ITool[] = [];
@@ -37,10 +38,15 @@ export class ResourceTypePickerDialog extends DialogBase {
super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true); super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true);
this._selectedResourceType = defaultResourceType; this._selectedResourceType = defaultResourceType;
this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools")); 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._toDispose.push(this._installToolButton.onClick(() => {
this.installTools().catch(error => console.log(error)); 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._installToolButton.hidden = true;
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select"); this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select");
this._dialogObject.okButton.enabled = false; // this is enabled after all tools are discovered. 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 || '']; return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || '', toolRequirement.version || '', tool.installationPathOrAdditionalInformation || ''];
}); });
this._installToolButton.hidden = erroredOrFailedTool || minVersionCheckFailed || (toolsToAutoInstall.length === 0); 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 !== 0) {
if (messages.length > 1) { if (messages.length > 1) {
messages = messages.map(message => `${message}`); 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 = { this._dialogObject.message = {
level: azdata.window.MessageLevel.Error, level: azdata.window.MessageLevel.Error,
text: messages.join(EOL) text: messages.join(EOL)
@@ -313,6 +319,21 @@ export class ResourceTypePickerDialog extends DialogBase {
this._toolsLoadingComponent.loading = false; 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() { private get toolRequirements() {
return this.getCurrentProvider().requiredTools; return this.getCurrentProvider().requiredTools;
} }

View File

@@ -194,6 +194,11 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== 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": "@types/yamljs@0.2.30":
version "0.2.30" version "0.2.30"
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382" 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" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 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: should-equal@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3"