Add deploy + destroy server flow; migrate to t5_<mode>_<slug>_<n> naming
Full server lifecycle management from Electric Cherry: pick game/mode/map/pluto key in the Fleet page, EC provisions the deployment via Komodo, allocates a port in 28960-28980, creates the 0o777 host dir, and starts the container. Destroy button tears everything down (Komodo Destroy+Delete, rm -rf host dir, transactional DB cleanup) and surfaces any partial-failure warnings inline. Same phase cuts over to the new naming scheme (t5_zombie_cosmodrome_1 etc.). Komodo entity names now match EC server names 1:1 (no more _/- transform), and the map catalog carries friendly/slug/canonical for each of the 11 t5 zombie maps. Server rows gain game_mode, map_slug, host_dir, config_file, game_id, image, created_by, and destroyed_at columns for provisioning + audit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -407,6 +407,118 @@ export async function komodoInspectHealthy(serverName: string): Promise<KomodoHe
|
||||
};
|
||||
}
|
||||
|
||||
// ---------- Write / Execute (create + destroy deployments) ----------
|
||||
|
||||
export type KomodoWriteResult =
|
||||
| { ok: true }
|
||||
| { ok: false; kind: 'unconfigured' | 'http-error' | 'unreachable'; message: string; url?: string };
|
||||
|
||||
/**
|
||||
* Env var pairs get flattened into a single newline-delimited string, matching
|
||||
* Komodo's DeploymentConfig.environment shape.
|
||||
*/
|
||||
export interface DeploymentEnvVar {
|
||||
variable: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CreateDeploymentSpec {
|
||||
name: string; // Komodo deployment name (== EC server name)
|
||||
server: string; // Komodo server (host) id or name
|
||||
image: string; // 'ghcr.io/ayymoss/plutainer:latest'
|
||||
network?: string; // 'komodo_default'
|
||||
ports: string[]; // ['28960:28960/udp', '28960:28960/tcp']
|
||||
volumes: string[]; // ['/host/path:/container/path', 'namedvol:/container/path:ro']
|
||||
environment: DeploymentEnvVar[];
|
||||
}
|
||||
|
||||
async function postAndClassify(path: string, body: unknown): Promise<KomodoWriteResult> {
|
||||
const url = `${config.komodo.address.replace(/\/+$/, '')}/${path}`;
|
||||
let res: Response;
|
||||
try {
|
||||
res = await komodoCall(path, body);
|
||||
} catch (err) {
|
||||
return { ok: false, kind: 'unreachable', message: describeFetchError(err, url), url };
|
||||
}
|
||||
if (!res.ok) {
|
||||
const snippet = await res.text().catch(() => '');
|
||||
return {
|
||||
ok: false,
|
||||
kind: 'http-error',
|
||||
message: `HTTP ${res.status}${snippet ? ` — ${truncate(snippet)}` : ''}`,
|
||||
url
|
||||
};
|
||||
}
|
||||
// Some Komodo endpoints return `{ success: false }` with a 200. Treat that
|
||||
// as a failure so callers can surface the real problem.
|
||||
try {
|
||||
const json = await res.json();
|
||||
if (json && typeof json === 'object' && 'success' in json && json.success === false) {
|
||||
const msg = typeof (json as { message?: unknown }).message === 'string'
|
||||
? (json as { message: string }).message
|
||||
: 'komodo returned success=false';
|
||||
return { ok: false, kind: 'http-error', message: msg, url };
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON body — trust the 2xx.
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new deployment entity in Komodo. Doesn't start the container —
|
||||
* caller must follow up with `komodoDeploy(name)` to actually run it.
|
||||
*
|
||||
* The config shape here targets Komodo's DeploymentConfig: newline-delimited
|
||||
* ports/volumes strings and an environment array with `{ variable, value }`
|
||||
* entries. If Komodo rejects the payload, the error surfaces in the http-error
|
||||
* message so we can iterate on the shape without guessing.
|
||||
*/
|
||||
export async function komodoCreateDeployment(spec: CreateDeploymentSpec): Promise<KomodoWriteResult> {
|
||||
if (!komodoConfigured()) {
|
||||
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
|
||||
}
|
||||
const body = {
|
||||
name: spec.name,
|
||||
config: {
|
||||
server_id: spec.server,
|
||||
image: { type: 'Image', params: { image: spec.image } },
|
||||
network: spec.network ?? 'komodo_default',
|
||||
ports: spec.ports.join('\n'),
|
||||
volumes: spec.volumes.join('\n'),
|
||||
environment: spec.environment,
|
||||
restart: 'unless-stopped'
|
||||
}
|
||||
};
|
||||
return postAndClassify('write/CreateDeployment', body);
|
||||
}
|
||||
|
||||
/** Start (or restart) the container backing a Komodo deployment. */
|
||||
export async function komodoDeploy(deployment: string): Promise<KomodoWriteResult> {
|
||||
if (!komodoConfigured()) {
|
||||
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
|
||||
}
|
||||
return postAndClassify('execute/Deploy', { deployment });
|
||||
}
|
||||
|
||||
/** Stop + remove the container backing a Komodo deployment. Entity remains. */
|
||||
export async function komodoDestroyDeployment(deployment: string): Promise<KomodoWriteResult> {
|
||||
if (!komodoConfigured()) {
|
||||
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
|
||||
}
|
||||
return postAndClassify('execute/DestroyDeployment', { deployment });
|
||||
}
|
||||
|
||||
/** Remove the deployment entity from Komodo entirely. */
|
||||
export async function komodoDeleteDeployment(deployment: string): Promise<KomodoWriteResult> {
|
||||
if (!komodoConfigured()) {
|
||||
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
|
||||
}
|
||||
// Komodo's DeleteDeployment historically accepts `{ id }` where id may be
|
||||
// the name; supply both to be forgiving.
|
||||
return postAndClassify('write/DeleteDeployment', { id: deployment, name: deployment });
|
||||
}
|
||||
|
||||
// ---------- Extractor ----------
|
||||
|
||||
// Property names Komodo/Docker use for the collection of port mappings.
|
||||
|
||||
Reference in New Issue
Block a user