Files
azuredatastudio/extensions/azdata/src/azdata.ts
Charles Gagnon 04dff9cdf2 Add acquireLoginSession API for Azdata (#13985)
* wip

* fix tests

* add tests

* PR comments
2021-01-20 10:35:26 -08:00

739 lines
31 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { SemVer } from 'semver';
import * as vscode from 'vscode';
import { getPlatformDownloadLink, getPlatformReleaseVersion } from './azdataReleaseInfo';
import { executeCommand, executeSudoCommand, ExitCodeError, ProcessOutput } from './common/childProcess';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { Deferred } from './common/promise';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants';
export const enum AzdataDeployOption {
dontPrompt = 'dontPrompt',
prompt = 'prompt'
}
/**
* Interface for an object to interact with the azdata tool installed on the box.
*/
export interface IAzdataTool extends azdataExt.IAzdataApi {
/**
* Executes azdata with the specified arguments (e.g. --version) and returns the result
* @param args The args to pass to azdata
* @param parseResult A function used to parse out the raw result into the desired shape
*/
executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>>
}
class AzdataSession implements azdataExt.AzdataSession {
private _session = new Deferred<void>();
public sessionEnded(): Promise<void> {
return this._session.promise;
}
public dispose(): void {
this._session.resolve();
}
}
/**
* An object to interact with the azdata tool installed on the box.
*/
export class AzdataTool implements azdataExt.IAzdataApi {
private _semVersion: SemVer;
private _currentSession: azdataExt.AzdataSession | undefined = undefined;
private _currentlyExecutingCommands: Deferred<void>[] = [];
private _queuedCommands: { deferred: Deferred<void>, session?: azdataExt.AzdataSession }[] = [];
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 async getSemVersion(): Promise<SemVer> {
return this._semVersion;
}
/**
* gets the path where azdata tool is installed
*/
public async getPath(): Promise<string> {
return this._path;
}
public arc = {
dc: {
create: (
namespace: string,
name: string,
connectivityMode: string,
resourceGroup: string,
location: string,
subscription: string,
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
const args = ['arc', 'dc', 'create',
'--namespace', namespace,
'--name', name,
'--connectivity-mode', connectivityMode,
'--resource-group', resourceGroup,
'--location', location,
'--subscription', subscription];
if (profileName) {
args.push('--profile-name', profileName);
}
if (storageClass) {
args.push('--storage-class', storageClass);
}
return this.executeCommand<void>(args, additionalEnvVars, session);
},
endpoint: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcEndpointListResult[]>> => {
return this.executeCommand<azdataExt.DcEndpointListResult[]>(['arc', 'dc', 'endpoint', 'list'], additionalEnvVars, session);
}
},
config: {
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigListResult[]>> => {
return this.executeCommand<azdataExt.DcConfigListResult[]>(['arc', 'dc', 'config', 'list'], additionalEnvVars, session);
},
show: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.DcConfigShowResult>> => {
return this.executeCommand<azdataExt.DcConfigShowResult>(['arc', 'dc', 'config', 'show'], additionalEnvVars, session);
}
}
},
postgres: {
server: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name, '--force'], additionalEnvVars, session);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerListResult[]>> => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list'], additionalEnvVars, session);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.PostgresServerShowResult>> => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name], additionalEnvVars, session);
},
edit: (
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workers?: number
},
engineVersion?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.engineSettings) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
}
}
},
sql: {
mi: {
delete: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<void>> => {
return this.executeCommand<void>(['arc', 'sql', 'mi', 'delete', '-n', name], additionalEnvVars, session);
},
list: (additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> => {
return this.executeCommand<azdataExt.SqlMiListResult[]>(['arc', 'sql', 'mi', 'list'], additionalEnvVars, session);
},
show: (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name], additionalEnvVars, session);
},
edit: (
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
session?: azdataExt.AzdataSession
): Promise<azdataExt.AzdataOutput<void>> => {
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray, additionalEnvVars, session);
}
}
}
};
public async login(endpoint: string, username: string, password: string, additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<azdataExt.AzdataOutput<void>> {
// Since login changes the context we want to wait until all currently executing commands are finished before this is executed
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0];
}
// Logins need to be done outside the session aware logic so call impl directly
return this.executeCommandImpl<void>(['login', '-e', endpoint, '-u', username], Object.assign({}, additionalEnvVars, { 'AZDATA_PASSWORD': password }));
}
public async acquireSession(endpoint: string, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataSession> {
const session = new AzdataSession();
session.sessionEnded().then(async () => {
// Wait for all commands running for this session to end
while (this._currentlyExecutingCommands.length > 0) {
await this._currentlyExecutingCommands[0].promise;
}
this._currentSession = undefined;
// Start our next command now that we're all done with this session
// TODO: Should we check if the command has a session that hasn't started? That should never happen..
// TODO: Look into kicking off multiple commands
this._queuedCommands.shift()?.deferred.resolve();
});
// We're not in a session or waiting on anything so just set the current session right now
if (!this._currentSession && this._queuedCommands.length === 0) {
this._currentSession = session;
} else {
// We're in a session or another command is executing so add this to the end of the queued commands and wait our turn
const deferred = new Deferred<void>();
deferred.promise.then(() => {
this._currentSession = session;
// We've started a new session so look at all our queued commands and start
// the ones for this session now.
this._queuedCommands = this._queuedCommands.filter(c => {
if (c.session === this._currentSession) {
c.deferred.resolve();
return false;
}
return true;
});
});
this._queuedCommands.push({ deferred, session: undefined });
await deferred.promise;
}
await this.login(endpoint, username, password, additionalEnvVars);
return session;
}
/**
* Gets the output of running '--version' command on the azdata tool.
* It also updates the cachedVersion property based on the return value from the tool.
*/
public async version(): Promise<azdataExt.AzdataOutput<string>> {
const output = await executeAzdataCommand(`"${this._path}"`, ['--version']);
this._semVersion = new SemVer(parseVersion(output.stdout));
return {
logs: [],
stdout: output.stdout.split(os.EOL),
stderr: output.stderr.split(os.EOL),
result: output.stdout
};
}
public async executeCommand<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars, session?: azdataExt.AzdataSession): Promise<azdataExt.AzdataOutput<R>> {
if (this._currentSession && this._currentSession !== session) {
const deferred = new Deferred<void>();
this._queuedCommands.push({ deferred, session: session });
await deferred.promise;
}
const executingDeferred = new Deferred<void>();
this._currentlyExecutingCommands.push(executingDeferred);
try {
return await this.executeCommandImpl<R>(args, additionalEnvVars);
}
finally {
this._currentlyExecutingCommands = this._currentlyExecutingCommands.filter(c => c !== executingDeferred);
executingDeferred.resolve();
// If there isn't an active session and we still have queued commands then we have to manually kick off the next one
if (this._queuedCommands.length > 0 && !this._currentSession) {
this._queuedCommands.shift()?.deferred.resolve();
}
}
}
/**
* Executes the specified azdata command. This is NOT session-aware so should only be used for calls that don't care about a session
* @param args The args to pass to azdata
* @param additionalEnvVars Additional environment variables to set for this execution
*/
private async executeCommandImpl<R>(args: string[], additionalEnvVars?: azdataExt.AdditionalEnvVars): Promise<azdataExt.AzdataOutput<R>> {
try {
const output = JSON.parse((await executeAzdataCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars)).stdout);
return {
logs: <string[]>output.log,
stdout: <string[]>output.stdout,
stderr: <string[]>output.stderr,
result: <R>output.result
};
} catch (err) {
if (err instanceof ExitCodeError) {
try {
// For azdata internal errors the output is JSON and so we need to do some extra parsing here
// to get the correct stderr out. The actual value we get is something like
// ERROR: { stderr: '...' }
// so we also need to trim off the start that isn't a valid JSON blob
err.stderr = JSON.parse(err.stderr.substring(err.stderr.indexOf('{'), err.stderr.indexOf('}') + 1)).stderr;
} catch {
// 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);
//this.path exists
} catch (e) {
// this.path does not exist
await vscode.commands.executeCommand('setContext', azdataFound, false);
throw new NoAzdataError();
}
throw err; // rethrow the original error
}
}
throw err;
}
}
}
export type AzdataDarwinPackageVersionInfo = {
versions: {
stable: string,
devel: string,
head: string,
bottle: boolean
}
};
/**
* Finds the existing installation of azdata, or throws an error if it couldn't find it
* or encountered an unexpected error.
* The promise is rejected when Azdata is not found.
*/
export async function findAzdata(): Promise<IAzdataTool> {
Logger.log(loc.searchingForAzdata);
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(await azdata.getPath(), (await azdata.getSemVersion()).raw));
return azdata;
} catch (err) {
Logger.log(loc.couldNotFindAzdata(err));
Logger.log(loc.noAzdata);
await vscode.commands.executeCommand('setContext', azdataFound, false);// save a context key that azdata was not found so that command for installing azdata is available in commandPalette and that for updating it is no longer available.
throw err;
}
}
/**
* runs the commands to install azdata, downloading the installation package if needed
*/
export async function installAzdata(): Promise<void> {
Logger.show();
Logger.log(loc.installingAzdata);
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.installingAzdata,
cancellable: false
},
async (_progress, _token): Promise<void> => {
switch (process.platform) {
case 'win32':
await downloadAndInstallAzdataWin32();
break;
case 'darwin':
await installAzdataDarwin();
break;
case 'linux':
await installAzdataLinux();
break;
default:
throw new Error(loc.platformUnsupported(process.platform));
}
}
);
}
/**
* Updates the azdata using os appropriate method
*/
export async function updateAzdata(): Promise<void> {
Logger.show();
Logger.log(loc.updatingAzdata);
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: loc.updatingAzdata,
cancellable: false
},
async (_progress, _token): Promise<void> => {
switch (process.platform) {
case 'win32':
await downloadAndInstallAzdataWin32();
break;
case 'darwin':
await updateAzdataDarwin();
break;
case 'linux':
await installAzdataLinux();
break;
default:
throw new Error(loc.platformUnsupported(process.platform));
}
}
);
}
/**
* Checks whether azdata is installed - and if it is not then invokes the process of azdata installation.
* @param userRequested true means that this operation by was requested by a user by executing an ads command.
*/
export async function checkAndInstallAzdata(userRequested: boolean = false): Promise<IAzdataTool | undefined> {
try {
return await findAzdata(); // find currently installed Azdata
} catch (err) {
// Calls will be made to handle azdata not being installed if user declines to install on the prompt
if (await promptToInstallAzdata(userRequested)) {
return await findAzdata();
}
}
return undefined;
}
/**
* Checks whether a newer version of azdata is available - and if it is then invokes the process of azdata update.
* @param currentAzdata The current version of azdata to check against
* @param userRequested true means that this operation by was requested by a user by executing an ads command.
* returns true if update was done and false otherwise.
*/
export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise<boolean> {
if (currentAzdata !== undefined) {
const newSemVersion = await discoverLatestAvailableAzdataVersion();
if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) {
Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw));
return await promptToUpdateAzdata(newSemVersion.raw, userRequested);
} else {
Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw));
}
} else {
Logger.log(loc.updateCheckSkipped);
Logger.log(loc.noAzdata);
await vscode.commands.executeCommand('setContext', azdataFound, false);
}
return false;
}
/**
* prompt user to install Azdata.
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* returns true if installation was done and false otherwise.
*/
async function promptToInstallAzdata(userRequested: boolean = false): Promise<boolean> {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataInstallKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedInstall);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipInstall(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataInstallLog);
response = await vscode.window.showErrorMessage(loc.promptForAzdataInstall, ...responses);
Logger.log(loc.userResponseToInstallPrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataInstallKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await installAzdata();
vscode.window.showInformationMessage(loc.azdataInstalled);
Logger.log(loc.azdataInstalled);
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.installError(err));
Logger.log(loc.installError(err));
}
}
}
return false;
}
/**
* prompt user to update Azdata.
* @param newVersion - provides the new version that the user will be prompted to update to
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* returns true if update was done and false otherwise.
*/
async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = false): Promise<boolean> {
let response: string | undefined = loc.yes;
const config = <AzdataDeployOption>getConfig(azdataUpdateKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedUpdate);
}
if (config === AzdataDeployOption.dontPrompt && !userRequested) {
Logger.log(loc.skipUpdate(config));
return false;
}
const responses = userRequested
? [loc.yes, loc.no]
: [loc.yes, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt) {
Logger.log(loc.promptForAzdataUpdateLog(newVersion));
response = await vscode.window.showInformationMessage(loc.promptForAzdataUpdate(newVersion), ...responses);
Logger.log(loc.userResponseToUpdatePrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataUpdateKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.yes) {
try {
await updateAzdata();
vscode.window.showInformationMessage(loc.azdataUpdated(newVersion));
Logger.log(loc.azdataUpdated(newVersion));
return true;
} catch (err) {
// Windows: 1602 is User cancelling installation/update - not unexpected so don't display
if (!(err instanceof ExitCodeError) || err.code !== 1602) {
vscode.window.showWarningMessage(loc.updateError(err));
Logger.log(loc.updateError(err));
}
}
}
return false;
}
/**
* Returns true if Eula has been accepted.
*
* @param memento The memento that stores the eulaAccepted state
*/
export function isEulaAccepted(memento: vscode.Memento): boolean {
return !!memento.get<boolean>(eulaAccepted);
}
/**
* Prompts user to accept EULA. Stores and returns the user response to EULA prompt.
* @param memento - memento where the user response is stored.
* @param userRequested - if true this operation was requested in response to a user issued command, if false it was issued at startup by system
* @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed.
* pre-requisite, the calling code has to ensure that the eula has not yet been previously accepted by the user.
* returns true if the user accepted the EULA.
*/
export async function promptForEula(memento: vscode.Memento, userRequested: boolean = false, requireUserAction: boolean = false): Promise<boolean> {
let response: string | undefined = loc.no;
const config = <AzdataDeployOption>getConfig(azdataAcceptEulaKey);
if (userRequested) {
Logger.show();
Logger.log(loc.userRequestedAcceptEula);
}
const responses = userRequested
? [loc.accept, loc.decline]
: [loc.accept, loc.askLater, loc.doNotAskAgain];
if (config === AzdataDeployOption.prompt || userRequested) {
Logger.show();
Logger.log(loc.promptForEulaLog(microsoftPrivacyStatementUrl, eulaUrl));
response = requireUserAction
? await vscode.window.showErrorMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses)
: await vscode.window.showInformationMessage(loc.promptForEula(microsoftPrivacyStatementUrl, eulaUrl), ...responses);
Logger.log(loc.userResponseToEulaPrompt(response));
}
if (response === loc.doNotAskAgain) {
await setConfig(azdataAcceptEulaKey, AzdataDeployOption.dontPrompt);
} else if (response === loc.accept) {
await memento.update(eulaAccepted, true); // save a memento that eula was accepted
await vscode.commands.executeCommand('setContext', eulaAccepted, true); // save a context key that eula was accepted so that command for accepting eula is no longer available in commandPalette
return true;
}
return false;
}
/**
* Downloads the Windows installer and runs it
*/
async function downloadAndInstallAzdataWin32(): Promise<void> {
const downLoadLink = await getPlatformDownloadLink();
const downloadFolder = os.tmpdir();
const downloadLogs = path.join(downloadFolder, 'ads_azdata_install_logs.log');
const downloadedFile = await HttpClient.downloadFile(downLoadLink, downloadFolder);
try {
await executeSudoCommand(`msiexec /qn /i "${downloadedFile}" /lvx "${downloadLogs}"`);
} catch (err) {
throw new Error(`${err.message}. See logs at ${downloadLogs} for more details.`);
}
}
/**
* Runs commands to install azdata on MacOS
*/
async function installAzdataDarwin(): Promise<void> {
await executeCommand('brew', ['tap', 'microsoft/azdata-cli-release']);
await executeCommand('brew', ['update']);
await executeCommand('brew', ['install', 'azdata-cli']);
}
/**
* Runs commands to update azdata on MacOS
*/
async function updateAzdataDarwin(): Promise<void> {
await executeCommand('brew', ['tap', 'microsoft/azdata-cli-release']);
await executeCommand('brew', ['update']);
await executeCommand('brew', ['upgrade', 'azdata-cli']);
}
/**
* Runs commands to install azdata on Linux
*/
async function installAzdataLinux(): Promise<void> {
// https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata-linux-package
// Get packages needed for install process
await executeSudoCommand('apt-get update');
await executeSudoCommand('apt-get install gnupg ca-certificates curl wget software-properties-common apt-transport-https lsb-release -y');
// Download and install the signing key
await executeSudoCommand('curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc.gpg > /dev/null');
// Add the azdata repository information
const release = (await executeCommand('lsb_release', ['-rs'])).stdout.trim();
await executeSudoCommand(`add-apt-repository "$(wget -qO- https://packages.microsoft.com/config/ubuntu/${release}/mssql-server-2019.list)"`);
// Update repository information and install azdata
await executeSudoCommand('apt-get update');
await executeSudoCommand('apt-get install -y azdata-cli');
}
/**
*/
async function findSpecificAzdata(): Promise<IAzdataTool> {
const path = await ((process.platform === 'win32') ? searchForCmd('azdata.cmd') : searchForCmd('azdata'));
const versionOutput = await executeAzdataCommand(`"${path}"`, ['--version']);
return new AzdataTool(path, parseVersion(versionOutput.stdout));
}
function getConfig(key: string): AzdataDeployOption | undefined {
const config = vscode.workspace.getConfiguration(azdataConfigSection);
const value = <AzdataDeployOption>config.get<AzdataDeployOption>(key);
Logger.log(loc.azdataUserSettingRead(key, value));
return value;
}
async function setConfig(key: string, value: string): Promise<void> {
const config = vscode.workspace.getConfiguration(azdataConfigSection);
await config.update(key, value, vscode.ConfigurationTarget.Global);
Logger.log(loc.azdataUserSettingUpdated(key, value));
}
/**
* Gets the latest azdata version available for a given platform
*/
export async function discoverLatestAvailableAzdataVersion(): Promise<SemVer> {
Logger.log(loc.checkingLatestAzdataVersion);
switch (process.platform) {
case 'darwin':
return await discoverLatestStableAzdataVersionDarwin();
// case 'linux':
// ideally we would not to discover linux package availability using the apt/apt-get/apt-cache package manager commands.
// However, doing discovery that way required apt update to be performed which requires sudo privileges. At least currently this code path
// gets invoked on extension start up and prompt user for sudo privileges is annoying at best. So for now basing linux discovery also on a releaseJson file.
default:
return await getPlatformReleaseVersion();
}
}
/**
* Parses out the azdata version from the raw azdata version output
* @param raw The raw version output from azdata --version
*/
function parseVersion(raw: string): string {
// Currently the version is a multi-line string that contains other version information such
// as the Python installation, with the first line being the version of azdata itself.
const lines = raw.split(os.EOL);
return lines[0].trim();
}
/**
* Gets the latest azdata version for MacOs clients
*/
async function discoverLatestStableAzdataVersionDarwin(): Promise<SemVer> {
// set brew tap to azdata-cli repository
await executeCommand('brew', ['tap', 'microsoft/azdata-cli-release']);
await executeCommand('brew', ['update']);
let brewInfoAzdataCliJson;
// Get the package version 'info' about 'azdata-cli' from 'brew' as a json object
const brewInfoOutput = (await executeCommand('brew', ['info', 'azdata-cli', '--json'])).stdout;
try {
brewInfoAzdataCliJson = JSON.parse(brewInfoOutput);
} catch (e) {
throw Error(`failed to parse the JSON contents output of: 'brew info azdata-cli --json', text being parsed: '${brewInfoOutput}', error:${getErrorMessage(e)}`);
}
// Get the 'info' about 'azdata-cli' from 'brew' as a json object
const azdataPackageVersionInfo: AzdataDarwinPackageVersionInfo = brewInfoAzdataCliJson.shift();
Logger.log(loc.latestAzdataVersionAvailable(azdataPackageVersionInfo.versions.stable));
return new SemVer(azdataPackageVersionInfo.versions.stable);
}
async function executeAzdataCommand(command: string, args: string[], additionalEnvVars: azdataExt.AdditionalEnvVars = {}): Promise<ProcessOutput> {
additionalEnvVars = Object.assign(additionalEnvVars, { 'ACCEPT_EULA': 'yes' });
const debug = vscode.workspace.getConfiguration(azdataConfigSection).get(debugConfigKey);
if (debug) {
args.push('--debug');
}
return executeCommand(command, args, additionalEnvVars);
}
/**
* Gets the latest azdata version for linux clients
* This method requires sudo permission so not suitable to be run during startup.
*/
// async function discoverLatestStableAzdataVersionLinux(): Promise<SemVer> {
// // Update repository information and install azdata
// await executeSudoCommand('apt-get update');
// const output = (await executeCommand('apt', ['list', 'azdata-cli', '--upgradeable'])).stdout;
// // the packageName (with version) string is the second space delimited token on the 2nd line
// const packageName = output.split('\n')[1].split(' ')[1];
// // the version string is the first part of the package sting before '~'
// const version = packageName.split('~')[0];
// Logger.log(loc.latestAzdataVersionAvailable(version));
// return new SemVer(version);
// }