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

DBOS Cloud login support #5

Merged
merged 3 commits into from
Feb 25, 2024
Merged
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
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@
"contributes": {
"commands": [
{
"command": "dbos-ttdbg.shutdown-debug-proxy",
"title": "Shutdown Debug Proxy",
"command": "dbos-ttdbg.cloud-login",
"title": "Log into DBOS Cloud",
"category": "DBOS"
},
{
"command": "dbos-ttdbg.delete-prov-db-passwords",
"title": "Delete Stored Provenance DB Passwords",
"category": "DBOS"
},
{
"command": "dbos-ttdbg.shutdown-debug-proxy",
"title": "Shutdown Debug Proxy",
"category": "DBOS"
}
],
"configuration": {
Expand Down
28 changes: 24 additions & 4 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ import * as vscode from 'vscode';
import { logger, config, provDB, debugProxy } from './extension';
import { DbosMethodType } from "./sourceParser";
import { stringify } from './utils';
import { dbos_cloud_login } from './configuration';

export const cloudLoginCommandName = "dbos-ttdbg.cloud-login";
export const startDebuggingCommandName = "dbos-ttdbg.startDebugging";
export const shutdownDebugProxyCommandName = "dbos-ttdbg.shutdown-debug-proxy";
export const deleteProvenanceDatabasePasswordCommandName = "dbos-ttdbg.delete-prov-db-passwords";
export const deleteProvDbPasswordsCommandName = "dbos-ttdbg.delete-prov-db-passwords";

export async function startDebugging(folder: vscode.WorkspaceFolder, name: string, $type: DbosMethodType) {
try {
const clientConfig = await config.getProvDbConfig(folder);
const statuses = await provDB.getWorkflowStatuses(clientConfig, name, $type);
if (!clientConfig) { return; }

const statuses = await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Window },
() => { return provDB.getWorkflowStatuses(clientConfig, name, $type); });

await debugProxy.launch(clientConfig);

// TODO: eventually, we'll need a better UI than "list all workflow IDs and let the user pick one"
Expand Down Expand Up @@ -40,9 +47,9 @@ export async function startDebugging(folder: vscode.WorkspaceFolder, name: strin
}
}

export async function shutdownDebugProxy() {
export function shutdownDebugProxy() {
try {
await debugProxy.shutdown();
debugProxy.shutdown();
} catch (e) {
logger.error("shutdownDebugProxy", e);
}
Expand All @@ -54,4 +61,17 @@ export async function deleteProvenanceDatabasePasswords() {
} catch (e) {
logger.error("deleteProvenanceDatabasePasswords", e);
}
}

export async function cloudLogin() {
try {
const folders = vscode.workspace.workspaceFolders ?? [];
if (folders.length === 1) {
await dbos_cloud_login(folders[0]);
} else {
throw new Error("This command only works when exactly one workspace folder is open");
}
} catch (e) {
logger.error("cloudLogin", e);
}
}
189 changes: 148 additions & 41 deletions src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import { SpawnOptions, spawn as cpSpawn } from "child_process";
import { ClientConfig } from 'pg';
import * as fs from 'node:fs/promises';
import { execFile, exists } from './utils';
import { logger } from './extension';

Expand All @@ -11,31 +11,106 @@ const PROV_DB_DATABASE = "prov_db_database";
const PROV_DB_USER = "prov_db_user";
const DEBUG_PROXY_PORT = "debug_proxy_port";

async function dbos_cloud_app_get(folder: vscode.WorkspaceFolder) {
const { stdout } = await execFile("npx", ["dbos-cloud", "application", "status", "--json"], {
cwd: folder.uri.fsPath,
});
return JSON.parse(stdout) as {
Name: string;
ID: string;
PostgresInstanceName: string;
ApplicationDatabaseName: string;
Status: string;
Version: string;
};
export interface DbosCloudApp {
Name: string;
ID: string;
PostgresInstanceName: string;
ApplicationDatabaseName: string;
Status: string;
Version: string;
}

export interface DbosCloudDatabase {
PostgresInstanceName: string;
HostName: string;
Status: string;
Port: number;
AdminUsername: string;
}

async function dbos_cloud_userdb_status(folder: vscode.WorkspaceFolder, databaseName: string) {
const { stdout } = await execFile("npx", ["dbos-cloud", "database", "status", databaseName, "--json"], {
async function dbos_cloud_cli<T>(folder: vscode.WorkspaceFolder, ...args: string[]): Promise<T> {
const { stdout } = await execFile("npx", ["dbos-cloud", ...args, "--json"], {
cwd: folder.uri.fsPath,
});
return JSON.parse(stdout) as {
PostgresInstanceName: string;
HostName: string;
Status: string;
Port: number;
AdminUsername: string;
};
return JSON.parse(stdout) as T;
}

async function dbos_cloud_app_status(folder: vscode.WorkspaceFolder) {
return dbos_cloud_cli<DbosCloudApp>(folder, "application", "status");
}

async function dbos_cloud_db_status(folder: vscode.WorkspaceFolder, databaseName: string) {
return dbos_cloud_cli<DbosCloudDatabase>(folder, "database", "status", databaseName);
}

export async function dbos_cloud_login(folder: vscode.WorkspaceFolder) {
logger.info("dbos_cloud_login", { folder: folder.uri.fsPath });

const cts = new vscode.CancellationTokenSource();
const loginProc = cpSpawn("npx", ["dbos-cloud", "login"], { cwd: folder.uri.fsPath });
const userCodeEmitter = new vscode.EventEmitter<string>();

const regexLoginUrl = /Login URL: (http.*\/activate\?user_code=([A-Z][A-Z][A-Z][A-Z]-[A-Z][A-Z][A-Z][A-Z]))/;
const regexSuccessfulLogin = /Successfully logged in as (.*)!/;

try {
const ctsPromise = new Promise<void>(resolve => {
cts.token.onCancellationRequested(() => resolve());
});

loginProc.on('exit', () => { logger.info("dbos-cloud login on exit"); cts.cancel(); });
loginProc.on('close', () => { logger.info("dbos-cloud login on close"); cts.cancel(); });
loginProc.on('error', err => { logger.error("dbos-cloud login on error", err); cts.cancel(); });

loginProc.stdout.on("data", async (buffer: Buffer) => {
const data = buffer.toString().trim();
logger.info("dbos-cloud login stdout on data", { data });

const loginUrlMatch = regexLoginUrl.exec(data);
if (loginUrlMatch && loginUrlMatch.length === 3) {
const [, loginUrl, userCode] = loginUrlMatch;
logger.info("dbos-cloud login url", { loginUri: loginUrl, userCode });
userCodeEmitter.fire(userCode);

const openResult = await vscode.env.openExternal(vscode.Uri.parse(loginUrl));
if (!openResult) {
logger.error("dbos_cloud_login openExternal failed", { loginUri: loginUrl, userCode });
cts.cancel();
}
}

const successfulLoginMatch = regexSuccessfulLogin.exec(data);
if (successfulLoginMatch && successfulLoginMatch.length === 2) {
const [, user] = successfulLoginMatch;
logger.info("dbos-cloud login successful", { user });
vscode.window.showInformationMessage(`Successfully logged in to DBOS Cloud as ${user}`);
}
});

await vscode.window.withProgress({
cancellable: true,
location: vscode.ProgressLocation.Notification,
title: "Launching browser to log into DBOS Cloud"
}, async (progress, token) => {
userCodeEmitter.event(userCode => {
progress.report({ message: `\nUser code: ${userCode}` });
});

token.onCancellationRequested(() => cts.cancel());
await ctsPromise;
});
} finally {
loginProc.stdout.removeAllListeners();
loginProc.stderr.removeAllListeners();
loginProc.removeAllListeners();

cts.dispose();
userCodeEmitter.dispose();

const killed = loginProc.killed;
const killResult = loginProc.kill();
logger.info("dbos_cloud_login exit", { killed, killResult });
}
}

interface ExecFileError {
Expand All @@ -55,30 +130,33 @@ function isExecFileError(e: unknown): e is ExecFileError {
return false;
}

async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
interface DatabaseConfig {
host: string | undefined;
port: number | undefined;
database: string | undefined;
user: string | undefined;
}

async function getDbConfigFromDbosCloud(folder: vscode.WorkspaceFolder): Promise<DatabaseConfig | undefined> {
try {
const app = await dbos_cloud_app_get(folder);
const db = await dbos_cloud_userdb_status(folder, app.PostgresInstanceName);
const app = await dbos_cloud_app_status(folder);
const db = await dbos_cloud_db_status(folder, app.PostgresInstanceName);
return {
host: db.HostName,
port: db.Port,
database: app.ApplicationDatabaseName + "_dbos_prov",
user: db.AdminUsername
};
} catch (e) {
if (isExecFileError(e)) {
if (e.stdout.trim().endsWith("Error: not logged in")) {
// TODO: initiate login
vscode.window.showErrorMessage("Not logged in to DBOS Cloud");
}
if (isExecFileError(e) && e.stdout.includes("Error: not logged in")) {
return undefined;
} else {
throw e;
}

logger.error("getDbosCloudInfo", e);
return {};
}
}

async function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): DatabaseConfig {
const cfg = vscode.workspace.getConfiguration(TTDBG_CONFIG_SECTION, folder);

const host = cfg.get<string>(PROV_DB_HOST);
Expand All @@ -94,19 +172,48 @@ async function getDbConfigFromVSCodeConfig(folder: vscode.WorkspaceFolder): Prom
};
}

async function startInvalidCredentialsFlow(folder: vscode.WorkspaceFolder): Promise<void> {
const credentialsPath = vscode.Uri.joinPath(folder.uri, ".dbos", "credentials");
const credentialsExists = await exists(credentialsPath);

// TODO: Register support
const result = await vscode.window.showWarningMessage(
"Invalid DBOS Cloud credentials",
"Login", "Cancel");

if (result === "Login") {
await dbos_cloud_login(folder);
}
}

export class Configuration {
constructor(private readonly secrets: vscode.SecretStorage) { }

async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig> {
async getProvDbConfig(folder: vscode.WorkspaceFolder): Promise<ClientConfig | undefined> {
const dbConfig = await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Window },
async () => {
const cloudConfig = await getDbConfigFromDbosCloud(folder);
const localConfig = getDbConfigFromVSCodeConfig(folder);

const cloudConfig = await getDbConfigFromDbosCloud(folder);
const localConfig = await getDbConfigFromVSCodeConfig(folder);
const host = localConfig?.host ?? cloudConfig?.host;
const port = localConfig?.port ?? cloudConfig?.port ?? 5432;
const database = localConfig?.database ?? cloudConfig?.database;
const user = localConfig?.user ?? cloudConfig?.user;

return { host, port, database, user };
});

if (!dbConfig.host || !dbConfig.database || !dbConfig.user) {
startInvalidCredentialsFlow(folder).catch(e => logger.error("startInvalidCredentialsFlow", e));
return undefined;
}

return {
host: localConfig.host ?? cloudConfig.host,
port: localConfig.port ?? cloudConfig.port,
database: localConfig.database ?? cloudConfig.database,
user: localConfig.user ?? cloudConfig.user,
host: dbConfig.host,
port: dbConfig.port,
database: dbConfig.database,
user: dbConfig.user,
password: () => this.#getPassword(folder),
ssl: {
rejectUnauthorized: false,
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { S3CloudStorage } from './CloudStorage';
import { TTDbgCodeLensProvider } from './codeLensProvider';
import { deleteProvenanceDatabasePasswords, deleteProvenanceDatabasePasswordCommandName as deleteProvDbPasswordsCommandName, startDebuggingCommandName, startDebugging, shutdownDebugProxyCommandName, shutdownDebugProxy } from './commands';
import { deleteProvenanceDatabasePasswords, deleteProvDbPasswordsCommandName, startDebuggingCommandName, startDebugging, shutdownDebugProxyCommandName, shutdownDebugProxy, cloudLoginCommandName, cloudLogin } from './commands';
import { Configuration } from './configuration';
import { DebugProxy, } from './DebugProxy';
import { LogOutputChannelTransport, Logger, createLogger } from './logger';
Expand Down Expand Up @@ -35,6 +35,8 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand(shutdownDebugProxyCommandName, shutdownDebugProxy));
context.subscriptions.push(
vscode.commands.registerCommand(deleteProvDbPasswordsCommandName, deleteProvenanceDatabasePasswords));
context.subscriptions.push(
vscode.commands.registerCommand(cloudLoginCommandName, cloudLogin));

context.subscriptions.push(
vscode.languages.registerCodeLensProvider(
Expand Down