refactor(context): Extract context modules from god object
- Create src/context/docker.js - Docker operations - Create src/context/caddy.js - Caddyfile manipulation - Create src/context/dns.js - DNS token management and API calls - Create src/context/session.js - Session wrapper - Create src/context/index.js - Context assembly (DI container) Breaks up the 50+ property ctx god object into domain-specific modules
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Caddy context - Caddyfile manipulation and reload
|
||||
*/
|
||||
const fsp = require('fs').promises;
|
||||
const { RETRIES } = require('../../constants');
|
||||
|
||||
/**
|
||||
* Atomically read-modify-write the Caddyfile and reload Caddy.
|
||||
* Uses a mutex to prevent concurrent modifications.
|
||||
* Rolls back on reload failure.
|
||||
*/
|
||||
let _caddyfileLock = Promise.resolve();
|
||||
|
||||
async function modifyCaddyfile(CADDYFILE_PATH, reloadCaddy, modifyFn) {
|
||||
let resolve;
|
||||
const prev = _caddyfileLock;
|
||||
_caddyfileLock = new Promise(r => { resolve = r; });
|
||||
await prev;
|
||||
|
||||
try {
|
||||
const original = await fsp.readFile(CADDYFILE_PATH, 'utf8');
|
||||
const modified = await modifyFn(original);
|
||||
|
||||
if (modified === null || modified === original) {
|
||||
return { success: false, error: 'No changes to apply' };
|
||||
}
|
||||
|
||||
await fsp.writeFile(CADDYFILE_PATH, modified, 'utf8');
|
||||
|
||||
try {
|
||||
await reloadCaddy(modified);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
// Rollback
|
||||
await fsp.writeFile(CADDYFILE_PATH, original, 'utf8');
|
||||
return { success: false, error: err.message, rolledBack: true };
|
||||
}
|
||||
} finally {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current Caddyfile content
|
||||
*/
|
||||
async function readCaddyfile(CADDYFILE_PATH) {
|
||||
return fsp.readFile(CADDYFILE_PATH, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload Caddy via admin API
|
||||
*/
|
||||
async function reloadCaddy(CADDY_ADMIN_URL, content, fetchT, log) {
|
||||
const maxRetries = RETRIES.CADDY_RELOAD;
|
||||
let lastError = null;
|
||||
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
const response = await fetchT(`${CADDY_ADMIN_URL}/load`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'text/caddyfile' },
|
||||
body: content
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
log.info('caddy', 'Caddy configuration reloaded successfully');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return;
|
||||
}
|
||||
|
||||
lastError = await response.text();
|
||||
log.warn('caddy', 'Caddy reload attempt failed', { attempt: i + 1, error: lastError });
|
||||
} catch (error) {
|
||||
lastError = error.message;
|
||||
log.warn('caddy', 'Caddy reload attempt error', { attempt: i + 1, error: lastError });
|
||||
}
|
||||
|
||||
if (i < maxRetries - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`[DC-303] Caddy reload failed after ${maxRetries} attempts: ${lastError}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a site is accessible via HTTPS
|
||||
*/
|
||||
async function verifySiteAccessible(domain, fetchT, httpsAgent, log, maxAttempts = 5) {
|
||||
const delay = 2000;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const response = await fetchT(`https://${domain}/`, {
|
||||
method: 'HEAD',
|
||||
agent: httpsAgent,
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
log.info('caddy', 'Site is accessible', { domain, status: response.status });
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.debug('caddy', 'Site verification attempt', {
|
||||
domain,
|
||||
attempt: i + 1,
|
||||
maxAttempts,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (i < maxAttempts - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
log.warn('caddy', 'Could not verify site accessibility', { domain });
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Caddy config block for a service
|
||||
*/
|
||||
function generateCaddyConfig(subdomain, ip, port, siteConfig, buildDomain, options = {}) {
|
||||
const { tailscaleOnly = false, allowedIPs = [], subpathSupport = 'strip' } = options;
|
||||
|
||||
// Subdirectory mode
|
||||
if (siteConfig.routingMode === 'subdirectory' && siteConfig.domain) {
|
||||
let config = '';
|
||||
|
||||
if (subpathSupport === 'native') {
|
||||
config += `\tredir /${subdomain} /${subdomain}/ permanent\n`;
|
||||
config += `\thandle /${subdomain}/* {\n`;
|
||||
} else {
|
||||
config += `\thandle_path /${subdomain}/* {\n`;
|
||||
}
|
||||
|
||||
if (tailscaleOnly) {
|
||||
config += `\t\t@blocked not remote_ip 100.64.0.0/10`;
|
||||
if (allowedIPs.length > 0) config += ` ${allowedIPs.join(' ')}`;
|
||||
config += `\n\t\trespond @blocked "Access denied. Tailscale connection required." 403\n`;
|
||||
}
|
||||
|
||||
config += `\t\treverse_proxy ${ip}:${port}\n`;
|
||||
config += `\t}`;
|
||||
return config;
|
||||
}
|
||||
|
||||
// Subdomain mode
|
||||
let config = `${buildDomain(subdomain)} {\n`;
|
||||
|
||||
if (tailscaleOnly) {
|
||||
config += ` @blocked not remote_ip 100.64.0.0/10`;
|
||||
if (allowedIPs.length > 0) {
|
||||
config += ` ${allowedIPs.join(' ')}`;
|
||||
}
|
||||
config += `\n respond @blocked "Access denied. Tailscale connection required." 403\n`;
|
||||
}
|
||||
|
||||
config += ` reverse_proxy ${ip}:${port}\n`;
|
||||
config += ` tls internal\n`;
|
||||
config += `}`;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function createCaddyContext(CADDYFILE_PATH, CADDY_ADMIN_URL, fetchT, httpsAgent, log, siteConfig, buildDomain) {
|
||||
const reload = (content) => reloadCaddy(CADDY_ADMIN_URL, content, fetchT, log);
|
||||
const read = () => readCaddyfile(CADDYFILE_PATH);
|
||||
const modify = (modifyFn) => modifyCaddyfile(CADDYFILE_PATH, reload, modifyFn);
|
||||
const verify = (domain, maxAttempts) => verifySiteAccessible(domain, fetchT, httpsAgent, log, maxAttempts);
|
||||
const generate = (subdomain, ip, port, options) => generateCaddyConfig(subdomain, ip, port, siteConfig, buildDomain, options);
|
||||
|
||||
return {
|
||||
modify,
|
||||
read,
|
||||
reload,
|
||||
generateConfig: generate,
|
||||
verifySite: verify,
|
||||
adminUrl: CADDY_ADMIN_URL,
|
||||
filePath: CADDYFILE_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createCaddyContext };
|
||||
Reference in New Issue
Block a user