From f550423c3f5dac8b76b7ac870165b21839462874 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sun, 22 Mar 2026 19:49:36 -0600 Subject: [PATCH] Add registry module and api .dockerignore to fix production build - Create api/src/modules/registry/ (service + routes) so server.ts import resolves and TypeScript compiles all 38 modules cleanly - Add api/.dockerignore to exclude stale local dist/ from Docker build context, preventing old compiled output from persisting in images - Registry routes: GET /status (Gitea packages API), POST /build-push and POST /mirror (write trigger files for host watcher, SUPER_ADMIN only) Bunker Admin --- api/.dockerignore | 7 ++ api/src/modules/registry/registry.routes.ts | 43 +++++++++++ api/src/modules/registry/registry.service.ts | 76 ++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 api/.dockerignore create mode 100644 api/src/modules/registry/registry.routes.ts create mode 100644 api/src/modules/registry/registry.service.ts diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 00000000..e4839118 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +*.log +test-*.sh +test*.ts +test*.js diff --git a/api/src/modules/registry/registry.routes.ts b/api/src/modules/registry/registry.routes.ts new file mode 100644 index 00000000..ff02404a --- /dev/null +++ b/api/src/modules/registry/registry.routes.ts @@ -0,0 +1,43 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { authenticate } from '../../middleware/auth.middleware'; +import { requireRole } from '../../middleware/rbac.middleware'; +import { registryService } from './registry.service'; + +const router = Router(); + +router.use(authenticate); +router.use(requireRole('SUPER_ADMIN')); + +// GET /api/registry/status — list container images in Gitea registry +router.get('/status', async (_req: Request, res: Response, next: NextFunction) => { + try { + const images = await registryService.getRegistryStatus(); + res.json({ images }); + } catch (err) { + next(err); + } +}); + +// POST /api/registry/build-push — trigger build-and-push.sh via trigger file +router.post('/build-push', async (req: Request, res: Response, next: NextFunction) => { + try { + const { skipCodeServer, services } = req.body as { skipCodeServer?: boolean; services?: string }; + registryService.triggerBuildPush(req.user?.email ?? 'api', { skipCodeServer, services }); + res.json({ ok: true, message: 'Build and push triggered (runs in background)' }); + } catch (err) { + next(err); + } +}); + +// POST /api/registry/mirror — trigger mirror-images.sh via trigger file +router.post('/mirror', async (req: Request, res: Response, next: NextFunction) => { + try { + const { all } = req.body as { all?: boolean }; + registryService.triggerMirror(req.user?.email ?? 'api', { all }); + res.json({ ok: true, message: 'Mirror triggered (runs in background)' }); + } catch (err) { + next(err); + } +}); + +export const registryRouter = router; diff --git a/api/src/modules/registry/registry.service.ts b/api/src/modules/registry/registry.service.ts new file mode 100644 index 00000000..0688fbfb --- /dev/null +++ b/api/src/modules/registry/registry.service.ts @@ -0,0 +1,76 @@ +import fs from 'fs'; +import path from 'path'; +import { env } from '../../config/env'; +import { logger } from '../../utils/logger'; + +/** + * Registry service — queries Gitea Packages API for container image status. + * Build/mirror triggers are handled via trigger files read by the host watcher + * (same pattern as upgrade service). + */ + +const UPGRADE_DIR = path.resolve('/app/upgrade'); +const REGISTRY_TRIGGER_FILE = path.join(UPGRADE_DIR, 'registry-trigger.json'); + +export interface RegistryImage { + name: string; + version: string; + created_at: string; +} + +async function getRegistryStatus(): Promise { + const registryUrl = env.GITEA_REGISTRY || 'gitea.bnkops.com/admin'; + const [host, org] = registryUrl.split('/'); + const user = env.GITEA_REGISTRY_USER; + const pass = env.GITEA_REGISTRY_PASS; + + if (!user || !pass) { + throw new Error('GITEA_REGISTRY_USER and GITEA_REGISTRY_PASS must be configured'); + } + + const auth = Buffer.from(`${user}:${pass}`).toString('base64'); + const res = await fetch(`https://${host}/api/v1/packages/${org}?type=container&limit=50`, { + headers: { Authorization: `Basic ${auth}` }, + }); + + if (!res.ok) { + throw new Error(`Gitea API error: ${res.status} ${res.statusText}`); + } + + const data = await res.json() as RegistryImage[]; + return data; +} + +interface RegistryTrigger { + action: 'build-push' | 'mirror'; + triggeredAt: string; + triggeredBy: string; + options: Record; +} + +function writeTrigger(action: RegistryTrigger['action'], triggeredBy: string, options: Record): void { + try { + fs.mkdirSync(UPGRADE_DIR, { recursive: true }); + const trigger: RegistryTrigger = { + action, + triggeredAt: new Date().toISOString(), + triggeredBy, + options, + }; + fs.writeFileSync(REGISTRY_TRIGGER_FILE, JSON.stringify(trigger, null, 2), 'utf-8'); + logger.info(`Registry trigger written: ${action} by ${triggeredBy}`); + } catch (err) { + logger.error('Failed to write registry trigger:', err); + throw err; + } +} + +function triggerBuildPush(triggeredBy: string, options: { skipCodeServer?: boolean; services?: string } = {}): void { + writeTrigger('build-push', triggeredBy, options); +} + +function triggerMirror(triggeredBy: string, options: { all?: boolean } = {}): void { + writeTrigger('mirror', triggeredBy, options); +} + +export const registryService = { getRegistryStatus, triggerBuildPush, triggerMirror };