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:
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user