feat: configure workspace runtime secrets through API

This commit is contained in:
2026-08-14 17:30:35 +02:00
parent 2114c94704
commit 87cefd120c
7 changed files with 325 additions and 41 deletions
+98 -2
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
@@ -9,6 +9,7 @@ import { loadConfig } from "../src/config.js";
import { createProductionWorkspaceDiagnoser } from "../src/workspaces/diagnostics.js";
import { WorkspaceRegistry, type WorkspaceRevision } from "../src/workspaces/registry.js";
import { serializeWorkspaceYaml, type CanonicalWorkspace } from "../src/workspaces/schema.js";
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
const workspace: CanonicalWorkspace = {
workspace: {
@@ -72,6 +73,7 @@ function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
function appFor(
registry: RegistryFake,
diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] })),
secretStore = testSecretStore(),
) {
return buildApp(loadConfig({
THT_HARNESS_DIR: "/missing-harness",
@@ -80,9 +82,19 @@ function appFor(
thtRunner: {} as any,
workspaceRegistry: registry as WorkspaceRegistry,
workspaceDiagnoser: diagnose,
workspaceSecretStore: secretStore,
} as any);
}
const secretStoreRoots: string[] = [];
function testSecretStore(): WorkspaceSecretStore {
const root = mkdtempSync(join(tmpdir(), "thoth-route-secret-store-"));
const runtimeRoot = mkdtempSync(join(tmpdir(), "thoth-route-secret-runtime-"));
secretStoreRoots.push(root, runtimeRoot);
return new WorkspaceSecretStore({ root, runtimeRoot, installationId: "route-test" });
}
test("returns a redacted registry status and pulls without Git credential details", async () => {
const registry = registryFake({
bootstrap: vi.fn(async () => ({
@@ -126,7 +138,7 @@ test("lists workspace summaries and reads a validated immutable workspace", asyn
expect(list.json()).toEqual([expect.objectContaining({
id: "psd-clinical",
displayName: "Policlinico San Donato",
configurationState: "ready",
configurationState: "configuration_required",
revision,
})]);
expect(read.statusCode).toBe(200);
@@ -174,6 +186,89 @@ test("runs diagnostics for a schema v3 workspace", async () => {
}, { writeProbe: false });
});
test("reports runtime secret requirements without returning stored values", async () => {
const secretStore = testSecretStore();
const app = appFor(registryFake(), undefined, secretStore);
const missing = await app.inject({
method: "GET", url: "/workspaces/psd-clinical/runtime-configuration",
});
expect(missing.statusCode).toBe(200);
expect(missing.json()).toMatchObject({
workspaceId: "psd-clinical",
revision,
configurationState: "configuration_required",
requirements: [{
id: "dwh.password",
connector: "dwh",
label: "Data warehouse password",
required: true,
configured: false,
}],
});
const secret = "never-return-this-password";
const save = await app.inject({
method: "PUT",
url: "/workspaces/psd-clinical/secrets",
payload: { values: { "dwh.password": secret } },
});
expect(save.statusCode).toBe(200);
expect(save.body).not.toContain(secret);
expect(save.json()).toMatchObject({
configurationState: "ready",
requirements: [{ id: "dwh.password", configured: true }],
});
const configured = await app.inject({
method: "GET", url: "/workspaces/psd-clinical/runtime-configuration",
});
expect(configured.body).not.toContain(secret);
expect(configured.json()).toMatchObject({ configurationState: "ready" });
});
test("rejects undeclared secret identifiers and supports forgetting a configured secret", async () => {
const secretStore = testSecretStore();
const app = appFor(registryFake(), undefined, secretStore);
const unknown = await app.inject({
method: "PUT",
url: "/workspaces/psd-clinical/secrets",
payload: { values: { "evidence.secret_key": "not-applicable" } },
});
expect(unknown.statusCode).toBe(400);
expect(secretStore.configured("psd-clinical")).toEqual([]);
secretStore.put("psd-clinical", "dwh.password", "temporary-password");
const forget = await app.inject({
method: "DELETE",
url: "/workspaces/psd-clinical/secrets/dwh.password",
});
expect(forget.statusCode).toBe(200);
expect(forget.json()).toMatchObject({ configurationState: "configuration_required" });
expect(secretStore.has("psd-clinical", "dwh.password")).toBe(false);
});
test("materializes stored secrets only for the diagnostic lease", async () => {
const secretStore = testSecretStore();
secretStore.put("psd-clinical", "dwh.password", "diagnostic-password");
let materializedPath = "";
const diagnose = vi.fn(async (_workspace, bindings) => {
materializedPath = bindings.dwh.values.THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE;
expect(readFileSync(materializedPath, "utf8")).toBe("diagnostic-password");
return { activatable: true, diagnostics: [] };
});
const app = appFor(registryFake(), diagnose, secretStore);
const response = await app.inject({
method: "POST", url: "/workspaces/psd-clinical/test", payload: {},
});
expect(response.statusCode).toBe(200);
expect(materializedPath).not.toBe("");
expect(existsSync(materializedPath)).toBe(false);
});
test("reports a missing Evidence credential without changing the registry revision", async () => {
const evidenceWorkspace: CanonicalWorkspace = {
...workspace,
@@ -282,6 +377,7 @@ async function createRealRouteFixture() {
afterEach(() => {
realRouteRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
secretStoreRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
});
test("a failed candidate pull keeps the last valid active workspace", async () => {
@@ -12,6 +12,7 @@ import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { ThtRunner } from "../src/tht/tht-runner.js";
import { WorkspaceRegistry } from "../src/workspaces/registry.js";
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js";
const runFile = promisify(execFile);
@@ -166,6 +167,36 @@ test("real schema-v3 registry revision loads through ThtRunner and the harness c
expect(readdirSync(join(f.registryConfig.root, "snapshots", "runtime"))).toEqual([]);
});
test("ThtRunner uses a vault secret only for the lifetime of its runtime lease", async () => {
const f = await fixture();
vi.stubEnv("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", "");
const vaultRoot = join(f.root, "workspace-secrets");
const runtimeRoot = join(f.root, "workspace-secret-runtime");
const secretStore = new WorkspaceSecretStore({
root: vaultRoot,
runtimeRoot,
installationId: "test",
});
secretStore.put("psd-clinical", "dwh.password", "vault-runtime-password");
const runner = new ThtRunner({
thtBin,
harnessDir,
configPath: "config/tht.yaml",
dataRoot: f.dataRoot,
runtimeSnapshotRoot: join(f.registryConfig.root, "snapshots", "runtime"),
secretRoots: f.registryConfig.secretRoots,
workspaceSecretStore: secretStore,
} as any);
const lease = runner.acquireWorkspaceRuntime(f.revision.snapshotPath);
const rendered = parse(readFileSync(lease.path, "utf8")) as {
database: { password_file: string };
};
expect(readFileSync(rendered.database.password_file, "utf8")).toBe("vault-runtime-password");
lease.release();
expect(existsSync(rendered.database.password_file)).toBe(false);
});
test("separate runtime leases hand off byte-identical revision Evidence configs accepted by tht", async () => {
const f = await fixture();
const runner = runnerFor(f);