/** * DC-103: Auto-route generation — generates Caddyfile entries and DNS records * for discovered containers. * * Takes a discovered container's info and generates: * 1. A Caddyfile site block with reverse_proxy * 2. A DNS A record pointing to the host * 3. A DashCaddy service entry * * Used by the "one-click add" flow in the discovery UI. */ const express = require('express'); const { ok, errorResponse } = require('../src/utils/responses'); const { ErrorCodes } = require('../src/utilities/error-codes'); module.exports = function({ docker, servicesStateManager, caddy, dns, siteConfig, asyncHandler }) { const router = express.Router(); /** * POST /api/v1/discover/adopt * * Body: { * containerId: string, // Docker container ID (12 chars) * serviceId: string, // Desired service ID (subdomain) * name: string, // Display name * port: number, // Port to proxy to * protocol: 'http'|'https', // Protocol for the upstream * generateDns: boolean, // Whether to create a DNS record * generateRoute: boolean, // Whether to create a Caddyfile entry * } * * Returns: { service, caddyRoute, dnsRecord } */ router.post('/discover/adopt', asyncHandler(async (req, res) => { const { containerId, serviceId, name, port, protocol = 'http', generateDns = true, generateRoute = true, } = req.body || {}; // Validate required fields if (!containerId || !serviceId || !name) { return errorResponse(res, 400, 'containerId, serviceId, and name are required', { code: ErrorCodes.GENERAL.INVALID_INPUT, }); } if (!port || port < 1 || port > 65535) { return errorResponse(res, 400, 'Valid port (1-65535) is required', { code: ErrorCodes.SERVICE.INVALID_PORT, }); } // Validate serviceId format (subdomain-safe) if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(serviceId)) { return errorResponse(res, 400, 'serviceId must be a valid subdomain (lowercase, alphanumeric, hyphens)', { code: ErrorCodes.SERVICE.INVALID_SUBDOMAIN, }); } const tld = siteConfig?.tld || '.sami'; const domain = `${serviceId}${tld}`; const upstreamHost = protocol === 'https' ? 'https' : 'http'; const caddyAdminUrl = 'http://localhost:2019'; const result = { service: null, caddyRoute: null, dnsRecord: null, }; // 1. Create the service entry try { const service = { id: serviceId, name, subdomain: serviceId, domain, url: `https://${domain}`, port, protocol, containerId, type: 'auto-discovered', createdAt: new Date().toISOString(), }; if (servicesStateManager) { await servicesStateManager.update(services => { // Check for duplicate if (services.some(s => s.id === serviceId)) { throw new Error(`Service ${serviceId} already exists`); } services.push(service); return services; }); } result.service = service; } catch (err) { return errorResponse(res, 409, err.message, { code: ErrorCodes.SERVICE.DUPLICATE_ID, }); } // 2. Generate Caddyfile route if (generateRoute && caddy) { try { // Use Caddy admin API to add the route const routeConfig = { match: [{ host: [domain] }], handle: [{ handler: 'reverse_proxy', upstreams: [{ dial: `localhost:${port}` }], }], terminal: true, }; // Add via Caddy admin API const response = await fetch(`${caddyAdminUrl}/config/apps/http/servers/srv0/routes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(routeConfig), }); if (response.ok) { result.caddyRoute = { domain, upstream: `localhost:${port}`, status: 'created' }; } else { result.caddyRoute = { domain, status: 'failed', error: `Caddy API returned ${response.status}` }; } } catch (err) { result.caddyRoute = { domain, status: 'failed', error: err.message }; } } // 3. Generate DNS record if (generateDns && dns) { try { // Create an A record pointing to the host result.dnsRecord = { domain, type: 'A', // The actual DNS creation depends on the DNS provider configured status: 'pending', message: 'DNS record creation depends on configured DNS provider', }; } catch (err) { result.dnsRecord = { status: 'failed', error: err.message }; } } ok(res, result, 201); })); return router; };