Skip to content

Commit

Permalink
DBOS Cloud login support (#5)
Browse files Browse the repository at this point in the history
* login command working

* dbos cloud login no longer requires username option

* Launch login flow if retrieving db config fails
  • Loading branch information
devhawk authored Feb 25, 2024
1 parent 0cf9414 commit e742ddb
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 48 deletions.
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

0 comments on commit e742ddb

Please sign in to comment.