Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Envoy Support #191

Draft
wants to merge 4 commits into
base: 163-2-grpc-transport
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/controllers/ExtensionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import type {
CoreAuthenticatedClient,
ICoreClientFactory,
CoreUnauthenticatedClient,
WorkerURL,
} from '../types';
import { ServerConnectionTreeDragAndDropController } from './ServerConnectionTreeDragAndDropController';
import { ConnectionController } from './ConnectionController';
Expand Down Expand Up @@ -113,6 +114,8 @@ export class ExtensionController implements Disposable {
this.initializeWebViews();
this.initializeServerUpdates();

this._context.subscriptions.push(NodeHttp2gRPCTransport);

logger.info(
'Congratulations, your extension "vscode-deephaven" is now active!'
);
Expand Down Expand Up @@ -342,14 +345,27 @@ export class ExtensionController implements Disposable {
url: URL
): Promise<CoreUnauthenticatedClient & Disposable> => {
assertDefined(this._coreJsApiCache, 'coreJsApiCache');

const workerInfo = await this._serverManager?.getWorkerInfo(
url as WorkerURL
);

const dhc = await this._coreJsApiCache.get(url);

const client = new dhc.CoreClient(url.toString(), {
debug: true,
// TODO: This should be optional, but types aren't happy yet
headers: {},
transportFactory: NodeHttp2gRPCTransport.factory,
}) as CoreUnauthenticatedClient;
const clientUrl = workerInfo == null ? url : workerInfo.grpcUrl;
const envoyPrefix = workerInfo?.envoyPrefix;

const headers: { [key: string]: string } =
// eslint-disable-next-line @typescript-eslint/naming-convention
envoyPrefix == null ? {} : { 'envoy-prefix': envoyPrefix };

const client = new dhc.CoreClient(
clientUrl.toString().replace(/\/$/, ''),
{
headers,
transportFactory: NodeHttp2gRPCTransport.factory,
}
) as CoreUnauthenticatedClient;

// Attach a dispose method so that client caches can dispose of the client
return Object.assign(client, {
Expand Down
12 changes: 7 additions & 5 deletions src/controllers/PanelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
IPanelService,
IServerManager,
VariableDefintion,
WorkerInfo,
WorkerURL,
} from '../types';
import {
Expand Down Expand Up @@ -192,8 +193,8 @@ export class PanelController extends ControllerBase {
const connection = this._serverManager.getConnection(serverUrl);
assertDefined(connection, 'connection');

const isWorkerUrl = Boolean(
await this._serverManager.getWorkerInfo(serverUrl as WorkerURL)
const workerInfo = await this._serverManager.getWorkerInfo(
serverUrl as WorkerURL
);

for (const { id, title } of variables) {
Expand All @@ -202,7 +203,7 @@ export class PanelController extends ControllerBase {
const iframeUrl = await getEmbedWidgetUrlForConnection(
connection,
title,
isWorkerUrl
workerInfo
);

panel.webview.html = getPanelHtml(iframeUrl, title);
Expand Down Expand Up @@ -232,7 +233,7 @@ export class PanelController extends ControllerBase {
export async function getEmbedWidgetUrlForConnection(
connection: ConnectionState,
title: string,
isWorkerUrl: boolean
workerInfo?: WorkerInfo
): Promise<URL> {
return getEmbedWidgetUrl({
serverUrl: connection.serverUrl,
Expand All @@ -241,7 +242,8 @@ export async function getEmbedWidgetUrlForConnection(
// For Core+ workers in DHE, we use `postMessage` apis for auth where DH
// iframe communicates with parent (the extension) to get login credentials
// from the DHE client. See `getPanelHtml` util for more details.
authProvider: isWorkerUrl ? 'parent' : undefined,
authProvider: workerInfo == null ? undefined : 'parent',
envoyPrefix: workerInfo?.envoyPrefix,
psk:
connection instanceof DhcService ? await connection.getPsk() : undefined,
});
Expand Down
27 changes: 16 additions & 11 deletions src/dh/NodeHttp2gRPCTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,29 @@ import { assertDefined } from '../util';
export class NodeHttp2gRPCTransport implements GrpcTransport {
static _sessionMap: Map<string, http2.ClientHttp2Session> = new Map();

/**
* Cleanup.
*/
static dispose(): void {
for (const session of NodeHttp2gRPCTransport._sessionMap.values()) {
session.close();
}
NodeHttp2gRPCTransport._sessionMap.clear();
}

/**
* TODO: Cleanup requests similar to https://github.com/deephaven/deephaven-core/blob/c05b35957e466fded4da61154ba106cfc3198bc5/web/client-api/src/main/java/io/deephaven/web/client/api/grpc/MultiplexedWebsocketTransport.java#L129
* Create a Transport instance.
* @param options Transport options.
* @returns Transport instance.
*/
static readonly factory: GrpcTransportFactory = {
create: options => {
/**
* Create a new transport instance.
* @param options - options for creating the transport
* @return a transport instance to use for gRPC communication
*/
create: (options): GrpcTransport => {
const { origin } = new URL(options.url);

if (!NodeHttp2gRPCTransport._sessionMap.has(origin)) {
Expand Down Expand Up @@ -126,14 +141,4 @@ export class NodeHttp2gRPCTransport implements GrpcTransport {
assertDefined(this._request, '_request');
this._request.close();
}

/**
* Cleanup.
*/
static dispose(): void {
for (const session of NodeHttp2gRPCTransport._sessionMap.values()) {
session.close();
}
NodeHttp2gRPCTransport._sessionMap.clear();
}
}
9 changes: 8 additions & 1 deletion src/dh/dhc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,25 @@ export async function getDhc(
* @param title Widget title
* @param themeKey Theme key
* @param authProvider Optional auth provider
* @param envoyPrefix Optional envoy prefix for Core+ workers
* @param psk Optional psk
*/
export function getEmbedWidgetUrl({
serverUrl,
title,
themeKey,
authProvider,
envoyPrefix,
psk,
}: {
serverUrl: URL;
title: string;
themeKey: string;
authProvider?: 'parent';
envoyPrefix?: string | null;
psk?: string | null;
}): URL {
const url = new URL('/iframe/widget', serverUrl);
const url = new URL('iframe/widget/', serverUrl);

url.searchParams.set('name', title);
url.searchParams.set('theme', themeKey);
Expand All @@ -82,6 +85,10 @@ export function getEmbedWidgetUrl({
url.searchParams.set('authProvider', authProvider);
}

if (envoyPrefix) {
url.searchParams.set('envoyPrefix', envoyPrefix);
}

if (psk) {
url.searchParams.set('psk', psk);
}
Expand Down
13 changes: 11 additions & 2 deletions src/dh/dhe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import type { AuthenticatedClient as DheAuthenticatedClient } from '@deephaven-e
import { hasStatusCode, loadModules } from '@deephaven/jsapi-nodejs';
import type {
ConsoleType,
GrpcURL,
IdeURL,
JsapiURL,
QuerySerial,
UniqueID,
WorkerConfig,
Expand Down Expand Up @@ -271,15 +273,22 @@ export async function getWorkerInfoFromQuery(
return;
}

const { grpcUrl, ideUrl, processInfoId, workerName } = queryInfo.designated;
const { envoyPrefix, grpcUrl, ideUrl, jsApiUrl, processInfoId, workerName } =
queryInfo.designated;

const workerUrl = new URL(jsApiUrl) as WorkerURL;
workerUrl.pathname = workerUrl.pathname.replace(/jsapi\/dh-core.js$/, '');

return {
tagId,
serial: querySerial,
grpcUrl: new URL(grpcUrl) as WorkerURL,
envoyPrefix,
grpcUrl: new URL(grpcUrl) as GrpcURL,
ideUrl: new URL(ideUrl) as IdeURL,
jsapiUrl: new URL(jsApiUrl) as JsapiURL,
processInfoId,
workerName,
workerUrl,
};
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/DheService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as vscode from 'vscode';
import type { AuthenticatedClient as DheAuthenticatedClient } from '@deephaven-enterprise/auth-nodejs';
import type { EnterpriseDhType as DheType } from '@deephaven-enterprise/jsapi-types';
import {
WorkerURL,
type ConsoleType,
type IAsyncCacheService,
type IConfigService,
Expand All @@ -13,6 +12,7 @@ import {
type UniqueID,
type WorkerConfig,
type WorkerInfo,
type WorkerURL,
} from '../types';
import { URLMap } from './URLMap';
import { Logger } from '../util';
Expand Down Expand Up @@ -231,7 +231,7 @@ export class DheService implements IDheService {
throw new Error('Failed to create worker.');
}

this._workerInfoMap.set(workerInfo.grpcUrl, workerInfo);
this._workerInfoMap.set(workerInfo.workerUrl, workerInfo);

return workerInfo;
};
Expand Down
11 changes: 7 additions & 4 deletions src/services/ServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import type {
WorkerInfo,
IDheService,
IAsyncCacheService,
WorkerURL,
UniqueID,
IToastService,
CoreAuthenticatedClient,
ISecretService,
Psk,
WorkerURL,
} from '../types';
import {
getInitialServerStates,
Expand Down Expand Up @@ -211,7 +211,7 @@ export class ServerManager implements IServerManager {
// this indicates that the user cancelled the creation before it was ready.
// In this case, dispose of the worker.
if (!this._connectionMap.has(placeholderUrl)) {
dheService.deleteWorker(workerInfo.grpcUrl);
dheService.deleteWorker(workerInfo.workerUrl);
this._onDidUpdate.fire();
return null;
}
Expand All @@ -228,11 +228,14 @@ export class ServerManager implements IServerManager {

// Map the worker URL to the server URL to make things easier to dispose
// later.
this._workerURLToServerURLMap.set(new URL(workerInfo.grpcUrl), serverUrl);
this._workerURLToServerURLMap.set(
new URL(workerInfo.workerUrl),
serverUrl
);

// Update the server URL to the worker url to be used below with core
// connection creation.
serverUrl = new URL(workerInfo.grpcUrl);
serverUrl = new URL(workerInfo.workerUrl);
}

const connection = this._dhcServiceFactory.create(serverUrl, tagId);
Expand Down
9 changes: 7 additions & 2 deletions src/types/commonTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,22 @@ export interface ConnectionState {
readonly tagId?: UniqueID;
}

export type WorkerURL = Brand<'GrpcUrl', URL>;
export type GrpcURL = Brand<'GrpcURL', URL>;
export type IdeURL = Brand<'IdeUrl', URL>;
export type JsapiURL = Brand<'JsapiURL', URL>;
export type QuerySerial = Brand<'QuerySerial', string>;
export type WorkerURL = Brand<'WorkerURL', URL>;

export interface WorkerInfo {
tagId: UniqueID;
grpcUrl: WorkerURL;
envoyPrefix: string | null;
grpcUrl: GrpcURL;
ideUrl: IdeURL;
jsapiUrl: JsapiURL;
processInfoId: string | null;
serial: QuerySerial;
workerName: string | null;
workerUrl: WorkerURL;
}

export interface Disposable {
Expand Down
5 changes: 2 additions & 3 deletions src/types/serviceTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,18 @@ import type {
EventListenerT,
ConnectionState,
ServerState,
UnsubscribeEventListener,
VariableChanges,
VariableDefintion,
VariableID,
WorkerInfo,
WorkerURL,
UniqueID,
UserKeyPairs,
UserLoginPreferences,
CoreUnauthenticatedClient,
Psk,
CoreAuthenticatedClient,
} from '../types/commonTypes';
WorkerURL,
} from './commonTypes';
import type {
AuthenticatedClient as DheAuthenticatedClient,
UnauthenticatedClient as DheUnauthenticatedClient,
Expand Down