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
This commit is contained in:
bunker-admin 2026-03-22 19:49:36 -06:00
parent be2fa5d80b
commit f550423c3f
3 changed files with 126 additions and 0 deletions

7
api/.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
.git
*.log
test-*.sh
test*.ts
test*.js

View File

@ -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;

View File

@ -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<RegistryImage[]> {
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<string, unknown>;
}
function writeTrigger(action: RegistryTrigger['action'], triggeredBy: string, options: Record<string, unknown>): 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 };