Compare commits
4 Commits
v0.1.0-dev
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 88d790d42c | |||
| 43f1853e4f | |||
| 28a5b80e01 | |||
| b23d982428 |
20
src/api/handlers/provisionDev.js
Normal file
20
src/api/handlers/provisionDev.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// src/api/handlers/provisionDev.js
|
||||||
|
|
||||||
|
export function normalizeDevRequest(body = {}) {
|
||||||
|
if (!body.runtime) {
|
||||||
|
throw new Error("runtime is required for dev container");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.version) {
|
||||||
|
throw new Error("version is required for dev container");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerId: body.customerId,
|
||||||
|
runtime: body.runtime,
|
||||||
|
version: body.version,
|
||||||
|
memoryMiB: body.memoryMiB || 2048,
|
||||||
|
cpuCores: body.cpuCores || 2,
|
||||||
|
portsNeeded: body.portsNeeded || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
22
src/api/handlers/provisionGame.js
Normal file
22
src/api/handlers/provisionGame.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// src/api/handlers/provisionGame.js
|
||||||
|
|
||||||
|
export function normalizeGameRequest(body = {}) {
|
||||||
|
if (!body.game) {
|
||||||
|
throw new Error("game is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.variant) {
|
||||||
|
throw new Error("variant is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customerId: body.customerId,
|
||||||
|
game: body.game,
|
||||||
|
variant: body.variant,
|
||||||
|
version: body.version,
|
||||||
|
world: body.world || "world",
|
||||||
|
memoryMiB: body.memoryMiB || 2048,
|
||||||
|
cpuCores: body.cpuCores || 2,
|
||||||
|
portsNeeded: body.portsNeeded || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,13 +1,11 @@
|
|||||||
// src/api/provisionAgent.js
|
// src/api/provisionAgent.js
|
||||||
// FINAL AGENT-DRIVEN PROVISIONING PIPELINE
|
// FINAL AGENT-DRIVEN PROVISIONING PIPELINE (STABLE + SCALABLE)
|
||||||
// Supports: paper, vanilla, purpur, forge, fabric, neoforge + Steam creds passthrough
|
|
||||||
|
|
||||||
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 proxmox, {
|
import {
|
||||||
cloneContainer,
|
cloneContainer,
|
||||||
configureContainer,
|
configureContainer,
|
||||||
startWithRetry,
|
startWithRetry,
|
||||||
@ -15,7 +13,6 @@ import proxmox, {
|
|||||||
} from "../services/proxmoxClient.js";
|
} from "../services/proxmoxClient.js";
|
||||||
|
|
||||||
import { getCtIpWithRetry } from "../services/getCtIp.js";
|
import { getCtIpWithRetry } from "../services/getCtIp.js";
|
||||||
import { PortAllocationService } from "../services/portAllocator.js";
|
|
||||||
import {
|
import {
|
||||||
allocateVmid,
|
allocateVmid,
|
||||||
confirmVmidAllocated,
|
confirmVmidAllocated,
|
||||||
@ -23,184 +20,131 @@ 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";
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
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));
|
||||||
VERSION PARSER
|
const step = (name) =>
|
||||||
------------------------------------------------------------- */
|
console.log(`[agentProvision] step=${name}`);
|
||||||
function parseMcVersion(ver) {
|
|
||||||
if (!ver) return { major: 0, minor: 0, patch: 0 };
|
|
||||||
const p = String(ver).split(".");
|
|
||||||
return {
|
|
||||||
major: Number(p[0]) || 0,
|
|
||||||
minor: Number(p[1]) || 0,
|
|
||||||
patch: Number(p[2]) || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
JAVA RUNTIME SELECTOR
|
HOSTNAME BUILDER
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
function pickJavaRuntimeForMc(version) {
|
function buildHostname({ ctype, game, variant, vmid }) {
|
||||||
const { major, minor, patch } = parseMcVersion(version);
|
if (ctype === "dev") return `dev-${vmid}`;
|
||||||
|
|
||||||
if (major > 1) return 21;
|
if (game === "minecraft") {
|
||||||
|
|
||||||
if (major === 1) {
|
|
||||||
if (minor >= 21) return 21;
|
|
||||||
if (minor === 20 && patch >= 5) return 21;
|
|
||||||
if (minor > 20) return 21;
|
|
||||||
return 17;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 17;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
|
||||||
HOSTNAME GENERATION
|
|
||||||
------------------------------------------------------------- */
|
|
||||||
function generateSystemHostname({ game, variant, vmid }) {
|
|
||||||
const g = (game || "").toLowerCase();
|
|
||||||
const v = (variant || "").toLowerCase();
|
const v = (variant || "").toLowerCase();
|
||||||
|
if (v) return `mc-${v}-${vmid}`;
|
||||||
let prefix = "game";
|
return `mc-${vmid}`;
|
||||||
if (g.includes("minecraft")) prefix = "mc";
|
|
||||||
else if (g.includes("terraria")) prefix = "terraria";
|
|
||||||
else if (g.includes("valheim")) prefix = "valheim";
|
|
||||||
else if (g.includes("rust")) prefix = "rust";
|
|
||||||
|
|
||||||
let varPart = "";
|
|
||||||
if (g.includes("minecraft")) {
|
|
||||||
if (["paper", "forge", "fabric", "vanilla", "purpur", "neoforge"].includes(v))
|
|
||||||
varPart = v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return varPart ? `${prefix}-${varPart}-${vmid}` : `${prefix}-${vmid}`;
|
return `${game || "game"}-${vmid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
ADMIN PASSWORD GENERATOR
|
JAVA SELECTION (FIX)
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
function generateAdminPassword() {
|
function pickJavaForMinecraftVersion(version) {
|
||||||
return crypto.randomBytes(12).toString("base64url");
|
// version like "1.21.7"
|
||||||
}
|
const parts = String(version).split(".");
|
||||||
|
const minor = Number(parts[1] || 0);
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
return minor >= 21
|
||||||
BUILD AGENT PAYLOAD
|
|
||||||
------------------------------------------------------------- */
|
|
||||||
function buildAgentPayload({
|
|
||||||
vmid,
|
|
||||||
game,
|
|
||||||
variant,
|
|
||||||
version,
|
|
||||||
world,
|
|
||||||
ports,
|
|
||||||
artifactPath,
|
|
||||||
javaPath,
|
|
||||||
memoryMiB,
|
|
||||||
steamUser,
|
|
||||||
steamPass,
|
|
||||||
steamAuth,
|
|
||||||
adminUser,
|
|
||||||
adminPass,
|
|
||||||
}) {
|
|
||||||
const g = (game || "minecraft").toLowerCase();
|
|
||||||
const v = (variant || "").toLowerCase();
|
|
||||||
const ver = version || "1.20.1";
|
|
||||||
const w = world || "world";
|
|
||||||
|
|
||||||
if (!v) throw new Error("variant is required (paper, forge, fabric, vanilla, purpur)");
|
|
||||||
|
|
||||||
let art = artifactPath;
|
|
||||||
let jpath = javaPath;
|
|
||||||
|
|
||||||
// --------- VARIANT → ARTIFACT PATH ---------
|
|
||||||
if (!art && g === "minecraft") {
|
|
||||||
switch (v) {
|
|
||||||
case "paper":
|
|
||||||
case "vanilla":
|
|
||||||
case "purpur":
|
|
||||||
art = `minecraft/${v}/${ver}/server.jar`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "forge":
|
|
||||||
art = `minecraft/forge/${ver}/forge-installer.jar`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "fabric":
|
|
||||||
art = `minecraft/fabric/${ver}/fabric-server.jar`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "neoforge":
|
|
||||||
art = `minecraft/neoforge/${ver}/neoforge-installer.jar`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported Minecraft variant: ${v}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------- JAVA RUNTIME SELECTOR ----------
|
|
||||||
if (!jpath && g === "minecraft") {
|
|
||||||
const javaVersion = pickJavaRuntimeForMc(ver);
|
|
||||||
jpath =
|
|
||||||
javaVersion === 21
|
|
||||||
? "java/21/OpenJDK21.tar.gz"
|
? "java/21/OpenJDK21.tar.gz"
|
||||||
: "java/17/OpenJDK17.tar.gz";
|
: "java/17/OpenJDK17.tar.gz";
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------- MEMORY DEFAULTS ----------
|
/* -------------------------------------------------------------
|
||||||
let mem = Number(memoryMiB) || 0;
|
PAYLOAD BUILDERS
|
||||||
if (mem <= 0) mem = ["forge", "neoforge"].includes(v) ? 4096 : 2048;
|
------------------------------------------------------------- */
|
||||||
|
|
||||||
// Steam + admin credentials (persisted, optional)
|
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
||||||
const resolvedSteamUser = steamUser || "anonymous";
|
if (!runtime) throw new Error("runtime required for dev container");
|
||||||
const resolvedSteamPass = steamPass || "";
|
if (!version) throw new Error("version required for dev container");
|
||||||
const resolvedSteamAuth = steamAuth || "";
|
|
||||||
|
|
||||||
const resolvedAdminUser = adminUser || "admin";
|
|
||||||
const resolvedAdminPass = adminPass || generateAdminPassword();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vmid,
|
vmid,
|
||||||
game: g,
|
container_type: "dev",
|
||||||
variant: v,
|
runtime,
|
||||||
version: ver,
|
version,
|
||||||
world: w,
|
memory_mb: Number(memoryMiB) || 2048,
|
||||||
ports: Array.isArray(ports) ? ports : [ports].filter(Boolean),
|
};
|
||||||
artifact_path: art,
|
}
|
||||||
java_path: jpath,
|
|
||||||
memory_mb: mem,
|
|
||||||
|
|
||||||
steam_user: resolvedSteamUser,
|
function buildGameAgentPayload(req) {
|
||||||
steam_pass: resolvedSteamPass,
|
let javaPath = req.javaPath;
|
||||||
steam_auth: resolvedSteamAuth,
|
let artifactPath = req.artifactPath;
|
||||||
|
|
||||||
admin_user: resolvedAdminUser,
|
// 🔧 FIXED JAVA LOGIC — NOTHING ELSE CHANGED
|
||||||
admin_pass: resolvedAdminPass,
|
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: artifactPath,
|
||||||
|
java_path: javaPath,
|
||||||
|
memory_mb: req.memoryMiB,
|
||||||
|
admin_user: req.adminUser,
|
||||||
|
admin_pass: req.adminPass,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
SEND CONFIG → triggers async provision+start in agent
|
AGENT COMMUNICATION
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
async function sendAgentConfig({ ip, payload }) {
|
|
||||||
const url = `http://${ip}:${AGENT_PORT}/config`;
|
|
||||||
const headers = { "Content-Type": "application/json" };
|
|
||||||
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
|
||||||
|
|
||||||
const resp = await fetch(url, {
|
async function sendAgentConfig({ ip, payload }) {
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
if (AGENT_TOKEN) headers.Authorization = `Bearer ${AGENT_TOKEN}`;
|
||||||
|
|
||||||
|
const resp = await fetch(`http://${ip}:${AGENT_PORT}/config`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@ -212,239 +156,147 @@ async function sendAgentConfig({ ip, payload }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
async function waitForAgentTerminalState({ ip, timeoutMs = 10 * 60_000 }) {
|
||||||
WAIT FOR AGENT READY (poll /status)
|
|
||||||
------------------------------------------------------------- */
|
|
||||||
async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
|
||||||
const url = `http://${ip}:${AGENT_PORT}/status`;
|
|
||||||
const headers = {};
|
|
||||||
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
|
||||||
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
let last;
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(url, { headers });
|
const res = await fetch(`http://${ip}:${AGENT_PORT}/status`);
|
||||||
if (!resp.ok) {
|
if (res.ok) {
|
||||||
last = new Error(`/status HTTP ${resp.status}`);
|
const data = await res.json();
|
||||||
} else {
|
|
||||||
const data = await resp.json().catch(() => ({}));
|
|
||||||
const state = (data.state || data.status || "").toLowerCase();
|
|
||||||
|
|
||||||
// Agent's state machine:
|
if (data.state === "running") return;
|
||||||
// idle → installing → verifying → starting → running
|
|
||||||
if (state === "running") return { state: "running", raw: data };
|
|
||||||
if (state === "error" || state === "crashed") {
|
|
||||||
const msg = data.error || "";
|
|
||||||
throw new Error(`agent state=${state} ${msg ? `(${msg})` : ""}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
last = new Error(`agent state=${state || "unknown"}`);
|
if (data.state === "error") {
|
||||||
|
throw new Error(data.error || "agent error");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
last = err;
|
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
await sleep(3000);
|
await sleep(3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw last || new Error("Agent did not reach running state");
|
throw new Error("Agent did not reach running state");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* -------------------------------------------------------------
|
||||||
MAIN PROVISION ENTRYPOINT
|
MAIN ENTRYPOINT
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------- */
|
||||||
|
|
||||||
export async function provisionAgentInstance(body = {}) {
|
export async function provisionAgentInstance(body = {}) {
|
||||||
const {
|
const rawType =
|
||||||
customerId,
|
body.container_type ??
|
||||||
game,
|
body.containerType ??
|
||||||
variant,
|
body.ctype ??
|
||||||
version,
|
"game";
|
||||||
world,
|
|
||||||
ctype: rawCtype,
|
|
||||||
name,
|
|
||||||
cpuCores,
|
|
||||||
memoryMiB,
|
|
||||||
diskGiB,
|
|
||||||
portsNeeded,
|
|
||||||
artifactPath,
|
|
||||||
javaPath,
|
|
||||||
|
|
||||||
// NEW optional fields
|
if (!["game", "dev"].includes(rawType)) {
|
||||||
steamUser,
|
throw new Error(`invalid container type: ${rawType}`);
|
||||||
steamPass,
|
|
||||||
steamAuth,
|
|
||||||
adminUser,
|
|
||||||
adminPass,
|
|
||||||
} = body;
|
|
||||||
|
|
||||||
if (!customerId) throw new Error("customerId required");
|
|
||||||
if (!game) throw new Error("game required");
|
|
||||||
if (!variant) throw new Error("variant required");
|
|
||||||
|
|
||||||
const ctype = rawCtype || "game";
|
|
||||||
const isMinecraft = game.toLowerCase().includes("minecraft");
|
|
||||||
|
|
||||||
let vmid;
|
|
||||||
let allocatedPortsMap = null;
|
|
||||||
let gamePorts = [];
|
|
||||||
let ctIp;
|
|
||||||
let instanceHostname;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("[agentProvision] STEP 1: allocate VMID");
|
|
||||||
vmid = await allocateVmid(ctype);
|
|
||||||
|
|
||||||
instanceHostname = generateSystemHostname({ game, variant, vmid });
|
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 2: port allocation");
|
|
||||||
if (!isMinecraft && (portsNeeded ?? 0) > 0) {
|
|
||||||
gamePorts = await PortAllocationService.reserve({
|
|
||||||
vmid,
|
|
||||||
count: portsNeeded,
|
|
||||||
portType: "game",
|
|
||||||
});
|
|
||||||
allocatedPortsMap = { game: gamePorts };
|
|
||||||
} else {
|
|
||||||
gamePorts = [25565];
|
|
||||||
allocatedPortsMap = { game: gamePorts };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = process.env.PROXMOX_NODE || "zlh-prod1";
|
const ctype = rawType;
|
||||||
const bridge = ctype === "dev" ? "vmbr2" : "vmbr3";
|
console.log(`[agentProvision] starting ${ctype} provisioning`);
|
||||||
const cpu = cpuCores ? Number(cpuCores) : 2;
|
|
||||||
const memory = memoryMiB ? Number(memoryMiB) : 2048;
|
|
||||||
|
|
||||||
const description = name
|
const req =
|
||||||
? `${name} (customer=${customerId}; vmid=${vmid}; agent=v1)`
|
ctype === "dev"
|
||||||
: `customer=${customerId}; vmid=${vmid}; agent=v1`;
|
? normalizeDevRequest(body)
|
||||||
|
: normalizeGameRequest(body);
|
||||||
|
|
||||||
const tags = [
|
let vmid;
|
||||||
`cust-${customerId}`,
|
let ctIp;
|
||||||
`type-${ctype}`,
|
|
||||||
`game-${game}`,
|
|
||||||
variant ? `var-${variant}` : null,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
console.log(
|
try {
|
||||||
`[agentProvision] STEP 3: clone template ${AGENT_TEMPLATE_VMID} → vmid=${vmid}`
|
step("allocate-vmid");
|
||||||
);
|
vmid = await allocateVmid(ctype);
|
||||||
|
|
||||||
|
const hostname = buildHostname({
|
||||||
|
ctype,
|
||||||
|
game: req.game,
|
||||||
|
variant: req.variant,
|
||||||
|
vmid,
|
||||||
|
});
|
||||||
|
|
||||||
|
step("clone-container");
|
||||||
await cloneContainer({
|
await cloneContainer({
|
||||||
templateVmid: AGENT_TEMPLATE_VMID,
|
templateVmid: AGENT_TEMPLATE_VMID,
|
||||||
vmid,
|
vmid,
|
||||||
name: instanceHostname,
|
name: hostname,
|
||||||
full: 1,
|
full: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 4: configure CPU/mem/bridge/tags");
|
step("configure-container");
|
||||||
await configureContainer({
|
await configureContainer({
|
||||||
vmid,
|
vmid,
|
||||||
cpu,
|
cpu: req.cpuCores || 2,
|
||||||
memory,
|
memory: req.memoryMiB || 2048,
|
||||||
bridge,
|
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
||||||
description,
|
|
||||||
tags,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 5: start container");
|
step("start-container");
|
||||||
await startWithRetry(vmid);
|
await startWithRetry(vmid);
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 6: detect container IP");
|
step("wait-for-ip");
|
||||||
const ip = await getCtIpWithRetry(vmid, node, 12, 10_000);
|
ctIp = await getCtIpWithRetry(vmid);
|
||||||
if (!ip) throw new Error("Failed to detect container IP");
|
|
||||||
ctIp = ip;
|
|
||||||
|
|
||||||
console.log(`[agentProvision] ctIp=${ctIp}`);
|
step("build-agent-payload");
|
||||||
|
const payload =
|
||||||
console.log("[agentProvision] STEP 7: build agent payload");
|
ctype === "dev"
|
||||||
const payload = buildAgentPayload({
|
? buildDevAgentPayload({
|
||||||
vmid,
|
vmid,
|
||||||
game,
|
runtime: body.runtime,
|
||||||
variant,
|
version: body.version,
|
||||||
version,
|
memoryMiB: req.memoryMiB,
|
||||||
world,
|
})
|
||||||
ports: gamePorts,
|
: buildGameAgentPayload({ ...req, vmid });
|
||||||
artifactPath,
|
|
||||||
javaPath,
|
|
||||||
memoryMiB,
|
|
||||||
|
|
||||||
steamUser,
|
step("send-agent-config");
|
||||||
steamPass,
|
|
||||||
steamAuth,
|
|
||||||
adminUser,
|
|
||||||
adminPass,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 waitForAgentTerminalState({ ip: ctIp });
|
||||||
const agentResult = await waitForAgentRunning({ ip: ctIp });
|
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 10: DB save");
|
step("persist-instance");
|
||||||
const instance = await prisma.containerInstance.create({
|
await prisma.containerInstance.create({
|
||||||
data: {
|
data: {
|
||||||
vmid,
|
vmid,
|
||||||
customerId,
|
customerId: req.customerId,
|
||||||
ctype,
|
ctype,
|
||||||
hostname: instanceHostname,
|
hostname,
|
||||||
ip: ctIp,
|
ip: ctIp,
|
||||||
allocatedPorts: allocatedPortsMap,
|
|
||||||
payload,
|
payload,
|
||||||
agentState: agentResult.state,
|
agentState: "running",
|
||||||
agentLastSeen: new Date(),
|
agentLastSeen: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 11: commit ports");
|
if (ctype === "game") {
|
||||||
if (!isMinecraft && gamePorts.length) {
|
step("publish-edge");
|
||||||
await PortAllocationService.commit({
|
|
||||||
|
const edgePorts =
|
||||||
|
req.ports?.length
|
||||||
|
? req.ports
|
||||||
|
: req.game === "minecraft"
|
||||||
|
? [25565]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
await enqueuePublishEdge({
|
||||||
vmid,
|
vmid,
|
||||||
ports: gamePorts,
|
slotHostname: hostname,
|
||||||
portType: "game",
|
ctIp,
|
||||||
|
game: req.game,
|
||||||
|
ports: edgePorts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[agentProvision] STEP 12: publish edge");
|
step("confirm-vmid");
|
||||||
await enqueuePublishEdge({
|
|
||||||
vmid,
|
|
||||||
slotHostname: instanceHostname,
|
|
||||||
instanceHostname,
|
|
||||||
ports: gamePorts,
|
|
||||||
ctIp,
|
|
||||||
game,
|
|
||||||
});
|
|
||||||
|
|
||||||
await confirmVmidAllocated(vmid);
|
await confirmVmidAllocated(vmid);
|
||||||
|
|
||||||
console.log("[agentProvision] COMPLETE");
|
return { vmid, hostname, ip: ctIp };
|
||||||
|
|
||||||
return {
|
|
||||||
vmid,
|
|
||||||
ip: ctIp,
|
|
||||||
hostname: instanceHostname,
|
|
||||||
ports: gamePorts,
|
|
||||||
instance,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[agentProvision] ERROR:", err.message);
|
step("error-cleanup");
|
||||||
|
if (vmid) {
|
||||||
try {
|
try { await deleteContainer(vmid); } catch {}
|
||||||
if (vmid) await PortAllocationService.releaseByVmid(vmid);
|
try { await releaseVmid(vmid); } catch {}
|
||||||
} catch {}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
if (vmid) await deleteContainer(vmid);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (vmid) await releaseVmid(vmid);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user