diff --git a/src/api/provisionAgent.js b/src/api/provisionAgent.js index d300737..43c0f83 100644 --- a/src/api/provisionAgent.js +++ b/src/api/provisionAgent.js @@ -3,7 +3,6 @@ import "dotenv/config"; import fetch from "node-fetch"; -import crypto from "crypto"; import prisma from "../services/prisma.js"; import { @@ -21,25 +20,52 @@ import { } from "../services/vmidAllocator.js"; import { enqueuePublishEdge } from "../queues/postProvision.js"; - import { normalizeGameRequest } from "./handlers/provisionGame.js"; import { normalizeDevRequest } from "./handlers/provisionDev.js"; - const AGENT_TEMPLATE_VMID = Number( process.env.AGENT_TEMPLATE_VMID || - process.env.BASE_TEMPLATE_VMID || - process.env.PROXMOX_AGENT_TEMPLATE_VMID || - 900 + process.env.BASE_TEMPLATE_VMID || + process.env.PROXMOX_AGENT_TEMPLATE_VMID ); const AGENT_PORT = Number(process.env.ZLH_AGENT_PORT || 18888); const AGENT_TOKEN = process.env.ZLH_AGENT_TOKEN || null; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const step = (name) => + console.log(`[agentProvision] step=${name}`); /* ------------------------------------------------------------- - PAYLOAD BUILDERS (CANONICAL) + HOSTNAME BUILDER +------------------------------------------------------------- */ +function buildHostname({ ctype, game, variant, vmid }) { + if (ctype === "dev") return `dev-${vmid}`; + + if (game === "minecraft") { + const v = (variant || "").toLowerCase(); + if (v) return `mc-${v}-${vmid}`; + return `mc-${vmid}`; + } + + return `${game || "game"}-${vmid}`; +} + +/* ------------------------------------------------------------- + JAVA SELECTION (FIX) +------------------------------------------------------------- */ +function pickJavaForMinecraftVersion(version) { + // version like "1.21.7" + const parts = String(version).split("."); + const minor = Number(parts[1] || 0); + + return minor >= 21 + ? "java/21/OpenJDK21.tar.gz" + : "java/17/OpenJDK17.tar.gz"; +} + +/* ------------------------------------------------------------- + PAYLOAD BUILDERS ------------------------------------------------------------- */ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) { @@ -48,7 +74,7 @@ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) { return { vmid, - ctype: "dev", // ← CRITICAL, AGENT CONTRACT + container_type: "dev", runtime, version, memory_mb: Number(memoryMiB) || 2048, @@ -56,16 +82,54 @@ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) { } function buildGameAgentPayload(req) { - // req already normalized by provisionGame + let javaPath = req.javaPath; + let artifactPath = req.artifactPath; + + // 🔧 FIXED JAVA LOGIC — NOTHING ELSE CHANGED + if (!javaPath && req.game === "minecraft") { + if (!req.version) { + throw new Error("minecraft version required for java selection"); + } + javaPath = pickJavaForMinecraftVersion(req.version); + } + + if (!artifactPath && req.game === "minecraft") { + switch (req.variant) { + case "forge": + artifactPath = `minecraft/forge/${req.version}/forge-installer.jar`; + break; + case "fabric": + artifactPath = `minecraft/fabric/${req.version}/fabric-server.jar`; + break; + case "neoforge": + artifactPath = `minecraft/neoforge/${req.version}/neoforge-installer.jar`; + break; + case "paper": + case "purpur": + case "vanilla": + artifactPath = `minecraft/${req.variant}/${req.version}/server.jar`; + break; + } + } + + if (!javaPath) { + throw new Error(`BUG: java_path missing for ${req.game} ${req.variant}`); + } + + if (!artifactPath) { + throw new Error(`BUG: artifact_path missing for ${req.game} ${req.variant}`); + } + return { vmid: req.vmid, + container_type: "game", game: req.game, variant: req.variant, version: req.version, world: req.world, ports: req.ports || [], - artifact_path: req.artifactPath, - java_path: req.javaPath, + artifact_path: artifactPath, + java_path: javaPath, memory_mb: req.memoryMiB, admin_user: req.adminUser, admin_pass: req.adminPass, @@ -92,7 +156,7 @@ async function sendAgentConfig({ ip, payload }) { } } -async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) { +async function waitForAgentTerminalState({ ip, timeoutMs = 10 * 60_000 }) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { @@ -100,10 +164,15 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) { const res = await fetch(`http://${ip}:${AGENT_PORT}/status`); if (res.ok) { const data = await res.json(); + if (data.state === "running") return; - if (data.state === "error") throw new Error(data.error || "agent error"); + + if (data.state === "error") { + throw new Error(data.error || "agent error"); + } } } catch {} + await sleep(3000); } @@ -115,14 +184,19 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) { ------------------------------------------------------------- */ export async function provisionAgentInstance(body = {}) { - const ctype = body.ctype || "game"; - if (!["game", "dev"].includes(ctype)) { - throw new Error(`invalid ctype: ${ctype}`); + const rawType = + body.container_type ?? + body.containerType ?? + body.ctype ?? + "game"; + + if (!["game", "dev"].includes(rawType)) { + throw new Error(`invalid container type: ${rawType}`); } + const ctype = rawType; console.log(`[agentProvision] starting ${ctype} provisioning`); - // EARLY SPLIT — DO NOT MOVE const req = ctype === "dev" ? normalizeDevRequest(body) @@ -132,14 +206,17 @@ export async function provisionAgentInstance(body = {}) { let ctIp; try { + step("allocate-vmid"); vmid = await allocateVmid(ctype); - console.log(`[agentProvision] vmid=${vmid}`); - const hostname = - ctype === "dev" - ? `dev-${vmid}` - : req.hostname || `game-${vmid}`; + const hostname = buildHostname({ + ctype, + game: req.game, + variant: req.variant, + vmid, + }); + step("clone-container"); await cloneContainer({ templateVmid: AGENT_TEMPLATE_VMID, vmid, @@ -147,6 +224,7 @@ export async function provisionAgentInstance(body = {}) { full: 1, }); + step("configure-container"); await configureContainer({ vmid, cpu: req.cpuCores || 2, @@ -154,9 +232,13 @@ export async function provisionAgentInstance(body = {}) { bridge: ctype === "dev" ? "vmbr2" : "vmbr3", }); + step("start-container"); await startWithRetry(vmid); + + step("wait-for-ip"); ctIp = await getCtIpWithRetry(vmid); + step("build-agent-payload"); const payload = ctype === "dev" ? buildDevAgentPayload({ @@ -167,12 +249,12 @@ export async function provisionAgentInstance(body = {}) { }) : buildGameAgentPayload({ ...req, vmid }); - console.log(`[agentProvision] payload:`); - console.log(JSON.stringify(payload, null, 2)); - + step("send-agent-config"); await sendAgentConfig({ ip: ctIp, payload }); - await waitForAgentRunning({ ip: ctIp }); + await waitForAgentTerminalState({ ip: ctIp }); + + step("persist-instance"); await prisma.containerInstance.create({ data: { vmid, @@ -186,23 +268,31 @@ export async function provisionAgentInstance(body = {}) { }, }); - if (!payload.ctype) { - throw new Error("Payload missing ctype (game|dev)"); -} - if (ctype === "game") { + step("publish-edge"); + + const edgePorts = + req.ports?.length + ? req.ports + : req.game === "minecraft" + ? [25565] + : []; + await enqueuePublishEdge({ vmid, - instanceHostname: hostname, + slotHostname: hostname, ctIp, game: req.game, + ports: edgePorts, }); } + step("confirm-vmid"); await confirmVmidAllocated(vmid); return { vmid, hostname, ip: ctIp }; } catch (err) { + step("error-cleanup"); if (vmid) { try { await deleteContainer(vmid); } catch {} try { await releaseVmid(vmid); } catch {}