Separate campaigns from server deployment lifecycle tracking
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { beginDeployment, deploymentStage, finishDeployment } from './deployments.js';
|
||||
import { mkdir, chmod, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { config, destOfKind } from '../config.js';
|
||||
@@ -138,13 +139,24 @@ function buildSpec(args: {
|
||||
|
||||
let creating = false;
|
||||
export async function createServer(input: CreateServerInput): Promise<CreateServerResult> {
|
||||
if (creating) return { ok: false, kind: 'no-slot', message: 'Another server is being created. Please retry shortly.' };
|
||||
if (creating || destroying.size > 0) return { ok: false, kind: 'no-slot', message: 'Another server operation is in progress. Please retry shortly.' };
|
||||
creating = true;
|
||||
try { return await createServerUnlocked(input); }
|
||||
let operation: string | undefined;
|
||||
try {
|
||||
const result = await createServerUnlocked(input, (name) => {
|
||||
operation = beginDeployment(name, 'create', input.createdBy);
|
||||
return operation;
|
||||
});
|
||||
if (operation) finishDeployment(operation, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (operation) finishDeployment(operation, { ok: false, message: (err as Error).message });
|
||||
throw err;
|
||||
}
|
||||
finally { creating = false; }
|
||||
}
|
||||
|
||||
async function createServerUnlocked(input: CreateServerInput): Promise<CreateServerResult> {
|
||||
async function createServerUnlocked(input: CreateServerInput, begin: (name: string) => string): Promise<CreateServerResult> {
|
||||
if (input.gameMode !== 'zombie') {
|
||||
return { ok: false, kind: 'unknown-map', message: `unsupported game mode: ${input.gameMode}` };
|
||||
}
|
||||
@@ -185,6 +197,8 @@ async function createServerUnlocked(input: CreateServerInput): Promise<CreateSer
|
||||
try { saveServerKey(name, plutoKey); }
|
||||
catch (err) { return { ok: false, kind: 'bad-request', message: (err as Error).message }; }
|
||||
|
||||
const operation = begin(name);
|
||||
deploymentStage(operation, 'Preparing server files');
|
||||
const hostDir = `${config.serversRoot}/${name}`;
|
||||
const initialConfig = getPromotedServerConfig('t5', mapEntry.canonical);
|
||||
const mapConfig = getServerMapConfig('t5', mapEntry.canonical)!;
|
||||
@@ -224,6 +238,7 @@ async function createServerUnlocked(input: CreateServerInput): Promise<CreateSer
|
||||
}
|
||||
|
||||
// Step 2: Komodo CreateDeployment.
|
||||
deploymentStage(operation, 'Creating Komodo deployment');
|
||||
const spec = buildSpec({ name, port, hostDir, plutoKey });
|
||||
const createRes = await komodoCreateDeployment(spec);
|
||||
if (!createRes.ok) {
|
||||
@@ -274,6 +289,7 @@ async function createServerUnlocked(input: CreateServerInput): Promise<CreateSer
|
||||
void applyDeploymentTags(name, 't5', mapEntry.canonical);
|
||||
|
||||
// Step 5: execute/Deploy — actually start the container.
|
||||
deploymentStage(operation, 'Starting container');
|
||||
const deployRes = await komodoDeploy(name);
|
||||
if (!deployRes.ok) {
|
||||
// Container never started — undo everything so retrying is clean.
|
||||
@@ -317,7 +333,23 @@ async function applyDeploymentTags(deployment: string, gameTagName: string, mapT
|
||||
}
|
||||
}
|
||||
|
||||
const destroying = new Set<string>();
|
||||
export async function destroyServer(name: string): Promise<DestroyServerResult> {
|
||||
if (creating || destroying.has(name)) return { ok: false, kind: 'komodo', message: 'A server operation is already in progress. Retry shortly.', warnings: [] };
|
||||
if (!getServer(name)) return { ok: false, kind: 'not-found', message: 'Server not found', warnings: [] };
|
||||
const operation = beginDeployment(name, 'destroy');
|
||||
destroying.add(name);
|
||||
try {
|
||||
const result = await destroyServerUnlocked(name, operation);
|
||||
finishDeployment(operation, result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
finishDeployment(operation, { ok: false, message: (err as Error).message });
|
||||
throw err;
|
||||
} finally { destroying.delete(name); }
|
||||
}
|
||||
|
||||
async function destroyServerUnlocked(name: string, operation: string): Promise<DestroyServerResult> {
|
||||
const row = getServer(name);
|
||||
if (!row) {
|
||||
return { ok: false, kind: 'not-found', message: `no server named '${name}'`, warnings: [] };
|
||||
@@ -327,6 +359,7 @@ export async function destroyServer(name: string): Promise<DestroyServerResult>
|
||||
|
||||
// 1: stop + remove the container. execute/DestroyDeployment queues the
|
||||
// teardown; the response comes back before the container is actually gone.
|
||||
deploymentStage(operation, 'Stopping and removing container');
|
||||
const destroyRes = await komodoDestroyDeployment(name);
|
||||
if (!destroyRes.ok) {
|
||||
warnings.push(`DestroyDeployment failed: ${destroyRes.message}`);
|
||||
@@ -344,6 +377,7 @@ export async function destroyServer(name: string): Promise<DestroyServerResult>
|
||||
}
|
||||
|
||||
// 3: remove the deployment entity itself.
|
||||
deploymentStage(operation, 'Deleting Komodo deployment');
|
||||
const deleteRes = await komodoDeleteDeployment(name);
|
||||
if (!deleteRes.ok) {
|
||||
warnings.push(`DeleteDeployment failed: ${deleteRes.message}`);
|
||||
@@ -351,6 +385,7 @@ export async function destroyServer(name: string): Promise<DestroyServerResult>
|
||||
|
||||
// 3: host directory. Fall back to derived path if host_dir wasn't set on
|
||||
// legacy rows.
|
||||
deploymentStage(operation, 'Removing server files and registry');
|
||||
const hostDir = row.host_dir ?? `${config.serversRoot}/${name}`;
|
||||
try {
|
||||
await rm(hostDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user