provisionAgentfixed 12-20-25
This commit is contained in:
parent
b23d982428
commit
28a5b80e01
@ -1,5 +1,5 @@
|
|||||||
// src/api/provisionDev.js
|
// src/api/provisionDev.js
|
||||||
// DEV-SIDE request normalization + validation (payload not implemented yet)
|
// DEV-SIDE request normalization + validation
|
||||||
|
|
||||||
export function normalizeDevRequest(body = {}) {
|
export function normalizeDevRequest(body = {}) {
|
||||||
const {
|
const {
|
||||||
@ -10,16 +10,24 @@ export function normalizeDevRequest(body = {}) {
|
|||||||
diskGiB,
|
diskGiB,
|
||||||
portsNeeded,
|
portsNeeded,
|
||||||
|
|
||||||
// dev-specific fields (future)
|
// dev fields
|
||||||
runtime,
|
runtime,
|
||||||
|
|
||||||
|
// canonical runtime version field for dev (matches your curl)
|
||||||
|
version,
|
||||||
|
|
||||||
|
// legacy/alternate naming (optional)
|
||||||
runtimeVersion,
|
runtimeVersion,
|
||||||
|
|
||||||
|
// optional addons
|
||||||
addons,
|
addons,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
if (!customerId) throw new Error("customerId required");
|
if (!customerId) throw new Error("customerId required");
|
||||||
|
if (!runtime) throw new Error("runtime required");
|
||||||
|
const resolvedVersion = version || runtimeVersion;
|
||||||
|
if (!resolvedVersion) throw new Error("version required");
|
||||||
|
|
||||||
// NOTE: Do NOT require game/variant/world for dev.
|
|
||||||
// Payload work is explicitly deferred per instruction.
|
|
||||||
return {
|
return {
|
||||||
customerId,
|
customerId,
|
||||||
name,
|
name,
|
||||||
@ -27,9 +35,11 @@ export function normalizeDevRequest(body = {}) {
|
|||||||
memoryMiB,
|
memoryMiB,
|
||||||
diskGiB,
|
diskGiB,
|
||||||
portsNeeded,
|
portsNeeded,
|
||||||
|
|
||||||
runtime,
|
runtime,
|
||||||
runtimeVersion,
|
version: String(resolvedVersion),
|
||||||
addons,
|
|
||||||
|
addons: Array.isArray(addons) ? addons : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
@ -44,6 +45,9 @@ 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";
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
VERSION PARSER (Minecraft only)
|
VERSION PARSER (Minecraft only)
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
@ -92,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}`;
|
||||||
}
|
}
|
||||||
@ -107,7 +114,7 @@ function generateAdminPassword() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
GAME PAYLOAD (UNCHANGED)
|
GAME PAYLOAD
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
function buildGameAgentPayload({
|
function buildGameAgentPayload({
|
||||||
vmid,
|
vmid,
|
||||||
@ -186,9 +193,9 @@ function buildGameAgentPayload({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
DEV PAYLOAD (NEW, MINIMAL, CANONICAL)
|
DEV PAYLOAD
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB, ports }) {
|
||||||
if (!runtime) throw new Error("runtime required for dev container");
|
if (!runtime) throw new Error("runtime required for dev container");
|
||||||
if (!version) throw new Error("version required for dev container");
|
if (!version) throw new Error("version required for dev container");
|
||||||
|
|
||||||
@ -198,6 +205,7 @@ function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
|||||||
runtime,
|
runtime,
|
||||||
version,
|
version,
|
||||||
memory_mb: Number(memoryMiB) || 2048,
|
memory_mb: Number(memoryMiB) || 2048,
|
||||||
|
ports: Array.isArray(ports) ? ports : [ports].filter(Boolean),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +238,7 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
|||||||
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
||||||
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
let lastLoggedStep = null;
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
try {
|
try {
|
||||||
@ -237,10 +246,33 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const state = (data.state || "").toLowerCase();
|
const state = (data.state || "").toLowerCase();
|
||||||
if (state === "running") return data;
|
const step = data.installStep || data.currentStep || "unknown";
|
||||||
if (state === "error") throw new Error(data.error || "agent error");
|
const progress = data.progress || "";
|
||||||
|
|
||||||
|
if (step !== lastLoggedStep) {
|
||||||
|
console.log(`[AGENT ${ip}] state=${state} step=${step} ${progress}`);
|
||||||
|
lastLoggedStep = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === "running") {
|
||||||
|
console.log(`[AGENT ${ip}] ✓ Provisioning complete`);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === "error") {
|
||||||
|
const errorMsg = data.error || "agent error";
|
||||||
|
console.error(`[AGENT ${ip}] ✗ ERROR:`, errorMsg);
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
!err.message.includes("ECONNREFUSED") &&
|
||||||
|
!err.message.includes("fetch failed")
|
||||||
|
) {
|
||||||
|
console.error(`[AGENT ${ip}] Poll error:`, err.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,17 +284,47 @@ 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`);
|
||||||
|
|
||||||
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;
|
||||||
|
let allocatedPorts = [];
|
||||||
|
let txnId = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log("[agentProvision] STEP 1: allocate VMID");
|
||||||
vmid = await allocateVmid(ctype);
|
vmid = await allocateVmid(ctype);
|
||||||
|
console.log(`[agentProvision] → Allocated vmid=${vmid}`);
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
game: req.game,
|
||||||
|
variant: req.variant,
|
||||||
|
customerId: req.customerId,
|
||||||
|
vmid,
|
||||||
|
purpose: ctype === "game" ? "game_main" : "dev",
|
||||||
|
txnId,
|
||||||
|
count: req.portsNeeded,
|
||||||
|
});
|
||||||
|
|
||||||
|
allocatedPorts = Array.isArray(portObjs)
|
||||||
|
? portObjs.map((p) => (typeof p === "object" ? p.port : p))
|
||||||
|
: [portObjs];
|
||||||
|
|
||||||
|
console.log(`[agentProvision] → Allocated ports: ${allocatedPorts.join(", ")}`);
|
||||||
|
} else {
|
||||||
|
console.log("[agentProvision] STEP 2: port allocation (skipped)");
|
||||||
|
}
|
||||||
|
|
||||||
const hostname = generateSystemHostname({
|
const hostname = generateSystemHostname({
|
||||||
ctype,
|
ctype,
|
||||||
@ -271,6 +333,12 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
vmid,
|
vmid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// V3 correct behavior: build FQDN here
|
||||||
|
const slotHostname = `${hostname}.${ZONE}`;
|
||||||
|
|
||||||
|
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,
|
||||||
@ -278,6 +346,7 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
full: 1,
|
full: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("[agentProvision] STEP 4: configure CPU/mem/bridge/tags");
|
||||||
await configureContainer({
|
await configureContainer({
|
||||||
vmid,
|
vmid,
|
||||||
cpu: req.cpuCores || 2,
|
cpu: req.cpuCores || 2,
|
||||||
@ -285,10 +354,14 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("[agentProvision] STEP 5: start container");
|
||||||
await startWithRetry(vmid);
|
await startWithRetry(vmid);
|
||||||
|
|
||||||
|
console.log("[agentProvision] STEP 6: detect container IP");
|
||||||
ctIp = await getCtIpWithRetry(vmid);
|
ctIp = await getCtIpWithRetry(vmid);
|
||||||
|
console.log(`[agentProvision] → ctIp=${ctIp}`);
|
||||||
|
|
||||||
|
console.log("[agentProvision] STEP 7: build agent payload");
|
||||||
const payload =
|
const payload =
|
||||||
ctype === "dev"
|
ctype === "dev"
|
||||||
? buildDevAgentPayload({
|
? buildDevAgentPayload({
|
||||||
@ -296,15 +369,22 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
runtime: body.runtime,
|
runtime: body.runtime,
|
||||||
version: body.version,
|
version: body.version,
|
||||||
memoryMiB: req.memoryMiB,
|
memoryMiB: req.memoryMiB,
|
||||||
|
ports: allocatedPorts,
|
||||||
})
|
})
|
||||||
: buildGameAgentPayload({
|
: buildGameAgentPayload({
|
||||||
vmid,
|
vmid,
|
||||||
...req,
|
...req,
|
||||||
|
// 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)");
|
||||||
await sendAgentConfig({ ip: ctIp, payload });
|
await sendAgentConfig({ ip: ctIp, payload });
|
||||||
|
|
||||||
|
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");
|
||||||
await prisma.containerInstance.create({
|
await prisma.containerInstance.create({
|
||||||
data: {
|
data: {
|
||||||
vmid,
|
vmid,
|
||||||
@ -312,27 +392,67 @@ export async function provisionAgentInstance(body = {}) {
|
|||||||
ctype,
|
ctype,
|
||||||
hostname,
|
hostname,
|
||||||
ip: ctIp,
|
ip: ctIp,
|
||||||
|
allocatedPorts, // matches schema
|
||||||
payload,
|
payload,
|
||||||
agentState: "running",
|
agentState: "running",
|
||||||
agentLastSeen: new Date(),
|
agentLastSeen: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// STEP 11: commit ports ONLY if allocated from PortPool
|
||||||
|
if (allocatedPorts.length > 0) {
|
||||||
|
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] : [];
|
||||||
|
|
||||||
await enqueuePublishEdge({
|
await enqueuePublishEdge({
|
||||||
vmid,
|
vmid,
|
||||||
instanceHostname: hostname,
|
slotHostname, // FQDN (V3 behavior)
|
||||||
|
instanceHostname: hostname, // short (optional, kept for compatibility)
|
||||||
|
ports: edgePorts,
|
||||||
ctIp,
|
ctIp,
|
||||||
game: req.game,
|
game: req.game,
|
||||||
|
txnId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.log("[agentProvision] STEP 12: publish edge (skipped - dev container)");
|
||||||
|
}
|
||||||
|
|
||||||
await confirmVmidAllocated(vmid);
|
await confirmVmidAllocated(vmid);
|
||||||
|
|
||||||
return { vmid, hostname, ip: ctIp };
|
console.log("[agentProvision] COMPLETE: success");
|
||||||
|
return { vmid, hostname, ip: ctIp, ports: allocatedPorts };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (vmid) {
|
console.error("[agentProvision] ERROR:", err.message);
|
||||||
try { await deleteContainer(vmid); } catch {}
|
|
||||||
try { await releaseVmid(vmid); } catch {}
|
// Rollback ports on failure
|
||||||
|
if (vmid && allocatedPorts.length > 0) {
|
||||||
|
try {
|
||||||
|
await PortAllocationService.releaseByVmid(vmid);
|
||||||
|
console.log(`[agentProvision] → Rolled back ports for vmid=${vmid}`);
|
||||||
|
} catch (rollbackErr) {
|
||||||
|
console.error("[agentProvision] → Port rollback failed:", rollbackErr.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmid) {
|
||||||
|
try {
|
||||||
|
await deleteContainer(vmid);
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
await releaseVmid(vmid);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user