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
+85
View File
@@ -519,6 +519,91 @@ export async function komodoDeleteDeployment(deployment: string): Promise<Komodo
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 ----------
// Property names Komodo/Docker use for the collection of port mappings.