Update src/api/provisionAgent.js

This commit is contained in:
jester 2025-12-20 19:39:43 +00:00
parent 7a336367e9
commit 1a960f05e3

View File

@ -2,10 +2,11 @@
// FINAL AGENT-DRIVEN PROVISIONING PIPELINE // FINAL AGENT-DRIVEN PROVISIONING PIPELINE
// Supports: paper, vanilla, purpur, forge, fabric, neoforge + dev containers // Supports: paper, vanilla, purpur, forge, fabric, neoforge + dev containers
// //
// Phase 12-14-25: // Updated (Dec 2025):
// - Orchestrator remains unified // - Keep V3 hostname behavior (FQDN: mc-vanilla-5072.zerolaghub.quest)
// - Game/Dev validation split // - Decouple edge publishing from PortPool allocation
// - Dev containers provision like game infra, diverge at runtime semantics // - Minecraft does NOT allocate PortPool ports, but still publishes edge using routing port 25565
// - Preserve game/dev validation split (normalizeGameRequest / normalizeDevRequest)
import "dotenv/config"; import "dotenv/config";
import fetch from "node-fetch"; import fetch from "node-fetch";
@ -43,6 +44,8 @@ const AGENT_TEMPLATE_VMID = Number(
const AGENT_PORT = Number(process.env.ZLH_AGENT_PORT || 18888); const AGENT_PORT = Number(process.env.ZLH_AGENT_PORT || 18888);
const AGENT_TOKEN = process.env.ZLH_AGENT_TOKEN || null; const AGENT_TOKEN = process.env.ZLH_AGENT_TOKEN || null;
// V3 behavior: slotHostname is FQDN built here
const ZONE = process.env.TECHNITIUM_ZONE || "zerolaghub.quest"; const ZONE = process.env.TECHNITIUM_ZONE || "zerolaghub.quest";
/* ------------------------------------------------------------- /* -------------------------------------------------------------
@ -93,9 +96,12 @@ function generateSystemHostname({ ctype, game, variant, vmid }) {
let varPart = ""; let varPart = "";
if (g.includes("minecraft")) { if (g.includes("minecraft")) {
if (["paper", "forge", "fabric", "vanilla", "purpur", "neoforge"].includes(v)) if (
["paper", "forge", "fabric", "vanilla", "purpur", "neoforge"].includes(v)
) {
varPart = v; varPart = v;
} }
}
return varPart ? `${prefix}-${varPart}-${vmid}` : `${prefix}-${vmid}`; return varPart ? `${prefix}-${varPart}-${vmid}` : `${prefix}-${vmid}`;
} }
@ -243,7 +249,6 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
const step = data.installStep || data.currentStep || "unknown"; const step = data.installStep || data.currentStep || "unknown";
const progress = data.progress || ""; const progress = data.progress || "";
// Log state changes and progress
if (step !== lastLoggedStep) { if (step !== lastLoggedStep) {
console.log(`[AGENT ${ip}] state=${state} step=${step} ${progress}`); console.log(`[AGENT ${ip}] state=${state} step=${step} ${progress}`);
lastLoggedStep = step; lastLoggedStep = step;
@ -261,8 +266,10 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
} }
} }
} catch (err) { } catch (err) {
// Only log non-connection errors (agent might not be up yet) if (
if (!err.message.includes("ECONNREFUSED") && !err.message.includes("fetch failed")) { !err.message.includes("ECONNREFUSED") &&
!err.message.includes("fetch failed")
) {
console.error(`[AGENT ${ip}] Poll error:`, err.message); console.error(`[AGENT ${ip}] Poll error:`, err.message);
} }
} }
@ -277,13 +284,13 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
------------------------------------------------------------- */ ------------------------------------------------------------- */
export async function provisionAgentInstance(body = {}) { export async function provisionAgentInstance(body = {}) {
const ctype = body.ctype || "game"; const ctype = body.ctype || "game";
console.log(`[agentProvision] STEP 0: Starting ${ctype} container provisioning`); console.log(`[agentProvision] STEP 0: Starting ${ctype} container provisioning`);
const req = const req =
ctype === "dev" ctype === "dev" ? normalizeDevRequest(body) : normalizeGameRequest(body);
? normalizeDevRequest(body)
: normalizeGameRequest(body); const gameLower = String(req.game || "").toLowerCase();
const isMinecraft = ctype === "game" && gameLower.includes("minecraft");
let vmid; let vmid;
let ctIp; let ctIp;
@ -291,13 +298,13 @@ export async function provisionAgentInstance(body = {}) {
let txnId = null; let txnId = null;
try { try {
console.log('[agentProvision] STEP 1: allocate VMID'); console.log("[agentProvision] STEP 1: allocate VMID");
vmid = await allocateVmid(ctype); vmid = await allocateVmid(ctype);
console.log(`[agentProvision] → Allocated vmid=${vmid}`); console.log(`[agentProvision] → Allocated vmid=${vmid}`);
// Allocate ports if needed // Allocate ports if needed (Minecraft skips PortPool; uses 25565 via Velocity)
if (req.portsNeeded && req.portsNeeded > 0) { if (!isMinecraft && req.portsNeeded && req.portsNeeded > 0) {
console.log(`[agentProvision] STEP 2: port allocation`); console.log("[agentProvision] STEP 2: port allocation");
txnId = crypto.randomUUID(); txnId = crypto.randomUUID();
const portObjs = await PortAllocationService.reserve({ const portObjs = await PortAllocationService.reserve({
@ -305,19 +312,18 @@ export async function provisionAgentInstance(body = {}) {
variant: req.variant, variant: req.variant,
customerId: req.customerId, customerId: req.customerId,
vmid, vmid,
purpose: ctype === 'game' ? 'game_main' : 'dev', purpose: ctype === "game" ? "game_main" : "dev",
txnId, txnId,
count: req.portsNeeded, count: req.portsNeeded,
}); });
// Extract port numbers from objects
allocatedPorts = Array.isArray(portObjs) allocatedPorts = Array.isArray(portObjs)
? portObjs.map(p => typeof p === 'object' ? p.port : p) ? portObjs.map((p) => (typeof p === "object" ? p.port : p))
: [portObjs]; : [portObjs];
console.log(`[agentProvision] → Allocated ports: ${allocatedPorts.join(', ')}`); console.log(`[agentProvision] → Allocated ports: ${allocatedPorts.join(", ")}`);
} else { } else {
console.log(`[agentProvision] STEP 2: port allocation (skipped - no ports needed)`); console.log("[agentProvision] STEP 2: port allocation (skipped)");
} }
const hostname = generateSystemHostname({ const hostname = generateSystemHostname({
@ -327,10 +333,12 @@ export async function provisionAgentInstance(body = {}) {
vmid, vmid,
}); });
// Generate FQDN for DNS/EdgePublisher // V3 correct behavior: build FQDN here
const slotHostname = `${hostname}.${ZONE}`; const slotHostname = `${hostname}.${ZONE}`;
console.log(`[agentProvision] STEP 3: clone template ${AGENT_TEMPLATE_VMID} → vmid=${vmid}`); console.log(
`[agentProvision] STEP 3: clone template ${AGENT_TEMPLATE_VMID} → vmid=${vmid}`
);
await cloneContainer({ await cloneContainer({
templateVmid: AGENT_TEMPLATE_VMID, templateVmid: AGENT_TEMPLATE_VMID,
vmid, vmid,
@ -338,7 +346,7 @@ export async function provisionAgentInstance(body = {}) {
full: 1, full: 1,
}); });
console.log(`[agentProvision] STEP 4: configure CPU/mem/bridge/tags`); console.log("[agentProvision] STEP 4: configure CPU/mem/bridge/tags");
await configureContainer({ await configureContainer({
vmid, vmid,
cpu: req.cpuCores || 2, cpu: req.cpuCores || 2,
@ -346,15 +354,14 @@ export async function provisionAgentInstance(body = {}) {
bridge: ctype === "dev" ? "vmbr2" : "vmbr3", bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
}); });
console.log(`[agentProvision] STEP 5: start container`); console.log("[agentProvision] STEP 5: start container");
await startWithRetry(vmid); await startWithRetry(vmid);
console.log(`[agentProvision] STEP 6: detect container IP`); console.log("[agentProvision] STEP 6: detect container IP");
ctIp = await getCtIpWithRetry(vmid); ctIp = await getCtIpWithRetry(vmid);
console.log(`[agentProvision] → ctIp=${ctIp}`); console.log(`[agentProvision] → ctIp=${ctIp}`);
console.log(`[agentProvision] STEP 7: build agent payload`); console.log("[agentProvision] STEP 7: build agent payload");
// Build payload WITH ports
const payload = const payload =
ctype === "dev" ctype === "dev"
? buildDevAgentPayload({ ? buildDevAgentPayload({
@ -367,16 +374,17 @@ export async function provisionAgentInstance(body = {}) {
: buildGameAgentPayload({ : buildGameAgentPayload({
vmid, vmid,
...req, ...req,
ports: allocatedPorts, // agent can still use ports; for minecraft, provide 25565 semantic port
ports: allocatedPorts.length > 0 ? allocatedPorts : isMinecraft ? [25565] : [],
}); });
console.log(`[agentProvision] STEP 8: POST /config to agent (async provision+start)`); console.log("[agentProvision] STEP 8: POST /config to agent (async provision+start)");
await sendAgentConfig({ ip: ctIp, payload }); await sendAgentConfig({ ip: ctIp, payload });
console.log(`[agentProvision] STEP 9: wait for agent to be running via /status`); console.log("[agentProvision] STEP 9: wait for agent to be running via /status");
await waitForAgentRunning({ ip: ctIp }); await waitForAgentRunning({ ip: ctIp });
console.log(`[agentProvision] STEP 10: DB save`); console.log("[agentProvision] STEP 10: DB save");
await prisma.containerInstance.create({ await prisma.containerInstance.create({
data: { data: {
vmid, vmid,
@ -384,38 +392,47 @@ export async function provisionAgentInstance(body = {}) {
ctype, ctype,
hostname, hostname,
ip: ctIp, ip: ctIp,
allocatedPorts: allocatedPorts, // ← FIXED: Was 'ports', now matches schema allocatedPorts, // matches schema
payload, payload,
agentState: "running", agentState: "running",
agentLastSeen: new Date(), agentLastSeen: new Date(),
}, },
}); });
// Enqueue EdgePublisher with ALL required fields // STEP 11: commit ports ONLY if allocated from PortPool
if (allocatedPorts.length > 0) { if (allocatedPorts.length > 0) {
console.log(`[agentProvision] STEP 11: commit ports`); console.log("[agentProvision] STEP 11: commit ports");
await PortAllocationService.commit({ vmid, ports: allocatedPorts }); await PortAllocationService.commit({ vmid, ports: allocatedPorts });
} else {
console.log("[agentProvision] STEP 11: commit ports (skipped - none allocated)");
}
// STEP 12: publish edge for ALL game servers (Minecraft included)
if (ctype === "game") {
console.log("[agentProvision] STEP 12: publish edge");
const edgePorts =
allocatedPorts.length > 0 ? allocatedPorts : isMinecraft ? [25565] : [];
console.log(`[agentProvision] STEP 12: publish edge`);
await enqueuePublishEdge({ await enqueuePublishEdge({
vmid, vmid,
slotHostname, // ← FQDN for DNS records slotHostname, // FQDN (V3 behavior)
instanceHostname: hostname, // ← Short hostname instanceHostname: hostname, // short (optional, kept for compatibility)
ports: allocatedPorts, // ← CRITICAL ports: edgePorts,
ctIp, ctIp,
game: req.game, game: req.game,
txnId, txnId,
}); });
} else { } else {
console.log(`[agentProvision] STEP 11-12: port commit + edge publish (skipped - no ports)`); console.log("[agentProvision] STEP 12: publish edge (skipped - dev container)");
} }
await confirmVmidAllocated(vmid); await confirmVmidAllocated(vmid);
console.log(`[agentProvision] COMPLETE: success`); console.log("[agentProvision] COMPLETE: success");
return { vmid, hostname, ip: ctIp, ports: allocatedPorts }; return { vmid, hostname, ip: ctIp, ports: allocatedPorts };
} catch (err) { } catch (err) {
console.error(`[agentProvision] ERROR:`, err.message); console.error("[agentProvision] ERROR:", err.message);
// Rollback ports on failure // Rollback ports on failure
if (vmid && allocatedPorts.length > 0) { if (vmid && allocatedPorts.length > 0) {
@ -423,14 +440,19 @@ export async function provisionAgentInstance(body = {}) {
await PortAllocationService.releaseByVmid(vmid); await PortAllocationService.releaseByVmid(vmid);
console.log(`[agentProvision] → Rolled back ports for vmid=${vmid}`); console.log(`[agentProvision] → Rolled back ports for vmid=${vmid}`);
} catch (rollbackErr) { } catch (rollbackErr) {
console.error(`[agentProvision] → Port rollback failed:`, rollbackErr.message); console.error("[agentProvision] → Port rollback failed:", rollbackErr.message);
} }
} }
if (vmid) { if (vmid) {
try { await deleteContainer(vmid); } catch {} try {
try { await releaseVmid(vmid); } catch {} await deleteContainer(vmid);
} catch {}
try {
await releaseVmid(vmid);
} catch {}
} }
throw err; throw err;
} }
} }