From 5c674615d3de8ccb2c358267fe14bd00b8efb28a Mon Sep 17 00:00:00 2001 From: josh Date: Thu, 17 Sep 2026 09:15:00 -0400 Subject: [PATCH] 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. --- src/lib/server/fleet/create.ts | 39 +++++++++++- src/lib/server/integrations/komodo.ts | 85 +++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/lib/server/fleet/create.ts b/src/lib/server/fleet/create.ts index 1c6466d..b2e15df 100644 --- a/src/lib/server/fleet/create.ts +++ b/src/lib/server/fleet/create.ts @@ -9,6 +9,8 @@ import { komodoDestroyDeployment, komodoDeleteDeployment, komodoInspectHealthy, + komodoEnsureTag, + komodoUpdateTagsOnResource, type CreateDeploymentSpec, type DeploymentEnvVar } from '../integrations/komodo.js'; @@ -198,7 +200,13 @@ export async function createServer(input: CreateServerInput): Promise console.error('[createServer] post-create kick failed:', err)); 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 { + 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 { const row = getServer(name); if (!row) { diff --git a/src/lib/server/integrations/komodo.ts b/src/lib/server/integrations/komodo.ts index 31bdffb..603b725 100644 --- a/src/lib/server/integrations/komodo.ts +++ b/src/lib/server/integrations/komodo.ts @@ -519,6 +519,91 @@ export async function komodoDeleteDeployment(deployment: string): Promise { + 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 { + 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.