feat: implement metadata catalog database management
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { PassThrough } from "node:stream";
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import type { Client, ClientConfig } from "pg";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import {
|
||||
buildSshArguments,
|
||||
ConcreteCatalogPostgresAccess,
|
||||
} from "../src/catalog/postgres-access.js";
|
||||
import { CATALOG_SECRET_IDS } from "../src/catalog/secrets.js";
|
||||
import type { WorkspaceDatabase } from "../src/catalog/types.js";
|
||||
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function secretStore() {
|
||||
const root = mkdtempSync(join(tmpdir(), "catalog-ssh-secrets-"));
|
||||
const runtimeRoot = mkdtempSync(join(tmpdir(), "catalog-ssh-runtime-"));
|
||||
roots.push(root, runtimeRoot);
|
||||
return new WorkspaceSecretStore({ root, runtimeRoot, installationId: "test" });
|
||||
}
|
||||
|
||||
function sshDatabase(): WorkspaceDatabase {
|
||||
return {
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
workspaceId: "psd-clinical",
|
||||
engine: "postgres",
|
||||
databaseName: "warehouse",
|
||||
schema: "datawarehouse",
|
||||
version: 4,
|
||||
createdAt: "2026-08-27T08:00:00Z",
|
||||
updatedAt: "2026-08-27T09:00:00Z",
|
||||
connectionStatus: "reachable",
|
||||
binding: {
|
||||
transport: "ssh_tunnel",
|
||||
username: "warehouse_reader",
|
||||
sshHost: "bastion.internal",
|
||||
sshPort: 2222,
|
||||
sshUsername: "tunnel_user",
|
||||
sshTargetHost: "postgres.internal",
|
||||
sshTargetPort: 5432,
|
||||
tlsServername: "postgres.internal",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fakeChild(): ChildProcessWithoutNullStreams {
|
||||
const child = new EventEmitter() as EventEmitter & {
|
||||
stdin: PassThrough;
|
||||
stdout: PassThrough;
|
||||
stderr: PassThrough;
|
||||
exitCode: number | null;
|
||||
signalCode: NodeJS.Signals | null;
|
||||
kill: (signal?: NodeJS.Signals | number) => boolean;
|
||||
};
|
||||
child.stdin = new PassThrough();
|
||||
child.stdout = new PassThrough();
|
||||
child.stderr = new PassThrough();
|
||||
child.exitCode = null;
|
||||
child.signalCode = null;
|
||||
child.kill = vi.fn((signal: NodeJS.Signals | number = "SIGTERM") => {
|
||||
child.signalCode = typeof signal === "string" ? signal : "SIGTERM";
|
||||
child.emit("exit", null, child.signalCode);
|
||||
return true;
|
||||
});
|
||||
return child as unknown as ChildProcessWithoutNullStreams;
|
||||
}
|
||||
|
||||
test("builds a strict host-verified OpenSSH stdio tunnel", () => {
|
||||
const args = buildSshArguments({
|
||||
sshHost: "bastion.internal",
|
||||
sshPort: 2222,
|
||||
sshUsername: "tunnel_user",
|
||||
targetHost: "postgres.internal",
|
||||
targetPort: 5432,
|
||||
privateKeyFile: "/runtime/id",
|
||||
knownHostsFile: "/runtime/known_hosts",
|
||||
passphraseFile: "/runtime/passphrase",
|
||||
connectTimeoutMs: 5_001,
|
||||
});
|
||||
|
||||
expect(args).toEqual(expect.arrayContaining([
|
||||
"-F", "/dev/null",
|
||||
"-o", "BatchMode=no",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "UserKnownHostsFile=/runtime/known_hosts",
|
||||
"-o", "GlobalKnownHostsFile=/dev/null",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "IdentityAgent=none",
|
||||
"-o", "PasswordAuthentication=no",
|
||||
"-o", "KbdInteractiveAuthentication=no",
|
||||
"-o", "ConnectTimeout=6",
|
||||
"-W", "postgres.internal:5432",
|
||||
"--", "tunnel_user@bastion.internal",
|
||||
]));
|
||||
});
|
||||
|
||||
test("connects pg through OpenSSH, supplies askpass, and releases all secret leases", async () => {
|
||||
const store = secretStore();
|
||||
store.putMany("psd-clinical", {
|
||||
[CATALOG_SECRET_IDS.password]: "db-password ",
|
||||
[CATALOG_SECRET_IDS.sshPrivateKey]: "PRIVATE KEY\n",
|
||||
[CATALOG_SECRET_IDS.sshPrivateKeyPassphrase]: "key-passphrase",
|
||||
[CATALOG_SECRET_IDS.sshKnownHosts]: "bastion.internal ssh-ed25519 AAAATEST\n",
|
||||
[CATALOG_SECRET_IDS.tlsCa]: "CA CERTIFICATE\n",
|
||||
});
|
||||
const child = fakeChild();
|
||||
let clientConfig: ClientConfig | undefined;
|
||||
let spawnCall: { command: string; args: readonly string[]; env: NodeJS.ProcessEnv } | undefined;
|
||||
const end = vi.fn(async () => undefined);
|
||||
const query = vi.fn(async () => ({ rows: [{ ok: true }] }));
|
||||
const connect = vi.fn(async () => undefined);
|
||||
|
||||
const access = new ConcreteCatalogPostgresAccess(store, {
|
||||
sshBinary: "/usr/bin/ssh",
|
||||
askpassPath: "/app/ssh-askpass.mjs",
|
||||
connectTimeoutMs: 5_000,
|
||||
spawnSsh: (command, args, options) => {
|
||||
spawnCall = { command, args, env: options.env };
|
||||
return child;
|
||||
},
|
||||
createClient: (config) => {
|
||||
clientConfig = config;
|
||||
return { connect, query, end } as unknown as Client;
|
||||
},
|
||||
});
|
||||
|
||||
const client = await access.connect(sshDatabase(), new AbortController().signal);
|
||||
expect(connect).toHaveBeenCalledOnce();
|
||||
expect(clientConfig).toMatchObject({
|
||||
host: "postgres.internal",
|
||||
port: 5432,
|
||||
database: "warehouse",
|
||||
user: "warehouse_reader",
|
||||
password: "db-password ",
|
||||
connectionTimeoutMillis: 5_000,
|
||||
ssl: {
|
||||
ca: "CA CERTIFICATE\n",
|
||||
servername: "postgres.internal",
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
});
|
||||
expect(clientConfig?.stream).toBeTypeOf("function");
|
||||
expect(spawnCall?.command).toBe("/usr/bin/ssh");
|
||||
expect(spawnCall?.args.some((argument) => argument.startsWith("IdentityFile="))).toBe(true);
|
||||
expect(spawnCall?.args.some((argument) => argument.startsWith("UserKnownHostsFile="))).toBe(true);
|
||||
expect(spawnCall?.env).toMatchObject({
|
||||
DISPLAY: "thothii",
|
||||
SSH_ASKPASS: "/app/ssh-askpass.mjs",
|
||||
SSH_ASKPASS_REQUIRE: "force",
|
||||
});
|
||||
const leasedPaths = spawnCall!.args
|
||||
.filter((argument) => argument.startsWith("IdentityFile=") || argument.startsWith("UserKnownHostsFile="))
|
||||
.map((argument) => argument.slice(argument.indexOf("=") + 1));
|
||||
leasedPaths.push(spawnCall!.env.THT_SSH_PASSPHRASE_FILE!);
|
||||
expect(leasedPaths.every(existsSync)).toBe(true);
|
||||
|
||||
await expect(client.query("SELECT 1", [])).resolves.toEqual({ rows: [{ ok: true }] });
|
||||
await client.end();
|
||||
|
||||
expect(end).toHaveBeenCalledOnce();
|
||||
expect(child.kill).toHaveBeenCalledWith("SIGTERM");
|
||||
expect(leasedPaths.some(existsSync)).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user