feat: configure workspace runtime secrets through API
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user