diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml index 9866de38..797ce445 100644 --- a/deploy/deploy.yaml +++ b/deploy/deploy.yaml @@ -808,6 +808,7 @@ lanes: HWLAB_PROJECT_MANAGEMENT_SOURCE_NAME: HWLAB v0.3 MDTODO HWLAB_PROJECT_MANAGEMENT_PROJECT_ID: project_hwlab_v03 HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops + HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY: secretRef:hwlab-v03-master-server-admin-api-key/api-key HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH: /etc/hwlab/project-management/sources.json OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces OTEL_SERVICE_NAME: hwlab-project-management diff --git a/internal/project-management/server.test.ts b/internal/project-management/server.test.ts index e2757988..0969ab4c 100644 --- a/internal/project-management/server.test.ts +++ b/internal/project-management/server.test.ts @@ -232,6 +232,65 @@ test("project management app bootstraps HWPOD sources from a mounted JSON file", } }); +test("project management HWPOD source adapter authenticates node-ops requests from configured env", async () => { + const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-hwpod-auth-")); + const defaultRoot = join(root, "default"); + const seen = []; + try { + await mkdir(defaultRoot, { recursive: true }); + await writeFile(join(defaultRoot, "MDTODO.md"), "# Default\n\n## R1 Default\n", "utf8"); + const app = createProjectManagementApp({ + env: { + HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL: "http://cloud-api.test/v1/hwpod-node-ops", + HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY: "hwl_live_project_management_test.redacted" + }, + store: createProjectManagementStore({ kind: "memory" }), + sourceRoot: defaultRoot, + sourceId: "default-mdtodo", + projectId: "project_test", + fetchImpl: async (url, init) => { + const headers = new Headers(init.headers); + seen.push({ + url: String(url), + authorization: headers.get("authorization"), + sourceServiceId: headers.get("x-source-service-id") + }); + return new Response(JSON.stringify({ + ok: true, + status: "completed", + results: [{ opId: "op_01", op: "workspace.ls", ok: true, status: "completed", output: { entries: [] } }] + }), { status: 200, headers: { "content-type": "application/json" } }); + } + }); + + const createResponse = await app.fetch(jsonRequest("/v1/project-management/mdtodo/sources", { + method: "POST", + body: { + sourceId: "hwpod-mdtodo", + sourceKind: "hwpod-workspace", + displayName: "D601 F103 MDTODO", + projectId: "project_test", + hwpodId: "d601-f103-v2", + nodeId: "node-d601-f103-v2", + workspaceRootRef: "F:/Work/ConStart", + mdtodoRootRef: "docs/MDTODO/" + } + })); + expect(createResponse.status).toBe(201); + + const probe = await post(app, "/v1/project-management/mdtodo/sources/hwpod-mdtodo/probe", {}); + expect(probe.probe.status).toBe("ok"); + expect(seen[0]).toEqual({ + url: "http://cloud-api.test/v1/hwpod-node-ops", + authorization: "Bearer hwl_live_project_management_test.redacted", + sourceServiceId: "hwlab-project-management" + }); + await app.close(); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + async function json(app, path) { const response = await app.fetch(new Request(`http://service${path}`, { headers: { "x-hwlab-actor-id": "usr_test" } })); expect(response.status).toBe(200); diff --git a/internal/project-management/source-adapter.ts b/internal/project-management/source-adapter.ts index 5e497e93..89eb1d7e 100644 --- a/internal/project-management/source-adapter.ts +++ b/internal/project-management/source-adapter.ts @@ -114,11 +114,14 @@ export function sourceProjection(source, documents) { function createHwpodNodeOpsHttpClient({ env, fetchImpl }) { const endpoint = String(env.HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL ?? env.HWLAB_HWPOD_NODE_OPS_URL ?? "").trim(); + const apiKey = String(env.HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY ?? "").trim(); if (!endpoint || typeof fetchImpl !== "function") return null; return async (plan) => { + const headers = { "content-type": "application/json", "x-source-service-id": "hwlab-project-management" }; + if (apiKey) headers.authorization = `Bearer ${apiKey}`; const response = await fetchImpl(endpoint, { method: "POST", - headers: { "content-type": "application/json", "x-source-service-id": "hwlab-project-management" }, + headers, body: JSON.stringify(plan), signal: AbortSignal.timeout(30000) });