diff --git a/extensions/arc/package.json b/extensions/arc/package.json index 66b77d123c..9acfee7b95 100644 --- a/extensions/arc/package.json +++ b/extensions/arc/package.json @@ -18,7 +18,8 @@ "onView:azureArc" ], "extensionDependencies": [ - "Microsoft.azdata" + "Microsoft.azdata", + "Microsoft.resource-deployment" ], "repository": { "type": "git", @@ -186,7 +187,7 @@ "editable": false, "options": { "source": { - "type": "ArcControllerConfigProfilesOptionsSource" + "providerId": "arc.controller.config.profiles" }, "defaultValue": "azure-arc-aks-default-storage", "optionsType": "radio" @@ -546,7 +547,7 @@ "required": true, "options": { "source": { - "type": "ArcControllersOptionsSource", + "providerId": "arc.controllers", "variableNames": { "endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT", "username": "AZDATA_NB_VAR_CONTROLLER_USERNAME", @@ -711,7 +712,7 @@ "required": true, "options": { "source": { - "type": "ArcControllersOptionsSource", + "providerId": "arc.controllers", "variableNames": { "endpoint": "AZDATA_NB_VAR_CONTROLLER_ENDPOINT", "username": "AZDATA_NB_VAR_CONTROLLER_USERNAME", diff --git a/extensions/arc/src/common/api.ts b/extensions/arc/src/common/api.ts new file mode 100644 index 0000000000..d9e82f4c0d --- /dev/null +++ b/extensions/arc/src/common/api.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as arc from 'arc'; +import { PasswordToControllerDialog } from '../ui/dialogs/connectControllerDialog'; +import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; +import { ControllerTreeNode } from '../ui/tree/controllerTreeNode'; +import { UserCancelledError } from './utils'; + +export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtension { + return { + getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider), + getControllerPassword: (controllerInfo: arc.ControllerInfo) => getControllerPassword(treeDataProvider, controllerInfo), + reacquireControllerPassword: (controllerInfo: arc.ControllerInfo) => reacquireControllerPassword(treeDataProvider, controllerInfo) + }; +} +export async function reacquireControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise { + const dialog = new PasswordToControllerDialog(treeDataProvider); + dialog.showDialog(controllerInfo); + const model = await dialog.waitForClose(); + if (!model) { + throw new UserCancelledError(); + } + return model.password; +} + +export async function getControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise { + return await treeDataProvider.getPassword(controllerInfo); +} + +export async function getRegisteredDataControllers(treeDataProvider: AzureArcTreeDataProvider): Promise { + return (await treeDataProvider.getChildren()) + .filter(node => node instanceof ControllerTreeNode) + .map(node => ({ + label: (node as ControllerTreeNode).model.label, + info: (node as ControllerTreeNode).model.info + })); +} + diff --git a/extensions/resource-deployment/src/helpers/cacheManager.ts b/extensions/arc/src/common/cacheManager.ts similarity index 100% rename from extensions/resource-deployment/src/helpers/cacheManager.ts rename to extensions/arc/src/common/cacheManager.ts diff --git a/extensions/arc/src/common/utils.ts b/extensions/arc/src/common/utils.ts index fea06d7c2f..f86c864342 100644 --- a/extensions/arc/src/common/utils.ts +++ b/extensions/arc/src/common/utils.ts @@ -216,3 +216,17 @@ export function parseIpAndPort(address: string): { ip: string, port: string } { export function createCredentialId(controllerId: string, resourceType: string, instanceName: string): string { return `${controllerId}::${resourceType}::${instanceName}`; } + +/** + * Throws an Error with given {@link message} unless {@link condition} is true. + * This also tells the typescript compiler that the condition is 'truthy' in the remainder of the scope + * where this function was called. + * + * @param condition + * @param message + */ +export function throwUnless(condition: boolean, message?: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} diff --git a/extensions/arc/src/extension.ts b/extensions/arc/src/extension.ts index 482d50c4fa..fccd30eb2f 100644 --- a/extensions/arc/src/extension.ts +++ b/extensions/arc/src/extension.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as arc from 'arc'; +import * as rd from 'resource-deployment'; import * as vscode from 'vscode'; -import { UserCancelledError } from './common/utils'; +import { arcApi } from './common/api'; import { IconPathHelper, refreshActionId } from './constants'; import * as loc from './localizedConstants'; -import { ConnectToControllerDialog, PasswordToControllerDialog } from './ui/dialogs/connectControllerDialog'; +import { ArcControllersOptionsSourceProvider } from './providers/arcControllersOptionsSourceProvider'; +import { ConnectToControllerDialog } from './ui/dialogs/connectControllerDialog'; import { AzureArcTreeDataProvider } from './ui/tree/azureArcTreeDataProvider'; import { ControllerTreeNode } from './ui/tree/controllerTreeNode'; import { TreeNode } from './ui/tree/treeNode'; @@ -63,27 +65,11 @@ export async function activate(context: vscode.ExtensionContext): Promise (await treeDataProvider.getChildren()) - .filter(node => node instanceof ControllerTreeNode) - .map(node => ({ - label: (node as ControllerTreeNode).model.label, - info: (node as ControllerTreeNode).model.info - })), - getControllerPassword: async (controllerInfo: arc.ControllerInfo) => { - return await treeDataProvider.getPassword(controllerInfo); - }, - reacquireControllerPassword: async (controllerInfo: arc.ControllerInfo) => { - let model; - const dialog = new PasswordToControllerDialog(treeDataProvider); - dialog.showDialog(controllerInfo); - model = await dialog.waitForClose(); - if (!model) { - throw new UserCancelledError(); - } - return model.password; - } - }; + // register option sources + const rdApi = vscode.extensions.getExtension(rd.extension.name)?.exports; + rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider)); + + return arcApi(treeDataProvider); } export function deactivate(): void { diff --git a/extensions/arc/src/localizedConstants.ts b/extensions/arc/src/localizedConstants.ts index 99c078640f..699309377e 100644 --- a/extensions/arc/src/localizedConstants.ts +++ b/extensions/arc/src/localizedConstants.ts @@ -168,3 +168,8 @@ export function passwordAcquisitionFailed(error: any): string { return localize( export const invalidPassword = localize('arc.invalidPassword', "The password did not work, try again."); export function errorVerifyingPassword(error: any): string { return localize('arc.errorVerifyingPassword', "Error encountered while verifying password. {0}", getErrorMessage(error)); } export const onlyOneControllerSupported = localize('arc.onlyOneControllerSupported', "Only one controller connection is currently supported at this time. Do you wish to remove the existing connection and add a new one?"); +export const noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again"); +export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName); +export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName); +export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name); +export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName); diff --git a/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts new file mode 100644 index 0000000000..ac1089c09d --- /dev/null +++ b/extensions/arc/src/providers/arcControllersOptionsSourceProvider.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as arc from 'arc'; +import * as azdata from 'azdata'; +import * as rd from 'resource-deployment'; +import { getControllerPassword, getRegisteredDataControllers, reacquireControllerPassword } from '../common/api'; +import { CacheManager } from '../common/cacheManager'; +import { throwUnless } from '../common/utils'; +import * as loc from '../localizedConstants'; +import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; + +/** + * Class that provides options sources for an Arc Data Controller + */ +export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider { + private _cacheManager = new CacheManager(); + readonly optionsSourceId = 'arc.controllers'; + constructor(private _treeProvider: AzureArcTreeDataProvider) { } + + async getOptions(): Promise { + const controllers = await getRegisteredDataControllers(this._treeProvider); + throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected); + return controllers.map(ci => { + return ci.label; + }); + } + + private async retrieveVariable(key: string): Promise { + const [variableName, controllerLabel] = JSON.parse(key); + const controller = (await getRegisteredDataControllers(this._treeProvider)).find(ci => ci.label === controllerLabel); + throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel)); + switch (variableName) { + case 'endpoint': return controller.info.url; + case 'username': return controller.info.username; + case 'password': return this.getPassword(controller); + default: throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName)); + } + } + + getVariableValue(variableName: string, controllerLabel: string): Promise { + return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), this.retrieveVariable); + } + + private async getPassword(controller: arc.DataController): Promise { + let password = await getControllerPassword(this._treeProvider, controller.info); + if (!password) { + password = await reacquireControllerPassword(this._treeProvider, controller.info); + } + throwUnless(password !== undefined, loc.noPasswordFound(controller.label)); + return password; + } + + getIsPassword(variableName: string): boolean { + switch (variableName) { + case 'endpoint': return false; + case 'username': return false; + case 'password': return true; + default: throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName)); + } + } +} diff --git a/extensions/arc/src/typings/refs.d.ts b/extensions/arc/src/typings/refs.d.ts index c0d0cf7a85..c4729cb36b 100644 --- a/extensions/arc/src/typings/refs.d.ts +++ b/extensions/arc/src/typings/refs.d.ts @@ -8,3 +8,4 @@ /// /// /// +/// diff --git a/extensions/azdata/package.json b/extensions/azdata/package.json index e9529ba086..9ff23ebea1 100644 --- a/extensions/azdata/package.json +++ b/extensions/azdata/package.json @@ -18,6 +18,9 @@ "type": "git", "url": "https://github.com/Microsoft/azuredatastudio.git" }, + "extensionDependencies": [ + "microsoft.resource-deployment" + ], "main": "./out/extension", "contributes": { "configuration": [ diff --git a/extensions/azdata/src/api.ts b/extensions/azdata/src/api.ts new file mode 100644 index 0000000000..70b3d0fe4f --- /dev/null +++ b/extensions/azdata/src/api.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import { IAzdataTool, isEulaAccepted, promptForEula } from './azdata'; +import Logger from './common/logger'; +import { NoAzdataError } from './common/utils'; +import * as constants from './constants'; +import * as loc from './localizedConstants'; +import { AzdataToolService } from './services/azdataToolService'; + +function throwIfNoAzdataOrEulaNotAccepted(azdata: IAzdataTool | undefined, eulaAccepted: boolean): asserts azdata { + throwIfNoAzdata(azdata); + if (!eulaAccepted) { + Logger.log(loc.eulaNotAccepted); + throw new Error(loc.eulaNotAccepted); + } +} + +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): azdataExt.IExtension { + return { + isEulaAccepted: async () => { + throwIfNoAzdata(await localAzdataDiscovered); // ensure that we have discovered Azdata + return !!memento.get(constants.eulaAccepted); + }, + promptForEula: async (requireUserAction: boolean = true): Promise => { + await localAzdataDiscovered; + return promptForEula(memento, true /* userRequested */, requireUserAction); + }, + azdata: getAzdataApi(localAzdataDiscovered, azdataToolService, memento) + }; +} + +export function getAzdataApi(localAzdataDiscovered: Promise, azdataToolService: AzdataToolService, memento: vscode.Memento): azdataExt.IAzdataApi { + return { + arc: { + dc: { + create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass); + }, + endpoint: { + list: async () => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.dc.endpoint.list(); + } + }, + config: { + list: async () => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.dc.config.list(); + }, + show: async () => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.dc.config.show(); + } + } + }, + postgres: { + server: { + delete: async (name: string) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.delete(name); + }, + list: async () => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.list(); + }, + show: async (name: string) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.show(name); + }, + edit: async ( + name: string, + args: { + adminPassword?: boolean; + coresLimit?: string; + coresRequest?: string; + engineSettings?: string; + extensions?: string; + memoryLimit?: string; + memoryRequest?: string; + noWait?: boolean; + port?: number; + replaceEngineSettings?: boolean; + workers?: number; + }, + additionalEnvVars?: { [key: string]: string; }) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars); + } + } + }, + sql: { + mi: { + delete: async (name: string) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.sql.mi.delete(name); + }, + list: async () => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.sql.mi.list(); + }, + show: async (name: string) => { + await localAzdataDiscovered; + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.arc.sql.mi.show(name); + } + } + } + }, + getPath: async () => { + await localAzdataDiscovered; + throwIfNoAzdata(azdataToolService.localAzdata); + return azdataToolService.localAzdata.getPath(); + }, + login: async (endpoint: string, username: string, password: string) => { + throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento)); + return azdataToolService.localAzdata.login(endpoint, username, password); + }, + getSemVersion: async () => { + await localAzdataDiscovered; + throwIfNoAzdata(azdataToolService.localAzdata); + return azdataToolService.localAzdata.getSemVersion(); + }, + version: async () => { + await localAzdataDiscovered; + throwIfNoAzdata(azdataToolService.localAzdata); + return azdataToolService.localAzdata.version(); + } + }; +} + diff --git a/extensions/azdata/src/azdata.ts b/extensions/azdata/src/azdata.ts index b521d29275..4ea09ffae7 100644 --- a/extensions/azdata/src/azdata.ts +++ b/extensions/azdata/src/azdata.ts @@ -37,7 +37,7 @@ export interface IAzdataTool extends azdataExt.IAzdataApi { /** * An object to interact with the azdata tool installed on the box. */ -export class AzdataTool implements IAzdataTool { +export class AzdataTool implements azdataExt.IAzdataApi { private _semVersion: SemVer; constructor(private _path: string, version: string) { @@ -49,14 +49,14 @@ export class AzdataTool implements IAzdataTool { * before fetching this value to ensure that correct value is returned. This is almost always correct unless * Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed. */ - public getSemVersion() { + public async getSemVersion(): Promise { return this._semVersion; } /** * gets the path where azdata tool is installed */ - public getPath() { + public async getPath(): Promise { return this._path; } @@ -225,7 +225,7 @@ export async function findAzdata(): Promise { 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(azdata.getPath(), azdata.getSemVersion().raw)); + Logger.log(loc.foundExistingAzdata(await azdata.getPath(), (await azdata.getSemVersion()).raw)); return azdata; } catch (err) { Logger.log(loc.couldNotFindAzdata(err)); @@ -312,11 +312,11 @@ export async function checkAndInstallAzdata(userRequested: boolean = false): Pro export async function checkAndUpdateAzdata(currentAzdata?: IAzdataTool, userRequested: boolean = false): Promise { if (currentAzdata !== undefined) { const newSemVersion = await discoverLatestAvailableAzdataVersion(); - if (newSemVersion.compare(currentAzdata.getSemVersion()) === 1) { - Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, currentAzdata.getSemVersion().raw)); + if (newSemVersion.compare(await currentAzdata.getSemVersion()) === 1) { + Logger.log(loc.foundAzdataVersionToUpdateTo(newSemVersion.raw, (await currentAzdata.getSemVersion()).raw)); return await promptToUpdateAzdata(newSemVersion.raw, userRequested); } else { - Logger.log(loc.currentlyInstalledVersionIsLatest(currentAzdata.getSemVersion().raw)); + Logger.log(loc.currentlyInstalledVersionIsLatest((await currentAzdata.getSemVersion()).raw)); } } else { Logger.log(loc.updateCheckSkipped); @@ -413,6 +413,17 @@ async function promptToUpdateAzdata(newVersion: string, userRequested: boolean = 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(eulaAccepted); +} + /** * Prompts user to accept EULA. Stores and returns the user response to EULA prompt. * @param memento - memento where the user response is stored. diff --git a/extensions/azdata/src/extension.ts b/extensions/azdata/src/extension.ts index 6f83f7f279..8697e3e334 100644 --- a/extensions/azdata/src/extension.ts +++ b/extensions/azdata/src/extension.ts @@ -4,40 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import * as azdataExt from 'azdata-ext'; +import * as rd from 'resource-deployment'; import * as vscode from 'vscode'; -import { checkAndInstallAzdata, checkAndUpdateAzdata, findAzdata, IAzdataTool, promptForEula } from './azdata'; +import { getExtensionApi } from './api'; +import { checkAndInstallAzdata, checkAndUpdateAzdata, findAzdata, isEulaAccepted, promptForEula } from './azdata'; import Logger from './common/logger'; -import { NoAzdataError } from './common/utils'; import * as constants from './constants'; import * as loc from './localizedConstants'; +import { ArcControllerConfigProfilesOptionsSource } from './providers/arcControllerConfigProfilesOptionsSource'; +import { AzdataToolService } from './services/azdataToolService'; -let localAzdata: IAzdataTool | undefined = undefined; -let eulaAccepted: boolean = false; export async function activate(context: vscode.ExtensionContext): Promise { + const azdataToolService = new AzdataToolService(); + let eulaAccepted: boolean = false; vscode.commands.registerCommand('azdata.acceptEula', async () => { - eulaAccepted = await promptForEula(context.globalState, true /* userRequested */); - + await promptForEula(context.globalState, true /* userRequested */); }); vscode.commands.registerCommand('azdata.install', async () => { - localAzdata = await checkAndInstallAzdata(true /* userRequested */); + azdataToolService.localAzdata = await checkAndInstallAzdata(true /* userRequested */); }); vscode.commands.registerCommand('azdata.update', async () => { - if (await checkAndUpdateAzdata(localAzdata, true /* userRequested */)) { // if an update was performed - localAzdata = await findAzdata(); // find and save the currently installed azdata + if (await checkAndUpdateAzdata(azdataToolService.localAzdata, true /* userRequested */)) { // if an update was performed + azdataToolService.localAzdata = await findAzdata(); // find and save the currently installed azdata } }); - eulaAccepted = !!context.globalState.get(constants.eulaAccepted); // fetch eula acceptance state from memento + 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)); // Don't block on this since we want the extension to finish activating without needing user input - checkAndInstallAzdata() // install if not installed and user wants it. + const localAzdataDiscovered = checkAndInstallAzdata() // install if not installed and user wants it. .then(async azdataTool => { - localAzdata = azdataTool; - if (localAzdata !== undefined) { + if (azdataTool !== undefined) { + azdataToolService.localAzdata = azdataTool; if (!eulaAccepted) { // Don't block on this since we want extension to finish activating without requiring user actions. // If EULA has not been accepted then we will check again while executing azdata commands. @@ -49,127 +51,23 @@ export async function activate(context: vscode.ExtensionContext): Promise !!context.globalState.get(constants.eulaAccepted), - promptForEula: (onError: boolean = true): Promise => promptForEula(context.globalState, true /* userRequested */, onError), - azdata: { - arc: { - dc: { - create: async (namespace: string, name: string, connectivityMode: string, resourceGroup: string, location: string, subscription: string, profileName?: string, storageClass?: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.dc.create(namespace, name, connectivityMode, resourceGroup, location, subscription, profileName, storageClass); - }, - endpoint: { - list: async () => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.dc.endpoint.list(); - } - }, - config: { - list: async () => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.dc.config.list(); - }, - show: async () => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.dc.config.show(); - } - } - }, - postgres: { - server: { - delete: async (name: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.postgres.server.delete(name); - }, - list: async () => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.postgres.server.list(); - }, - show: async (name: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.postgres.server.show(name); - }, - edit: async ( - name: string, - args: { - adminPassword?: boolean, - coresLimit?: string, - coresRequest?: string, - engineSettings?: string, - extensions?: string, - memoryLimit?: string, - memoryRequest?: string, - noWait?: boolean, - port?: number, - replaceEngineSettings?: boolean, - workers?: number - }, - additionalEnvVars?: { [key: string]: string }) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.postgres.server.edit(name, args, additionalEnvVars); - } - } - }, - sql: { - mi: { - delete: async (name: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.sql.mi.delete(name); - }, - list: async () => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.sql.mi.list(); - }, - show: async (name: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.arc.sql.mi.show(name); - } - } - } - }, - getPath: () => { - throwIfNoAzdata(); - return localAzdata!.getPath(); - }, - login: async (endpoint: string, username: string, password: string) => { - throwIfNoAzdataOrEulaNotAccepted(); - return localAzdata!.login(endpoint, username, password); - }, - getSemVersion: () => { - throwIfNoAzdata(); - return localAzdata!.getSemVersion(); - }, - version: async () => { - throwIfNoAzdata(); - return localAzdata!.version(); - } - } - }; -} + const azdataApi = getExtensionApi(context.globalState, azdataToolService, localAzdataDiscovered); -function throwIfNoAzdataOrEulaNotAccepted(): void { - throwIfNoAzdata(); - if (!eulaAccepted) { - Logger.log(loc.eulaNotAccepted); - throw new Error(loc.eulaNotAccepted); - } -} + // register option source(s) + const rdApi = vscode.extensions.getExtension(rd.extension.name)?.exports; + rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi)); -function throwIfNoAzdata() { - if (!localAzdata) { - Logger.log(loc.noAzdata); - throw new NoAzdataError(); - } + return azdataApi; } export function deactivate(): void { } diff --git a/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts new file mode 100644 index 0000000000..3108afe80c --- /dev/null +++ b/extensions/azdata/src/providers/arcControllerConfigProfilesOptionsSource.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; +import * as azdataExt from 'azdata-ext'; + +/** + * Class that provides options sources for an Arc Data Controller + */ +export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider { + readonly optionsSourceId = 'arc.controller.config.profiles'; + constructor(private _azdataExtApi: azdataExt.IExtension) { } + async getOptions(): Promise { + if (!this._azdataExtApi.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; + } +} diff --git a/extensions/azdata/src/services/azdataToolService.ts b/extensions/azdata/src/services/azdataToolService.ts new file mode 100644 index 0000000000..d94e86822a --- /dev/null +++ b/extensions/azdata/src/services/azdataToolService.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAzdataTool } from '../azdata'; + +export class AzdataToolService { + private _localAzdata: IAzdataTool | undefined; + constructor() { + } + + /** + * Gets the localAzdata that was last saved + */ + get localAzdata(): IAzdataTool | undefined { + return this._localAzdata; + } + + /** + * Sets the localAzdata that was last saved + * + * @param memento The memento that stores the localAzdata object + */ + set localAzdata(azdata: IAzdataTool | undefined) { + this._localAzdata = azdata; + } +} + diff --git a/extensions/azdata/src/typings/azdata-ext.d.ts b/extensions/azdata/src/typings/azdata-ext.d.ts index 32ec6171e9..8eef9ced13 100644 --- a/extensions/azdata/src/typings/azdata-ext.d.ts +++ b/extensions/azdata/src/typings/azdata-ext.d.ts @@ -267,14 +267,14 @@ declare module 'azdata-ext' { } } }, - getPath(): string, + getPath(): Promise, login(endpoint: string, username: string, password: string): Promise>, /** * The semVersion corresponding to this installation of azdata. version() method should have been run * before fetching this value to ensure that correct value is returned. This is almost always correct unless * Azdata has gotten reinstalled in the background after this IAzdataApi object was constructed. */ - getSemVersion(): SemVer, + getSemVersion(): Promise, version(): Promise> } @@ -284,7 +284,7 @@ declare module 'azdata-ext' { /** * returns true if AZDATA CLI EULA has been previously accepted by the user. */ - isEulaAccepted(): boolean; + isEulaAccepted(): Promise; /** * Prompts user to accept EULA. Stores and returns the user response to EULA prompt. @@ -293,6 +293,7 @@ declare module 'azdata-ext' { * 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 + promptForEula(requireUserAction?: boolean): Promise; + } } diff --git a/extensions/azdata/src/typings/refs.d.ts b/extensions/azdata/src/typings/refs.d.ts index dad0d96412..b3b7c2ef88 100644 --- a/extensions/azdata/src/typings/refs.d.ts +++ b/extensions/azdata/src/typings/refs.d.ts @@ -6,3 +6,4 @@ /// /// /// +/// diff --git a/extensions/resource-deployment/src/api.ts b/extensions/resource-deployment/src/api.ts new file mode 100644 index 0000000000..79deef29a7 --- /dev/null +++ b/extensions/resource-deployment/src/api.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; +import { optionsSourcesService } from './services/optionSourcesService'; + +export function getExtensionApi(): rd.IExtension { + return { + registerOptionsSourceProvider: (provider: rd.IOptionsSourceProvider) => optionsSourcesService.registerOptionsSourceProvider(provider) + }; +} + diff --git a/extensions/resource-deployment/src/utils.ts b/extensions/resource-deployment/src/common/utils.ts similarity index 94% rename from extensions/resource-deployment/src/utils.ts rename to extensions/resource-deployment/src/common/utils.ts index 9f3a03cddb..59c6963506 100644 --- a/extensions/resource-deployment/src/utils.ts +++ b/extensions/resource-deployment/src/common/utils.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { ToolsInstallPath } from './constants'; -import { ITool, NoteBookEnvironmentVariablePrefix } from './interfaces'; +import { ToolsInstallPath } from '../constants'; +import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces'; export function getErrorMessage(error: any): string { return (error instanceof Error) diff --git a/extensions/resource-deployment/src/helpers/optionSources.ts b/extensions/resource-deployment/src/helpers/optionSources.ts deleted file mode 100644 index 84142bcc0a..0000000000 --- a/extensions/resource-deployment/src/helpers/optionSources.ts +++ /dev/null @@ -1,102 +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 arc from 'arc'; -import { CategoryValue } from 'azdata'; -import { IOptionsSource } from '../interfaces'; -import * as loc from '../localizedConstants'; -import { apiService } from '../services/apiService'; -import { throwUnless } from '../utils'; -import { CacheManager } from './cacheManager'; - -export const enum OptionsSourceType { - ArcControllersOptionsSource = 'ArcControllersOptionsSource', - ArcControllerConfigProfilesOptionsSource = 'ArcControllerConfigProfilesOptionsSource' -} - -export abstract class OptionsSource implements IOptionsSource { - - get type(): OptionsSourceType { return this._type; } - get variableNames(): { [index: string]: string; } { return this._variableNames; } - - abstract getOptions(): Promise; - getVariableValue(variableName: string, controllerLabel: string): Promise { - throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName)); - } - - getIsPassword(variableName: string): boolean { - throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName)); - } - - constructor(private _variableNames: { [index: string]: string }, private _type: OptionsSourceType) { - } -} - -/** - * Class that provides options sources for an Arc Data Controller - */ -export class ArcControllersOptionsSource extends OptionsSource { - private _cacheManager = new CacheManager(); - - async getOptions(): Promise { - const controllers = await apiService.arcApi.getRegisteredDataControllers(); - throwUnless(controllers !== undefined && controllers.length !== 0, loc.noControllersConnected); - return controllers.map(ci => { - return ci.label; - }); - } - - async getVariableValue(variableName: string, controllerLabel: string): Promise { - const retrieveVariable = async (key: string) => { - const [variableName, controllerLabel] = JSON.parse(key); - const controllers = await apiService.arcApi.getRegisteredDataControllers(); - const controller = controllers!.find(ci => ci.label === controllerLabel); - throwUnless(controller !== undefined, loc.noControllerInfoFound(controllerLabel)); - switch (variableName) { - case 'endpoint': - return controller.info.url; - case 'username': - return controller.info.username; - case 'password': - const fetchedPassword = await this.getPassword(controller); - return fetchedPassword; - default: - throw new Error(loc.variableValueFetchForUnsupportedVariable(variableName)); - } - }; - const variableValue = await this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), retrieveVariable); - return variableValue; - } - - private async getPassword(controller: arc.DataController): Promise { - let password = await apiService.arcApi.getControllerPassword(controller.info); - if (!password) { - password = await apiService.arcApi.reacquireControllerPassword(controller.info, password); - } - throwUnless(password !== undefined, loc.noPasswordFound(controller.label)); - return password; - } - - getIsPassword(variableName: string): boolean { - switch (variableName) { - case 'endpoint': - case 'username': - return false; - case 'password': - return true; - default: - throw new Error(loc.isPasswordFetchForUnsupportedVariable(variableName)); - } - } -} - -/** - * Class that provides options sources for an Arc Data Controller's Config Profiles - */ -export class ArcControllerConfigProfilesOptionsSource extends OptionsSource { - async getOptions(): Promise { - return (await apiService.azdataApi.azdata.arc.dc.config.list()).result; - } -} diff --git a/extensions/resource-deployment/src/helpers/promise.ts b/extensions/resource-deployment/src/helpers/promise.ts deleted file mode 100644 index 53f62a287b..0000000000 --- a/extensions/resource-deployment/src/helpers/promise.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * Deferred promise - */ -export class Deferred { - promise: Promise; - resolve!: (value?: T | PromiseLike) => void; - reject!: (reason?: any) => void; - constructor() { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable; - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; - then(onfulfilled?: (value: T) => TResult | Thenable, onrejected?: (reason: any) => TResult | Thenable): Thenable { - return this.promise.then(onfulfilled, onrejected); - } -} diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index 91cd374b3d..05d748c72d 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; +import { IOptionsSourceProvider } from 'resource-deployment'; import * as vscode from 'vscode'; -import { OptionsSourceType } from './helpers/optionSources'; export const NoteBookEnvironmentVariablePrefix = 'AZDATA_NB_VAR_'; @@ -219,11 +219,9 @@ export type ComponentCSSStyles = { }; export interface IOptionsSource { - readonly type: OptionsSourceType; - readonly variableNames: { [index: string]: string; }; - getOptions(): Promise; - getVariableValue(variableName: string, input: string): Promise; - getIsPassword(variableName: string): boolean; + provider?: IOptionsSourceProvider + readonly variableNames?: { [index: string]: string; }; + readonly providerId: string; } diff --git a/extensions/resource-deployment/src/localizedConstants.ts b/extensions/resource-deployment/src/localizedConstants.ts index 659246847e..d20e184c65 100644 --- a/extensions/resource-deployment/src/localizedConstants.ts +++ b/extensions/resource-deployment/src/localizedConstants.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; -import { OptionsSourceType } from './helpers/optionSources'; import { FieldType, OptionsType } from './interfaces'; const localize = nls.loadMessageBundle(); @@ -23,14 +22,11 @@ export const refresh = localize('azure.refresh', "Refresh"); export const createNewResourceGroup = localize('azure.resourceGroup.createNewResourceGroup', "Create a new resource group"); export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResourceGroupAriaLabel', "New resource group name"); export const realm = localize('deployCluster.Realm', "Realm"); -export const unexpectedOptionsSourceType = (type: OptionsSourceType) => localize('optionsSourceType.Invalid', "Invalid options source type:{0}", type); export const unknownFieldTypeError = (type: FieldType) => localize('UnknownFieldTypeError', "Unknown field type: \"{0}\"", type); +export const optionsSourceAlreadyDefined = (optionsSourceId: string) => localize('optionsSource.alreadyDefined', "Options Source with id:{0} is already defined", optionsSourceId); +export const noOptionsSourceDefined = (optionsSourceId: string) => localize('optionsSource.notDefined', "No Options Source defined for id: {0}", optionsSourceId); export const variableValueFetchForUnsupportedVariable = (variableName: string) => localize('getVariableValue.unknownVariableName', "Attempt to get variable value for unknown variable:{0}", variableName); export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName); -export const noControllersConnected = localize('noControllersConnected', "No Azure Arc controllers are currently connected. Please run the command: 'Connect to Existing Azure Arc Controller' and then try again"); -export const noOptionsSourceDefined = (optionsSourceType: string) => localize('noOptionsSourceDefined', "No OptionsSource defined for type: {0}", optionsSourceType); -export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "controllerInfo could not be found with name: {0}", name); -export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName); export const optionsNotDefined = (fieldType: FieldType) => localize('optionsNotDefined', "FieldInfo.options was not defined for field type: {0}", fieldType); export const optionsNotObjectOrArray = localize('optionsNotObjectOrArray', "FieldInfo.options must be an object if it is not an array"); export const optionsTypeNotFound = localize('optionsTypeNotFound', "When FieldInfo.options is an object it must have 'optionsType' property"); diff --git a/extensions/resource-deployment/src/main.ts b/extensions/resource-deployment/src/main.ts index e83bbe306e..24bf33fc13 100644 --- a/extensions/resource-deployment/src/main.ts +++ b/extensions/resource-deployment/src/main.ts @@ -12,10 +12,12 @@ import { ResourceTypeService } from './services/resourceTypeService'; import { ToolsService } from './services/toolsService'; import { DeploymentInputDialog } from './ui/deploymentInputDialog'; import { ResourceTypePickerDialog } from './ui/resourceTypePickerDialog'; +import * as rd from 'resource-deployment'; +import { getExtensionApi } from './api'; const localize = nls.loadMessageBundle(); -export async function activate(context: vscode.ExtensionContext): Promise { +export async function activate(context: vscode.ExtensionContext): Promise { const platformService = new PlatformService(context.globalStoragePath); await platformService.initialize(); const toolsService = new ToolsService(platformService); @@ -27,7 +29,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const errorMessage = localize('resourceDeployment.FailedToLoadExtension', "Failed to load extension: {0}, Error detected in the resource type definition in package.json, check debug console for details.", context.extensionPath); vscode.window.showErrorMessage(errorMessage); validationFailures.forEach(message => console.error(message)); - return; + return undefined; } /** * Opens a new ResourceTypePickerDialog @@ -73,6 +75,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const dialog = new DeploymentInputDialog(notebookService, platformService, toolsService, dialogInfo); dialog.open(); }); + return getExtensionApi(); } // this method is called when your extension is deactivated diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 3e26069671..c6c9c481f5 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -10,7 +10,7 @@ import { isString } from 'util'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { NotebookPathInfo } from '../interfaces'; -import { getDateTimeString, getErrorMessage } from '../utils'; +import { getDateTimeString, getErrorMessage } from '../common/utils'; import { IPlatformService } from './platformService'; const localize = nls.loadMessageBundle(); diff --git a/extensions/resource-deployment/src/services/optionSourcesService.ts b/extensions/resource-deployment/src/services/optionSourcesService.ts new file mode 100644 index 0000000000..5d613eeb09 --- /dev/null +++ b/extensions/resource-deployment/src/services/optionSourcesService.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as rd from 'resource-deployment'; +import * as loc from '../localizedConstants'; + +class OptionsSourcesService { + private _optionsSourceStore = new Map(); + registerOptionsSourceProvider(provider: rd.IOptionsSourceProvider): void { + if (this._optionsSourceStore.has(provider.optionsSourceId)) { + throw new Error(loc.optionsSourceAlreadyDefined(provider.optionsSourceId)); + } + this._optionsSourceStore.set(provider.optionsSourceId, provider); + } + + getOptionsSource(optionsSourceProviderId: string): rd.IOptionsSourceProvider { + const optionsSource = this._optionsSourceStore.get(optionsSourceProviderId); + if (optionsSource === undefined) { + throw new Error(loc.noOptionsSourceDefined(optionsSourceProviderId)); + } + return optionsSource; + } +} + +export const optionsSourcesService = new OptionsSourcesService(); diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts index 11b2c7970b..f2f46f7c98 100644 --- a/extensions/resource-deployment/src/services/platformService.ts +++ b/extensions/resource-deployment/src/services/platformService.ts @@ -10,7 +10,7 @@ import * as sudo from 'sudo-prompt'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { OsDistribution, OsRelease } from '../interfaces'; -import { getErrorMessage } from '../utils'; +import { getErrorMessage } from '../common/utils'; const localize = nls.loadMessageBundle(); const extensionOutputChannel = localize('resourceDeployment.outputChannel', "Deployments"); diff --git a/extensions/resource-deployment/src/services/tools/azdataTool.ts b/extensions/resource-deployment/src/services/tools/azdataTool.ts index 5b6399645d..fb807ac26f 100644 --- a/extensions/resource-deployment/src/services/tools/azdataTool.ts +++ b/extensions/resource-deployment/src/services/tools/azdataTool.ts @@ -2,6 +2,7 @@ * 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 { EOL } from 'os'; import * as path from 'path'; import { SemVer } from 'semver'; @@ -9,10 +10,9 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { AzdataInstallLocationKey, DeploymentConfigurationKey } from '../../constants'; import { Command, OsDistribution, ToolStatus, ToolType } from '../../interfaces'; -import { apiService } from '../apiService'; +import * as loc from '../../localizedConstants'; import { IPlatformService } from '../platformService'; import { dependencyType, ToolBase } from './toolBase'; -import * as loc from '../../localizedConstants'; const localize = nls.loadMessageBundle(); export const AzdataToolName = 'azdata'; @@ -21,6 +21,7 @@ const macInstallationRoot = '/usr/local/bin'; const debianInstallationRoot = '/usr/local/bin'; export class AzdataTool extends ToolBase { + private azdataApi!: azdataExt.IExtension; constructor(platformService: IPlatformService) { super(platformService); } @@ -46,7 +47,7 @@ export class AzdataTool extends ToolBase { } public isEulaAccepted(): boolean { - if (apiService.azdataApi.isEulaAccepted()) { + if (this.azdataApi.isEulaAccepted()) { return true; } else { this.setStatusDescription(loc.azdataEulaNotAccepted); @@ -55,7 +56,7 @@ export class AzdataTool extends ToolBase { } public async promptForEula(): Promise { - const eulaAccepted = await apiService.azdataApi.promptForEula(); + const eulaAccepted = await this.azdataApi.promptForEula(); if (!eulaAccepted) { this.setStatusDescription(loc.azdataEulaDeclined); } @@ -80,15 +81,16 @@ export class AzdataTool extends ToolBase { * updates the version and status for the tool. */ protected async updateVersionAndStatus(): Promise { + this.azdataApi = await vscode.extensions.getExtension(azdataExt.extension.name)?.activate(); this.setStatusDescription(''); await this.addInstallationSearchPathsToSystemPath(); - const commandOutput = await apiService.azdataApi.azdata.version(); - this.version = apiService.azdataApi.azdata.getSemVersion(); + const commandOutput = await this.azdataApi.azdata.version(); + this.version = await this.azdataApi.azdata.getSemVersion(); if (this.version) { if (this.autoInstallSupported) { // set the installationPath - this.setInstallationPathOrAdditionalInformation(apiService.azdataApi.azdata.getPath()); + this.setInstallationPathOrAdditionalInformation(await this.azdataApi.azdata.getPath()); } this.setStatus(ToolStatus.Installed); } @@ -99,8 +101,8 @@ export class AzdataTool extends ToolBase { } } - protected getVersionFromOutput(output: string): SemVer | undefined { - return apiService.azdataApi.azdata.getSemVersion(); + protected getVersionFromOutput(output: string): SemVer | Promise | undefined { + return this.azdataApi.azdata.getSemVersion(); } diff --git a/extensions/resource-deployment/src/services/tools/toolBase.ts b/extensions/resource-deployment/src/services/tools/toolBase.ts index e1a24e4f16..eb415c4581 100644 --- a/extensions/resource-deployment/src/services/tools/toolBase.ts +++ b/extensions/resource-deployment/src/services/tools/toolBase.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { EOL } from 'os'; import * as path from 'path'; -import { SemVer, compare as SemVerCompare } from 'semver'; +import { compare as SemVerCompare, SemVer } from 'semver'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; +import { getErrorMessage } from '../../common/utils'; import { Command, ITool, OsDistribution, ToolStatus, ToolType } from '../../interfaces'; -import { getErrorMessage } from '../../utils'; import { IPlatformService } from '../platformService'; const localize = nls.loadMessageBundle(); @@ -52,7 +52,7 @@ export abstract class ToolBase implements ITool { protected abstract readonly allInstallationCommands: Map; protected readonly dependenciesByOsType: Map = new Map(); - protected abstract getVersionFromOutput(output: string): SemVer | undefined; + protected abstract getVersionFromOutput(output: string): SemVer | Promise | undefined; protected readonly _onDidUpdateData = new vscode.EventEmitter(); protected readonly uninstallCommand?: string; @@ -274,7 +274,7 @@ export abstract class ToolBase implements ITool { ignoreError: true }, ); - this.version = this.getVersionFromOutput(commandOutput); + this.version = await this.getVersionFromOutput(commandOutput); if (this.version) { if (this.autoInstallSupported) { // discover and set the installationPath diff --git a/extensions/resource-deployment/src/typings/resource-deployment.d.ts b/extensions/resource-deployment/src/typings/resource-deployment.d.ts new file mode 100644 index 0000000000..2a4cec7456 --- /dev/null +++ b/extensions/resource-deployment/src/typings/resource-deployment.d.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'resource-deployment' { + import * as azdata from 'azdata'; + + export const enum extension { + name = 'Microsoft.resource-deployment' + } + export interface IOptionsSourceProvider { + readonly optionsSourceId: string, + getOptions(): Promise | string[] | azdata.CategoryValue[]; + getVariableValue?: (variableName: string, input: string) => Promise | string; + getIsPassword?: (variableName: string) => boolean | Promise; + } + + /** + * Covers defining what the resource-deployment 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 interface IExtension { + registerOptionsSourceProvider(provider: IOptionsSourceProvider): void + } +} diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts index b959997a79..6879c3e864 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizard.ts @@ -14,7 +14,7 @@ import { IAzdataService } from '../../services/azdataService'; import { IKubeService } from '../../services/kubeService'; import { INotebookService } from '../../services/notebookService'; import { IToolsService } from '../../services/toolsService'; -import { getErrorMessage } from '../../utils'; +import { getErrorMessage } from '../../common/utils'; import { InputComponents } from '../modelViewUtils'; import { WizardBase } from '../wizardBase'; import { WizardPageBase } from '../wizardPageBase'; diff --git a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts index edcb9a83d7..6e973712e3 100644 --- a/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts +++ b/extensions/resource-deployment/src/ui/deployClusterWizard/deployClusterWizardModel.ts @@ -7,7 +7,7 @@ import { delimiter } from 'path'; import { BdcDeploymentType, ITool } from '../../interfaces'; import { BigDataClusterDeploymentProfile, DataResource, HdfsResource, SqlServerMasterResource } from '../../services/bigDataClusterDeploymentProfile'; import { KubeCtlToolName } from '../../services/tools/kubeCtlTool'; -import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../utils'; +import { getRuntimeBinaryPathEnvironmentVariableName, setEnvironmentVariablesForInstallPaths } from '../../common/utils'; import { Model } from '../model'; import { ToolsInstallPath } from './../../constants'; import * as VariableNames from './constants'; diff --git a/extensions/resource-deployment/src/ui/model.ts b/extensions/resource-deployment/src/ui/model.ts index 536e389919..31d22db4cc 100644 --- a/extensions/resource-deployment/src/ui/model.ts +++ b/extensions/resource-deployment/src/ui/model.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { EOL } from 'os'; import { ITool, NoteBookEnvironmentVariablePrefix } from '../interfaces'; -import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../utils'; +import { setEnvironmentVariablesForInstallPaths, getRuntimeBinaryPathEnvironmentVariableName } from '../common/utils'; import { ToolsInstallPath } from '../constants'; import { delimiter } from 'path'; diff --git a/extensions/resource-deployment/src/ui/modelViewUtils.ts b/extensions/resource-deployment/src/ui/modelViewUtils.ts index 542f11bc27..2966cfc178 100644 --- a/extensions/resource-deployment/src/ui/modelViewUtils.ts +++ b/extensions/resource-deployment/src/ui/modelViewUtils.ts @@ -9,17 +9,18 @@ import { EOL, homedir as os_homedir } from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { ArcControllerConfigProfilesOptionsSource, ArcControllersOptionsSource, OptionsSourceType } from '../helpers/optionSources'; import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, FilePickerFieldInfo, IOptionsSource, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces'; import * as loc from '../localizedConstants'; import { apiService } from '../services/apiService'; import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService'; import { KubeCtlTool, KubeCtlToolName } from '../services/tools/kubeCtlTool'; import { IToolsService } from '../services/toolsService'; -import { getDateTimeString, getErrorMessage, throwUnless } from '../utils'; +import { getDateTimeString, getErrorMessage, throwUnless } from '../common/utils'; import { WizardInfoBase } from './../interfaces'; import { Model } from './model'; import { RadioGroupLoadingComponentBuilder } from './radioGroupLoadingComponentBuilder'; +import { optionsSourcesService } from '../services/optionSourcesService'; +import { IOptionsSourceProvider } from 'resource-deployment'; const localize = nls.loadMessageBundle(); @@ -432,21 +433,10 @@ async function processOptionsTypeField(context: FieldContext): Promise { } throwUnless(typeof context.fieldInfo.options === 'object', loc.optionsNotObjectOrArray); throwUnless('optionsType' in context.fieldInfo.options, loc.optionsTypeNotFound); - if (context.fieldInfo.options.source) { + if (context.fieldInfo.options.source?.providerId) { try { - let optionsSource: IOptionsSource; - switch (context.fieldInfo.options.source.type) { - case OptionsSourceType.ArcControllersOptionsSource: - optionsSource = new ArcControllersOptionsSource(context.fieldInfo.options.source.variableNames, context.fieldInfo.options.source.type); - break; - case OptionsSourceType.ArcControllerConfigProfilesOptionsSource: - optionsSource = new ArcControllerConfigProfilesOptionsSource(context.fieldInfo.options.source.variableNames, context.fieldInfo.options.source.type); - break; - default: - throw new Error(loc.noOptionsSourceDefined(context.fieldInfo.options.source.type)); - } - context.fieldInfo.options.source = optionsSource; - context.fieldInfo.options.values = await context.fieldInfo.options.source.getOptions(); + context.fieldInfo.options.source.provider = optionsSourcesService.getOptionsSource(context.fieldInfo.options.source.providerId); + context.fieldInfo.options.values = await context.fieldInfo.options.source.provider.getOptions(); } catch (e) { disableControlButtons(context.container); @@ -466,35 +456,39 @@ async function processOptionsTypeField(context: FieldContext): Promise { throwUnless(context.fieldInfo.options.optionsType === OptionsType.Dropdown, loc.optionsTypeRadioOrDropdown); optionsComponent = processDropdownOptionsTypeField(context); } - if (context.fieldInfo.options.source) { - const optionsSource = context.fieldInfo.options.source; - for (const key of Object.keys(optionsSource.variableNames ?? {})) { - context.fieldInfo.subFields!.push({ - label: context.fieldInfo.label, - variableName: optionsSource.variableNames[key] - }); - context.onNewInputComponentCreated(optionsSource.variableNames[key], { - component: optionsComponent, - inputValueTransformer: async (controllerName: string) => { - try { - const variableValue = await optionsSource.getVariableValue(key, controllerName); - return variableValue; - } catch (e) { - disableControlButtons(context.container); - context.container.message = { - text: getErrorMessage(e), - description: '', - level: azdata.window.MessageLevel.Error - }; - return ''; - } - }, - isPassword: optionsSource.getIsPassword(key) - }); - } + const optionsSource = context.fieldInfo.options.source; + if (optionsSource?.provider) { + const optionsSourceProvider = optionsSource.provider; + await Promise.all(Object.keys(context.fieldInfo.options.source?.variableNames ?? {}).map(async key => { + await configureOptionsSourceSubfields(context, optionsSource, key, optionsComponent, optionsSourceProvider); + })); } } +async function configureOptionsSourceSubfields(context: FieldContext, optionsSource: IOptionsSource, variableKey: string, optionsComponent: InputComponent, optionsSourceProvider: IOptionsSourceProvider) { + context.fieldInfo.subFields!.push({ + label: context.fieldInfo.label, + variableName: optionsSource.variableNames![variableKey] + }); + context.onNewInputComponentCreated(optionsSource.variableNames![variableKey], { + component: optionsComponent, + inputValueTransformer: async (optionValue: string) => { + try { + return await optionsSourceProvider.getVariableValue!(variableKey, optionValue); + } catch (e) { + disableControlButtons(context.container); + context.container.message = { + text: getErrorMessage(e), + description: '', + level: azdata.window.MessageLevel.Error + }; + return ''; + } + }, + isPassword: await optionsSourceProvider.getIsPassword!(variableKey) + }); +} + function processDropdownOptionsTypeField(context: FieldContext): azdata.DropDownComponent { const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles }); const options = context.fieldInfo.options as OptionsInfo; @@ -1304,7 +1298,7 @@ async function getInputComponentValue(inputComponents: InputComponents, key: str if (input === undefined) { return undefined; } - let value; + let value: string | undefined; if (input instanceof RadioGroupLoadingComponentBuilder) { value = input.value; } else if ('checked' in input) { // CheckBoxComponent @@ -1319,14 +1313,7 @@ async function getInputComponentValue(inputComponents: InputComponents, key: str } else { throw new Error(`Unknown input type with ID ${input.id}`); } - const inputValueTransformer = inputComponents[key].inputValueTransformer; - if (inputValueTransformer) { - value = inputValueTransformer(value ?? ''); - if (typeof value !== 'string') { - value = await value; - } - } - return value; + return inputComponents[key].inputValueTransformer ? await inputComponents[key].inputValueTransformer!(value ?? '') : value; } export function isInputBoxEmpty(input: azdata.InputBoxComponent): boolean { diff --git a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts index f82cfe8634..e101d9d7d0 100644 --- a/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts +++ b/extensions/resource-deployment/src/ui/radioGroupLoadingComponentBuilder.ts @@ -5,7 +5,7 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { OptionsInfo, FieldInfo } from '../interfaces'; -import { getErrorMessage } from '../utils'; +import { getErrorMessage } from '../common/utils'; export class RadioGroupLoadingComponentBuilder implements azdata.ComponentBuilder { private _optionsDivContainer!: azdata.DivContainer; diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index fea5a3e1d4..e68a25a697 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -9,7 +9,7 @@ import { AgreementInfo, DeploymentProvider, ITool, ResourceType, ToolStatus } fr import { select } from '../localizedConstants'; import { IResourceTypeService } from '../services/resourceTypeService'; import { IToolsService } from '../services/toolsService'; -import { getErrorMessage } from '../utils'; +import { getErrorMessage } from '../common/utils'; import * as loc from './../localizedConstants'; import { DialogBase } from './dialogBase'; import { createFlexContainer } from './modelViewUtils';