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