mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Feat/tool install master merge back to master (#7819)
* add install tools button (#7454) * add install tools button * address comments * remove description for install tools hint message * First working version of AutoDeployment of tools (#7647) First working version of AutoDeployment of tools. This pull request adds feature to install the tools needed for doing BDC/TINA deployments. This has been tested so far only on win32 and testing on other platforms is in progress. * removing TODO and redundant code * Not localizing azuredatastudio product name * convert methods returning Promises to async-await * changing from null to undefined * Localize all the command labels * using existing sudo-prompt typings * progres/error status in ModalDialogue && PR fixes * review feedback to change warning to information * revert settings.json changes * fix resource-Deployment Extension Unit Test * ensuring platform service's working directory * incorporate review feedback * review feedback * addressing PR feedback * PR fixes * PR Feedback * remove debug logs * disable UI deployment containers when installing * addding data type to stdout/stderr messaging * remove commented code * revert accidental change * addressing review feedback * fix failed install with zero exit code * fixing bug due to typo * fixes for linux * Misc fixes during mac testing * PR fixes
This commit is contained in:
@@ -235,7 +235,7 @@
|
||||
"name": "kubectl"
|
||||
},
|
||||
{
|
||||
"name": "azcli"
|
||||
"name": "azure-cli"
|
||||
},
|
||||
{
|
||||
"name": "azdata"
|
||||
@@ -338,14 +338,16 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"promisify-child-process": "^3.1.1",
|
||||
"sudo-prompt": "^9.0.0",
|
||||
"vscode": "^1.1.26",
|
||||
"vscode-nls": "^4.0.0",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typemoq": "^2.1.0",
|
||||
"vscode": "^1.1.26",
|
||||
"@types/yamljs": "0.2.30",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7"
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
"typemoq": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import { SemVer } from 'semver';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_';
|
||||
|
||||
@@ -168,12 +168,12 @@ export interface FieldInfo {
|
||||
editable?: boolean; // for editable dropdown
|
||||
}
|
||||
|
||||
export enum LabelPosition {
|
||||
export const enum LabelPosition {
|
||||
Top = 'top',
|
||||
Left = 'left'
|
||||
}
|
||||
|
||||
export enum FontStyle {
|
||||
export const enum FontStyle {
|
||||
Normal = 'normal',
|
||||
Italic = 'italic'
|
||||
}
|
||||
@@ -200,6 +200,13 @@ export interface NotebookInfo {
|
||||
linux: string;
|
||||
}
|
||||
|
||||
export enum OsType {
|
||||
win32 = 'win32',
|
||||
darwin = 'darwin',
|
||||
linux = 'linux',
|
||||
others = 'others'
|
||||
}
|
||||
|
||||
export interface ToolRequirementInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
@@ -212,20 +219,46 @@ export enum ToolType {
|
||||
Azdata
|
||||
}
|
||||
|
||||
export const enum ToolStatus {
|
||||
NotInstalled = 'NotInstalled',
|
||||
Installed = 'Installed',
|
||||
Installing = 'Installing',
|
||||
Error = 'Error',
|
||||
Failed = 'Failed'
|
||||
}
|
||||
|
||||
export interface ITool {
|
||||
isInstalling: any;
|
||||
readonly name: string;
|
||||
readonly displayName: string;
|
||||
readonly description: string;
|
||||
readonly type: ToolType;
|
||||
readonly version: SemVer | undefined;
|
||||
readonly homePage: string;
|
||||
readonly isInstalled: boolean;
|
||||
loadInformation(): Promise<void>;
|
||||
readonly displayStatus: string;
|
||||
readonly statusDescription: string | undefined;
|
||||
readonly autoInstallSupported: boolean;
|
||||
readonly autoInstallRequired: boolean;
|
||||
readonly isNotInstalled: boolean;
|
||||
readonly needsInstallation: boolean;
|
||||
readonly outputChannelName: string;
|
||||
readonly fullVersion: string | undefined;
|
||||
readonly onDidUpdateData: vscode.Event<ITool>;
|
||||
showOutputChannel(preserveFocus?: boolean): void;
|
||||
loadInformation(): Promise<void>;
|
||||
install(): Promise<void>;
|
||||
}
|
||||
|
||||
export enum BdcDeploymentType {
|
||||
export const enum BdcDeploymentType {
|
||||
NewAKS = 'new-aks',
|
||||
ExistingAKS = 'existing-aks',
|
||||
ExistingKubeAdm = 'existing-kubeadm'
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
command: string;
|
||||
sudo?: boolean;
|
||||
comment?: string;
|
||||
workingDirectory?: string;
|
||||
additionalEnvironmentVariables?: NodeJS.ProcessEnv;
|
||||
ignoreError?: boolean;
|
||||
}
|
||||
|
||||
@@ -66,9 +66,7 @@ export class AzdataService implements IAzdataService {
|
||||
}
|
||||
|
||||
private async ensureWorkingDirectoryExists(): Promise<void> {
|
||||
if (! await this.platformService.fileExists(this.platformService.storagePath())) {
|
||||
await this.platformService.makeDirectory(this.platformService.storagePath());
|
||||
}
|
||||
await this.platformService.ensureDirectoryExists(this.platformService.storagePath());
|
||||
}
|
||||
|
||||
private async getJsonObjectFromFile(path: string): Promise<any> {
|
||||
|
||||
@@ -2,37 +2,61 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as cp from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as cp from 'promisify-child-process';
|
||||
import * as sudo from 'sudo-prompt';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { OsType } from '../interfaces';
|
||||
import { getErrorMessage } from '../utils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const extensionOutputChannel = localize('resourceDeployment.outputChannel', "Deployments");
|
||||
const sudoPromptTitle = 'AzureDataStudio';
|
||||
/**
|
||||
* Abstract of platform dependencies
|
||||
*/
|
||||
export interface IPlatformService {
|
||||
osType(): OsType;
|
||||
platform(): string;
|
||||
storagePath(): string;
|
||||
copyFile(source: string, target: string): Promise<void>;
|
||||
fileExists(file: string): Promise<boolean>;
|
||||
openFile(filePath: string): void;
|
||||
showErrorMessage(message: string): void;
|
||||
showErrorMessage(error: Error | string): void;
|
||||
logToOutputChannel(data: string | Buffer, header?: string): void;
|
||||
outputChannelName(): string;
|
||||
showOutputChannel(preserveFocus?: boolean): void;
|
||||
isNotebookNameUsed(title: string): boolean;
|
||||
makeDirectory(path: string): Promise<void>;
|
||||
ensureDirectoryExists(directory: string): Promise<void>;
|
||||
readTextFile(filePath: string): Promise<string>;
|
||||
runCommand(command: string, options?: CommandOptions): Promise<string>;
|
||||
saveTextFile(content: string, path: string): Promise<void>;
|
||||
deleteFile(path: string, ignoreError?: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
interface CommandOutput {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
export interface CommandOptions {
|
||||
workingDirectory?: string;
|
||||
additionalEnvironmentVariables?: NodeJS.ProcessEnv;
|
||||
sudo?: boolean;
|
||||
commandTitle?: string;
|
||||
ignoreError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that provides various services to interact with the platform on which the code runs
|
||||
*/
|
||||
export class PlatformService implements IPlatformService {
|
||||
|
||||
private _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(extensionOutputChannel);
|
||||
|
||||
constructor(private _storagePath: string = '') {
|
||||
}
|
||||
|
||||
@@ -44,57 +68,185 @@ export class PlatformService implements IPlatformService {
|
||||
return process.platform;
|
||||
}
|
||||
|
||||
copyFile(source: string, target: string): Promise<void> {
|
||||
return fs.promises.copyFile(source, target);
|
||||
outputChannelName(): string {
|
||||
return this._outputChannel.name;
|
||||
}
|
||||
|
||||
fileExists(file: string): Promise<boolean> {
|
||||
return fs.promises.access(file).then(() => {
|
||||
showOutputChannel(preserveFocus?: boolean): void {
|
||||
this._outputChannel.show(preserveFocus);
|
||||
}
|
||||
|
||||
osType(platform: string = this.platform()): OsType {
|
||||
if (Object.values(OsType).includes(<OsType>platform)) {
|
||||
return <OsType>platform;
|
||||
} else {
|
||||
return OsType.others;
|
||||
}
|
||||
}
|
||||
|
||||
async copyFile(source: string, target: string): Promise<void> {
|
||||
return await fs.promises.copyFile(source, target);
|
||||
}
|
||||
|
||||
async fileExists(file: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(file);
|
||||
return true;
|
||||
}).catch(error => {
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openFile(filePath: string): void {
|
||||
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(filePath));
|
||||
}
|
||||
|
||||
showErrorMessage(message: string): void {
|
||||
vscode.window.showErrorMessage(message);
|
||||
showErrorMessage(error: Error | string): void {
|
||||
vscode.window.showErrorMessage(getErrorMessage(error));
|
||||
}
|
||||
|
||||
isNotebookNameUsed(title: string): boolean {
|
||||
return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1);
|
||||
}
|
||||
|
||||
makeDirectory(path: string): Promise<void> {
|
||||
return fs.promises.mkdir(path);
|
||||
async makeDirectory(path: string): Promise<void> {
|
||||
await fs.promises.mkdir(path);
|
||||
}
|
||||
|
||||
readTextFile(filePath: string): Promise<string> {
|
||||
return fs.promises.readFile(filePath, 'utf8');
|
||||
/**
|
||||
*This function ensures that the given {@link directory} does not exist it creates it. It creates only the most leaf folder so if any ancestor folders are missing then this command throws an error.
|
||||
* @param directory - the path to ensure
|
||||
*/
|
||||
async ensureDirectoryExists(directory: string): Promise<void> {
|
||||
if (!await this.fileExists(directory)) {
|
||||
await this.makeDirectory(directory);
|
||||
}
|
||||
}
|
||||
|
||||
runCommand(command: string, options?: CommandOptions): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const env = Object.assign({}, process.env, options && options.additionalEnvironmentVariables);
|
||||
cp.exec(command, {
|
||||
cwd: options && options.workingDirectory,
|
||||
env: env
|
||||
}, (error, stdout, stderror) => {
|
||||
async readTextFile(filePath: string): Promise<string> {
|
||||
return await fs.promises.readFile(filePath, 'utf8');
|
||||
}
|
||||
|
||||
public logToOutputChannel(data: string | Buffer, header?: string): void {
|
||||
//input data is localized by caller
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
this._outputChannel.appendLine(header ? header + line : line);
|
||||
});
|
||||
}
|
||||
|
||||
private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
|
||||
data.toString().split(/\r?\n/)
|
||||
.forEach(line => {
|
||||
outputChannel.appendLine(header + line);
|
||||
});
|
||||
}
|
||||
|
||||
async runCommand(command: string, options?: CommandOptions): Promise<string> {
|
||||
if (options && options.commandTitle !== undefined && options.commandTitle !== null) {
|
||||
this._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`); // commandTitle inputs are localized by caller
|
||||
}
|
||||
|
||||
try {
|
||||
if (options && options.sudo) {
|
||||
return await this.runSudoCommand(command, this._outputChannel, options);
|
||||
} else {
|
||||
return await this.runStreamedCommand(command, this._outputChannel, options);
|
||||
}
|
||||
} catch (error) {
|
||||
this._outputChannel.append(localize('platformService.RunCommand.ErroredOut', "\t>>> {0} ... errored out: {1}", command, getErrorMessage(error))); //errors are localized in our code where emitted, other errors are pass through from external components that are not easily localized
|
||||
if (!(options && options.ignoreError)) {
|
||||
throw error;
|
||||
} else {
|
||||
this._outputChannel.append(localize('platformService.RunCommand.IgnoringError', "\t>>> Ignoring error in execution and continuing tool deployment"));
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sudoExec(command: string, options: sudo.SudoOptions): Promise<CommandOutput> {
|
||||
return new Promise<CommandOutput>((resolve, reject) => {
|
||||
sudo.exec(command, options, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async runSudoCommand(command: string, outputChannel: vscode.OutputChannel, options?: CommandOptions): Promise<string> {
|
||||
outputChannel.appendLine(` sudo> ${command}`);
|
||||
|
||||
if (options && options.workingDirectory) {
|
||||
process.chdir(options.workingDirectory);
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/jorangreef/sudo-prompt/issues/111
|
||||
// DevNote: The environment variable being excluded from getting passed to sudo will never exist on a 'unixy' box. So this affects windows only.
|
||||
// On my testing on windows machine for our usage the environment variables being excluded were not important for the process execution being used here.
|
||||
// If one is trying to use this code elsewhere, one should test on windows thoroughly unless the above issue is fixed.
|
||||
const origEnv: NodeJS.ProcessEnv = Object.assign({}, process.env, options && options.additionalEnvironmentVariables);
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
Object.keys(origEnv).filter(key => /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)).forEach((key) => {
|
||||
env[key] = origEnv[key];
|
||||
});
|
||||
// Workaround for https://github.com/jorangreef/sudo-prompt/issues/111 done
|
||||
|
||||
const sudoOptions = {
|
||||
name: sudoPromptTitle,
|
||||
env: env
|
||||
};
|
||||
|
||||
try {
|
||||
const { stdout, stderr } = await this.sudoExec(command, sudoOptions);
|
||||
this.outputDataChunk(stdout, outputChannel, localize('platformService.RunCommand.stdout', " stdout: "));
|
||||
this.outputDataChunk(stderr, outputChannel, localize('platformService.RunCommand.stderr', " stderr: "));
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
this.outputDataChunk(error, outputChannel, localize('platformService.RunCommand.stderr', " stderr: "));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: CommandOptions): Promise<string> {
|
||||
const stdoutData: string[] = [];
|
||||
outputChannel.appendLine(` > ${command}`);
|
||||
|
||||
const spawnOptions = {
|
||||
cwd: options && options.workingDirectory,
|
||||
env: Object.assign({}, process.env, options && options.additionalEnvironmentVariables),
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024, // 10 Mb of output can be captured.
|
||||
shell: true,
|
||||
detached: false,
|
||||
windowsHide: true
|
||||
};
|
||||
const child = cp.spawn(command, [], spawnOptions);
|
||||
|
||||
// Add listeners to print stdout and stderr and exit code
|
||||
child.on('exit', (code: number | null, signal: string | null) => {
|
||||
if (code !== null) {
|
||||
outputChannel.appendLine(localize('platformService.RunStreamedCommand.ExitedWithCode', " >>> ${0} ... exited with code: ${1}", command, code));
|
||||
} else {
|
||||
outputChannel.appendLine(localize('platformService.RunStreamedCommand.ExitedWithSignal', " >>> ${0} ... exited with signal: ${1}", command, signal));
|
||||
}
|
||||
});
|
||||
child.stdout.on('data', (data: string | Buffer) => {
|
||||
stdoutData.push(data.toString());
|
||||
this.outputDataChunk(data, outputChannel, localize('platformService.RunCommand.stdout', " stdout: "));
|
||||
});
|
||||
child.stderr.on('data', (data: string | Buffer) => { this.outputDataChunk(data, outputChannel, localize('platformService.RunCommand.stderr', " stderr: ")); });
|
||||
|
||||
await child;
|
||||
return stdoutData.join('');
|
||||
}
|
||||
|
||||
saveTextFile(content: string, path: string): Promise<void> {
|
||||
return fs.promises.writeFile(path, content, 'utf8');
|
||||
}
|
||||
@@ -108,7 +260,7 @@ export class PlatformService implements IPlatformService {
|
||||
}
|
||||
catch (error) {
|
||||
if (ignoreError) {
|
||||
console.error('Error occured deleting file: ', getErrorMessage(error));
|
||||
console.error('Error occurred deleting file: ', getErrorMessage(error));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolType } from '../../interfaces';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SemVer } from 'semver';
|
||||
import { IPlatformService } from '../platformService';
|
||||
import { EOL } from 'os';
|
||||
import { SemVer } from 'semver';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Command, OsType, ToolType } from '../../interfaces';
|
||||
import { IPlatformService } from '../platformService';
|
||||
import { ToolBase } from './toolBase';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const defaultInstallationRoot = '~/.local/bin';
|
||||
const win32InstallationRoot = `${process.env['ProgramFiles(x86)']}\\Microsoft SDKs\\Azure\\CLI2\\wbin`;
|
||||
|
||||
export class AzCliTool extends ToolBase {
|
||||
constructor(platformService: IPlatformService) {
|
||||
@@ -17,11 +19,11 @@ export class AzCliTool extends ToolBase {
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return 'azcli';
|
||||
return 'azure-cli';
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return localize('resourceDeployment.AzCLIDescription', 'A command-line tool for managing Azure resources');
|
||||
return localize('resourceDeployment.AzCLIDescription', "A command-line tool for managing Azure resources");
|
||||
}
|
||||
|
||||
get type(): ToolType {
|
||||
@@ -29,13 +31,33 @@ export class AzCliTool extends ToolBase {
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return localize('resourceDeployment.AzCLIDisplayName', 'Azure CLI');
|
||||
return localize('resourceDeployment.AzCLIDisplayName', "Azure CLI");
|
||||
}
|
||||
|
||||
get homePage(): string {
|
||||
return 'https://docs.microsoft.com/cli/azure/install-azure-cli';
|
||||
}
|
||||
|
||||
get autoInstallSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async getInstallationPath(): Promise<string | undefined> {
|
||||
switch (this.osType) {
|
||||
case OsType.win32:
|
||||
return win32InstallationRoot;
|
||||
default:
|
||||
return defaultInstallationRoot;
|
||||
}
|
||||
}
|
||||
|
||||
readonly allInstallationCommands: Map<OsType, Command[]> = new Map<OsType, Command[]>([
|
||||
[OsType.linux, linuxInstallationCommands],
|
||||
[OsType.win32, win32InstallationCommands],
|
||||
[OsType.darwin, macOsInstallationCommands],
|
||||
[OsType.others, defaultInstallationCommands]
|
||||
]);
|
||||
|
||||
protected getVersionFromOutput(output: string): SemVer | undefined {
|
||||
if (output && output.includes('azure-cli')) {
|
||||
return new SemVer(output.split(EOL)[0].replace('azure-cli', '').replace(/ /g, '').replace('*', ''));
|
||||
@@ -43,7 +65,75 @@ export class AzCliTool extends ToolBase {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
protected get versionCommand(): string {
|
||||
return 'az --version';
|
||||
protected get versionCommand(): Command {
|
||||
return {
|
||||
command: 'az --version'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const win32InstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.AziCli.DeletingPreviousAzureCli.msi', "deleting previously downloaded azurecli.msi if one exists ..."),
|
||||
command: `IF EXIST .\\AzureCLI.msi DEL /F .\\AzureCLI.msi`
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.DownloadingAndInstallingAzureCli', "downloading azurecli.msi and installing azure-cli ..."),
|
||||
command: `powershell -Command "& {(New-Object System.Net.WebClient).DownloadFile('https://aka.ms/installazurecliwindows', 'AzureCLI.msi'); Start-Process msiexec.exe -Wait -ArgumentList '/I AzureCLI.msi /passive /quiet /lvx ADS_AzureCliInstall.log'}"`
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.AziCli.DisplayingInstallationLog', "displaying the installation log ..."),
|
||||
command: `type AzureCliInstall.log | findstr /i /v /c:"cached product context" | findstr /i /v /c:"has no eligible binary patches" `,
|
||||
ignoreError: true
|
||||
}
|
||||
];
|
||||
const macOsInstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.AziCli.UpdatingBrewRepository', "updating your brew repository for azure-cli installation ..."),
|
||||
command: 'brew update'
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.AziCli.InstallingAzureCli', "installing azure-cli ..."),
|
||||
command: 'brew install azure-cli'
|
||||
}
|
||||
];
|
||||
const linuxInstallationCommands = [
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.AptGetUpdate', "updating repository information before installing azure-cli ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.AptGetPackages', "getting packages needed for azure-cli installation ..."),
|
||||
command: 'apt-get install ca-certificates curl apt-transport-https lsb-release gnupg -y'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.DownloadAndInstallingSigningKey', "downloading and installing the signing key for azure-cli ..."),
|
||||
command: 'curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.asc.gpg > /dev/null'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.AddingAzureCliRepositoryInformation', "adding the azure-cli repository information ..."),
|
||||
command: 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ `lsb_release -cs` main" | tee /etc/apt/sources.list.d/azure-cli.list'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.AptGetUpdateAgain', "updating repository information again for azure-cli ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.InstallingAzureCli', "installing azure-cli ..."),
|
||||
command: 'apt-get install azure-cli'
|
||||
}
|
||||
];
|
||||
const defaultInstallationCommands = [
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.AziCli.ScriptedInstall', "download and invoking script to install azure-cli ..."),
|
||||
command: 'curl -sL https://aka.ms/InstallAzureCLIDeb | bash'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolType } from '../../interfaces';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SemVer } from 'semver';
|
||||
import { EOL } from 'os';
|
||||
import * as path from 'path';
|
||||
import { SemVer } from 'semver';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Command, OsType, ToolType } from '../../interfaces';
|
||||
import { IPlatformService } from '../platformService';
|
||||
import { ToolBase } from './toolBase';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const installationRoot = '~/.local/bin';
|
||||
|
||||
export class AzdataTool extends ToolBase {
|
||||
constructor(platformService: IPlatformService) {
|
||||
@@ -37,8 +38,10 @@ export class AzdataTool extends ToolBase {
|
||||
return 'https://docs.microsoft.com/sql/big-data-cluster/deploy-install-azdata';
|
||||
}
|
||||
|
||||
protected get versionCommand(): string {
|
||||
return 'azdata -v';
|
||||
protected get versionCommand(): Command {
|
||||
return {
|
||||
command: 'azdata -v'
|
||||
};
|
||||
}
|
||||
|
||||
protected getVersionFromOutput(output: string): SemVer | undefined {
|
||||
@@ -48,4 +51,79 @@ export class AzdataTool extends ToolBase {
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
get autoInstallSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async getInstallationPath(): Promise<string | undefined> {
|
||||
switch (this.osType) {
|
||||
case OsType.linux:
|
||||
return installationRoot;
|
||||
default:
|
||||
const azdataCliInstallLocation = await this.getPip3InstallLocation('azdata-cli');
|
||||
return azdataCliInstallLocation && path.join(azdataCliInstallLocation, '..', 'Scripts');
|
||||
}
|
||||
}
|
||||
|
||||
readonly allInstallationCommands: Map<OsType, Command[]> = new Map<OsType, Command[]>([
|
||||
[OsType.linux, linuxInstallationCommands],
|
||||
[OsType.win32, defaultInstallationCommands],
|
||||
[OsType.darwin, defaultInstallationCommands],
|
||||
[OsType.others, defaultInstallationCommands]
|
||||
]);
|
||||
|
||||
protected get uninstallCommand(): string | undefined {
|
||||
if (this.osType !== OsType.linux) {
|
||||
return defaultUninstallCommand;
|
||||
} else {
|
||||
return super.uninstallCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const linuxInstallationCommands = [
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.AptGetUpdate', "updating repository information ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.AptGetPackages', "getting packages needed for azdata installation ..."),
|
||||
command: 'apt-get install gnupg ca-certificates curl apt-transport-https lsb-release -y'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.DownloadAndInstallingSigningKey', "downloading and installing the signing key for azdata ..."),
|
||||
command: 'wget -qO- https://packages.microsoft.com/keys/microsoft.asc | apt-key add -'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.AddingAzureCliRepositoryInformation', "adding the azdata repository information ..."),
|
||||
command: 'add-apt-repository "$(wget -qO- https://packages.microsoft.com/config/ubuntu/16.04/mssql-server-preview.list)"'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.AptGetUpdate', "updating repository information ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Azdata.InstallingAzdata', "installing azdata ..."),
|
||||
command: 'apt-get install -y azdata-cli'
|
||||
}
|
||||
];
|
||||
|
||||
const defaultInstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.Azdata.InstallUpdatePythonRequestsPackage', "installing/updating to latest version of requests python package azdata ..."),
|
||||
command: `pip3 install -U requests`
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Azdata.InstallingAzdata', "installing azdata ..."),
|
||||
command: `pip3 install -r https://aka.ms/azdata --quiet --user`
|
||||
}
|
||||
];
|
||||
|
||||
const defaultUninstallCommand = `pip3 uninstall -r https://aka.ms/azdata -y `;
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolType } from '../../interfaces';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SemVer } from 'semver';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Command, ToolType, OsType } from '../../interfaces';
|
||||
import { IPlatformService } from '../platformService';
|
||||
import { ToolBase } from './toolBase';
|
||||
|
||||
@@ -21,7 +20,7 @@ export class DockerTool extends ToolBase {
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return localize('resourceDeployment.DockerDescription', 'Provides the ability to package and run an application in isolated containers');
|
||||
return localize('resourceDeployment.DockerDescription', "Provides the ability to package and run an application in isolated containers");
|
||||
}
|
||||
|
||||
get type(): ToolType {
|
||||
@@ -29,7 +28,7 @@ export class DockerTool extends ToolBase {
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return localize('resourceDeployment.DockerDisplayName', 'docker');
|
||||
return localize('resourceDeployment.DockerDisplayName', "docker");
|
||||
}
|
||||
|
||||
get homePage(): string {
|
||||
@@ -43,7 +42,15 @@ export class DockerTool extends ToolBase {
|
||||
}
|
||||
return version;
|
||||
}
|
||||
protected get versionCommand(): string {
|
||||
return 'docker version --format "{{json .}}"';
|
||||
protected get versionCommand(): Command {
|
||||
return { command: 'docker version --format "{{json .}}"' };
|
||||
}
|
||||
|
||||
get autoInstallSupported(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
get allInstallationCommands(): Map<OsType, Command[]> {
|
||||
throw Error('Installation of DockerTool is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolType } from '../../interfaces';
|
||||
import { Command, ToolType, OsType } from '../../interfaces';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { SemVer } from 'semver';
|
||||
import { IPlatformService } from '../platformService';
|
||||
@@ -21,7 +21,7 @@ export class KubeCtlTool extends ToolBase {
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return localize('resourceDeployment.KubeCtlDescription', 'A command-line tool allows you to run commands against Kubernetes clusters');
|
||||
return localize('resourceDeployment.KubeCtlDescription', "A command-line tool allows you to run commands against Kubernetes clusters");
|
||||
}
|
||||
|
||||
get type(): ToolType {
|
||||
@@ -29,7 +29,7 @@ export class KubeCtlTool extends ToolBase {
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return localize('resourceDeployment.KubeCtlDisplayName', 'kubectl');
|
||||
return localize('resourceDeployment.KubeCtlDisplayName', "kubectl");
|
||||
}
|
||||
|
||||
get homePage(): string {
|
||||
@@ -45,7 +45,101 @@ export class KubeCtlTool extends ToolBase {
|
||||
return version;
|
||||
}
|
||||
|
||||
protected get versionCommand(): string {
|
||||
return 'kubectl version -o json --client';
|
||||
protected get versionCommand(): Command {
|
||||
return { command: 'kubectl version -o json --client' };
|
||||
}
|
||||
|
||||
get autoInstallSupported(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
readonly allInstallationCommands: Map<OsType, Command[]> = new Map<OsType, Command[]>([
|
||||
[OsType.linux, linuxInstallationCommands],
|
||||
[OsType.win32, win32InstallationCommands],
|
||||
[OsType.darwin, macOsInstallationCommands],
|
||||
[OsType.others, defaultInstallationCommands]
|
||||
]);
|
||||
}
|
||||
|
||||
const macOsInstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.UpdatingBrewRepository', "updating your brew repository for kubectl installation ..."),
|
||||
command: 'brew update'
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.InstallingKubeCtl', "installing kubectl ..."),
|
||||
command: 'brew install kubectl'
|
||||
}
|
||||
];
|
||||
const linuxInstallationCommands = [
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.AptGetUpdate', "updating repository information ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.AptGetPackages', "getting packages needed for kubectl installation ..."),
|
||||
command: 'apt-get install -y apt-transport-https'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.DownloadAndInstallingSigningKey', "downloading and installing the signing key for kubectl ..."),
|
||||
command: 'curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.AddingKubectlRepositoryInformation', "adding the kubectl repository information ..."),
|
||||
command: 'echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.AptGetUpdate', "updating repository information ..."),
|
||||
command: 'apt-get update'
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.InstallingKubectl', "installing kubectl ..."),
|
||||
command: 'apt-get install -y kubectl'
|
||||
}
|
||||
];
|
||||
// TODO: Remove dependency on curl on Win32 and use powershell Invoke-WebRequest instead
|
||||
const win32InstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.DeletePreviousDownloadedKubectl.exe', "deleting previously downloaded kubectl.exe if one exists ..."),
|
||||
command: `IF EXIST .\kubectl.exe DEL /F .\kubectl.exe`,
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.DownloadingAndInstallingKubectl', "downloading and installing the latest kubectl.exe ..."),
|
||||
command: `for /f %i in ('curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt') do curl -LO https://storage.googleapis.com/kubernetes-release/release/%i/bin/windows/amd64/kubectl.exe`
|
||||
}
|
||||
];
|
||||
const defaultInstallationCommands = [
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.DeletePreviousDownloadedKubectl', "deleting previously downloaded kubectl if one exists ..."),
|
||||
command: `[ -e ./kubectl ] && rm -f ./kubectl`,
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.DownloadingKubectl', "downloading the latest kubectl release ..."),
|
||||
command: 'curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl'
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.MakingExecutable', "making kubectl executable ..."),
|
||||
command: 'chmod +x ./kubectl',
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.CleaningUpOldBackups', "cleaning up any previously backed up version in the install location if they exist ..."),
|
||||
command: `[ -e /usr/local/bin/kubectl] && [ -e /usr/local/bin/kubectl_movedByADS ] && rm -f /usr/local/bin/kubectl_movedByADS`
|
||||
},
|
||||
{
|
||||
sudo: true,
|
||||
comment: localize('resourceDeployment.Kubectl.BackupCurrentBinary', "backing up any existing kubectl in the install location ..."),
|
||||
command: `[ -e /usr/local/bin/kubectl ] && mv /usr/local/bin/kubectl /usr/local/bin/kubectl_movedByADS`
|
||||
},
|
||||
{
|
||||
comment: localize('resourceDeployment.Kubectl.MoveToSystemPath', "moving kubectl into the install location in the PATH ..."),
|
||||
sudo: true,
|
||||
command: 'mv ./kubectl /usr/local/bin/kubectl'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -2,62 +2,246 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ToolType, ITool } from '../../interfaces';
|
||||
import { SemVer } from 'semver';
|
||||
import { IPlatformService } from '../platformService';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { EOL } from 'os';
|
||||
import { delimiter } from 'path';
|
||||
import { SemVer } from 'semver';
|
||||
import * as vscode from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Command, ITool, OsType, ToolStatus, ToolType } from '../../interfaces';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { IPlatformService } from '../platformService';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
const toolStatusNotInstalled: string = localize('deploymentDialog.ToolStatus.NotInstalled', "Not Installed");
|
||||
const toolStatusInstalled: string = localize('deploymentDialog.ToolStatus.Installed', "Installed");
|
||||
const toolStatusInstalling: string = localize('deploymentDialog.ToolStatus.NotInstalling', "Installing ...");
|
||||
const toolStatusError: string = localize('deploymentDialog.ToolStatus.Error', "Error");
|
||||
const toolStatusFailed: string = localize('deploymentDialog.ToolStatus.Failed', "Failed");
|
||||
|
||||
const toolStatusLocalized: Map<ToolStatus, string> = new Map<ToolStatus, string>([
|
||||
[ToolStatus.Error, toolStatusError],
|
||||
[ToolStatus.Installed, toolStatusInstalled],
|
||||
[ToolStatus.Installing, toolStatusInstalling],
|
||||
[ToolStatus.NotInstalled, toolStatusNotInstalled],
|
||||
[ToolStatus.Failed, toolStatusFailed]
|
||||
]);
|
||||
|
||||
export abstract class ToolBase implements ITool {
|
||||
constructor(private _platformService: IPlatformService) { }
|
||||
constructor(private _platformService: IPlatformService) {
|
||||
this._osType = this._platformService.osType();
|
||||
}
|
||||
|
||||
abstract name: string;
|
||||
abstract displayName: string;
|
||||
abstract description: string;
|
||||
abstract type: ToolType;
|
||||
abstract homePage: string;
|
||||
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
|
||||
protected abstract readonly versionCommand: string;
|
||||
abstract autoInstallSupported: boolean;
|
||||
abstract readonly allInstallationCommands: Map<OsType, Command[]>;
|
||||
|
||||
public get version(): SemVer | undefined {
|
||||
protected abstract getVersionFromOutput(output: string): SemVer | undefined;
|
||||
protected readonly _onDidUpdateData = new vscode.EventEmitter<ITool>();
|
||||
protected readonly uninstallCommand?: string;
|
||||
|
||||
|
||||
protected abstract readonly versionCommand: Command;
|
||||
|
||||
protected async getInstallationPath(): Promise<string | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected get installationSearchPaths(): (string | undefined)[] {
|
||||
return [this.storagePath];
|
||||
}
|
||||
|
||||
protected get downloadPath(): string {
|
||||
return this.storagePath;
|
||||
}
|
||||
|
||||
protected logToOutputChannel(data: string | Buffer, header?: string): void {
|
||||
this._platformService.logToOutputChannel(data, header); // data and header are localized by caller
|
||||
}
|
||||
|
||||
public get onDidUpdateData(): vscode.Event<ITool> {
|
||||
return this._onDidUpdateData.event;
|
||||
}
|
||||
|
||||
protected get status(): ToolStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
protected set status(value: ToolStatus) {
|
||||
this._status = value;
|
||||
this._onDidUpdateData.fire(this);
|
||||
}
|
||||
|
||||
public get displayStatus(): string {
|
||||
return <string>toolStatusLocalized.get(this._status);
|
||||
}
|
||||
|
||||
public get autoInstallRequired(): boolean {
|
||||
return this.status !== ToolStatus.Installed && this.autoInstallSupported;
|
||||
}
|
||||
|
||||
public get isNotInstalled(): boolean {
|
||||
return this.status === ToolStatus.NotInstalled;
|
||||
}
|
||||
|
||||
public get isInstalling(): boolean {
|
||||
return this.status === ToolStatus.Installing;
|
||||
}
|
||||
|
||||
public get needsInstallation(): boolean {
|
||||
return this.status !== ToolStatus.Installed;
|
||||
}
|
||||
|
||||
public get storagePath(): string {
|
||||
const storagePath = this._platformService.storagePath();
|
||||
if (!this._storagePathEnsured) {
|
||||
this._platformService.ensureDirectoryExists(storagePath);
|
||||
this._storagePathEnsured = true;
|
||||
}
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
public get osType(): OsType {
|
||||
return this._osType;
|
||||
}
|
||||
|
||||
protected get version(): SemVer | undefined {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
public get isInstalled(): boolean {
|
||||
return this._isInstalled;
|
||||
protected set version(value: SemVer | undefined) {
|
||||
this._version = value;
|
||||
this._onDidUpdateData.fire(this);
|
||||
}
|
||||
|
||||
public get fullVersion(): string | undefined {
|
||||
return this._version && this._version.version;
|
||||
|
||||
}
|
||||
|
||||
public get statusDescription(): string | undefined {
|
||||
return this._statusDescription;
|
||||
}
|
||||
|
||||
public loadInformation(): Promise<void> {
|
||||
if (this._isInstalled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
this._isInstalled = false;
|
||||
this._statusDescription = undefined;
|
||||
this._version = undefined;
|
||||
this._versionOutput = undefined;
|
||||
return this._platformService.runCommand(this.versionCommand).then((stdout) => {
|
||||
this._versionOutput = stdout;
|
||||
this._version = this.getVersionFromOutput(stdout);
|
||||
if (this._version) {
|
||||
this._isInstalled = true;
|
||||
} else {
|
||||
throw localize('deployCluster.InvalidToolVersionOutput', "Invalid output received.");
|
||||
}
|
||||
}).catch((error) => {
|
||||
const errorMessage = typeof error === 'string' ? error :
|
||||
typeof error.message === 'string' ? error.message : '';
|
||||
this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Error: {1}{0}stdout: {2} ", EOL, errorMessage, this._versionOutput);
|
||||
});
|
||||
protected get installationCommands(): Command[] | undefined {
|
||||
return this.allInstallationCommands.get(this.osType);
|
||||
}
|
||||
|
||||
private _isInstalled: boolean = false;
|
||||
protected async getPip3InstallLocation(packageName: string): Promise<string> {
|
||||
const command = `pip3 show ${packageName}`;
|
||||
const pip3ShowOutput: string = await this._platformService.runCommand(command, { sudo: false, ignoreError: true });
|
||||
const installLocation = /^Location\: (.*)$/gim.exec(pip3ShowOutput);
|
||||
let retValue = installLocation && installLocation[1];
|
||||
if (retValue === undefined || retValue === null) {
|
||||
this.logToOutputChannel(` >${command}`); //command is localized by caller
|
||||
this.logToOutputChannel(localize('toolBase.getPip3InstallationLocation.LocationNotFound', " Could not find 'Location' in the output:"));
|
||||
this.logToOutputChannel(pip3ShowOutput, localize('toolBase.getPip3InstallationLocation.Output', " output:"));
|
||||
return '';
|
||||
} else {
|
||||
return retValue;
|
||||
}
|
||||
}
|
||||
|
||||
public get outputChannelName(): string {
|
||||
return this._platformService.outputChannelName();
|
||||
}
|
||||
|
||||
public showOutputChannel(preserveFocus?: boolean | undefined): void {
|
||||
this._platformService.showOutputChannel(preserveFocus);
|
||||
}
|
||||
|
||||
public async install(): Promise<void> {
|
||||
try {
|
||||
this.status = ToolStatus.Installing;
|
||||
await this.installCore();
|
||||
await this.addInstallationSearchPathsToSystemPath();
|
||||
this.status = await this.updateVersionAndGetStatus();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this._statusDescription = localize('toolBase.InstallError', "Error installing tool '{0}' [ {1} ].{2}Error: {3}{2}See output channel '{4}' for more details", this.displayName, this.homePage, EOL, errorMessage, this.outputChannelName);
|
||||
this.status = ToolStatus.Error;
|
||||
throw error;
|
||||
}
|
||||
// Since we just completed installation, the status should be ToolStatus.Installed
|
||||
// but if it is ToolStatus.NotInstalled then it means that installation failed with 0 exit code.
|
||||
if (this.status === ToolStatus.NotInstalled) {
|
||||
this._statusDescription = localize('toolBase.InstallFailed', "Installation commands completed but version of tool '{0}' could not be detected so our installation attempt has failed. Detection Error: {1}{2}Cleaning up previous installations would help.", this.displayName, this._statusDescription, EOL);
|
||||
if (this.uninstallCommand) {
|
||||
this._statusDescription += localize('toolBase.ManualUninstallCommand', " A possibly way to uninstall is using this command:{0} >{1}", EOL, this.uninstallCommand);
|
||||
}
|
||||
this._statusDescription += localize('toolBase.SeeOutputChannel', "{0}See output channel '{1}' for more details", EOL, this.outputChannelName);
|
||||
this.status = ToolStatus.Failed;
|
||||
throw new Error(this._statusDescription);
|
||||
}
|
||||
}
|
||||
|
||||
protected async installCore() {
|
||||
const installationCommands: Command[] | undefined = this.installationCommands;
|
||||
if (!installationCommands || installationCommands.length === 0) {
|
||||
throw new Error(localize('toolBase.installCore.CannotInstallTool', "Cannot install tool:${0}::${1} as installation commands are unknown", this.displayName, this.description));
|
||||
}
|
||||
for (let i: number = 0; i < installationCommands.length; i++) {
|
||||
await this._platformService.runCommand(installationCommands[i].command,
|
||||
{
|
||||
workingDirectory: installationCommands[i].workingDirectory || this.downloadPath,
|
||||
additionalEnvironmentVariables: installationCommands[i].additionalEnvironmentVariables,
|
||||
sudo: installationCommands[i].sudo,
|
||||
commandTitle: installationCommands[i].comment,
|
||||
ignoreError: installationCommands[i].ignoreError
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async addInstallationSearchPathsToSystemPath(): Promise<void> {
|
||||
const installationPath = await this.getInstallationPath();
|
||||
const searchPaths = [installationPath, ...this.installationSearchPaths].filter(path => !!path);
|
||||
this.logToOutputChannel(localize('toolBase.addInstallationSearchPathsToSystemPath.SearchPaths', "Search Paths for tool '{0}': {1}", this.displayName, JSON.stringify(searchPaths, undefined, '\t'))); //this.displayName is localized and searchPaths are OS filesystem paths.
|
||||
searchPaths.forEach(installationSearchPath => {
|
||||
if (process.env.PATH) {
|
||||
if (!`${delimiter}${process.env.PATH}${delimiter}`.includes(`${delimiter}${installationSearchPath}${delimiter}`)) {
|
||||
process.env.PATH += `${delimiter}${installationSearchPath}`;
|
||||
console.log(`Appending to Path -> ${delimiter}${installationSearchPath}`);
|
||||
}
|
||||
} else {
|
||||
process.env.PATH = installationSearchPath;
|
||||
console.log(`Appending to Path -> '${delimiter}${installationSearchPath}':${delimiter}${installationSearchPath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
public async loadInformation(): Promise<void> {
|
||||
if (this.status === ToolStatus.NotInstalled) {
|
||||
await this.addInstallationSearchPathsToSystemPath();
|
||||
this.status = await this.updateVersionAndGetStatus();
|
||||
}
|
||||
}
|
||||
private async updateVersionAndGetStatus(): Promise<ToolStatus> {
|
||||
const commandOutput = await this._platformService.runCommand(
|
||||
this.versionCommand.command,
|
||||
{
|
||||
workingDirectory: this.versionCommand.workingDirectory,
|
||||
additionalEnvironmentVariables: this.versionCommand.additionalEnvironmentVariables,
|
||||
sudo: false,
|
||||
ignoreError: true
|
||||
},
|
||||
);
|
||||
this.version = this.getVersionFromOutput(commandOutput);
|
||||
if (this.version) {
|
||||
return ToolStatus.Installed;
|
||||
}
|
||||
else {
|
||||
this._statusDescription = localize('deployCluster.GetToolVersionError', "Error retrieving version information.{0}Invalid output received, get version command output: '{1}' ", EOL, commandOutput);
|
||||
return ToolStatus.NotInstalled;
|
||||
}
|
||||
}
|
||||
|
||||
private _storagePathEnsured: boolean = false;
|
||||
private _status: ToolStatus = ToolStatus.NotInstalled;
|
||||
private _osType: OsType;
|
||||
private _version?: SemVer;
|
||||
private _statusDescription?: string;
|
||||
private _versionOutput?: string;
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ suite('Tools Service Tests', function (): void {
|
||||
const toolsService = new ToolsService(mockPlatformService.object);
|
||||
|
||||
const tools: { name: string; type: ToolType }[] = [
|
||||
{ name: 'azcli', type: ToolType.AzCli },
|
||||
{ name: 'azure-cli', type: ToolType.AzCli },
|
||||
{ name: 'docker', type: ToolType.Docker },
|
||||
{ name: 'kubectl', type: ToolType.KubeCtl },
|
||||
{ name: 'azdata', type: ToolType.Azdata }];
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
/// <reference path='../../../../src/typings/sudo-prompt.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { DialogBase } from './dialogBase';
|
||||
import { ResourceType, AgreementInfo, DeploymentProvider } from '../interfaces';
|
||||
import { AgreementInfo, DeploymentProvider, ITool, ResourceType } from '../interfaces';
|
||||
import { IResourceTypeService } from '../services/resourceTypeService';
|
||||
import { IToolsService } from '../services/toolsService';
|
||||
import { EOL } from 'os';
|
||||
import { getErrorMessage } from '../utils';
|
||||
import { DialogBase } from './dialogBase';
|
||||
import { createFlexContainer } from './modelViewUtils';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
@@ -27,6 +27,9 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
private _toolsLoadingComponent!: azdata.LoadingComponent;
|
||||
private _agreementContainer!: azdata.DivContainer;
|
||||
private _agreementCheckboxChecked: boolean = false;
|
||||
private _installToolButton: azdata.window.Button;
|
||||
private _tools: ITool[] = [];
|
||||
private _cardsContainer!: azdata.FlexContainer;
|
||||
|
||||
constructor(
|
||||
private toolsService: IToolsService,
|
||||
@@ -34,7 +37,14 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
resourceType: ResourceType) {
|
||||
super(localize('resourceTypePickerDialog.title', "Select the deployment options"), 'ResourceTypePickerDialog', true);
|
||||
this._selectedResourceType = resourceType;
|
||||
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', 'Select');
|
||||
this._dialogObject.okButton.onClick(() => this.onComplete());
|
||||
this._installToolButton = azdata.window.createButton(localize('deploymentDialog.InstallToolsButton', "Install tools"));
|
||||
this._toDispose.push(this._installToolButton.onClick(() => {
|
||||
this.installTools();
|
||||
}));
|
||||
this._dialogObject.customButtons = [this._installToolButton];
|
||||
this._installToolButton.hidden = true;
|
||||
this._dialogObject.okButton.label = localize('deploymentDialog.OKButtonText', "Select");
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -55,24 +65,24 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
this.resourceTypeService.getResourceTypes().sort((a: ResourceType, b: ResourceType) => {
|
||||
return (a.displayIndex || Number.MAX_VALUE) - (b.displayIndex || Number.MAX_VALUE);
|
||||
}).forEach(resourceType => this.addCard(resourceType));
|
||||
const cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row' }).component();
|
||||
this._cardsContainer = view.modelBuilder.flexContainer().withItems(this._resourceTypeCards, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '10px' } }).withLayout({ flexFlow: 'row' }).component();
|
||||
this._resourceDescriptionLabel = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: this._selectedResourceType ? this._selectedResourceType.description : undefined }).component();
|
||||
this._optionsContainer = view.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this._agreementContainer = view.modelBuilder.divContainer().component();
|
||||
const toolColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolNameColumnHeader', 'Tool'),
|
||||
value: localize('deploymentDialog.toolNameColumnHeader', "Tool"),
|
||||
width: 150
|
||||
};
|
||||
const descriptionColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolDescriptionColumnHeader', 'Description'),
|
||||
value: localize('deploymentDialog.toolDescriptionColumnHeader', "Description"),
|
||||
width: 650
|
||||
};
|
||||
const installStatusColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolStatusColumnHeader', 'Installed'),
|
||||
value: localize('deploymentDialog.toolStatusColumnHeader', "Status"),
|
||||
width: 100
|
||||
};
|
||||
const versionColumn: azdata.TableColumn = {
|
||||
value: localize('deploymentDialog.toolVersionColumnHeader', 'Version'),
|
||||
value: localize('deploymentDialog.toolVersionColumnHeader', "Version"),
|
||||
width: 100
|
||||
};
|
||||
|
||||
@@ -88,7 +98,7 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
const formBuilder = view.modelBuilder.formContainer().withFormItems(
|
||||
[
|
||||
{
|
||||
component: cardsContainer,
|
||||
component: this._cardsContainer,
|
||||
title: ''
|
||||
}, {
|
||||
component: this._resourceDescriptionLabel,
|
||||
@@ -99,10 +109,10 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
},
|
||||
{
|
||||
component: this._optionsContainer,
|
||||
title: localize('deploymentDialog.OptionsTitle', 'Options')
|
||||
title: localize('deploymentDialog.OptionsTitle', "Options")
|
||||
}, {
|
||||
component: this._toolsLoadingComponent,
|
||||
title: localize('deploymentDialog.RequiredToolsTitle', 'Required tools')
|
||||
title: localize('deploymentDialog.RequiredToolsTitle', "Required tools")
|
||||
}
|
||||
],
|
||||
{
|
||||
@@ -178,15 +188,15 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
width: '300px'
|
||||
}).component();
|
||||
|
||||
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateTools(); }));
|
||||
this._toDispose.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); }));
|
||||
this._optionDropDownMap.set(option.name, optionSelectBox);
|
||||
const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component();
|
||||
this._optionsContainer.addItem(row);
|
||||
});
|
||||
this.updateTools();
|
||||
this.updateToolsDisplayTable();
|
||||
}
|
||||
|
||||
private updateTools(): void {
|
||||
private updateToolsDisplayTable(): void {
|
||||
this.toolRefreshTimestamp = new Date().getTime();
|
||||
const currentRefreshTimestamp = this.toolRefreshTimestamp;
|
||||
const toolRequirements = this.getCurrentProvider().requiredTools;
|
||||
@@ -195,40 +205,58 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
this._dialogObject.message = {
|
||||
text: ''
|
||||
};
|
||||
this._installToolButton.hidden = true;
|
||||
if (toolRequirements.length === 0) {
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
this._toolsTable.data = [[localize('deploymentDialog.NoRequiredTool', "No tools required"), '']];
|
||||
this._tools = [];
|
||||
} else {
|
||||
const tools = toolRequirements.map(toolReq => {
|
||||
this._tools = toolRequirements.map(toolReq => {
|
||||
return this.toolsService.getToolByName(toolReq.name)!;
|
||||
});
|
||||
this._toolsLoadingComponent.loading = true;
|
||||
this._dialogObject.okButton.enabled = false;
|
||||
|
||||
Promise.all(tools.map(tool => tool.loadInformation())).then(() => {
|
||||
Promise.all(this._tools.map(tool => tool.loadInformation())).then(async () => {
|
||||
// If the local timestamp does not match the class level timestamp, it means user has changed options, ignore the results
|
||||
if (this.toolRefreshTimestamp !== currentRefreshTimestamp) {
|
||||
return;
|
||||
}
|
||||
let autoInstallRequired = false;
|
||||
const messages: string[] = [];
|
||||
this._toolsTable.data = toolRequirements.map(toolRef => {
|
||||
const tool = this.toolsService.getToolByName(toolRef.name)!;
|
||||
if (!tool.isInstalled) {
|
||||
messages.push(localize('deploymentDialog.ToolInformation', "{0}: {1}", tool.displayName, tool.homePage));
|
||||
this._toolsTable.data = toolRequirements.map(toolReq => {
|
||||
const tool = this.toolsService.getToolByName(toolReq.name)!;
|
||||
// subscribe to onUpdateData event of the tool.
|
||||
this._toDispose.push(tool.onDidUpdateData((t: ITool) => {
|
||||
this.updateToolsDisplayTableData(t);
|
||||
}));
|
||||
if (tool.isNotInstalled && !tool.autoInstallSupported) {
|
||||
messages.push(localize('deploymentDialog.ToolInformation', "'{0}' [ {1} ]", tool.displayName, tool.homePage));
|
||||
if (tool.statusDescription !== undefined) {
|
||||
console.warn(localize('deploymentDialog.DetailToolStatusDescription', "Additional status information for tool: {0}. {1}", tool.name, tool.statusDescription));
|
||||
console.warn(localize('deploymentDialog.DetailToolStatusDescription', "Additional status information for tool: '{0}' [ {1} ]. {2}", tool.name, tool.homePage, tool.statusDescription));
|
||||
}
|
||||
}
|
||||
return [tool.displayName, tool.description, tool.isInstalled ? localize('deploymentDialog.YesText', "Yes") : localize('deploymentDialog.NoText', "No"), tool.version ? tool.version.version : ''];
|
||||
|
||||
autoInstallRequired = tool.autoInstallRequired;
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || ''];
|
||||
});
|
||||
this._dialogObject.okButton.enabled = messages.length === 0;
|
||||
|
||||
this._installToolButton.hidden = !autoInstallRequired;
|
||||
this._dialogObject.okButton.enabled = messages.length === 0 && !autoInstallRequired;
|
||||
if (messages.length !== 0) {
|
||||
messages.push(localize('deploymentDialog.VersionInformationDebugHint', "You will need to restart Azure Data Studio if the tools are installed after Azure Data Studio is launched to pick up the updated PATH environment variable. You may find additional details in the debug console."));
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: localize('deploymentDialog.ToolCheckFailed', "Some required tools are not installed or do not meet the minimum version requirement."),
|
||||
text: localize('deploymentDialog.ToolCheckFailed', "Some required tools are not installed."),
|
||||
description: messages.join(EOL)
|
||||
};
|
||||
} else if (autoInstallRequired) {
|
||||
// we don't have scenarios that have mixed type of tools
|
||||
// either we don't support auto install: docker, or we support auto install for all required tools
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstallToolsHint', "Some required tools are not installed, you can click the \"{0}\" button to install them.", this._installToolButton.label)
|
||||
};
|
||||
}
|
||||
this._toolsLoadingComponent.loading = false;
|
||||
});
|
||||
@@ -263,4 +291,56 @@ export class ResourceTypePickerDialog extends DialogBase {
|
||||
protected onComplete(): void {
|
||||
this.resourceTypeService.startDeployment(this.getCurrentProvider());
|
||||
}
|
||||
|
||||
public updateToolsDisplayTableData(tool: ITool) {
|
||||
this._toolsTable.data = this._toolsTable.data.map(rowData => {
|
||||
if (rowData[0] === tool.displayName) {
|
||||
return [tool.displayName, tool.description, tool.displayStatus, tool.fullVersion || ''];
|
||||
} else {
|
||||
return rowData;
|
||||
}
|
||||
});
|
||||
this.enableUiControlsWhenNotInstalling(!tool.isInstalling); // if installing the disableContainers else enable them
|
||||
}
|
||||
|
||||
private enableUiControlsWhenNotInstalling(enabled: boolean): void {
|
||||
this._cardsContainer.enabled = enabled;
|
||||
this._agreementContainer.enabled = enabled;
|
||||
this._optionsContainer.enabled = enabled;
|
||||
this._dialogObject.cancelButton.enabled = enabled;
|
||||
// select and install tools button are controlled separately
|
||||
}
|
||||
|
||||
private async installTools(): Promise<void> {
|
||||
this._installToolButton.enabled = false;
|
||||
let i: number = 0;
|
||||
try {
|
||||
for (; i < this._tools.length; i++) {
|
||||
if (this._tools[i].needsInstallation) {
|
||||
// Update the informational message
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstallingTool', "Required tool '{0}' [ {1} ] is being installed now.", this._tools[i].displayName, this._tools[i].homePage)
|
||||
};
|
||||
await this._tools[i].install();
|
||||
}
|
||||
}
|
||||
// Update the informational message
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Information,
|
||||
text: localize('deploymentDialog.InstalledTools', "All required tools are installed now.")
|
||||
};
|
||||
this._dialogObject.okButton.enabled = true;
|
||||
} catch (error) {
|
||||
const errorMessage = this._tools[i].statusDescription || getErrorMessage(error);
|
||||
if (errorMessage) {
|
||||
// Let the tooltip status show the errorMessage just shown so that last status is visible even after showError dialogue has been dismissed.
|
||||
this._dialogObject.message = {
|
||||
level: azdata.window.MessageLevel.Error,
|
||||
text: errorMessage
|
||||
};
|
||||
}
|
||||
this._tools[i].showOutputChannel(/*preserverFocus*/false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,19 +68,19 @@ export abstract class WizardBase<T, M extends Model> {
|
||||
}
|
||||
|
||||
private dispose() {
|
||||
let errorOccured = false;
|
||||
let errorOccurred = false;
|
||||
this.toDispose.forEach((disposable: vscode.Disposable) => {
|
||||
try {
|
||||
disposable.dispose();
|
||||
}
|
||||
catch (error) {
|
||||
errorOccured = true;
|
||||
errorOccurred = true;
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
if (errorOccured) {
|
||||
vscode.window.showErrorMessage(localize('resourceDeployment.DisposableError', "Error occured while closing the wizard: {0}, open 'Debugger Console' for more information."), this.title);
|
||||
if (errorOccurred) {
|
||||
vscode.window.showErrorMessage(localize('resourceDeployment.DisposableError', "Error occurred while closing the wizard: {0}, open 'Debugger Console' for more information."), this.title);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function getErrorMessage(error: string | Error): string {
|
||||
return typeof error === 'string' ? error : error.message;
|
||||
export function getErrorMessage(error: any): string {
|
||||
return (error instanceof Error)
|
||||
? (typeof error.message === 'string' ? error.message : '')
|
||||
: typeof error === 'string' ? error : `${JSON.stringify(error, undefined, '\t')}`;
|
||||
}
|
||||
|
||||
export function getDateTimeString(): string {
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/runtime@^7.1.5":
|
||||
version "7.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.3.tgz#935122c74c73d2240cafd32ddb5fc2a6cd35cf1f"
|
||||
integrity sha512-kq6anf9JGjW8Nt5rYfEuGRaEAaH1mkv3Bbu6rYvLOpPh/RusSJXuKPEAoZ7L7gybZkchE8+NV5g9vKF4AGAtsA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@types/yamljs@0.2.30":
|
||||
version "0.2.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.30.tgz#d034e1d329e46e8d0f737c9a8db97f68f81b5382"
|
||||
@@ -506,6 +513,13 @@ postinstall-build@^5.0.1:
|
||||
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
|
||||
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
|
||||
|
||||
promisify-child-process@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/promisify-child-process/-/promisify-child-process-3.1.1.tgz#3a029c1d97bdb8bbcc8862c765b91f1cee0f2691"
|
||||
integrity sha512-683UHZEP4Bm75BvBujEe87AdE9lxnoWpcU5pEw4FG9HCSwwZC9pF7HUj3QmlDAvhyvulkWHLZs1lVRBNTvkbXQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.5"
|
||||
|
||||
psl@^1.1.24:
|
||||
version "1.1.31"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
|
||||
@@ -531,6 +545,11 @@ querystringify@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e"
|
||||
integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==
|
||||
|
||||
regenerator-runtime@^0.13.2:
|
||||
version "0.13.3"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
|
||||
integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==
|
||||
|
||||
request@^2.88.0:
|
||||
version "2.88.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||
@@ -617,6 +636,11 @@ strip-ansi@^4.0.0:
|
||||
dependencies:
|
||||
ansi-regex "^3.0.0"
|
||||
|
||||
sudo-prompt@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.0.0.tgz#eebedeee9fcd6f661324e6bb46335e3288e8dc8a"
|
||||
integrity sha512-kUn5fiOk0nhY2oKD9onIkcNCE4Zt85WTsvOfSmqCplmlEvXCcPOmp1npH5YWuf8Bmyy9wLWkIxx+D+8cThBORQ==
|
||||
|
||||
supports-color@4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e"
|
||||
|
||||
Reference in New Issue
Block a user