Added azcli extension only (#16415)

* Changed azdata to az in azcli extension and resource-deployment, and some arc. Removed user, pass, url from controller connect blade. Commented out tests. Ported over work from old branch.

* Changed unit tests, all unit tests passing. Changed parameters to new ones, fixed some Controller Connect issues.

* Connect data controller and create dc working.

* Changed az back to azdata in necessary places in resource-deployment.

* Changed notebook values and added namespace to some params.

* Reverted all changes that are not in azcli. Also deleted some unused variables in azcli constants.ts and some tests.

* Fixed package.json

* Deleted en-us from links, changed az. to azcli.arc in package.json

* Addressed PR comments.

Co-authored-by: Candice Ye <canye@microsoft.com>
This commit is contained in:
Candice Ye
2021-07-27 12:12:15 -07:00
committed by GitHub
parent 7c14ec2b6d
commit 35207a1e04
23 changed files with 654 additions and 2249 deletions

View File

@@ -1,12 +1,12 @@
# Microsoft Azure Data CLI Extension for Azure Data Studio
# Microsoft Azure CLI Extension for Azure Data Studio
Welcome to Microsoft Azure Data CLI Extension for Azure Data Studio!
Welcome to Microsoft Azure CLI Extension for Azure Data Studio!
**This extension is only applicable to customers in the Azure Arc data services public preview. Other usage is not supported at this time.**
## Overview
This extension adds support for the Azure Data CLI (azdata) within Azure Data Studio.
This extension adds support for the Azure CLI (az) within Azure Data Studio.
See https://docs.microsoft.com/cli/azure/?view=azure-cli-latest for more information on the tool.

View File

@@ -1,7 +1,7 @@
{
"name": "azcli",
"displayName": "%azdata.displayName%",
"description": "%azdata.description%",
"displayName": "%azcli.arc.displayName%",
"description": "%azcli.arc.description%",
"version": "0.1.0",
"publisher": "Microsoft",
"preview": true,
@@ -26,104 +26,19 @@
"configuration": [
{
"type": "object",
"title": "%azdata.config.title%",
"title": "%azcli.arc.config.title%",
"properties": {
"azcli.logDebugInfo": {
"type": "boolean",
"default": false,
"description": "%azdata.config.debug%"
},
"azcli.acceptEula": {
"type": "string",
"default": "prompt",
"enum": [
"dontPrompt",
"prompt"
],
"enumDescriptions": [
"%azdata.acceptEula.dontPrompt.description%",
"%azdata.acceptEula.prompt.description%"
],
"description": "%azdata.acceptEula.description%"
},
"azcli.install": {
"type": "string",
"default": "prompt",
"enum": [
"dontPrompt",
"prompt"
],
"enumDescriptions": [
"%azdata.install.dontPrompt.description%",
"%azdata.install.prompt.description%"
],
"description": "%azdata.install.description%"
},
"azcli.update": {
"type": "string",
"default": "prompt",
"enum": [
"dontPrompt",
"prompt"
],
"enumDescriptions": [
"%azdata.update.dontPrompt.description%",
"%azdata.update.prompt.description%"
],
"description": "%azdata.update.description%"
},
"azcli.requiredUpdate": {
"type": "string",
"default": "prompt",
"enum": [
"dontPrompt",
"prompt"
],
"enumDescriptions": [
"%azdata.update.dontPrompt.description%",
"%azdata.update.prompt.description%"
],
"description": "%azdata.requiredUpdate.description%"
"description": "%azcli.arc.config.debug%"
}
}
}
],
"commands": [
{
"command": "azcli.acceptEula",
"title": "%azdata.acceptEula.command.name%",
"category": "%command.category%"
},
{
"command": "azcli.install",
"title": "%azdata.install.command.name%",
"category": "%command.category%"
},
{
"command": "azcli.update",
"title": "%azdata.update.command.name%",
"category": "%command.category%"
}
],
"menus": {
"commandPalette": [
{
"command": "azcli.acceptEula",
"when": "!azcli.eulaAccepted"
},
{
"command": "azcli.install",
"when": "!azcli.found"
},
{
"command": "azcli.update",
"when": "azcli.found"
}
]
},
"resourceDeploymentOptionsSources": [
{
"id": "arc.controller.config.profiles"
"id": "azcli.arc.controller.config.profiles"
}
]
},

View File

@@ -1,25 +1,9 @@
{
"azdata.displayName": "Azure CLI",
"azdata.description": "Support for Azure CLI.",
"azdata.config.title": "Azure CLI Configuration",
"azdata.config.debug": "Log debug info to the output channel for all executed az commands",
"azcli.arc.displayName": "Azure CLI",
"azcli.arc.description": "Support for Azure CLI.",
"azcli.arc.config.title": "Azure CLI Configuration",
"azcli.arc.config.debug": "Log debug info to the output channel for all executed az commands",
"command.category": "Azure CLI",
"azdata.acceptEula.command.name": "Accept Eula",
"azdata.install.command.name": "Install",
"azdata.update.command.name": "Check for Update",
"azdata.category": "Azure CLI",
"azdata.acceptEula.description": "Choose how acceptance of EULA for the Azure CLI is done",
"azdata.acceptEula.prompt.description": "The user will be prompted for acceptance of EULA for the Azure CLI",
"azdata.acceptEula.dontPrompt.description": "The user will not be prompted for acceptance of EULA for the Azure CLI",
"azdata.install.description": "Choose how install of Azure CLI is done",
"azdata.install.prompt.description": "The user will be prompted for installation of the Azure CLI",
"azdata.install.dontPrompt.description": "The user will not be prompted for installation of the Azure CLI",
"azdata.update.description": "Choose whether you will be prompted when an update of the Azure CLI is available.",
"azdata.requiredUpdate.description": "Choose whether you will be prompted when a required update of the Azure CLI is available.",
"azdata.update.prompt.description": "The user will be prompted for update of the Azure CLI",
"azdata.update.dontPrompt.description": "The user will not be prompted for update of the Azure CLI"
"azcli.arc.category": "Azure CLI"
}

View File

@@ -3,67 +3,38 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as vscode from 'vscode';
import { IAzdataTool, isEulaAccepted, MIN_AZDATA_VERSION, promptForEula } from './azdata';
import * as azExt from 'az-ext';
import { IAzTool } from './az';
import Logger from './common/logger';
import { NoAzdataError } from './common/utils';
import * as constants from './constants';
import { NoAzureCLIError } from './common/utils';
import * as loc from './localizedConstants';
import { AzdataToolService } from './services/azdataToolService';
import { AzToolService } from './services/azToolService';
/**
* Validates that :
* - Azdata is installed
* - The Azdata version is >= the minimum required version
* - The Azdata CLI has been accepted
* @param azdata The azdata tool to check
* @param eulaAccepted Whether the Azdata CLI EULA has been accepted
* - Az is installed
* @param az The az tool to check
*/
async function validateAzdata(azdata: IAzdataTool | undefined, eulaAccepted: boolean): Promise<void> {
throwIfNoAzdataOrEulaNotAccepted(azdata, eulaAccepted);
await throwIfRequiredVersionMissing(azdata);
export function validateAz(az: IAzTool | undefined) {
throwIfNoAz(az);
}
export function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata {
throwIfNoAzdata(azdata);
if (!eulaAccepted) {
Logger.log(loc.eulaNotAccepted);
throw new Error(loc.eulaNotAccepted);
export function throwIfNoAz(localAz: IAzTool | undefined): asserts localAz {
if (!localAz) {
Logger.log(loc.noAzureCLI);
throw new NoAzureCLIError();
}
}
export async function throwIfRequiredVersionMissing(azdata: IAzdataTool): Promise<void> {
const currentVersion = await azdata.getSemVersion();
if (currentVersion.compare(MIN_AZDATA_VERSION) < 0) {
throw new Error(loc.missingRequiredVersion(MIN_AZDATA_VERSION.raw));
}
}
export function throwIfNoAzdata(localAzdata: IAzdataTool | undefined): asserts localAzdata {
if (!localAzdata) {
Logger.log(loc.noAzdata);
throw new NoAzdataError();
}
}
export function getExtensionApi(memento: vscode.Memento, azdataToolService: AzdataToolService, localAzdataDiscovered: Promise<IAzdataTool | undefined>): azdataExt.IExtension {
export function getExtensionApi(azToolService: AzToolService): azExt.IExtension {
return {
isEulaAccepted: async () => {
throwIfNoAzdata(await localAzdataDiscovered); // ensure that we have discovered Azdata
return !!memento.get<boolean>(constants.eulaAccepted);
},
promptForEula: async (requireUserAction: boolean = true): Promise<boolean> => {
await localAzdataDiscovered;
return promptForEula(memento, true /* userRequested */, requireUserAction);
},
azdata: getAzdataApi(localAzdataDiscovered, azdataToolService, memento)
az: getAzApi(azToolService)
};
}
export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefined>, azdataToolService: AzdataToolService, memento: vscode.Memento): azdataExt.IAzdataApi {
export function getAzApi(azToolService: AzToolService): azExt.IAzApi {
return {
arc: {
arcdata: {
dc: {
create: async (
namespace: string,
@@ -74,129 +45,108 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
subscription: string,
profileName?: string,
storageClass?: string,
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars, azdataContext);
additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.arcdata.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass, additionalEnvVars);
},
endpoint: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.endpoint.list(additionalEnvVars, azdataContext);
list: async (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.arcdata.dc.endpoint.list(namespace, additionalEnvVars);
}
},
config: {
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.list(additionalEnvVars, azdataContext);
list: async (additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.arcdata.dc.config.list(additionalEnvVars);
},
show: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.dc.config.show(additionalEnvVars, azdataContext);
}
}
},
postgres: {
server: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.delete(name, additionalEnvVars, azdataContext);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.list(additionalEnvVars, azdataContext);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.show(name, additionalEnvVars, azdataContext);
},
edit: async (
name: string,
args: {
adminPassword?: boolean;
coresLimit?: string;
coresRequest?: string;
coordinatorEngineSettings?: string;
engineSettings?: string;
extensions?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
port?: number;
replaceEngineSettings?: boolean;
workerEngineSettings?: string;
workers?: number;
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars, azdataContext);
}
}
},
sql: {
mi: {
delete: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.delete(name, additionalEnvVars, azdataContext);
},
list: async (additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.list(additionalEnvVars, azdataContext);
},
show: async (name: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.show(name, additionalEnvVars, azdataContext);
},
edit: async (
name: string,
args: {
coresLimit?: string;
coresRequest?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
},
additionalEnvVars?: azdataExt.AdditionalEnvVars,
azdataContext?: string
) => {
await localAzdataDiscovered;
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.arc.sql.mi.edit(name, args, additionalEnvVars, azdataContext);
show: async (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.arcdata.dc.config.show(namespace, additionalEnvVars);
}
}
}
},
getPath: async () => {
await localAzdataDiscovered;
throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getPath();
postgres: {
arcserver: {
delete: async (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.postgres.arcserver.delete(name, namespace, additionalEnvVars);
},
list: async (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.postgres.arcserver.list(namespace, additionalEnvVars);
},
show: async (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.postgres.arcserver.show(name, namespace, additionalEnvVars);
},
edit: async (
name: string,
args: {
adminPassword?: boolean;
coresLimit?: string;
coresRequest?: string;
coordinatorEngineSettings?: string;
engineSettings?: string;
extensions?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
port?: number;
replaceEngineSettings?: boolean;
workerEngineSettings?: string;
workers?: number;
},
namespace: string,
additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.postgres.arcserver.edit(name, args, namespace, additionalEnvVars);
}
}
},
login: async (endpointOrNamespace: azdataExt.EndpointOrNamespace, username: string, password: string, additionalEnvVars?: azdataExt.AdditionalEnvVars, azdataContext?: string) => {
await validateAzdata(azdataToolService.localAzdata, isEulaAccepted(memento));
return azdataToolService.localAzdata!.login(endpointOrNamespace, username, password, additionalEnvVars, azdataContext);
sql: {
miarc: {
delete: async (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.sql.miarc.delete(name, namespace, additionalEnvVars);
},
list: async (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.sql.miarc.list(namespace, additionalEnvVars);
},
show: async (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.sql.miarc.show(name, namespace, additionalEnvVars);
},
edit: async (
name: string,
args: {
coresLimit?: string;
coresRequest?: string;
memoryLimit?: string;
memoryRequest?: string;
noWait?: boolean;
},
namespace: string,
additionalEnvVars?: azExt.AdditionalEnvVars
) => {
validateAz(azToolService.localAz);
return azToolService.localAz!.sql.miarc.edit(name, args, namespace, additionalEnvVars);
}
}
},
getPath: async () => {
throwIfNoAz(azToolService.localAz);
return azToolService.localAz.getPath();
},
getSemVersion: async () => {
await localAzdataDiscovered;
throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.getSemVersion();
throwIfNoAz(azToolService.localAz);
return azToolService.localAz.getSemVersion();
},
version: async () => {
await localAzdataDiscovered;
throwIfNoAzdata(azdataToolService.localAzdata);
return azdataToolService.localAzdata.version();
throwIfNoAz(azToolService.localAz);
return azToolService.localAz.version();
}
};
}

301
extensions/azcli/src/az.ts Normal file
View File

@@ -0,0 +1,301 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azExt from 'az-ext';
import * as os from 'os';
import { SemVer } from 'semver';
import * as vscode from 'vscode';
import { executeCommand, ProcessOutput } from './common/childProcess';
import Logger from './common/logger';
import { AzureCLIArcExtError, searchForCmd } from './common/utils';
import { azConfigSection, debugConfigKey, latestAzArcExtensionVersion } from './constants';
import * as loc from './localizedConstants';
/**
* The latest Az CLI arcdata extension version for this extension to function properly
*/
export const LATEST_AZ_ARC_EXTENSION_VERSION = new SemVer(latestAzArcExtensionVersion);
/**
* Interface for an object to interact with the az tool installed on the box.
*/
export interface IAzTool extends azExt.IAzApi {
/**
* Executes az with the specified arguments (e.g. --version) and returns the result
* @param args The args to pass to az
* @param parseResult A function used to parse out the raw result into the desired shape
*/
executeCommand<R>(args: string[], additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<R>>
}
/**
* An object to interact with the az tool installed on the box.
*/
export class AzTool implements azExt.IAzApi {
private _semVersion: SemVer;
constructor(private _path: string, version: string) {
this._semVersion = new SemVer(version);
}
/**
* The semVersion corresponding to this installation of az. version() method should have been run
* before fetching this value to ensure that correct value is returned. This is almost always correct unless
* Az has gotten reinstalled in the background after this IAzApi object was constructed.
*/
public async getSemVersion(): Promise<SemVer> {
return this._semVersion;
}
/**
* gets the path where az tool is installed
*/
public async getPath(): Promise<string> {
return this._path;
}
public arcdata = {
dc: {
create: (
namespace: string,
name: string,
connectivityMode: string,
resourceGroup: string,
location: string,
subscription: string,
profileName?: string,
storageClass?: string,
additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
const args = ['arcdata', 'dc', 'create',
'--k8s-namespace', namespace,
'--name', name,
'--connectivity-mode', connectivityMode,
'--resource-group', resourceGroup,
'--location', location,
'--subscription', subscription];
if (profileName) {
args.push('--profile-name', profileName);
}
if (storageClass) {
args.push('--storage-class', storageClass);
}
return this.executeCommand<void>(args, additionalEnvVars);
},
endpoint: {
list: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcEndpointListResult[]>> => {
return this.executeCommand<azExt.DcEndpointListResult[]>(['arcdata', 'dc', 'endpoint', 'list', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
}
},
config: {
list: (additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcConfigListResult[]>> => {
return this.executeCommand<azExt.DcConfigListResult[]>(['arcdata', 'dc', 'config', 'list'], additionalEnvVars);
},
show: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.DcConfigShowResult>> => {
return this.executeCommand<azExt.DcConfigShowResult>(['arcdata', 'dc', 'config', 'show', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
}
}
}
};
public postgres = {
arcserver: {
delete: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
return this.executeCommand<void>(['postgres', 'arc-server', 'delete', '-n', name, '--k8s-namespace', namespace, '--force', '--use-k8s'], additionalEnvVars);
},
list: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.PostgresServerListResult[]>> => {
return this.executeCommand<azExt.PostgresServerListResult[]>(['postgres', 'arc-server', 'list', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
},
show: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.PostgresServerShowResult>> => {
return this.executeCommand<azExt.PostgresServerShowResult>(['postgres', 'arc-server', 'show', '-n', name, '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
},
edit: (
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
coordinatorEngineSettings?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workerEngineSettings?: string,
workers?: number
},
namespace: string,
additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
const argsArray = ['postgres', 'arc-server', 'edit', '-n', name, '--k8s-namespace', namespace, '--use-k8s'];
if (args.adminPassword) { argsArray.push('--admin-password'); }
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.coordinatorEngineSettings) { argsArray.push('--coordinator-engine-settings', args.coordinatorEngineSettings); }
if (args.engineSettings) { argsArray.push('--engine-settings', args.engineSettings); }
if (args.extensions) { argsArray.push('--extensions', args.extensions); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
if (args.port) { argsArray.push('--port', args.port.toString()); }
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
if (args.workerEngineSettings) { argsArray.push('--worker-engine-settings', args.workerEngineSettings); }
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
};
public sql = {
miarc: {
delete: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<void>> => {
return this.executeCommand<void>(['sql', 'mi-arc', 'delete', '-n', name, '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
},
list: (namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.SqlMiListResult[]>> => {
return this.executeCommand<azExt.SqlMiListResult[]>(['sql', 'mi-arc', 'list', '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
},
show: (name: string, namespace: string, additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<azExt.SqlMiShowResult>> => {
return this.executeCommand<azExt.SqlMiShowResult>(['sql', 'mi-arc', 'show', '-n', name, '--k8s-namespace', namespace, '--use-k8s'], additionalEnvVars);
},
edit: (
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
},
namespace: string,
additionalEnvVars?: azExt.AdditionalEnvVars
): Promise<azExt.AzOutput<void>> => {
const argsArray = ['sql', 'mi-arc', 'edit', '-n', name, '--k8s-namespace', namespace, '--use-k8s'];
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
if (args.noWait) { argsArray.push('--no-wait'); }
return this.executeCommand<void>(argsArray, additionalEnvVars);
}
}
};
/**
* Gets the output of running '--version' command on the az tool.
* It also updates the cachedVersion property based on the return value from the tool.
*/
public async version(): Promise<azExt.AzOutput<string>> {
const output = await executeAzCommand(`"${this._path}"`, ['--version']);
this._semVersion = new SemVer(parseVersion(output.stdout));
return {
stdout: output.stdout
// stderr: output.stderr.split(os.EOL)
// result: output.stdout
};
}
/**
* Executes the specified az command.
* @param args The args to pass to az
* @param additionalEnvVars Additional environment variables to set for this execution
*/
public async executeCommand<R>(args: string[], additionalEnvVars?: azExt.AdditionalEnvVars): Promise<azExt.AzOutput<R>> {
try {
const result = await executeAzCommand(`"${this._path}"`, args.concat(['--output', 'json']), additionalEnvVars);
const output = JSON.parse(result.stdout);
return {
stdout: <R>output
};
} catch (err) {
throw err;
}
}
}
/**
* Finds the existing installation of az, or throws an error if it couldn't find it
* or encountered an unexpected error.
* The promise is rejected when Az is not found.
*/
export async function findAz(): Promise<IAzTool> {
Logger.log(loc.searchingForAz);
try {
const az = await findSpecificAz();
Logger.log(loc.foundExistingAz(await az.getPath(), (await az.getSemVersion()).raw));
return az;
} catch (err) {
Logger.log(loc.noAzureCLI);
throw err;
}
}
/**
* Parses out the Azure CLI version from the raw az version output
* @param raw The raw version output from az --version
*/
function parseVersion(raw: string): string {
// Currently the version is a multi-line string that contains other version information such
// as the Python installation, with the first line holding the version of az itself.
//
// The output of az --version looks like:
// azure-cli 2.26.1
// ...
const start = raw.search('azure-cli');
const end = raw.search('core');
raw = raw.slice(start, end).replace('azure-cli', '');
return raw.trim();
}
/**
* Parses out the arcdata extension version from the raw az version output
* @param raw The raw version output from az --version
*/
function parseArcExtensionVersion(raw: string): string {
// Currently the version is a multi-line string that contains other version information such
// as the Python installation and any extensions.
//
// The output of az --version looks like:
// azure-cli 2.26.1
// ...
// Extensions:
// arcdata 1.0.0
// connectedk8s 1.1.5
// ...
const start = raw.search('arcdata');
if (start === -1) {
vscode.window.showErrorMessage(loc.arcdataExtensionNotInstalled);
throw new AzureCLIArcExtError();
} else {
raw = raw.slice(start + 7);
raw = raw.split(os.EOL)[0].trim();
}
return raw.trim();
}
async function executeAzCommand(command: string, args: string[], additionalEnvVars: azExt.AdditionalEnvVars = {}): Promise<ProcessOutput> {
const debug = vscode.workspace.getConfiguration(azConfigSection).get(debugConfigKey);
if (debug) {
args.push('--debug');
}
return executeCommand(command, args, additionalEnvVars);
}
/**
*/
async function findSpecificAz(): Promise<IAzTool> {
const path = await ((process.platform === 'win32') ? searchForCmd('az.cmd') : searchForCmd('az'));
const versionOutput = await executeAzCommand(`"${path}"`, ['--version']);
const version = parseArcExtensionVersion(versionOutput.stdout);
const semVersion = new SemVer(version);
if (LATEST_AZ_ARC_EXTENSION_VERSION.compare(semVersion) === 1) {
// If there is a greater version of az arc extension available, prompt to update
vscode.window.showErrorMessage(loc.requiredArcDataVersionNotAvailable(latestAzArcExtensionVersion, version));
} else if (LATEST_AZ_ARC_EXTENSION_VERSION.compare(semVersion) === -1) {
// Current version should not be greater than latest version
vscode.window.showErrorMessage(loc.unsupportedArcDataVersion(latestAzArcExtensionVersion, version));
}
return new AzTool(path, version);
}

View File

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

View File

@@ -1,76 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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"
}
export 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

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { AdditionalEnvVars } from 'azdata-ext';
import { AdditionalEnvVars } from 'az-ext';
import * as cp from 'child_process';
import * as sudo from 'sudo-prompt';
import * as loc from '../localizedConstants';

View File

@@ -10,7 +10,7 @@ export class Log {
private _output: vscode.OutputChannel;
constructor() {
this._output = vscode.window.createOutputChannel(loc.azdata);
this._output = vscode.window.createOutputChannel(loc.az);
}
log(msg: string): void {

View File

@@ -3,19 +3,30 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as which from 'which';
import * as loc from '../localizedConstants';
export class NoAzdataError extends Error implements azdataExt.ErrorWithLink {
export class NoAzureCLIError extends Error implements azExt.ErrorWithLink {
constructor() {
super(loc.noAzdata);
super(loc.noAzureCLI);
}
public get messageWithLink(): string {
return loc.noAzdataWithLink;
return loc.noAzureCLI;
}
}
export class AzureCLIArcExtError extends Error implements azExt.ErrorWithLink {
constructor() {
super(loc.arcdataExtensionNotInstalled);
}
public get messageWithLink(): string {
return loc.arcdataExtensionNotInstalled;
}
}
/**
* Searches for the first instance of the specified executable in the PATH environment variable
* @param exe The executable to search for

View File

@@ -4,21 +4,12 @@
*--------------------------------------------------------------------------------------------*/
// config setting keys
export const azdataConfigSection: string = 'azcli';
export const azdataAcceptEulaKey: string = 'acceptEula';
export const azdataInstallKey: string = 'install';
export const azdataUpdateKey: string = 'update';
export const azdatarequiredUpdateKey: string = 'requiredUpdate';
export const azConfigSection: string = 'azcli';
export const debugConfigKey = 'logDebugInfo';
// context keys && memento keys
export const eulaAccepted = 'azcli.eulaAccepted';
export const azdataFound = 'azcli.found';
export const azFound = 'az.found';
// other constants
export const azdataHostname = 'https://aka.ms';
export const azdataUri = 'azdata-msi';
export const azdataReleaseJson = 'azdata/release.json';
export const microsoftPrivacyStatementUrl = 'https://privacy.microsoft.com/privacystatement';
export const eulaUrl = 'https://aka.ms/eula-azdata-en';
export const latestAzArcExtensionVersion = '1.0.0';

View File

@@ -3,44 +3,26 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
import * as rd from 'resource-deployment';
import * as vscode from 'vscode';
import { getExtensionApi } from './api';
import { checkAndInstallAzdata, checkAndUpdateAzdata, findAzdata, isEulaAccepted, promptForEula } from './azdata';
import Logger from './common/logger';
import * as constants from './constants';
import * as loc from './localizedConstants';
import { AzdataToolService } from './services/azdataToolService';
import { findAz } from './az';
import { ArcControllerConfigProfilesOptionsSource } from './providers/arcControllerConfigProfilesOptionsSource';
import { AzToolService } from './services/azToolService';
export async function activate(context: vscode.ExtensionContext): Promise<azdataExt.IExtension> {
const azdataToolService = new AzdataToolService();
let eulaAccepted: boolean = false;
vscode.commands.registerCommand('azcli.acceptEula', async () => {
await promptForEula(context.globalState, true /* userRequested */);
});
export async function activate(context: vscode.ExtensionContext): Promise<azExt.IExtension> {
const azToolService = new AzToolService();
vscode.commands.registerCommand('azcli.install', async () => {
azdataToolService.localAzdata = await checkAndInstallAzdata(true /* userRequested */);
});
azToolService.localAz = await findAz();
vscode.commands.registerCommand('azcli.update', async () => {
if (await checkAndUpdateAzdata(azdataToolService.localAzdata, true /* userRequested */)) { // if an update was performed
azdataToolService.localAzdata = await findAzdata(); // find and save the currently installed 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));
const azdataApi = getExtensionApi(context.globalState, azdataToolService, Promise.resolve(undefined));
const azApi = getExtensionApi(azToolService);
// register option source(s)
// TODO: Uncomment this once azdata extension is removed
// const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
// context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi)));
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azApi)));
return azdataApi;
return azApi;
}
export function deactivate(): void { }

View File

@@ -5,69 +5,28 @@
import * as nls from 'vscode-nls';
import { getErrorMessage } from './common/utils';
import { azdataConfigSection, azdataInstallKey, azdataUpdateKey, azdatarequiredUpdateKey } 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 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");
export const no = localize('azdata.no', "No");
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, 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 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 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 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 promptForRequiredAzdataUpdate = (requiredVersion: string, latestVersion: string): string => localize('azdata.promptForRequiredAzdataUpdate', "This extension requires Azure Data CLI >= {0} to be installed, do you wish to update to the latest version ({1}) now? If you do not then some functionality may not work.", requiredVersion, latestVersion);
export const requiredVersionNotAvailable = (requiredVersion: string, currentVersion: string): string => localize('azdata.requiredVersionNotAvailable', "This extension requires Azure Data CLI >= {0} to be installed, but the current version available is only {1}. Install the correct version manually from [here](https://docs.microsoft.com/sql/azdata/install/deploy-install-azdata) and then restart Azure Data Studio.", requiredVersion, currentVersion);
export const promptForAzdataUpdateLog = (version: string): string => promptLog(promptForAzdataUpdate(version));
export const promptForRequiredAzdataUpdateLog = (requiredVersion: string, latestVersion: string): string => promptLog(promptForRequiredAzdataUpdate(requiredVersion, latestVersion));
export const missingRequiredVersion = (requiredVersion: string): string => localize('azdata.missingRequiredVersion', "Azure Data CLI >= {0} is required for this feature. Run the 'Azure Data CLI: Check for Update' command to install this and then try again.", requiredVersion);
export const downloadError = localize('azdata.downloadError', "Error while downloading");
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, 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 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 skipRequiredUpdate = (config: string): string => localize('azdata.skipRequiredUpdate', "Skipping required update of Azure Data CLI, since the operation was not user requested and config option: {0}.{1} is {2}", azdataConfigSection, azdatarequiredUpdateKey, 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 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 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 endpointOrNamespaceRequired = localize('azdata.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");
export const az = localize('az.az', "Azure CLI");
export const searchingForAz = localize('az.searchingForAz', "Searching for existing Azure CLI installation...");
export const foundExistingAz = (path: string, version: string): string => localize('az.foundExistingAz', "Found existing Azure CLI installation of version (v{0}) at path:{1}", version, path);
export const downloadingProgressMb = (currentMb: string, totalMb: string): string => localize('az.downloadingProgressMb', "Downloading ({0} / {1} MB)", currentMb, totalMb);
export const downloadFinished = localize('az.downloadFinished', "Download finished");
export const downloadingTo = (name: string, url: string, location: string): string => localize('az.downloadingTo', "Downloading {0} from {1} to {2}", name, url, location);
export const executingCommand = (command: string, args: string[]): string => localize('az.executingCommand', "Executing command: '{0} {1}'", command, args?.join(' '));
export const stdoutOutput = (stdout: string): string => localize('az.stdoutOutput', "stdout: {0}", stdout);
export const stderrOutput = (stderr: string): string => localize('az.stderrOutput', "stderr: {0}", stderr);
export const gettingTextContentsOfUrl = (url: string): string => localize('az.gettingTextContentsOfUrl', "Getting text contents of resource at URL {0}", url);
export const promptLog = (logEntry: string) => localize('az.promptLog', "Prompting the user to accept the following: {0}", logEntry);
export const downloadError = localize('az.downloadError', "Error while downloading");
export const platformUnsupported = (platform: string): string => localize('az.platformUnsupported', "Platform '{0}' is currently unsupported", platform);
export const unexpectedCommandError = (errMsg: string): string => localize('az.unexpectedCommandError', "Unexpected error executing command: {0}", errMsg);
export const unexpectedExitCode = (code: number, err: string): string => localize('az.unexpectedExitCode', "Unexpected exit code from command: {1} ({0})", code, err);
export const noReleaseVersion = (platform: string, releaseInfo: string): string => localize('az.noReleaseVersion', "No release version available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const noDownloadLink = (platform: string, releaseInfo: string): string => localize('az.noDownloadLink', "No download link available for platform '{0}'\nRelease info: ${1}", platform, releaseInfo);
export const failedToParseReleaseInfo = (url: string, fileContents: string, err: any): string => localize('az.failedToParseReleaseInfo', "Failed to parse the JSON of contents at: {0}.\nFile contents:\n{1}\nError: {2}", url, fileContents, getErrorMessage(err));
export const endpointOrNamespaceRequired = localize('az.endpointOrNamespaceRequired', "Either an endpoint or a namespace must be specified");
export const arcdataExtensionNotInstalled = localize('az.arcdataExtensionNotInstalled', "This extension requires the Azure CLI extension 'arcdata' to be installed. Install the latest version manually from [here](https://docs.microsoft.com/cli/azure/azure-cli-extensions-overview) and then restart Azure Data Studio.");
export const noAzureCLI = localize('az.noAzureCLI', "No Azure CLI is available. Install the latest version manually from [here](https://docs.microsoft.com/cli/azure/install-azure-cli) and then restart Azure Data Studio.");
export const requiredArcDataVersionNotAvailable = (requiredVersion: string, currentVersion: string): string => localize('az.requiredVersionNotAvailable', "This extension requires the Azure CLI extension 'arcdata' version >= {0} to be installed, but the current version available is only {1}. Install the correct version manually from [here](https://docs.microsoft.com/cli/azure/azure-cli-extensions-overview) and then restart Azure Data Studio.", requiredVersion, currentVersion);
export const unsupportedArcDataVersion = (requiredVersion: string, currentVersion: string): string => localize('az.unsupportedArcDataVersion', "Your downloaded version {1} of the Azure CLI extension 'arcdata' is not yet supported. The latest version is is {0}. Install the correct version manually from [here](https://docs.microsoft.com/cli/azure/azure-cli-extensions-overview) and then restart Azure Data Studio.", requiredVersion, currentVersion);

View File

@@ -4,19 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import * as rd from 'resource-deployment';
import * as azdataExt from 'azdata-ext';
import * as azExt from 'az-ext';
/**
* Class that provides options sources for an Arc Data Controller
*/
export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider {
readonly id = 'arc.controller.config.profiles';
constructor(private _azdataExtApi: azdataExt.IExtension) { }
readonly id = 'azcli.arc.controller.config.profiles';
constructor(private _azExtApi: azExt.IExtension) { }
async getOptions(): Promise<string[]> {
const isEulaAccepted = await this._azdataExtApi.isEulaAccepted();
if (!isEulaAccepted) { // if eula has not yet be accepted then give user a chance to accept it
await this._azdataExtApi.promptForEula();
}
return (await this._azdataExtApi.azdata.arc.dc.config.list()).result;
return (await this._azExtApi.az.arcdata.dc.config.list()).stdout;
}
}

View File

@@ -3,25 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAzdataTool } from '../azdata';
import { IAzTool } from '../az';
export class AzdataToolService {
private _localAzdata: IAzdataTool | undefined;
export class AzToolService {
private _localAz: IAzTool | undefined;
constructor() {
}
/**
* Gets the localAzdata that was last saved
* Gets the localAz that was last saved
*/
get localAzdata(): IAzdataTool | undefined {
return this._localAzdata;
get localAz(): IAzTool | undefined {
return this._localAz;
}
/**
* Sets the localAzdata object to be used for azdata operations
* Sets the localAz object to be used for az operations
*/
set localAzdata(azdata: IAzdataTool | undefined) {
this._localAzdata = azdata;
set localAz(az: IAzTool | undefined) {
this._localAz = az;
}
}

View File

@@ -1,121 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdataExt from 'azdata-ext';
import * as childProcess from '../common/childProcess';
import * as sinon from 'sinon';
import * as should from 'should';
import * as vscode from 'vscode';
import * as TypeMoq from 'typemoq';
import { getExtensionApi, throwIfNoAzdataOrEulaNotAccepted } from '../api';
import { AzdataToolService } from '../services/azdataToolService';
import { assertRejected } from './testUtils';
import { AzdataTool, IAzdataTool, AzdataDeployOption } from '../azdata';
describe('api', function (): void {
afterEach(function (): void {
sinon.restore();
});
describe('throwIfNoAzdataOrEulaNotAccepted', function (): void {
it('throws when no azdata', function (): void {
should(() => throwIfNoAzdataOrEulaNotAccepted(undefined, false)).throw();
});
it('throws when EULA not accepted', function (): void {
should(() => throwIfNoAzdataOrEulaNotAccepted(TypeMoq.Mock.ofType<IAzdataTool>().object, false)).throw();
});
it('passes with AzdataTool and EULA accepted', function (): void {
throwIfNoAzdataOrEulaNotAccepted(TypeMoq.Mock.ofType<IAzdataTool>().object, true);
});
});
describe('getExtensionApi', function (): void {
it('throws when no azdata', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const azdataToolService = new AzdataToolService();
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(undefined));
await assertRejected(api.isEulaAccepted(), 'isEulaAccepted');
await assertApiCalls(api, assertRejected);
});
it('throws when EULA not accepted', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => false);
const azdataToolService = new AzdataToolService();
// Not using a mock here because it'll hang when resolving mocked objects
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(new AzdataTool('', '1.0.0')));
should(await api.isEulaAccepted()).be.false('EULA should not be accepted');
await assertApiCalls(api, assertRejected);
});
it('succeed when azdata present and EULA accepted', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
const azdataTool = new AzdataTool('', '99.0.0');
const azdataToolService = new AzdataToolService();
azdataToolService.localAzdata = azdataTool;
// Not using a mock here because it'll hang when resolving mocked objects
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(azdataTool));
should(await api.isEulaAccepted()).be.true('EULA should be accepted');
sinon.stub(childProcess, 'executeCommand').callsFake(async (_command, args) => {
// Version needs to be valid so it can be parsed correctly
if (args[0] === '--version') {
return { stdout: `99.0.0`, stderr: '' };
}
console.log(args[0]);
return { stdout: `{ }`, stderr: '' };
});
await assertApiCalls(api, async (promise, message) => {
try {
await promise;
} catch (err) {
throw new Error(`API call to ${message} should have succeeded. ${err}`);
}
});
});
it('promptForEula', async function (): Promise<void> {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
const azdataToolService = new AzdataToolService();
// Not using a mock here because it'll hang when resolving mocked objects
const api = getExtensionApi(mementoMock.object, azdataToolService, Promise.resolve(new AzdataTool('', '1.0.0')));
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => AzdataDeployOption.dontPrompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const showErrorMessageStub = sinon.stub(vscode.window, 'showErrorMessage');
should(await api.promptForEula()).be.false();
should(showErrorMessageStub.called).be.true('User should have been prompted to accept');
});
/**
* Asserts that calls to the Azdata API behave as expected
* @param api The API object to test the calls with
* @param assertCallback The function to assert that the results are as expected
*/
async function assertApiCalls(api: azdataExt.IExtension, assertCallback: (promise: Promise<any>, message: string) => Promise<void>): Promise<void> {
await assertCallback(api.azdata.getPath(), 'getPath');
await assertCallback(api.azdata.getSemVersion(), 'getSemVersion');
await assertCallback(api.azdata.login({ endpoint: 'https://127.0.0.1' }, '', ''), 'login');
await assertCallback(api.azdata.login({ namespace: 'namespace' }, '', ''), 'login');
await assertCallback(api.azdata.version(), 'version');
await assertCallback(api.azdata.arc.dc.create('', '', '', '', '', ''), 'arc dc create');
await assertCallback(api.azdata.arc.dc.config.list(), 'arc dc config list');
await assertCallback(api.azdata.arc.dc.config.show(), 'arc dc config show');
await assertCallback(api.azdata.arc.dc.endpoint.list(), 'arc dc endpoint list');
await assertCallback(api.azdata.arc.sql.mi.list(), 'arc sql mi list');
await assertCallback(api.azdata.arc.sql.mi.delete(''), 'arc sql mi delete');
await assertCallback(api.azdata.arc.sql.mi.show(''), 'arc sql mi show');
await assertCallback(api.azdata.arc.sql.mi.edit('', {}), 'arc sql mi edit');
await assertCallback(api.azdata.arc.postgres.server.list(), 'arc sql postgres server list');
await assertCallback(api.azdata.arc.postgres.server.delete(''), 'arc sql postgres server delete');
await assertCallback(api.azdata.arc.postgres.server.show(''), 'arc sql postgres server show');
await assertCallback(api.azdata.arc.postgres.server.edit('', {}), 'arc sql postgres server edit');
}
});
});

View File

@@ -5,65 +5,35 @@
import * as should from 'should';
import * as sinon from 'sinon';
import * as vscode from 'vscode';
import * as azdata from '../azdata';
import * as childProcess from '../common/childProcess';
import { HttpClient } from '../common/httpClient';
import * as utils from '../common/utils';
import * as loc from '../localizedConstants';
import * as os from 'os';
import * as fs from 'fs';
import { AzdataReleaseInfo } from '../azdataReleaseInfo';
import * as TypeMoq from 'typemoq';
import { eulaAccepted } from '../constants';
import * as azdata from '../az';
const oldAzdataMock = new azdata.AzdataTool('/path/to/azdata', azdata.MIN_AZDATA_VERSION.raw);
const currentAzdataMock = new azdata.AzdataTool('/path/to/azdata', '9999.999.999');
/**
* 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: AzdataReleaseInfo = {
win32: {
'version': '9999.999.999',
'link': 'https://download.com/azdata-20.0.1.msi'
},
darwin: {
'version': '9999.999.999'
},
linux: {
'version': '9999.999.999'
}
};
let executeSudoCommandStub: sinon.SinonStub;
describe('azdata', function () {
describe('az', function () {
afterEach(function (): void {
sinon.restore();
});
describe('azdataTool', function (): void {
const azdataTool = new azdata.AzdataTool(os.tmpdir(), '1.0.0');
describe('azTool', function (): void {
const azTool = new azdata.AzTool('C:/Program Files (x86)/Microsoft SDKs/Azure/CLI2/wbin/az.cmd', '2.26.0');
let executeCommandStub: sinon.SinonStub;
const namespace = 'myNamespace';
const name = 'myName';
const connectivityMode = 'myConnectivityMode';
const resourceGroup = 'myResourceGroup';
const location = 'myLocation';
const subscription = 'mySubscription';
const namespace = 'arc4';
const name = 'cy-dc-4';
const connectivityMode = 'direct';
const resourceGroup = 'canye-rg-2';
const location = 'eastus2euap';
const subscription = 'a5082b19-8a6e-4bc5-8fdd-8ef39dfebc39';
const profileName = 'myProfileName';
const storageClass = 'myStorageClass';
const storageClass = 'local-storage';
beforeEach(function (): void {
executeCommandStub = sinon.stub(childProcess, 'executeCommand').resolves({ stdout: '{}', stderr: '' });
});
describe('arc', function (): void {
describe('arcdata', function (): void {
describe('dc', function (): void {
it('create', async function (): Promise<void> {
await azdataTool.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
await azTool.arcdata.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass);
verifyExecuteCommandCalledWithArgs([
'arc', 'dc', 'create',
'arcdata', 'dc', 'create',
namespace,
name,
connectivityMode,
@@ -75,158 +45,107 @@ describe('azdata', function () {
});
describe('endpoint', async function (): Promise<void> {
it('list', async function (): Promise<void> {
await azdataTool.arc.dc.endpoint.list();
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'endpoint', 'list']);
await azTool.arcdata.dc.endpoint.list(namespace);
verifyExecuteCommandCalledWithArgs(['arcdata', 'dc', 'endpoint', 'list', '--k8s-namespace', namespace, '--use-k8s']);
});
});
describe('config', async function (): Promise<void> {
it('list', async function (): Promise<void> {
await azdataTool.arc.dc.config.list();
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'list']);
await azTool.arcdata.dc.config.list();
verifyExecuteCommandCalledWithArgs(['arcdata', 'dc', 'config', 'list']);
});
it('show', async function (): Promise<void> {
await azdataTool.arc.dc.config.show();
verifyExecuteCommandCalledWithArgs(['arc', 'dc', 'config', 'show']);
await azTool.arcdata.dc.config.show(namespace);
verifyExecuteCommandCalledWithArgs(['arcdata', 'dc', 'config', 'show', '--k8s-namespace', namespace, '--use-k8s']);
});
});
});
describe('postgres', function (): void {
describe('server', function (): void {
it('delete', async function (): Promise<void> {
await azdataTool.arc.postgres.server.delete(name);
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'delete', name]);
});
it('list', async function (): Promise<void> {
await azdataTool.arc.postgres.server.list();
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'list']);
});
it('show', async function (): Promise<void> {
await azdataTool.arc.postgres.server.show(name);
verifyExecuteCommandCalledWithArgs(['arc', 'postgres', 'server', 'show', name]);
});
it('edit', async function (): Promise<void> {
const args = {
adminPassword: true,
coresLimit: 'myCoresLimit',
coresRequest: 'myCoresRequest',
engineSettings: 'myEngineSettings',
extensions: 'myExtensions',
memoryLimit: 'myMemoryLimit',
memoryRequest: 'myMemoryRequest',
noWait: true,
port: 1337,
replaceEngineSettings: true,
workers: 2
};
await azdataTool.arc.postgres.server.edit(name, args);
verifyExecuteCommandCalledWithArgs([
'arc', 'postgres', 'server', 'edit',
name,
'--admin-password',
args.coresLimit,
args.coresRequest,
args.engineSettings,
args.extensions,
args.memoryLimit,
args.memoryRequest,
'--no-wait',
args.port.toString(),
'--replace-engine-settings',
args.workers.toString()]);
});
it('edit no optional args', async function (): Promise<void> {
await azdataTool.arc.postgres.server.edit(name, {});
verifyExecuteCommandCalledWithArgs([
'arc', 'postgres', 'server', 'edit',
name]);
verifyExecuteCommandCalledWithoutArgs([
'--admin-password',
'--cores-limit',
'--cores-request',
'--engine-settings',
'--extensions',
'--memory-limit',
'--memory-request',
'--no-wait',
'--port',
'--replace-engine-settings',
'--workers']);
});
});
});
describe('sql', function (): void {
describe('mi', function (): void {
it('delete', async function (): Promise<void> {
await azdataTool.arc.sql.mi.delete(name);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'delete', name]);
});
it('list', async function (): Promise<void> {
await azdataTool.arc.sql.mi.list();
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'list']);
});
it('show', async function (): Promise<void> {
await azdataTool.arc.sql.mi.show(name);
verifyExecuteCommandCalledWithArgs(['arc', 'sql', 'mi', 'show', name]);
});
});
});
it('general error throws', async function (): Promise<void> {
const err = new Error();
executeCommandStub.throws(err);
try {
await azdataTool.arc.dc.endpoint.list();
throw new Error('command should have failed');
} catch (error) {
should(error).equal(err);
}
});
it('ExitCodeError handled and parsed correctly', async function (): Promise<void> {
const errorInnerText = 'my error text';
const err = new childProcess.ExitCodeError(1, `ERROR { "stderr": "${errorInnerText}"}`);
executeCommandStub.throws(err);
try {
await azdataTool.arc.dc.endpoint.list();
throw new Error('command should have failed');
} catch (error) {
should(error).equal(err);
should((error as childProcess.ExitCodeError).stderr).equal(errorInnerText);
}
});
it('ExitCodeError general error with azdata tool existing rethrows original error', async function (): Promise<void> {
sinon.stub(fs.promises, 'access').resolves();
const err = new childProcess.ExitCodeError(1, 'some other error');
executeCommandStub.throws(err);
try {
await azdataTool.arc.dc.endpoint.list();
throw new Error('command should have failed');
} catch (error) {
should(error).equal(err);
}
});
it('ExitCodeError general error with azdata tool not existing throws NoAzdataError', async function (): Promise<void> {
sinon.stub(fs.promises, 'access').throws(new Error('not found'));
const err = new childProcess.ExitCodeError(1, 'some other error');
executeCommandStub.throws(err);
try {
await azdataTool.arc.dc.endpoint.list();
throw new Error('command should have failed');
} catch (error) {
should(error instanceof utils.NoAzdataError).be.true('error should have been instance of NoAzdataError');
}
});
});
it('login', async function (): Promise<void> {
const endpoint = 'myEndpoint';
const username = 'myUsername';
const password = 'myPassword';
await azdataTool.login({ endpoint: endpoint }, username, password);
verifyExecuteCommandCalledWithArgs(['login', endpoint, username]);
describe('postgres', function (): void {
describe('arc-server', function (): void {
it('delete', async function (): Promise<void> {
await azTool.postgres.arcserver.delete(name, namespace);
verifyExecuteCommandCalledWithArgs(['postgres', 'arc-server', 'delete', name, '--k8s-namespace', namespace]);
});
it('list', async function (): Promise<void> {
await azTool.postgres.arcserver.list(namespace);
verifyExecuteCommandCalledWithArgs(['postgres', 'arc-server', 'list', '--k8s-namespace', namespace]);
});
it('show', async function (): Promise<void> {
await azTool.postgres.arcserver.show(name, namespace);
verifyExecuteCommandCalledWithArgs(['postgres', 'arc-server', 'show', name, '--k8s-namespace', namespace]);
});
it('edit', async function (): Promise<void> {
const args = {
adminPassword: true,
coresLimit: 'myCoresLimit',
coresRequest: 'myCoresRequest',
engineSettings: 'myEngineSettings',
extensions: 'myExtensions',
memoryLimit: 'myMemoryLimit',
memoryRequest: 'myMemoryRequest',
noWait: true,
port: 1337,
replaceEngineSettings: true,
workers: 2
};
await azTool.postgres.arcserver.edit(name, args, namespace);
verifyExecuteCommandCalledWithArgs([
'postgres', 'arc-server', 'edit',
name,
'--admin-password',
args.coresLimit,
args.coresRequest,
args.engineSettings,
args.extensions,
args.memoryLimit,
args.memoryRequest,
'--no-wait',
args.port.toString(),
'--replace-engine-settings',
args.workers.toString()]);
});
it('edit no optional args', async function (): Promise<void> {
await azTool.postgres.arcserver.edit(name, {}, namespace);
verifyExecuteCommandCalledWithArgs([
'postgres', 'arc-server', 'edit',
name]);
verifyExecuteCommandCalledWithoutArgs([
'--admin-password',
'--cores-limit',
'--cores-request',
'--engine-settings',
'--extensions',
'--memory-limit',
'--memory-request',
'--no-wait',
'--port',
'--replace-engine-settings',
'--workers']);
});
});
});
describe('sql', function (): void {
describe('mi-arc', function (): void {
it('delete', async function (): Promise<void> {
await azTool.sql.miarc.delete(name, namespace);
verifyExecuteCommandCalledWithArgs(['sql', 'mi-arc', 'delete', name, '--k8s-namespace', namespace, '--use-k8s']);
});
it('list', async function (): Promise<void> {
await azTool.sql.miarc.list(namespace);
verifyExecuteCommandCalledWithArgs(['sql', 'mi-arc', 'list', '--k8s-namespace', namespace, '--use-k8s']);
});
it('show', async function (): Promise<void> {
await azTool.sql.miarc.show(name, namespace);
verifyExecuteCommandCalledWithArgs(['sql', 'mi-arc', 'show', name, '--k8s-namespace', namespace, '--use-k8s']);
});
});
});
it('version', async function (): Promise<void> {
executeCommandStub.resolves({ stdout: '1.0.0', stderr: '' });
await azdataTool.version();
await azTool.version();
verifyExecuteCommandCalledWithArgs(['--version']);
});
@@ -249,557 +168,4 @@ describe('azdata', function () {
}
});
describe('findAzdata', function (): void {
it('successful', async function (): Promise<void> {
// Mock searchForCmd to return a path to azdata.cmd
sinon.stub(utils, 'searchForCmd').returns(Promise.resolve('/path/to/azdata'));
// Mock call to --version to simulate azdata being installed
sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '1.0.0', stderr: '' }));
await should(azdata.findAzdata()).not.be.rejected();
});
it('unsuccessful', async function (): Promise<void> {
if (process.platform === 'win32') {
// Mock searchForCmd to return a failure to find azdata.cmd
sinon.stub(utils, 'searchForCmd').returns(Promise.reject(new Error('Could not find azdata')));
} else {
// Mock call to executeCommand to simulate azdata --version returning error
sinon.stub(childProcess, 'executeCommand').returns(Promise.reject({ stdout: '', stderr: 'command not found: azdata' }));
}
await should(azdata.findAzdata()).be.rejected();
});
});
describe('installAzdata', function (): void {
let errorMessageStub: sinon.SinonStub;
beforeEach(function (): void {
errorMessageStub = 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> {
switch (process.platform) {
case 'win32':
await testWin32SuccessfulInstall();
break;
case 'darwin':
await testDarwinSuccessfulInstall();
break;
case 'linux':
await testLinuxSuccessfulInstall();
break;
}
});
it('skipped install - dont prompt config', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => 'dontPrompt');
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
switch (process.platform) {
case 'win32':
await testWin32SkippedInstall();
break;
case 'darwin':
await testDarwinSkippedInstall();
break;
case 'linux':
await testLinuxSkippedInstall();
break;
}
});
it('skipped install - user chose not to prompt', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
errorMessageStub.resolves(<any>loc.doNotAskAgain);
switch (process.platform) {
case 'win32':
await testWin32SkippedInstall();
break;
case 'darwin':
await testDarwinSkippedInstall();
break;
case 'linux':
await testLinuxSkippedInstall();
break;
}
configMock.verify(x => x.update(TypeMoq.It.isAny(), azdata.AzdataDeployOption.dontPrompt, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
if (process.platform === 'win32') {
it('unsuccessful download - win32', async function (): Promise<void> {
sinon.stub(HttpClient, 'downloadFile').rejects();
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();
});
}
it('unsuccessful install', async function (): Promise<void> {
switch (process.platform) {
case 'win32':
await testWin32UnsuccessfulInstall();
break;
case 'darwin':
await testDarwinUnsuccessfulInstall();
break;
case 'linux':
await testLinuxUnsuccessfulInstall();
break;
}
});
});
describe('updateAzdata', function (): void {
let showInformationMessageStub: sinon.SinonStub;
beforeEach(function (): void {
showInformationMessageStub = sinon.stub(vscode.window, 'showInformationMessage').returns(Promise.resolve(<any>loc.yes));
executeSudoCommandStub = sinon.stub(childProcess, 'executeSudoCommand').returns(Promise.resolve({ stdout: '', stderr: '' }));
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(releaseJson));
});
it('successful update', async function (): Promise<void> {
switch (process.platform) {
case 'win32':
await testWin32SuccessfulUpdate();
break;
case 'darwin':
await testDarwinSuccessfulUpdate();
break;
case 'linux':
await testLinuxSuccessfulUpdate();
break;
}
});
it('successful update - always prompt if user requested', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
switch (process.platform) {
case 'win32':
await testWin32SuccessfulUpdate(true);
break;
case 'darwin':
await testDarwinSuccessfulUpdate(true);
break;
case 'linux':
await testLinuxSuccessfulUpdate(true);
break;
}
});
it('skipped update - config set not to prompt', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
switch (process.platform) {
case 'win32':
await testWin32SkippedUpdateDontPrompt();
break;
case 'darwin':
await testDarwinSkippedUpdateDontPrompt();
break;
case 'linux':
await testLinuxSkippedUpdateDontPrompt();
break;
}
});
it('skipped update - user chose to never prompt again', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
showInformationMessageStub.resolves(<any>loc.doNotAskAgain);
switch (process.platform) {
case 'win32':
await testWin32SkippedUpdateDontPrompt();
break;
case 'darwin':
await testDarwinSkippedUpdateDontPrompt();
break;
case 'linux':
await testLinuxSkippedUpdateDontPrompt();
break;
}
// Config should have been updated since user chose never to prompt again
configMock.verify(x => x.update(TypeMoq.It.isAny(), azdata.AzdataDeployOption.dontPrompt, TypeMoq.It.isAny()), TypeMoq.Times.once());
});
it('skipped update - no new version', async function (): Promise<void> {
switch (process.platform) {
case 'win32':
await testWin32SkippedUpdate();
break;
case 'darwin':
await testDarwinSkippedUpdate();
break;
case 'linux':
await testLinuxSkippedUpdate();
break;
}
});
it('skipped update - no azdata', async function (): Promise<void> {
const result = await azdata.checkAndUpdateAzdata();
should(result).be.false();
});
it('unsuccessful update', async function (): Promise<void> {
switch (process.platform) {
case 'win32':
await testWin32UnsuccessfulUpdate();
break;
case 'darwin':
await testDarwinUnsuccessfulUpdate();
break;
case 'linux':
await testLinuxUnsuccessfulUpdate();
}
});
describe('discoverLatestAvailableAzdataVersion', function (): void {
it('finds latest available version of azdata successfully', async function (): Promise<void> {
await azdata.discoverLatestAvailableAzdataVersion();
});
});
});
describe('promptForEula', function (): void {
it('skipped because of config', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const result = await azdata.promptForEula(mementoMock.object);
should(result).be.false();
});
it('always prompt if user requested', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.dontPrompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage');
const result = await azdata.promptForEula(mementoMock.object, true);
should(result).be.false();
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
});
it('prompt if config set to do so', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage');
const result = await azdata.promptForEula(mementoMock.object);
should(result).be.false();
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
});
it('update config if user chooses not to prompt', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').resolves(<any>loc.doNotAskAgain);
const result = await azdata.promptForEula(mementoMock.object);
configMock.verify(x => x.update(TypeMoq.It.isAny(), azdata.AzdataDeployOption.dontPrompt, TypeMoq.It.isAny()), TypeMoq.Times.once());
should(result).be.false('EULA should not have been accepted');
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
});
it('user accepted EULA', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const showInformationMessage = sinon.stub(vscode.window, 'showInformationMessage').resolves(<any>loc.accept);
const result = await azdata.promptForEula(mementoMock.object);
mementoMock.verify(x => x.update(eulaAccepted, true), TypeMoq.Times.once());
should(result).be.true('EULA should have been accepted');
should(showInformationMessage.calledOnce).be.true('showInformationMessage should have been called to prompt user');
});
it('user accepted EULA - require user action', async function (): Promise<void> {
const configMock = TypeMoq.Mock.ofType<vscode.WorkspaceConfiguration>();
configMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => azdata.AzdataDeployOption.prompt);
sinon.stub(vscode.workspace, 'getConfiguration').returns(configMock.object);
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
const showErrorMessage = sinon.stub(vscode.window, 'showErrorMessage').resolves(<any>loc.accept);
const result = await azdata.promptForEula(mementoMock.object, true, true);
mementoMock.verify(x => x.update(eulaAccepted, true), TypeMoq.Times.once());
should(result).be.true('EULA should have been accepted');
should(showErrorMessage.calledOnce).be.true('showErrorMessage should have been called to prompt user');
});
});
describe('isEulaAccepted', function (): void {
const mementoMock = TypeMoq.Mock.ofType<vscode.Memento>();
mementoMock.setup(x => x.get(TypeMoq.It.isAny())).returns(() => true);
should(azdata.isEulaAccepted(mementoMock.object)).be.true();
});
});
async function testLinuxUnsuccessfulUpdate() {
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeSudoCommandStub.calledOnce).be.true();
}
async function testDarwinUnsuccessfulUpdate() {
const brewInfoOutput = [{
name: 'azdata-cli',
full_name: 'microsoft/azdata-cli-release/azdata-cli',
versions: {
'stable': '9999.999.999',
'devel': null,
'head': null,
'bottle': true
}
}];
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[]) => {
return Promise.resolve({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
});
})
.onCall(5) //6th call is the first one to do actual update, the call number are 0 indexed
.callsFake(async (_command: string, _args: string[]) => {
return Promise.reject(new Error('not Found'));
})
.callsFake(async (_command: string, _args: string[]) => { // by default return success
return Promise.resolve({ stderr: '', stdout: 'success' });
});
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false();
should(executeCommandStub.callCount).equal(6);
}
async function testWin32UnsuccessfulUpdate() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
executeSudoCommandStub.rejects();
const updateDone = await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(updateDone).be.false('Update should not have been successful');
should(executeSudoCommandStub.calledOnce).be.true();
}
async function testLinuxSuccessfulUpdate(userRequested = false) {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
should(executeSudoCommandStub.callCount).be.equal(6);
should(executeCommandStub.calledOnce).be.true();
}
async function testDarwinSuccessfulUpdate(userRequested = false) {
const brewInfoOutput = [{
name: 'azdata-cli',
full_name: 'microsoft/azdata-cli-release/azdata-cli',
versions: {
'stable': '9999.999.999',
'devel': null,
'head': null,
'bottle': true
}
}];
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.
.resolves({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
})
.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
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(userRequested = false) {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock, userRequested);
should(executeSudoCommandStub.calledOnce).be.true('executeSudoCommand should have been called once');
should(executeSudoCommandStub.getCall(0).args[0]).startWith('msiexec /qn /i');
}
async function testLinuxSkippedUpdate() {
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(currentAzdataMock);
should(executeSudoCommandStub.callCount).be.equal(0, 'executeSudoCommand was not expected to be called');
}
async function testDarwinSkippedUpdateDontPrompt() {
const brewInfoOutput = [{
name: 'azdata-cli',
full_name: 'microsoft/azdata-cli-release/azdata-cli',
versions: {
'stable': '9999.999.999',
'devel': null,
'head': null,
'bottle': true
}
}];
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.
.resolves({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
})
.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeCommandStub.callCount).be.equal(6);
should(executeCommandStub.notCalledWith(sinon.match.any, sinon.match.array.contains(['upgrade', 'azdata-cli'])));
}
async function testWin32SkippedUpdateDontPrompt() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.notCalled).be.true(`executeSudoCommand should not have been called ${executeSudoCommandStub.getCalls().join(os.EOL)}`);
}
async function testLinuxSkippedUpdateDontPrompt() {
sinon.stub(childProcess, 'executeCommand').returns(Promise.resolve({ stdout: '0.0.0', stderr: '' }));
executeSudoCommandStub.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(oldAzdataMock);
should(executeSudoCommandStub.callCount).be.equal(0, 'executeSudoCommand was not expected to be called');
}
async function testDarwinSkippedUpdate() {
const brewInfoOutput = [{
name: 'azdata-cli',
full_name: 'microsoft/azdata-cli-release/azdata-cli',
versions: {
'stable': '9999.999.999',
'devel': null,
'head': null,
'bottle': true
}
}];
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.
.resolves({
stderr: '',
stdout: JSON.stringify(brewInfoOutput)
})
.resolves({ stdout: '0.0.0', stderr: '' });
await azdata.checkAndUpdateAzdata(currentAzdataMock);
should(executeCommandStub.callCount).be.equal(6);
should(executeCommandStub.notCalledWith(sinon.match.any, sinon.match.array.contains(['upgrade', 'azdata-cli'])));
}
async function testWin32SkippedUpdate() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
await azdata.checkAndUpdateAzdata(currentAzdataMock);
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
}
async function testDarwinSkippedInstall() {
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 result = await azdata.checkAndInstallAzdata();
should(result).equal(undefined, 'result should be undefined');
should(executeCommandStub.callCount).be.equal(0);
}
async function testLinuxSkippedInstall() {
sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.rejects(new Error('not Found'))
.resolves({ stdout: '0.0.0', stderr: '' });
executeSudoCommandStub
.resolves({ stdout: 'success', stderr: '' });
const result = await azdata.checkAndInstallAzdata();
should(result).equal(undefined, 'result should be undefined');
should(executeSudoCommandStub.callCount).be.equal(0);
}
async function testWin32SkippedInstall() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.rejects(new Error('not Found')) // First call mock the tool not being found
.resolves({ stdout: '1.0.0', stderr: '' });
executeSudoCommandStub
.returns({ stdout: '', stderr: '' });
const result = await azdata.checkAndInstallAzdata();
should(result).equal(undefined, 'result should be undefined');
should(executeSudoCommandStub.notCalled).be.true('executeSudoCommand should not have been called');
}
async function testWin32SuccessfulInstall() {
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.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(`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() {
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: '' });
});
await azdata.checkAndInstallAzdata();
should(executeCommandStub.callCount).be.equal(5);
}
async function testLinuxSuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand')
.onFirstCall()
.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() {
executeSudoCommandStub.rejects();
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeSudoCommandStub.calledOnce).be.true();
}
async function testDarwinUnsuccessfulInstall() {
const executeCommandStub = sinon.stub(childProcess, 'executeCommand').rejects();
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeCommandStub.calledOnce).be.true();
}
async function testWin32UnsuccessfulInstall() {
executeSudoCommandStub.rejects();
sinon.stub(HttpClient, 'downloadFile').returns(Promise.resolve(__filename));
const downloadPromise = azdata.installAzdata();
await should(downloadPromise).be.rejected();
should(executeSudoCommandStub.calledOnce).be.true();
}

View File

@@ -1,75 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as sinon from 'sinon';
import { HttpClient } from '../common/httpClient';
import { getPlatformReleaseVersion, getPlatformDownloadLink, AzdataReleaseInfo } from '../azdataReleaseInfo';
const emptyReleaseJson = {
win32: {},
darwin: {},
linux: {}
};
const releaseVersion = '999.999.999';
const releaseLink = 'https://microsoft.com';
const validReleaseJson: AzdataReleaseInfo = {
win32: {
version: releaseVersion,
link: releaseLink
},
darwin: {
version: releaseVersion,
link: releaseLink
},
linux: {
version: releaseVersion,
link: releaseLink
}
};
describe('azdataReleaseInfo', function (): void {
afterEach(function (): void {
sinon.restore();
});
describe('getPlatformReleaseVersion', function(): void {
it('gets version successfully', async function(): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(validReleaseJson));
const version = await getPlatformReleaseVersion();
should(version.format()).equal(releaseVersion);
});
it('throws with invalid JSON', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves('invalid JSON');
await should(getPlatformReleaseVersion()).be.rejected();
});
it('throws when no version', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(emptyReleaseJson));
await should(getPlatformReleaseVersion()).be.rejected();
});
});
describe('getPlatformDownloadLink', function(): void {
it('gets link successfully', async function(): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(validReleaseJson));
const link = await getPlatformDownloadLink();
should(link).equal(releaseLink);
});
it('throws with invalid JSON', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves('invalid JSON');
await should(getPlatformDownloadLink()).be.rejected();
});
it('throws when no version', async function (): Promise<void> {
sinon.stub(HttpClient, 'getTextContent').resolves(JSON.stringify(emptyReleaseJson));
await should(getPlatformDownloadLink()).be.rejected();
});
});
});

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import { NoAzdataError, searchForCmd as searchForExe } from '../../common/utils';
import { searchForCmd as searchForExe } from '../../common/utils';
describe('utils', function () {
describe('searchForExe', function (): void {
@@ -15,12 +15,4 @@ describe('utils', function () {
});
});
describe('NoAzdataError', function (): void {
it('error contains message with and without links', function (): void {
const error = new NoAzdataError();
should(error.message).not.be.empty();
should(error.messageWithLink).not.be.empty();
should(error.message).not.equal(error.messageWithLink, 'Messages should not be equal');
});
});
});

View File

@@ -1,38 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdataExt from 'azdata-ext';
import * as should from 'should';
import * as sinon from 'sinon';
import { ArcControllerConfigProfilesOptionsSource } from '../../providers/arcControllerConfigProfilesOptionsSource';
describe('arcControllerConfigProfilesOptionsSource', async function (): Promise<void> {
afterEach(function(): void {
sinon.restore();
});
it('eula accepted returns list', async function (): Promise<void> {
const options = ['option1', 'option2'];
const api = vscode.extensions.getExtension(azdataExt.extension.name)?.exports as azdataExt.IExtension;
sinon.stub(api, 'isEulaAccepted').resolves(true);
sinon.stub(api.azdata.arc.dc.config, 'list').resolves({ stdout: [''], stderr: [''], logs: [''], result: options});
const source = new ArcControllerConfigProfilesOptionsSource(api);
const result = await source.getOptions();
should(result).deepEqual(options);
});
it('eula not accepted prompts for acceptance', async function (): Promise<void> {
const options = ['option1', 'option2'];
const api = vscode.extensions.getExtension(azdataExt.extension.name)?.exports as azdataExt.IExtension;
sinon.stub(api, 'isEulaAccepted').resolves(false);
const promptStub = sinon.stub(api, 'promptForEula').resolves(true);
sinon.stub(api.azdata.arc.dc.config, 'list').resolves({ stdout: [''], stderr: [''], logs: [''], result: options});
const source = new ArcControllerConfigProfilesOptionsSource(api);
const result = await source.getOptions();
should(result).deepEqual(options);
should(promptStub.calledOnce).be.true('promptForEula should have been called');
});
});

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import { AzdataTool } from '../../azdata';
import { AzdataToolService } from '../../services/azdataToolService';
import { AzTool } from '../../az';
import { AzToolService } from '../../services/azToolService';
describe('azdataToolService', function (): void {
describe('azToolService', function (): void {
it('Tool should be set correctly', async function (): Promise<void> {
const service = new AzdataToolService();
should(service.localAzdata).be.undefined();
service.localAzdata = new AzdataTool('my path', '1.0.0');
const service = new AzToolService();
should(service.localAz).be.undefined();
service.localAz = new AzTool('my path', '1.0.0');
should(service).not.be.undefined();
});
});

View File

@@ -3,18 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'azdata-ext' {
declare module 'az-ext' {
import { SemVer } from 'semver';
import * as vscode from 'vscode';
/**
* Covers defining what the azdata extension exports to other extensions
* Covers defining what the az extension exports to other extensions
*
* IMPORTANT: THIS IS NOT A HARD DEFINITION unlike vscode; therefore no enums or classes should be defined here
* (const enums get evaluated when typescript -> javascript so those are fine)
*/
export const enum extension {
name = 'Microsoft.azdata'
name = 'Microsoft.azcli'
}
export type AdditionalEnvVars = { [key: string]: string };
@@ -258,11 +257,8 @@ declare module 'azdata-ext' {
}
}
export interface AzdataOutput<R> {
logs: string[],
result: R,
stderr: string[],
stdout: string[],
export interface AzOutput<R> {
stdout: R,
code?: number
}
@@ -270,92 +266,76 @@ declare module 'azdata-ext' {
endpoint?: string,
namespace?: string
}
export interface IAzdataApi {
arc: {
export interface IAzApi {
arcdata: {
dc: {
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
create(namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<void>>,
endpoint: {
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcEndpointListResult[]>>
list(namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<DcEndpointListResult[]>>
},
config: {
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigListResult[]>>,
show(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<DcConfigShowResult>>
}
},
postgres: {
server: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<PostgresServerShowResult>>,
edit(
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
coordinatorEngineSettings?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workerEngineSettings?: string,
workers?: number
},
additionalEnvVars?: AdditionalEnvVars,
azdataContext?: string
): Promise<AzdataOutput<void>>
}
},
sql: {
mi: {
delete(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
list(additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiListResult[]>>,
show(name: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<SqlMiShowResult>>,
edit(
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
},
additionalEnvVars?: AdditionalEnvVars,
azdataContext?: string
): Promise<AzdataOutput<void>>
list(additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<DcConfigListResult[]>>,
show(namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<DcConfigShowResult>>
}
}
},
postgres: {
arcserver: {
delete(name: string, namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<void>>,
list(namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<PostgresServerListResult[]>>,
show(name: string, namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<PostgresServerShowResult>>,
edit(
name: string,
args: {
adminPassword?: boolean,
coresLimit?: string,
coresRequest?: string,
coordinatorEngineSettings?: string,
engineSettings?: string,
extensions?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
port?: number,
replaceEngineSettings?: boolean,
workerEngineSettings?: string,
workers?: number
},
namespace?: string,
additionalEnvVars?: AdditionalEnvVars
): Promise<AzOutput<void>>
}
},
sql: {
miarc: {
delete(name: string, namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<void>>,
list(namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<SqlMiListResult[]>>,
show(name: string, namespace?: string, additionalEnvVars?: AdditionalEnvVars): Promise<AzOutput<SqlMiShowResult>>,
edit(
name: string,
args: {
coresLimit?: string,
coresRequest?: string,
memoryLimit?: string,
memoryRequest?: string,
noWait?: boolean,
},
namespace?: string,
additionalEnvVars?: AdditionalEnvVars
): Promise<AzOutput<void>>
}
},
getPath(): Promise<string>,
login(endpointOrNamespace: EndpointOrNamespace, username: string, password: string, additionalEnvVars?: AdditionalEnvVars, azdataContext?: string): Promise<AzdataOutput<void>>,
/**
* The semVersion corresponding to this installation of azdata. version() method should have been run
* The semVersion corresponding to this installation of az. version() method should have been run
* before fetching this value to ensure that correct value is returned. This is almost always correct unless
* Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed.
* Az has gotten reinstalled in the background after this IAzApi object was constructed.
*/
getSemVersion(): Promise<SemVer>,
version(): Promise<AzdataOutput<string>>
version(): Promise<AzOutput<string>>
}
export interface IExtension {
azdata: IAzdataApi;
/**
* returns true if AZDATA CLI EULA has been previously accepted by the user.
*/
isEulaAccepted(): Promise<boolean>;
/**
* Prompts user to accept EULA. Stores and returns the user response to EULA prompt.
* @param requireUserAction - if the prompt is required to be acted upon by the user. This is typically 'true' when this method is called to address an Error when the EULA needs to be accepted to proceed.
*
* pre-requisite, the calling code has to ensure that the EULA has not yet been previously accepted by the user. The code can use @see isEulaAccepted() call to ascertain this.
* returns true if the user accepted the EULA.
*/
promptForEula(requireUserAction?: boolean): Promise<boolean>;
az: IAzApi;
}
}

View File

@@ -13,6 +13,7 @@ const extensionList = [
'admin-tool-ext-win',
'agent',
'arc',
'azcli',
'azdata',
'azurecore',
'cms',