allow registering options source providers to resource-deployment (#12712)

* first draft

* compile fixes

* uncomment code

* waitForAzdataToolDisovery added to azdata api

* missed change in last commit

* remove switchReturn

* contributeOptionsSource renamed

* remove switchReturn reference

* create optionSourceService

* azdataTool usage more reliable

* package.json fixes and cleanup

* cleanup

* revert 4831a6e6b8b08684488b2c9e18092fa252e3057f

* pr feedback

* pr feedback

* pr feedback

* cleanup

* cleanup

* fix eulaAccepted check

* fix whitespade in doc comments.
This commit is contained in:
Arvind Ranasaria
2020-10-05 19:29:40 -07:00
committed by GitHub
parent 362605cea0
commit c679d5e1f0
35 changed files with 534 additions and 377 deletions

View File

@@ -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<string> {
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<string> {
return await treeDataProvider.getPassword(controllerInfo);
}
export async function getRegisteredDataControllers(treeDataProvider: AzureArcTreeDataProvider): Promise<arc.DataController[]> {
return (await treeDataProvider.getChildren())
.filter(node => node instanceof ControllerTreeNode)
.map(node => ({
label: (node as ControllerTreeNode).model.label,
info: (node as ControllerTreeNode).model.info
}));
}

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Deferred } from './promise';
const enum Status {
notStarted,
inProgress,
done
}
interface State<T> {
entry?: T,
error?: Error,
status: Status,
id: number,
pendingOperation: Deferred<void>
}
/**
* An implementation of Cache Manager which ensures that only one call to populate cache miss is pending at a given time.
* All remaining calls for retrieval are awaited until the one in progress finishes and then all awaited calls are resolved with the value
* from the cache.
*/
export class CacheManager<K, T> {
private _cache = new Map<K, State<T>>();
private _id = 0;
public async getCacheEntry(key: K, retrieveEntry: (key: K) => Promise<T>): Promise<T> {
const cacheHit: State<T> | undefined = this._cache.get(key);
// each branch either throws or returns the password.
if (cacheHit === undefined) {
// populate a new state entry and add it to the cache
const state: State<T> = {
status: Status.notStarted,
id: this._id++,
pendingOperation: new Deferred<void>()
};
this._cache.set(key, state);
// now that we have the state entry initialized, retry to fetch the cacheEntry
let returnValue: T = await this.getCacheEntry(key, retrieveEntry);
await state.pendingOperation;
return returnValue!;
} else {
switch (cacheHit.status) {
case Status.notStarted: {
cacheHit.status = Status.inProgress;
// retrieve and populate the missed cache hit.
try {
cacheHit.entry = await retrieveEntry(key);
} catch (error) {
cacheHit.error = error;
} finally {
cacheHit.status = Status.done;
// we do not reject here even in error case because we do not want our awaits on pendingOperation to throw
// We track our own error state and when all done we throw if an error had happened. This results
// in the rejection of the promised returned by this method.
cacheHit.pendingOperation.resolve();
}
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.inProgress: {
await cacheHit.pendingOperation;
return await this.getCacheEntry(key, retrieveEntry);
}
case Status.done: {
if (cacheHit.error !== undefined) {
await cacheHit.pendingOperation;
throw cacheHit.error;
}
else {
await cacheHit.pendingOperation;
return cacheHit.entry!;
}
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<arc.IE
}
});
return {
getRegisteredDataControllers: async () => (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 = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider));
return arcApi(treeDataProvider);
}
export function deactivate(): void {

View File

@@ -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);

View File

@@ -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<string, string>();
readonly optionsSourceId = 'arc.controllers';
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
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<string> {
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<string> {
return this._cacheManager.getCacheEntry(JSON.stringify([variableName, controllerLabel]), this.retrieveVariable);
}
private async getPassword(controller: arc.DataController): Promise<string> {
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));
}
}
}

View File

@@ -8,3 +8,4 @@
/// <reference path='../../../azurecore/src/azurecore.d.ts'/>
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../azdata/src/typings/azdata-ext.d.ts'/>
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>