diff --git a/server-tools/lake-lamp-authz/deployment-event-worker.js b/server-tools/lake-lamp-authz/deployment-event-worker.js new file mode 100644 index 0000000..d968992 --- /dev/null +++ b/server-tools/lake-lamp-authz/deployment-event-worker.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +"use strict"; + +// Resident deployment hand: it consumes only immutable events created by the +// explicit dispatch endpoint. It has no public listener and no command field. +const fs = require("node:fs"); +const path = require("node:path"); +const { provision } = require("./architecture-provision-broker"); + +const QUEUE_DIR = process.env.LAKE_LAMP_DEPLOYMENT_EVENT_DIR || "/var/lib/guanghu/deployment-events"; +const RECEIPTS_DIR = process.env.LAKE_LAMP_DEPLOYMENT_RECEIPTS_DIR || "/var/lib/guanghu/deployment-events/receipts"; +const REGISTRY_FILE = process.env.LAKE_LAMP_DEPLOYMENT_REPOSITORIES || "/etc/guanghu/lake-lamp/deployment-repositories.json"; + +function loadRegistry(file = REGISTRY_FILE) { + const parsed = JSON.parse(fs.readFileSync(file, "utf8")); + if (!parsed || !parsed.repos || typeof parsed.repos !== "object") throw new Error("invalid_deployment_repository_registry"); + return parsed.repos; +} + +async function processOne(options = {}) { + const queueDir = options.queueDir || QUEUE_DIR, receiptsDir = options.receiptsDir || RECEIPTS_DIR; + fs.mkdirSync(queueDir, { recursive: true, mode: 0o750 }); + fs.mkdirSync(receiptsDir, { recursive: true, mode: 0o700 }); + const file = fs.readdirSync(queueDir).filter(name => name.endsWith(".json")).sort()[0]; + if (!file) return { ok: true, state: "idle" }; + const original = path.join(queueDir, file), processing = `${original}.processing`; + try { fs.renameSync(original, processing); } catch { return { ok: true, state: "contended" }; } + let event; + try { + event = JSON.parse(fs.readFileSync(processing, "utf8")); + const checked = validateEvent(event, options.registry || loadRegistry(options.registryFile)); + if (checked) throw new Error(checked); + const result = await (options.provisionFn || provision)({ target: "JD-FD-PRIMARY", action: "provision-approved-architecture", resource: event.resource }, { repoUrl: options.registry ? options.registry[event.repo].repo_url : loadRegistry(options.registryFile)[event.repo].repo_url }); + const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event.event_id, workorder_id: event.workorder_id, repo: event.repo, branch: event.branch, commit_sha: event.commit_sha, resource: event.resource, result: result.ok ? "DEPLOYED_AND_VERIFIED" : "FAILED_OR_ROLLED_BACK", diagnostic_code: result.ok ? "deployment_succeeded" : String(result.error || "deployment_failed"), evidence: result, recorded_at: new Date().toISOString() }; + writeAtomic(path.join(receiptsDir, `${event.event_id}.json`), `${JSON.stringify(receipt, null, 2)}\n`); + fs.renameSync(processing, `${processing}.${result.ok ? "done" : "failed"}`); + return { ok: result.ok, state: receipt.result, receipt }; + } catch (error) { + const receipt = { schema: "guanghu.deployment-agent-receipt/v1", event_id: event && event.event_id || "", result: "REJECTED", diagnostic_code: String(error && error.message || "deployment_event_invalid").slice(0, 160), recorded_at: new Date().toISOString() }; + writeAtomic(path.join(receiptsDir, `${path.basename(file, ".json")}.rejected.json`), `${JSON.stringify(receipt, null, 2)}\n`); + fs.renameSync(processing, `${processing}.rejected`); + return { ok: false, state: "REJECTED", receipt }; + } +} +function validateEvent(event, registry) { + if (!event || event.schema !== "guanghu.deployment-event/v1" || event.state !== "queued_for_resident_agent") return "deployment_event_schema_invalid"; + if (!registry[event.repo] || !/^bingshuo\/[a-z0-9._-]+$/.test(event.repo) || event.branch !== "main" || !/^[0-9a-f]{40}$/.test(event.commit_sha || "")) return "deployment_event_binding_invalid"; + if (!/^[A-Z0-9][A-Z0-9._-]{5,119}@[0-9a-f]{40}$/.test(event.resource || "") || !event.resource.endsWith(`@${event.commit_sha}`)) return "deployment_event_resource_invalid"; + if (!/^deployment\/requests\/[A-Za-z0-9._/-]{1,180}\.json$/.test(event.manifest || "")) return "deployment_event_manifest_invalid"; + return ""; +} +function writeAtomic(file, content) { const temp = `${file}.${process.pid}.tmp`; fs.writeFileSync(temp, content, { mode: 0o600 }); fs.renameSync(temp, file); } + +if (require.main === module) setInterval(() => { processOne().catch(error => process.stderr.write(`deployment event error: ${error.message}\n`)); }, 1500); +module.exports = { processOne, validateEvent }; diff --git a/server-tools/lake-lamp-authz/deployment-event-worker.test.js b/server-tools/lake-lamp-authz/deployment-event-worker.test.js new file mode 100644 index 0000000..8a963c4 --- /dev/null +++ b/server-tools/lake-lamp-authz/deployment-event-worker.test.js @@ -0,0 +1,16 @@ +"use strict"; +const test = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { processOne } = require("./deployment-event-worker"); +test("resident deployment agent consumes only an explicit immutable event and writes a receipt", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-deploy-worker-")), queue = path.join(root, "queue"), receipts = path.join(root, "receipts"), sha = "a".repeat(40); + fs.mkdirSync(queue); fs.writeFileSync(path.join(queue, "event.json"), JSON.stringify({ schema: "guanghu.deployment-event/v1", event_id: "event-1", state: "queued_for_resident_agent", repo: "bingshuo/guanghu-ice-heart", branch: "main", commit_sha: sha, workorder_id: "order-1", resource: `GLS-0239-DEPLOY@${sha}`, manifest: "deployment/requests/GLS-0239.json" })); + try { + const result = await processOne({ queueDir: queue, receiptsDir: receipts, registry: { "bingshuo/guanghu-ice-heart": { repo_url: "https://example.invalid/code.git" } }, provisionFn: async request => { assert.equal(request.resource, `GLS-0239-DEPLOY@${sha}`); return { ok: true, unit: "example.service" }; } }); + assert.equal(result.state, "DEPLOYED_AND_VERIFIED"); + assert.equal(JSON.parse(fs.readFileSync(path.join(receipts, "event-1.json"))).diagnostic_code, "deployment_succeeded"); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/server-tools/lake-lamp-authz/deployment-repositories.example.json b/server-tools/lake-lamp-authz/deployment-repositories.example.json new file mode 100644 index 0000000..3ee6c3e --- /dev/null +++ b/server-tools/lake-lamp-authz/deployment-repositories.example.json @@ -0,0 +1 @@ +{"repos":{"bingshuo/guanghu-ice-heart":{"repo_url":"https://guanghulab.com/code/bingshuo/guanghu-ice-heart.git"}}} diff --git a/server-tools/lake-lamp-authz/lake-lamp-deployment-event-worker.service b/server-tools/lake-lamp-authz/lake-lamp-deployment-event-worker.service new file mode 100644 index 0000000..48b133a --- /dev/null +++ b/server-tools/lake-lamp-authz/lake-lamp-deployment-event-worker.service @@ -0,0 +1,21 @@ +[Unit] +Description=Guanghu explicit deployment event worker +After=network-online.target lake-lamp-authz.service lake-lamp-architecture-provision.service + +[Service] +Type=simple +User=root +Group=root +EnvironmentFile=/etc/guanghu/secrets/lake-lamp/action-broker.env +ExecStart=/usr/bin/node /opt/guanghu/lake-lamp-authz/deployment-event-worker.js +Restart=on-failure +RestartSec=3 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/var/lib/guanghu/deployment-events /var/lib/guanghu/architecture-provision /opt/guanghu/architecture-releases /etc/systemd/system +LockPersonality=true + +[Install] +WantedBy=multi-user.target