Tag Komodo deployments with game + canonical map name

New deployments get two tags attached (e.g. "t5" and "zombie_pentagon")
so operators can filter the Komodo deployment list by game and by map.
Tags are ensured via read/ListTags + write/CreateTag fallback, then
attached with write/UpdateTagsOnResource against the deployment name.

Tag application runs fire-and-forget after CreateDeployment: any failure
logs a warning and the server creation proceeds. Tags are cosmetic
organization inside Komodo, not a correctness requirement.
This commit is contained in:
2026-09-17 09:15:00 -04:00
parent 5142a672ee
commit 5c674615d3
2 changed files with 122 additions and 2 deletions
+37 -2
View File
@@ -9,6 +9,8 @@ import {
komodoDestroyDeployment, komodoDestroyDeployment,
komodoDeleteDeployment, komodoDeleteDeployment,
komodoInspectHealthy, komodoInspectHealthy,
komodoEnsureTag,
komodoUpdateTagsOnResource,
type CreateDeploymentSpec, type CreateDeploymentSpec,
type DeploymentEnvVar type DeploymentEnvVar
} from '../integrations/komodo.js'; } from '../integrations/komodo.js';
@@ -198,7 +200,13 @@ export async function createServer(input: CreateServerInput): Promise<CreateServ
return { ok: false, kind: 'db', message: `failed to insert server row: ${(err as Error).message}` }; return { ok: false, kind: 'db', message: `failed to insert server row: ${(err as Error).message}` };
} }
// Step 4: execute/Deploy — actually start the container. // Step 4: apply Komodo tags so operators can filter the deployment list
// by game/map. Best-effort — a tag failure doesn't roll back the server;
// the deployment is fully functional without cosmetic tags. Fires in
// parallel to keep the create latency down.
void applyDeploymentTags(name, 't5', mapEntry.canonical);
// Step 5: execute/Deploy — actually start the container.
const deployRes = await komodoDeploy(name); const deployRes = await komodoDeploy(name);
if (!deployRes.ok) { if (!deployRes.ok) {
// Container never started — undo everything so retrying is clean. // Container never started — undo everything so retrying is clean.
@@ -208,13 +216,40 @@ export async function createServer(input: CreateServerInput): Promise<CreateServ
return { ok: false, kind: 'komodo', message: `Deploy: ${deployRes.message}` }; return { ok: false, kind: 'komodo', message: `Deploy: ${deployRes.message}` };
} }
// Step 5: kick the sweep so RCON status polling for this server starts // Step 6: kick the sweep so RCON status polling for this server starts
// now instead of on the next 15s tick. // now instead of on the next 15s tick.
void kick().catch((err) => console.error('[createServer] post-create kick failed:', err)); void kick().catch((err) => console.error('[createServer] post-create kick failed:', err));
return { ok: true, name, port }; return { ok: true, name, port };
} }
/**
* Ensure `game` and `map` tags exist in Komodo, then attach both to the given
* deployment. Purely cosmetic — any failure here logs and returns; the caller
* treats it as fire-and-forget.
*/
async function applyDeploymentTags(deployment: string, gameTagName: string, mapTagName: string): Promise<void> {
try {
const [gameTag, mapTag] = await Promise.all([
komodoEnsureTag(gameTagName),
komodoEnsureTag(mapTagName)
]);
const tagIds: string[] = [];
if (gameTag.ok) tagIds.push(gameTag.id);
else console.warn(`[createServer] tag "${gameTagName}": ${gameTag.message}`);
if (mapTag.ok) tagIds.push(mapTag.id);
else console.warn(`[createServer] tag "${mapTagName}": ${mapTag.message}`);
if (tagIds.length === 0) return;
const res = await komodoUpdateTagsOnResource('Deployment', deployment, tagIds);
if (!res.ok) {
console.warn(`[createServer] UpdateTagsOnResource for ${deployment}: ${res.message}`);
}
} catch (err) {
console.warn(`[createServer] tag application for ${deployment} threw:`, err);
}
}
export async function destroyServer(name: string): Promise<DestroyServerResult> { export async function destroyServer(name: string): Promise<DestroyServerResult> {
const row = getServer(name); const row = getServer(name);
if (!row) { if (!row) {
+85
View File
@@ -519,6 +519,91 @@ export async function komodoDeleteDeployment(deployment: string): Promise<Komodo
return postAndClassify('write/DeleteDeployment', { id: deployment, name: deployment }); return postAndClassify('write/DeleteDeployment', { id: deployment, name: deployment });
} }
// ---------- Tags ----------
export type KomodoTagResult =
| { ok: true; id: string }
| { ok: false; message: string };
/**
* Resolve a tag by name, creating it if it doesn't exist. Returns the tag's
* Komodo id (Mongo ObjectId string) for use in UpdateTagsOnResource.
*
* Best-effort: callers should treat failure as "skip tagging", not "roll back
* the whole operation". Tags are cosmetic organization inside Komodo.
*/
export async function komodoEnsureTag(name: string): Promise<KomodoTagResult> {
if (!komodoConfigured()) return { ok: false, message: 'komodo not configured' };
// 1. Look for an existing tag by name.
try {
const listRes = await komodoCall('read/ListTags', {});
if (listRes.ok) {
const tags = await listRes.json().catch(() => null);
if (Array.isArray(tags)) {
const match = tags.find(
(t: unknown): t is { _id: string; name: string } =>
!!t && typeof t === 'object' &&
typeof (t as { name?: unknown }).name === 'string' &&
(t as { name: string }).name === name &&
typeof (t as { _id?: unknown })._id === 'string'
);
if (match) return { ok: true, id: match._id };
}
}
} catch (err) {
// Fall through to create — list failure isn't fatal, we can still try
// to create; if the name is taken, CreateTag will surface a clean error.
console.warn(`[komodoEnsureTag] ListTags failed for "${name}":`, (err as Error).message);
}
// 2. Create the tag. Response shape: `{ _id, name, ... }` on success.
let createRes: Response;
try {
createRes = await komodoCall('write/CreateTag', { name });
} catch (err) {
return { ok: false, message: `CreateTag ${name}: ${(err as Error).message}` };
}
if (!createRes.ok) {
const snippet = await createRes.text().catch(() => '');
return {
ok: false,
message: `CreateTag ${name}: HTTP ${createRes.status}${snippet ? ` — ${truncate(snippet)}` : ''}`
};
}
try {
const body = await createRes.json();
if (body && typeof body === 'object' && typeof (body as { _id?: unknown })._id === 'string') {
return { ok: true, id: (body as { _id: string })._id };
}
return { ok: false, message: `CreateTag ${name}: unexpected response shape` };
} catch (err) {
return { ok: false, message: `CreateTag ${name}: ${(err as Error).message}` };
}
}
/**
* Attach tags to a Komodo resource. `tagIds` is the array of tag Mongo IDs
* from `komodoEnsureTag`. Replaces the resource's existing tag set — pass the
* full desired list, not a delta.
*
* Komodo's target.id accepts either the true id or the resource name for
* Deployments, so we pass the name here to save a lookup.
*/
export async function komodoUpdateTagsOnResource(
targetType: 'Deployment' | 'Stack' | 'Server' | 'Repo' | 'Build' | 'Procedure',
targetName: string,
tagIds: string[]
): Promise<KomodoWriteResult> {
if (!komodoConfigured()) {
return { ok: false, kind: 'unconfigured', message: 'komodo not configured' };
}
return postAndClassify('write/UpdateTagsOnResource', {
target: { type: targetType, id: targetName },
tags: tagIds
});
}
// ---------- Extractor ---------- // ---------- Extractor ----------
// Property names Komodo/Docker use for the collection of port mappings. // Property names Komodo/Docker use for the collection of port mappings.