provisionAgent fixed 12-21-25
This commit is contained in:
parent
43f1853e4f
commit
88d790d42c
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
import prisma from "../services/prisma.js";
|
import prisma from "../services/prisma.js";
|
||||||
import {
|
import {
|
||||||
@ -21,25 +20,52 @@ import {
|
|||||||
} from "../services/vmidAllocator.js";
|
} from "../services/vmidAllocator.js";
|
||||||
|
|
||||||
import { enqueuePublishEdge } from "../queues/postProvision.js";
|
import { enqueuePublishEdge } from "../queues/postProvision.js";
|
||||||
|
|
||||||
import { normalizeGameRequest } from "./handlers/provisionGame.js";
|
import { normalizeGameRequest } from "./handlers/provisionGame.js";
|
||||||
import { normalizeDevRequest } from "./handlers/provisionDev.js";
|
import { normalizeDevRequest } from "./handlers/provisionDev.js";
|
||||||
|
|
||||||
|
|
||||||
const AGENT_TEMPLATE_VMID = Number(
|
const AGENT_TEMPLATE_VMID = Number(
|
||||||
process.env.AGENT_TEMPLATE_VMID ||
|
process.env.AGENT_TEMPLATE_VMID ||
|
||||||
process.env.BASE_TEMPLATE_VMID ||
|
process.env.BASE_TEMPLATE_VMID ||
|
||||||
process.env.PROXMOX_AGENT_TEMPLATE_VMID ||
|
process.env.PROXMOX_AGENT_TEMPLATE_VMID
|
||||||
900
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
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 }) {
|
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
||||||
@ -48,7 +74,7 @@ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
vmid,
|
vmid,
|
||||||
ctype: "dev", // ← CRITICAL, AGENT CONTRACT
|
container_type: "dev",
|
||||||
runtime,
|
runtime,
|
||||||
version,
|
version,
|
||||||
memory_mb: Number(memoryMiB) || 2048,
|
memory_mb: Number(memoryMiB) || 2048,
|
||||||
@ -56,16 +82,54 @@ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildGameAgentPayload(req) {
|
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 {
|
return {
|
||||||
vmid: req.vmid,
|
vmid: req.vmid,
|
||||||
|
container_type: "game",
|
||||||
game: req.game,
|
game: req.game,
|
||||||
variant: req.variant,
|
variant: req.variant,
|
||||||
version: req.version,
|
version: req.version,
|
||||||
world: req.world,
|
world: req.world,
|
||||||
ports: req.ports || [],
|
ports: req.ports || [],
|
||||||
artifact_path: req.artifactPath,
|
artifact_path: artifactPath,
|
||||||
java_path: req.javaPath,
|
java_path: javaPath,
|
||||||
memory_mb: req.memoryMiB,
|
memory_mb: req.memoryMiB,
|
||||||
admin_user: req.adminUser,
|
admin_user: req.adminUser,
|
||||||
admin_pass: req.adminPass,
|
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;
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
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`);
|
const res = await fetch(`http://${ip}:${AGENT_PORT}/status`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (data.state === "running") return;
|
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 {}
|
} catch {}
|
||||||
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,14 +184,19 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
|||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
|
|
||||||
export async function provisionAgentInstance(body = {}) {
|
export async function provisionAgentInstance(body = {}) {
|
||||||
const ctype = body.ctype || "game";
|
const rawType =
|
||||||
if (!["game", "dev"].includes(ctype)) {
|
body.container_type ??
|
||||||
throw new Error(`invalid ctype: ${ctype}`);
|
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`);
|
console.log(`[agentProvision] starting ${ctype} provisioning`);
|
||||||
|
|
||||||
// EARLY SPLIT — DO NOT MOVE
|
|
||||||
const req =
|
const req =
|
||||||
ctype === "dev"
|
ctype === "dev"
|
||||||
? normalizeDevRequest(body)
|
? normalizeDevRequest(body)
|
||||||
@ -132,14 +206,17 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
let ctIp;
|
let ctIp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
step("allocate-vmid");
|
||||||
vmid = await allocateVmid(ctype);
|
vmid = await allocateVmid(ctype);
|
||||||
console.log(`[agentProvision] vmid=${vmid}`);
|
|
||||||
|
|
||||||
const hostname =
|
const hostname = buildHostname({
|
||||||
ctype === "dev"
|
ctype,
|
||||||
? `dev-${vmid}`
|
game: req.game,
|
||||||
: req.hostname || `game-${vmid}`;
|
variant: req.variant,
|
||||||
|
vmid,
|
||||||
|
});
|
||||||
|
|
||||||
|
step("clone-container");
|
||||||
await cloneContainer({
|
await cloneContainer({
|
||||||
templateVmid: AGENT_TEMPLATE_VMID,
|
templateVmid: AGENT_TEMPLATE_VMID,
|
||||||
vmid,
|
vmid,
|
||||||
@ -147,6 +224,7 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
full: 1,
|
full: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
step("configure-container");
|
||||||
await configureContainer({
|
await configureContainer({
|
||||||
vmid,
|
vmid,
|
||||||
cpu: req.cpuCores || 2,
|
cpu: req.cpuCores || 2,
|
||||||
@ -154,9 +232,13 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
step("start-container");
|
||||||
await startWithRetry(vmid);
|
await startWithRetry(vmid);
|
||||||
|
|
||||||
|
step("wait-for-ip");
|
||||||
ctIp = await getCtIpWithRetry(vmid);
|
ctIp = await getCtIpWithRetry(vmid);
|
||||||
|
|
||||||
|
step("build-agent-payload");
|
||||||
const payload =
|
const payload =
|
||||||
ctype === "dev"
|
ctype === "dev"
|
||||||
? buildDevAgentPayload({
|
? buildDevAgentPayload({
|
||||||
@ -167,12 +249,12 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
})
|
})
|
||||||
: buildGameAgentPayload({ ...req, vmid });
|
: buildGameAgentPayload({ ...req, vmid });
|
||||||
|
|
||||||
console.log(`[agentProvision] payload:`);
|
step("send-agent-config");
|
||||||
console.log(JSON.stringify(payload, null, 2));
|
|
||||||
|
|
||||||
await sendAgentConfig({ ip: ctIp, payload });
|
await sendAgentConfig({ ip: ctIp, payload });
|
||||||
await waitForAgentRunning({ ip: ctIp });
|
|
||||||
|
|
||||||
|
await waitForAgentTerminalState({ ip: ctIp });
|
||||||
|
|
||||||
|
step("persist-instance");
|
||||||
await prisma.containerInstance.create({
|
await prisma.containerInstance.create({
|
||||||
data: {
|
data: {
|
||||||
vmid,
|
vmid,
|
||||||
@ -186,23 +268,31 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!payload.ctype) {
|
|
||||||
throw new Error("Payload missing ctype (game|dev)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctype === "game") {
|
if (ctype === "game") {
|
||||||
|
step("publish-edge");
|
||||||
|
|
||||||
|
const edgePorts =
|
||||||
|
req.ports?.length
|
||||||
|
? req.ports
|
||||||
|
: req.game === "minecraft"
|
||||||
|
? [25565]
|
||||||
|
: [];
|
||||||
|
|
||||||
await enqueuePublishEdge({
|
await enqueuePublishEdge({
|
||||||
vmid,
|
vmid,
|
||||||
instanceHostname: hostname,
|
slotHostname: hostname,
|
||||||
ctIp,
|
ctIp,
|
||||||
game: req.game,
|
game: req.game,
|
||||||
|
ports: edgePorts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
step("confirm-vmid");
|
||||||
await confirmVmidAllocated(vmid);
|
await confirmVmidAllocated(vmid);
|
||||||
|
|
||||||
return { vmid, hostname, ip: ctIp };
|
return { vmid, hostname, ip: ctIp };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
step("error-cleanup");
|
||||||
if (vmid) {
|
if (vmid) {
|
||||||
try { await deleteContainer(vmid); } catch {}
|
try { await deleteContainer(vmid); } catch {}
|
||||||
try { await releaseVmid(vmid); } catch {}
|
try { await releaseVmid(vmid); } catch {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user