[Port] Sync up arc and azdata extensions with main (#12810)

* Sync up arc and azdata extensions with main

* capture 'this' to use retrieveVariable as callback (#12828)

* capture 'this' to use retrieveVariable as callback

* remove change not needed for #12082

Co-authored-by: Arvind Ranasaria <ranasaria@outlook.com>
This commit is contained in:
Charles Gagnon
2020-10-08 16:03:27 -07:00
committed by GitHub
parent 6adeffbc8e
commit 7429407029
57 changed files with 1246 additions and 1509 deletions

View File

@@ -72,6 +72,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
},
postgres: {
server: {
delete: async (name: string) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.delete(name);
},
list: async () => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
@@ -81,6 +86,26 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.show(name);
},
edit: async (
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;
},
additionalEnvVars?: { [key: string]: string; }) => {
await localAzdataDiscovered;
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
}
}
},

View File

@@ -6,13 +6,15 @@
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 { getErrorMessage, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataHostname, azdataInstallKey, azdataReleaseJson, azdataUpdateKey, azdataUri, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import { getErrorMessage, NoAzdataError, searchForCmd } from './common/utils';
import { azdataAcceptEulaKey, azdataConfigSection, azdataFound, azdataInstallKey, azdataUpdateKey, debugConfigKey, eulaAccepted, eulaUrl, microsoftPrivacyStatementUrl } from './constants';
import * as loc from './localizedConstants';
const enum AzdataDeployOption {
@@ -92,11 +94,44 @@ export class AzdataTool implements azdataExt.IAzdataApi {
},
postgres: {
server: {
delete: async (name: string) => {
return this.executeCommand<void>(['arc', 'postgres', 'server', 'delete', '-n', name]);
},
list: async () => {
return this.executeCommand<azdataExt.PostgresServerListResult[]>(['arc', 'postgres', 'server', 'list']);
},
show: async (name: string) => {
return this.executeCommand<azdataExt.PostgresServerShowResult>(['arc', 'postgres', 'server', 'show', '-n', name]);
},
edit: async (
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
},
additionalEnvVars?: { [key: string]: string }) => {
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); }
if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
},
@@ -151,18 +186,18 @@ export class AzdataTool implements azdataExt.IAzdataApi {
// 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 (err) {
} 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
throw err; // rethrow the error
} catch (e) {
// this.path does not exist
await vscode.commands.executeCommand('setContext', azdataFound, false);
throw (loc.noAzdata);
throw new NoAzdataError();
}
throw err; // rethrow the original error
}
}
@@ -311,6 +346,7 @@ async function promptToInstallAzdata(userRequested: boolean = false): Promise<bo
? [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));
}
@@ -354,6 +390,7 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean =
? [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));
}
@@ -427,9 +464,16 @@ export async function promptForEula(memento: vscode.Memento, userRequested: bool
* Downloads the Windows installer and runs it
*/
async function downloadAndInstallAzdataWin32(): Promise<void> {
const downLoadLink = await getPlatformDownloadLink();
const downloadFolder = os.tmpdir();
const downloadedFile = await HttpClient.downloadFile(`${azdataHostname}/${azdataUri}`, downloadFolder);
await executeCommand('msiexec', ['/qn', '/i', downloadedFile]);
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.`);
}
}
/**
@@ -502,27 +546,10 @@ export async function discoverLatestAvailableAzdataVersion(): Promise<SemVer> {
// 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 discoverLatestAzdataVersionFromJson();
return await getPlatformReleaseVersion();
}
}
/**
* Gets the latest azdata version from a json document published by azdata release
*/
async function discoverLatestAzdataVersionFromJson(): Promise<SemVer> {
// get version information for current platform from http://aka.ms/azdata/release.json
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
let azdataReleaseInfo;
try {
azdataReleaseInfo = JSON.parse(fileContents);
} catch (e) {
throw Error(`failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}, text being parsed: '${fileContents}', error:${getErrorMessage(e)}`);
}
const version = azdataReleaseInfo[process.platform]['version'];
Logger.log(loc.latestAzdataVersionAvailable(version));
return new SemVer(version);
}
/**
* Parses out the azdata version from the raw azdata version output
* @param raw The raw version output from azdata --version

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as loc from './localizedConstants';
import { SemVer } from 'semver';
import { HttpClient } from './common/httpClient';
import Logger from './common/logger';
import { getErrorMessage } from './common/utils';
import { azdataHostname, azdataReleaseJson } from './constants';
interface PlatformReleaseInfo {
version: string; // "20.0.1"
link?: string; // "https://aka.ms/azdata-msi"
}
interface AzdataReleaseInfo {
win32: PlatformReleaseInfo,
darwin: PlatformReleaseInfo,
linux: PlatformReleaseInfo
}
function getPlatformAzdataReleaseInfo(releaseInfo: AzdataReleaseInfo): PlatformReleaseInfo {
switch (os.platform()) {
case 'win32':
return releaseInfo.win32;
case 'linux':
return releaseInfo.linux;
case 'darwin':
return releaseInfo.darwin;
default:
Logger.log(loc.platformUnsupported(os.platform()));
throw new Error(`Unsupported AzdataReleaseInfo platform '${os.platform()}`);
}
}
/**
* Gets the release version for the current platform from the release info - throwing an error if it doesn't exist.
* @param releaseInfo The AzdataReleaseInfo object
*/
export async function getPlatformReleaseVersion(): Promise<SemVer> {
const releaseInfo = await getAzdataReleaseInfo();
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
if (!platformReleaseInfo.version) {
Logger.log(loc.noReleaseVersion(os.platform(), JSON.stringify(releaseInfo)));
throw new Error(`No release version available for platform ${os.platform()}`);
}
Logger.log(loc.latestAzdataVersionAvailable(platformReleaseInfo.version));
return new SemVer(platformReleaseInfo.version);
}
/**
* Gets the download link for the current platform from the release info - throwing an error if it doesn't exist.
* @param releaseInfo The AzdataReleaseInfo object
*/
export async function getPlatformDownloadLink(): Promise<string> {
const releaseInfo = await getAzdataReleaseInfo();
const platformReleaseInfo = getPlatformAzdataReleaseInfo(releaseInfo);
if (!platformReleaseInfo.link) {
Logger.log(loc.noDownloadLink(os.platform(), JSON.stringify(releaseInfo)));
throw new Error(`No download link available for platform ${os.platform()}`);
}
return platformReleaseInfo.link;
}
async function getAzdataReleaseInfo(): Promise<AzdataReleaseInfo> {
const fileContents = await HttpClient.getTextContent(`${azdataHostname}/${azdataReleaseJson}`);
try {
return JSON.parse(fileContents);
} catch (e) {
Logger.log(loc.failedToParseReleaseInfo(`${azdataHostname}/${azdataReleaseJson}`, fileContents, e));
throw Error(`Failed to parse the JSON of contents at: ${azdataHostname}/${azdataReleaseJson}. Error: ${getErrorMessage(e)}`);
}
}

View File

@@ -65,7 +65,7 @@ export async function executeCommand(command: string, args: string[], additional
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stdoutOutput(stderr));
Logger.log(loc.stderrOutput(stderr));
}
if (code) {
const err = new ExitCodeError(code, stderr);
@@ -94,7 +94,7 @@ export async function executeSudoCommand(command: string): Promise<ProcessOutput
Logger.log(loc.stdoutOutput(stdout));
}
if (stderr) {
Logger.log(loc.stdoutOutput(stderr));
Logger.log(loc.stderrOutput(stderr));
}
if (error) {
Logger.log(loc.unexpectedCommandError(error.message));

View File

@@ -61,7 +61,7 @@ export namespace HttpClient {
if (targetFolder !== undefined) {
const filename = path.basename(response.request.path);
const targetPath = path.join(targetFolder, filename);
Logger.log(loc.downloadingTo(filename, targetPath));
Logger.log(loc.downloadingTo(filename, downloadUrl, targetPath));
// Wait to create the WriteStream until here so we can use the actual
// filename based off of the URI.
downloadRequest.pipe(fs.createWriteStream(targetPath))
@@ -73,6 +73,11 @@ export namespace HttpClient {
reject(downloadError);
downloadRequest.abort();
});
} else {
response.on('end', () => {
Logger.log(loc.downloadFinished);
resolve(strings.join(''));
});
}
let contentLength = response.headers['content-length'];
let totalBytes = parseInt(contentLength || '0');
@@ -92,13 +97,6 @@ export namespace HttpClient {
printThreshold += 0.1;
}
}
})
.on('close', async () => {
if (targetFolder === undefined) {
Logger.log(loc.downloadFinished);
resolve(strings.join(''));
}
});
});
}

View File

@@ -4,16 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as loc from '../localizedConstants';
export class Log {
private _output: vscode.OutputChannel;
constructor() {
this._output = vscode.window.createOutputChannel('azdata');
this._output = vscode.window.createOutputChannel(loc.azdata);
}
log(msg: string): void {
this._output.appendLine(msg);
this._output.appendLine(`[${new Date().toISOString()}] ${msg}`);
}
show(): void {

View File

@@ -7,7 +7,6 @@ import * as azdataExt from 'azdata-ext';
import * as which from 'which';
import * as loc from '../localizedConstants';
export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
constructor() {
super(loc.noAzdata);
@@ -17,7 +16,6 @@ export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
return loc.noAzdataWithLink;
}
}
/**
* Searches for the first instance of the specified executable in the PATH environment variable
* @param exe The executable to search for

View File

@@ -34,15 +34,6 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
eulaAccepted = isEulaAccepted(context.globalState); // fetch eula acceptance state from memento
await vscode.commands.executeCommand('setContext', constants.eulaAccepted, eulaAccepted); // set a context key for current value of eulaAccepted state retrieved from memento so that command for accepting eula is available/unavailable in commandPalette appropriately.
Logger.log(loc.eulaAcceptedStateOnStartup(eulaAccepted));
if (!eulaAccepted) {
// Don't block on this since we want extension to finish activating without requiring user actions.
// If EULA has not been accepted then we will check again while executing azdata commands.
promptForEula(context.globalState)
.then(async (userResponse: boolean) => {
eulaAccepted = userResponse;
})
.catch((err) => console.log(err));
}
// Don't block on this since we want the extension to finish activating without needing user input
const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it.

View File

@@ -4,16 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { getErrorMessage } from './common/utils';
import { azdataConfigSection, azdataInstallKey, azdataUpdateKey } from './constants';
const localize = nls.loadMessageBundle();
export const azdata = localize('azdata.azdata', "Azure Data CLI");
export const searchingForAzdata = localize('azdata.searchingForAzdata', "Searching for existing Azure Data CLI installation...");
export const foundExistingAzdata = (path: string, version: string): string => localize('azdata.foundExistingAzdata', "Found existing Azure Data CLI installation of version (v{0}) at path:{1}", version, path);
export const downloadingProgressMb = (currentMb: string, totalMb: string): string => localize('azdata.downloadingProgressMb', "Downloading ({0} / {1} MB)", currentMb, totalMb);
export const downloadFinished = localize('azdata.downloadFinished', "Download finished");
export const installingAzdata = localize('azdata.installingAzdata', "Installing azdata...");
export const updatingAzdata = localize('azdata.updatingAzdata', "updating azdata...");
export const installingAzdata = localize('azdata.installingAzdata', "Installing Azure Data CLI...");
export const updatingAzdata = localize('azdata.updatingAzdata', "Updating Azure Data CLI...");
export const azdataInstalled = localize('azdata.azdataInstalled', "Azure Data CLI was successfully installed. Restarting Azure Data Studio is required to complete configuration - features will not be activated until this is done.");
export const azdataUpdated = (version: string) => localize('azdata.azdataUpdated', "Azure Data CLI was successfully updated to version: {0}.", version);
export const yes = localize('azdata.yes', "Yes");
@@ -22,46 +24,45 @@ export const accept = localize('azdata.accept', "Accept");
export const decline = localize('azdata.decline', "Decline");
export const doNotAskAgain = localize('azdata.doNotAskAgain', "Don't Ask Again");
export const askLater = localize('azdata.askLater', "Ask Later");
export const downloadingTo = (name: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} to {1}", name, location);
export const downloadingTo = (name: string, url: string, location: string): string => localize('azdata.downloadingTo', "Downloading {0} from {1} to {2}", name, url, location);
export const executingCommand = (command: string, args: string[]): string => localize('azdata.executingCommand', "Executing command: '{0} {1}'", command, args?.join(' '));
export const stdoutOutput = (stdout: string): string => localize('azdata.stdoutOutput', "stdout: {0}", stdout);
export const stderrOutput = (stderr: string): string => localize('azdata.stderrOutput', "stderr: {0}", stderr);
export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of azdata");
export const checkingLatestAzdataVersion = localize('azdata.checkingLatestAzdataVersion', "Checking for latest available version of Azure Data CLI");
export const gettingTextContentsOfUrl = (url: string): string => localize('azdata.gettingTextContentsOfUrl', "Getting text contents of resource at URL {0}", url);
export const foundAzdataVersionToUpdateTo = (newVersion: string, currentVersion: string): string => localize('azdata.versionForUpdate', "Found version: {0} that Azure Data CLI can be updated to from current version: {1}.", newVersion, currentVersion);
export const latestAzdataVersionAvailable = (version: string): string => localize('azdata.latestAzdataVersionAvailable', "Latest available Azure Data CLI version: {0}.", version);
export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find azdata. Error: {0}", err.message ?? err);
export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of azdata: {0} is same or newer than any other version available", currentVersion);
export const couldNotFindAzdata = (err: any): string => localize('azdata.couldNotFindAzdata', "Could not find Azure Data CLI. Error: {0}", err.message ?? err);
export const currentlyInstalledVersionIsLatest = (currentVersion: string): string => localize('azdata.currentlyInstalledVersionIsLatest', "Currently installed version of Azure Data CLI: {0} is same or newer than any other version available", currentVersion);
export const promptLog = (logEntry: string) => localize('azdata.promptLog', "Prompting the user to accept the following: {0}", logEntry);
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find azdata, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstall = localize('azdata.couldNotFindAzdataWithPrompt', "Could not find Azure Data CLI, install it now? If not then some features will not be able to function.");
export const promptForAzdataInstallLog = promptLog(promptForAzdataInstall);
export const promptForAzdataUpdate = (version: string): string => localize('azdata.promptForAzdataUpdate', "A new version of Azure Data CLI ( {0} ) is available, do you wish to update to it now?", version);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const downloadError = localize('azdata.downloadError', "Error while downloading");
export const installError = (err: any): string => localize('azdata.installError', "Error installing azdata: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating azdata: {0}", err.message ?? err);
export const installError = (err: any): string => localize('azdata.installError', "Error installing Azure Data CLI: {0}", err.message ?? err);
export const updateError = (err: any): string => localize('azdata.updateError', "Error updating Azure Data CLI: {0}", err.message ?? err);
export const platformUnsupported = (platform: string): string => localize('azdata.platformUnsupported', "Platform '{0}' is currently unsupported", platform);
export const unexpectedCommandError = (errMsg: string): string => localize('azdata.unexpectedCommandError', "Unexpected error executing command: {0}", errMsg);
export const unexpectedExitCode = (code: number, err: string): string => localize('azdata.unexpectedExitCode', "Unexpected exit code from command: {1} ({0})", code, err);
export const noAzdata = localize('azdata.NoAzdata', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it.");
export const noAzdata = localize('azdata.noAzdata', "No Azure Data CLI is available, run the command 'Azure Data CLI: Install' to enable the features that require it.");
export const noAzdataWithLink = localize('azdata.noAzdataWithLink', "No Azure Data CLI is available, [install the Azure Data CLI](command:azdata.install) to enable the features that require it.");
export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config);
export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of azdata, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config);
export const skipInstall = (config: string): string => localize('azdata.skipInstall', "Skipping installation of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataInstallKey, config);
export const skipUpdate = (config: string): string => localize('azdata.skipUpdate', "Skipping update of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdataUpdateKey, config);
export const noReleaseVersion = (platform: string, releaseInfo: string): string => localize('azdata.noReleaseVersion', "No release version available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const noDownloadLink = (platform: string, releaseInfo: string): string => localize('azdata.noDownloadLink', "No download link available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const failedToParseReleaseInfo = (url: string, fileContents: string, err: any): string => localize('azdata.failedToParseReleaseInfo', "Failed to parse the JSON of contents at: {0}.\nFile contents:\n{1}\nError: {2}", url, fileContents, getErrorMessage(err));
export const azdataUserSettingRead = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingReadLog', "Azure Data CLI user setting: {0}.{1} read, value: {2}", azdataConfigSection, configName, configValue);
export const azdataUserSettingUpdated = (configName: string, configValue: string): string => localize('azdata.azdataUserSettingUpdatedLog', "Azure Data CLI user setting: {0}.{1} updated, newValue: {2}", azdataConfigSection, configName, configValue);
export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install azdata: {0}", response);
export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update azdata: {0}", response);
export const userResponseToInstallPrompt = (response: string | undefined): string => localize('azdata.userResponseInstall', "User Response on prompt to install Azure Data CLI: {0}", response);
export const userResponseToUpdatePrompt = (response: string | undefined): string => localize('azdata.userResponseUpdate', "User Response on prompt to update Azure Data CLI: {0}", response);
export const userRequestedInstall = localize('azdata.userRequestedInstall', "User requested to install Azure Data CLI using 'Azure Data CLI: Install' 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 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. 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', "'EULA Accepted' state on startup: {0}", eulaAccepted);
export const eulaAcceptedStateUpdated = (eulaAccepted: boolean) => localize('azdata.eulaAcceptedStateUpdated', "Updated 'EULA Accepted' state to: {0}", eulaAccepted);

View File

@@ -3,7 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
@@ -11,10 +10,14 @@ import * as azdata from '../azdata';
import * as childProcess from '../common/childProcess';
import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils';
import * as constants from '../constants';
import * as loc from '../localizedConstants';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', '0.0.0');
/**
* This matches the schema of the JSON file used to determine the current version of
* azdata - do not modify unless also updating the corresponding JSON file
*/
const releaseJson = {
win32: {
'version': '9999.999.999',
@@ -27,7 +30,7 @@ const releaseJson = {
'version': '9999.999.999'
}
};
let executeSudoCommandStub: sinon.SinonStub;
describe('azdata', function () {
afterEach(function (): void {
@@ -55,9 +58,11 @@ describe('azdata', function () {
});
describe('installAzdata', function (): void {
beforeEach(function (): void {
sinon.stub(vscode.window, 'showErrorMessage').returns(Promise.resolve(<any>loc.yes));
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata'));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
});
it('successful install', async function (): Promise<void> {
@@ -77,8 +82,12 @@ describe('azdata', function () {
if (process.platform === 'win32') {
it('unsuccessful download - win32', async function (): Promise<void> {
sinon.stub(HttpClient, 'downloadFile').rejects();
const downloadPromise = azdata.checkAndInstallAzdata();
await should(downloadPromise).be.rejected();
sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.rejects(new Error('not Found')) // First call mock the tool not being found
.resolves({ stdout: '1.0.0', stderr: '' });
const azdataTool = await azdata.checkAndInstallAzdata();
should(azdataTool).be.undefined();
});
}
@@ -100,6 +109,7 @@ describe('azdata', function () {
describe('updateAzdata', function (): void {
beforeEach(function (): void {
sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
});
it('successful update', async function (): Promise<void> {
@@ -107,7 +117,6 @@ describe('azdata', function () {
case 'win32':
await testWin32SuccessfulUpdate();
break;
case 'darwin':
await testDarwinSuccessfulUpdate();
break;
@@ -132,9 +141,8 @@ describe('azdata', function () {
});
describe('discoverLatestAvailableAzdataVersion', function (): void {
this.timeout(20000);
it(`finds latest available version of azdata successfully`, async function (): Promise<void> {
// if the latest version is not discovered then the following call throws failing the test
it('finds latest available version of azdata successfully', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
await azdata.discoverLatestAvailableAzdataVersion();
});
});
@@ -142,7 +150,7 @@ describe('azdata', function () {
});
async function testLinuxUnsuccessfulUpdate() {
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeSudoCommandStub.calledOnce).be.true();
@@ -172,7 +180,7 @@ async function testDarwinUnsuccessfulUpdate() {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (_command: string, _args: string[]) => { // by default return success
return Promise.resolve({stderr: '', stdout: 'success'});
return Promise.resolve({ stderr: '', stdout: 'success' });
});
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
@@ -181,16 +189,16 @@ async function testDarwinUnsuccessfulUpdate() {
async function testWin32UnsuccessfulUpdate() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeCommandStub.calledOnce).be.true();
should(updateDone).be.false('Update should not have been successful');
should(executeSudoCommandStub.calledOnce).be.true();
}
async function testLinuxSuccessfulUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.callCount).be.equal(6);
should(executeCommandStub.calledOnce).be.true();
@@ -209,51 +217,39 @@ async function testDarwinSuccessfulUpdate() {
}];
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onThirdCall() //third call is brew info azdata-cli --json which needs to return json of new available azdata versions.
.callsFake(async (command: string, args: string[]) => {
should(command).be.equal('brew');
should(args).deepEqual(['info', 'azdata-cli', '--json']);
return Promise.resolve({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
});
.resolves({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
})
.callsFake(async (_command: string, _args: string[]) => { // return success on all other command executions
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
});
.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeCommandStub.callCount).be.equal(6);
should(executeCommandStub.getCall(2).args[0]).be.equal('brew', '3rd call should have been to brew');
should(executeCommandStub.getCall(2).args[1]).deepEqual(['info', 'azdata-cli', '--json'], '3rd call did not have expected arguments');
}
async function testWin32SuccessfulUpdate() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').callsFake(async (command: string, args: string[]) => {
should(command).be.equal('msiexec');
should(args[0]).be.equal('/qn');
should(args[1]).be.equal('/i');
should(path.basename(args[2])).be.equal(constants.azdataUri);
return { stdout: '0.0.0', stderr: '' };
});
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeCommandStub.calledOnce).be.true();
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
}
async function testWin32SuccessfulInstall() {
sinon.stub(HttpClient, 'getTextContent').returns(Promise.resolve(JSON.stringify(releaseJson)));
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.callsFake(async (_command: string, _args: string[]) => {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (command: string, args: string[]) => {
should(command).be.equal('msiexec');
should(args[0]).be.equal('/qn');
should(args[1]).be.equal('/i');
should(path.basename(args[2])).be.equal(constants.azdataUri);
return { stdout: '0.0.0', stderr: '' };
});
.rejects(new Error('not Found')) // First call mock the tool not being found
.resolves({ stdout: '1.0.0', stderr: '' });
executeSudoCommandStub
.returns({ stdout: '', stderr: '' });
await azdata.checkAndInstallAzdata();
should(executeCommandStub.calledTwice).be.true();
should(executeCommandStub.calledTwice).be.true(`executeCommand should have been called twice. Actual ${executeCommandStub.getCalls().length}`);
should(executeSudoCommandStub.calledOnce).be.true(`executeSudoCommand should have been called once. Actual ${executeSudoCommandStub.getCalls().length}`);
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
}
async function testDarwinSuccessfulInstall() {
@@ -272,23 +268,17 @@ async function testDarwinSuccessfulInstall() {
async function testLinuxSuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.callsFake(async (_command: string, _args: string[]) => {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (_command: string, _args: string[]) => {
return Promise.resolve({ stdout: '0.0.0', stderr: '' });
});
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand')
.callsFake(async (_command: string ) => {
return Promise.resolve({ stdout: 'success', stderr: '' });
});
.rejects(new Error('not Found'))
.resolves({ stdout: '0.0.0', stderr: '' });
executeSudoCommandStub
.resolves({ stdout: 'success', stderr: '' });
await azdata.checkAndInstallAzdata();
should(executeSudoCommandStub.callCount).be.equal(6);
should(executeCommandStub.calledThrice).be.true();
}
async function testLinuxUnsuccessfulInstall() {
const executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').rejects();
executeSudoCommandStub.rejects();
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeSudoCommandStub.calledOnce).be.true();
@@ -302,9 +292,9 @@ async function testDarwinUnsuccessfulInstall() {
}
async function testWin32UnsuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
executeSudoCommandStub.rejects();
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeCommandStub.calledOnce).be.true();
should(executeSudoCommandStub.calledOnce).be.true();
}

View File

@@ -73,12 +73,12 @@ describe('HttpClient', function (): void {
});
describe('getTextContent', function (): void {
it.skip('Gets file contents correctly', async function (): Promise<void> {
it('Gets file contents correctly', async function (): Promise<void> {
nock('https://127.0.0.1')
.get('/arbitraryFile')
.replyWithFile(200, __filename);
const receivedContents = await HttpClient.getTextContent(`https://127.0.0.1/arbitraryFile`);
should(receivedContents).equal(await fs.promises.readFile(__filename));
should(receivedContents).equal((await fs.promises.readFile(__filename)).toString());
});
it('rejects on response error', async function (): Promise<void> {

View File

@@ -19,7 +19,7 @@ declare module 'azdata-ext' {
export interface ErrorWithLink extends Error {
messageWithLink: string;
}
export interface DcEndpointListResult {
description: string, // "Management Proxy"
endpoint: string, // "https://10.91.86.39:30777"
@@ -143,25 +143,13 @@ declare module 'azdata-ext' {
},
status: {
readyReplicas: string, // "1/1"
state: string, // "Ready"
state: string, // "Ready",
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:miaa1'))
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=miaa1-0
externalEndpoint?: string // "10.91.86.39:32718"
}
}
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12"
metadata: {
creationTimestamp: string, // "2020-08-19T20:25:11Z"
generation: number, // 1
name: string, // "chgagnon-pg"
namespace: string, // "arc",
resourceVersion: string, // "214944",
selfLink: string, // "/apis/arcdata.microsoft.com/v1alpha1/namespaces/arc/postgresql-12s/chgagnon-pg",
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
}
}
export interface PostgresServerShowResult {
apiVersion: string, // "arcdata.microsoft.com/v1alpha1"
kind: string, // "postgresql-12"
@@ -175,25 +163,56 @@ declare module 'azdata-ext' {
uid: string, // "26d0f5bb-0c0b-4225-a6b5-5be2bf6feac0"
},
spec: {
backups: {
deltaMinutes: number, // 3,
fullMinutes: number, // 10,
tiers: [
{
retention: {
maximums: string[], // [ "6", "512MB" ],
minimums: string[], // [ "3" ]
engine: {
extensions: {
name: string // "citus"
}[],
settings: {
default: { [key: string]: string } // { "max_connections": "101", "work_mem": "4MB" }
}
},
scale: {
shards: number // 1
},
scheduling: {
default: {
resources: {
requests: {
cpu: string, // "1.5"
memory: string // "256Mi"
},
storage: {
volumeSize: string, // "1Gi"
limits: {
cpu: string, // "1.5"
memory: string // "256Mi"
}
}
]
}
},
status: {
readyPods: string, // "1/1",
state: string // "Ready"
service: {
type: string, // "NodePort"
port: number // 5432
},
storage: {
data: {
className: string, // "local-storage"
size: string // "5Gi"
},
logs: {
className: string, // "local-storage"
size: string // "5Gi"
},
backups: {
className: string, // "local-storage"
size: string // "5Gi"
}
}
},
status: {
externalEndpoint: string, // "10.130.12.136:26630"
readyPods: string, // "1/1",
state: string, // "Ready"
logSearchDashboard: string, // https://127.0.0.1:30777/kibana/app/kibana#/discover?_a=(query:(language:kuery,query:'custom_resource_name:pg1'))
metricsDashboard: string, // https://127.0.0.1:30777/grafana/d/40q72HnGk/sql-managed-instance-metrics?var-hostname=pg1
}
}
@@ -219,8 +238,25 @@ declare module 'azdata-ext' {
},
postgres: {
server: {
delete(name: string): Promise<AzdataOutput<void>>,
list(): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>
show(name: string): Promise<AzdataOutput<PostgresServerShowResult>>,
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
},
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
}
},
sql: {