provisionAgent fixed 12-21-25

This commit is contained in:
jester 2025-12-21 22:12:26 +00:00
parent 43f1853e4f
commit 88d790d42c

View File

@ -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 {}