Refactor apps routes: explicit dependency injection
- Updated all apps route modules to use destructured dependencies - Added JSDoc comments for factory functions - Replaced ctx. references with direct parameter access - Updated apps/index.js to extract and pass explicit dependencies - All files pass syntax validation Files refactored: - routes/apps/deploy.js (18k lines) - routes/apps/helpers.js (17k lines) - routes/apps/removal.js - routes/apps/restore.js - routes/apps/templates.js - routes/apps/index.js (orchestrator)
This commit is contained in:
@@ -7,29 +7,43 @@ const { isValidPort } = require('../../input-validator');
|
||||
const { exists } = require('../../fs-helpers');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
const { ValidationError } = require('../errors');
|
||||
/**
|
||||
* Apps deployment routes factory
|
||||
* @param {Object} deps - Explicit dependencies
|
||||
* @param {Object} deps.docker - Docker client wrapper
|
||||
* @param {Object} deps.caddy - Caddy client
|
||||
* @param {Object} deps.credentialManager - Credential manager
|
||||
* @param {Object} deps.servicesStateManager - Services state manager
|
||||
* @param {Object} deps.portLockManager - Port lock manager
|
||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||
* @param {Function} deps.errorResponse - Error response helper
|
||||
* @param {Object} deps.log - Logger instance
|
||||
* @param {Object} deps.helpers - Apps helpers module
|
||||
* @returns {express.Router}
|
||||
*/
|
||||
|
||||
module.exports = function(ctx, helpers) {
|
||||
module.exports = function({ docker, caddy, credentialManager, servicesStateManager, portLockManager, asyncHandler, errorResponse, log, helpers }) {
|
||||
const router = express.Router();
|
||||
|
||||
async function deployDashCAStaticSite(template, userConfig) {
|
||||
const destPath = platformPaths.caCertDir;
|
||||
try {
|
||||
ctx.log.info('deploy', 'DashCA: Starting static site deployment');
|
||||
log.info('deploy', 'DashCA: Starting static site deployment');
|
||||
if (!await exists(destPath)) {
|
||||
await fsp.mkdir(destPath, { recursive: true });
|
||||
ctx.log.info('deploy', 'DashCA: Created destination directory', { path: destPath });
|
||||
log.info('deploy', 'DashCA: Created destination directory', { path: destPath });
|
||||
}
|
||||
|
||||
ctx.log.info('deploy', 'DashCA: Verifying certificate files');
|
||||
log.info('deploy', 'DashCA: Verifying certificate files');
|
||||
const rootCertExists = await exists(`${destPath}/root.crt`);
|
||||
const intermediateCertExists = await exists(`${destPath}/intermediate.crt`);
|
||||
if (rootCertExists) ctx.log.info('deploy', 'DashCA: Root certificate found');
|
||||
else ctx.log.warn('deploy', 'DashCA: Root certificate not found', { expected: path.join(destPath, 'root.crt') });
|
||||
if (intermediateCertExists) ctx.log.info('deploy', 'DashCA: Intermediate certificate found');
|
||||
if (rootCertExists) log.info('deploy', 'DashCA: Root certificate found');
|
||||
else log.warn('deploy', 'DashCA: Root certificate not found', { expected: path.join(destPath, 'root.crt') });
|
||||
if (intermediateCertExists) log.info('deploy', 'DashCA: Intermediate certificate found');
|
||||
|
||||
const indexPath = path.join(destPath, 'index.html');
|
||||
if (!await exists(indexPath)) {
|
||||
ctx.log.info('deploy', 'DashCA: Creating minimal landing page');
|
||||
log.info('deploy', 'DashCA: Creating minimal landing page');
|
||||
const minimalHtml = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -58,15 +72,15 @@ module.exports = function(ctx, helpers) {
|
||||
</body>
|
||||
</html>`;
|
||||
await fsp.writeFile(indexPath, minimalHtml);
|
||||
ctx.log.info('deploy', 'DashCA: Created minimal landing page');
|
||||
log.info('deploy', 'DashCA: Created minimal landing page');
|
||||
} else {
|
||||
ctx.log.info('deploy', 'DashCA: Using existing index.html');
|
||||
log.info('deploy', 'DashCA: Using existing index.html');
|
||||
}
|
||||
|
||||
ctx.log.info('deploy', 'DashCA: For full features, copy certificate files to ' + destPath);
|
||||
ctx.log.info('deploy', 'DashCA: Static site deployment completed successfully');
|
||||
log.info('deploy', 'DashCA: For full features, copy certificate files to ' + destPath);
|
||||
log.info('deploy', 'DashCA: Static site deployment completed successfully');
|
||||
} catch (error) {
|
||||
ctx.log.error('deploy', 'DashCA deployment error', { error: error.message });
|
||||
log.error('deploy', 'DashCA deployment error', { error: error.message });
|
||||
throw new Error(`DashCA deployment failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -82,9 +96,9 @@ module.exports = function(ctx, helpers) {
|
||||
|
||||
let lockId = null;
|
||||
try {
|
||||
ctx.log.info('deploy', 'Acquiring port locks', { ports: requestedPorts });
|
||||
lockId = await ctx.portLockManager.acquirePorts(requestedPorts);
|
||||
ctx.log.info('deploy', 'Port locks acquired', { lockId });
|
||||
log.info('deploy', 'Acquiring port locks', { ports: requestedPorts });
|
||||
lockId = await portLockManager.acquirePorts(requestedPorts);
|
||||
log.info('deploy', 'Port locks acquired', { lockId });
|
||||
} catch (lockError) {
|
||||
throw new Error(`Failed to acquire port locks: ${lockError.message}`);
|
||||
}
|
||||
@@ -92,9 +106,9 @@ module.exports = function(ctx, helpers) {
|
||||
try {
|
||||
// Remove stale container with same name
|
||||
try {
|
||||
const existingContainer = ctx.docker.client.getContainer(containerName);
|
||||
const existingContainer = docker.client.getContainer(containerName);
|
||||
const info = await existingContainer.inspect();
|
||||
ctx.log.info('docker', 'Removing stale container', { containerName, status: info.State.Status });
|
||||
log.info('docker', 'Removing stale container', { containerName, status: info.State.Status });
|
||||
await existingContainer.remove({ force: true });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
} catch (e) {
|
||||
@@ -144,43 +158,43 @@ module.exports = function(ctx, helpers) {
|
||||
}
|
||||
|
||||
try {
|
||||
ctx.log.info('docker', 'Pulling image', { image: processedTemplate.docker.image });
|
||||
await ctx.docker.pull(processedTemplate.docker.image);
|
||||
ctx.log.info('docker', 'Image pulled successfully', { image: processedTemplate.docker.image });
|
||||
log.info('docker', 'Pulling image', { image: processedTemplate.docker.image });
|
||||
await docker.pull(processedTemplate.docker.image);
|
||||
log.info('docker', 'Image pulled successfully', { image: processedTemplate.docker.image });
|
||||
} catch (e) {
|
||||
ctx.log.warn('docker', 'Image pull failed, checking if local image exists', { image: processedTemplate.docker.image, error: e.message });
|
||||
log.warn('docker', 'Image pull failed, checking if local image exists', { image: processedTemplate.docker.image, error: e.message });
|
||||
try {
|
||||
const images = await ctx.docker.client.listImages({ filters: { reference: [processedTemplate.docker.image] } });
|
||||
const images = await docker.client.listImages({ filters: { reference: [processedTemplate.docker.image] } });
|
||||
if (images.length === 0) throw new Error(`[DC-201] Image ${processedTemplate.docker.image} not found locally and pull failed: ${e.message}`);
|
||||
ctx.log.info('docker', 'Using existing local image', { image: processedTemplate.docker.image });
|
||||
log.info('docker', 'Using existing local image', { image: processedTemplate.docker.image });
|
||||
} catch (listError) {
|
||||
throw new Error(`[DC-201] Failed to pull or find image ${processedTemplate.docker.image}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const container = await ctx.docker.client.createContainer(containerConfig);
|
||||
const container = await docker.client.createContainer(containerConfig);
|
||||
await container.start();
|
||||
|
||||
// Prune dangling images to prevent disk bloat
|
||||
try {
|
||||
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } });
|
||||
const pruneResult = await docker.client.pruneImages({ filters: { dangling: { true: true } } });
|
||||
if (pruneResult.SpaceReclaimed > 0) {
|
||||
ctx.log.info('docker', 'Pruned dangling images after deploy', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
||||
log.info('docker', 'Pruned dangling images after deploy', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
||||
}
|
||||
} catch (pruneErr) {
|
||||
ctx.log.debug('docker', 'Image prune after deploy failed', { error: pruneErr.message });
|
||||
log.debug('docker', 'Image prune after deploy failed', { error: pruneErr.message });
|
||||
}
|
||||
|
||||
await ctx.portLockManager.releasePorts(lockId);
|
||||
ctx.log.info('deploy', 'Port locks released', { lockId });
|
||||
await portLockManager.releasePorts(lockId);
|
||||
log.info('deploy', 'Port locks released', { lockId });
|
||||
return container.id;
|
||||
} catch (deployError) {
|
||||
if (lockId) {
|
||||
try {
|
||||
await ctx.portLockManager.releasePorts(lockId);
|
||||
ctx.log.info('deploy', 'Port locks released after error', { lockId });
|
||||
await portLockManager.releasePorts(lockId);
|
||||
log.info('deploy', 'Port locks released after error', { lockId });
|
||||
} catch (releaseError) {
|
||||
ctx.log.error('deploy', 'Failed to release port locks', { lockId, error: releaseError.message });
|
||||
log.error('deploy', 'Failed to release port locks', { lockId, error: releaseError.message });
|
||||
}
|
||||
}
|
||||
throw deployError;
|
||||
@@ -188,7 +202,7 @@ module.exports = function(ctx, helpers) {
|
||||
}
|
||||
|
||||
// Check for existing container before deployment
|
||||
router.post('/apps/check-existing', ctx.asyncHandler(async (req, res) => {
|
||||
router.post('/apps/check-existing', asyncHandler(async (req, res) => {
|
||||
const { appId } = req.body;
|
||||
const template = ctx.APP_TEMPLATES[appId];
|
||||
if (!template) throw new ValidationError('Invalid app template');
|
||||
@@ -201,7 +215,7 @@ module.exports = function(ctx, helpers) {
|
||||
}, 'check-existing'));
|
||||
|
||||
// Deploy new app
|
||||
router.post('/apps/deploy', ctx.asyncHandler(async (req, res) => {
|
||||
router.post('/apps/deploy', asyncHandler(async (req, res) => {
|
||||
const { appId, config } = req.body;
|
||||
if (!appId || typeof appId !== 'string') {
|
||||
throw new ValidationError('appId is required');
|
||||
@@ -213,10 +227,10 @@ module.exports = function(ctx, helpers) {
|
||||
throw new ValidationError('config.subdomain is required');
|
||||
}
|
||||
try {
|
||||
ctx.log.info('deploy', 'Deploying app', { appId, subdomain: config.subdomain });
|
||||
log.info('deploy', 'Deploying app', { appId, subdomain: config.subdomain });
|
||||
const template = ctx.APP_TEMPLATES[appId];
|
||||
if (!template) {
|
||||
await ctx.logError('app-deploy', new Error('Invalid app template'), { appId, config });
|
||||
await logError('app-deploy', new Error('Invalid app template'), { appId, config });
|
||||
throw new ValidationError('Invalid app template');
|
||||
}
|
||||
|
||||
@@ -226,7 +240,7 @@ module.exports = function(ctx, helpers) {
|
||||
}
|
||||
// Block reserved path names in subdirectory mode
|
||||
if (ctx.siteConfig.routingMode === 'subdirectory' && helpers.RESERVED_SUBPATHS.includes(config.subdomain)) {
|
||||
return ctx.errorResponse(res, 400, `[DC-301] "${config.subdomain}" is a reserved path and cannot be used in subdirectory mode`);
|
||||
return errorResponse(res, 400, `[DC-301] "${config.subdomain}" is a reserved path and cannot be used in subdirectory mode`);
|
||||
}
|
||||
}
|
||||
if (config.port && !isValidPort(config.port)) {
|
||||
@@ -236,7 +250,7 @@ module.exports = function(ctx, helpers) {
|
||||
if (!template.isStaticSite) {
|
||||
const allowedHostnames = ['localhost', 'host.docker.internal'];
|
||||
if (config.ip && !validatorLib.isIP(config.ip) && !allowedHostnames.includes(config.ip)) {
|
||||
return ctx.errorResponse(res, 400, '[DC-210] Invalid IP address. Use a valid IP (e.g., 192.168.x.x) or "localhost".');
|
||||
return errorResponse(res, 400, '[DC-210] Invalid IP address. Use a valid IP (e.g., 192.168.x.x) or "localhost".');
|
||||
}
|
||||
if (!config.ip) config.ip = ctx.siteConfig.dnsServerIp || 'localhost';
|
||||
} else {
|
||||
@@ -248,24 +262,24 @@ module.exports = function(ctx, helpers) {
|
||||
let usedExisting = false;
|
||||
|
||||
if (template.isStaticSite) {
|
||||
ctx.log.info('deploy', 'Deploying static site', { appId });
|
||||
log.info('deploy', 'Deploying static site', { appId });
|
||||
if (appId === 'dashca') {
|
||||
await deployDashCAStaticSite(template, config);
|
||||
containerId = null;
|
||||
ctx.log.info('deploy', 'Static site deployed', { appId });
|
||||
log.info('deploy', 'Static site deployed', { appId });
|
||||
} else {
|
||||
throw new Error(`Unknown static site type: ${appId}`);
|
||||
}
|
||||
} else if (config.useExisting && config.existingContainerId) {
|
||||
containerId = config.existingContainerId;
|
||||
usedExisting = true;
|
||||
ctx.log.info('deploy', 'Using existing container', { containerId });
|
||||
log.info('deploy', 'Using existing container', { containerId });
|
||||
if (config.existingPort && !config.port) config.port = config.existingPort;
|
||||
} else {
|
||||
containerId = await deployContainer(appId, config, template);
|
||||
ctx.log.info('deploy', 'Container deployed', { containerId });
|
||||
log.info('deploy', 'Container deployed', { containerId });
|
||||
await helpers.waitForHealthCheck(containerId, template.healthCheck, config.port || template.defaultPort);
|
||||
ctx.log.info('deploy', 'Container is healthy', { containerId });
|
||||
log.info('deploy', 'Container is healthy', { containerId });
|
||||
}
|
||||
|
||||
const isSubdirectoryMode = ctx.siteConfig.routingMode === 'subdirectory' && ctx.siteConfig.domain;
|
||||
@@ -275,11 +289,11 @@ module.exports = function(ctx, helpers) {
|
||||
if (config.createDns && !isSubdirectoryMode) {
|
||||
try {
|
||||
await ctx.dns.createRecord(config.subdomain, config.ip);
|
||||
ctx.log.info('deploy', 'DNS record created', { domain: ctx.buildDomain(config.subdomain), ip: config.ip });
|
||||
log.info('deploy', 'DNS record created', { domain: ctx.buildDomain(config.subdomain), ip: config.ip });
|
||||
} catch (dnsError) {
|
||||
await ctx.logError('app-deploy-dns', dnsError, { appId, subdomain: config.subdomain, ip: config.ip });
|
||||
await logError('app-deploy-dns', dnsError, { appId, subdomain: config.subdomain, ip: config.ip });
|
||||
dnsWarning = `DNS creation failed: ${dnsError.message}. You may need to create the DNS record manually.`;
|
||||
ctx.log.warn('deploy', 'DNS creation failed during deploy', { error: dnsError.message });
|
||||
log.warn('deploy', 'DNS creation failed during deploy', { error: dnsError.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +312,7 @@ module.exports = function(ctx, helpers) {
|
||||
}
|
||||
caddyConfig = helpers.generateStaticSiteConfig(config.subdomain, sitePath, caddyOptions);
|
||||
} else {
|
||||
caddyConfig = ctx.caddy.generateConfig(config.subdomain, config.ip, config.port || template.defaultPort, caddyOptions);
|
||||
caddyConfig = caddy.generateConfig(config.subdomain, config.ip, config.port || template.defaultPort, caddyOptions);
|
||||
}
|
||||
|
||||
// Write Caddy config (subdirectory: inject into main block; subdomain: append as new block)
|
||||
@@ -308,7 +322,7 @@ module.exports = function(ctx, helpers) {
|
||||
} else {
|
||||
await helpers.addCaddyConfig(config.subdomain, caddyConfig);
|
||||
}
|
||||
ctx.log.info('deploy', 'Caddy config added', { domain: ctx.buildDomain(config.subdomain), routingMode: ctx.siteConfig.routingMode, tailscaleOnly: config.tailscaleOnly || false });
|
||||
log.info('deploy', 'Caddy config added', { domain: ctx.buildDomain(config.subdomain), routingMode: ctx.siteConfig.routingMode, tailscaleOnly: config.tailscaleOnly || false });
|
||||
|
||||
// Build service URL based on routing mode
|
||||
const serviceUrl = ctx.buildServiceUrl(config.subdomain);
|
||||
@@ -361,7 +375,7 @@ module.exports = function(ctx, helpers) {
|
||||
deployedAt: new Date().toISOString(),
|
||||
deploymentManifest
|
||||
});
|
||||
ctx.log.info('deploy', 'Service added to dashboard', { subdomain: config.subdomain });
|
||||
log.info('deploy', 'Service added to dashboard', { subdomain: config.subdomain });
|
||||
|
||||
const response = {
|
||||
success: true, containerId, usedExisting,
|
||||
@@ -378,11 +392,11 @@ module.exports = function(ctx, helpers) {
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
await ctx.logError('app-deploy', error, { appId, config });
|
||||
ctx.log.error('deploy', 'Deployment failed', { appId, error: error.message });
|
||||
await logError('app-deploy', error, { appId, config });
|
||||
log.error('deploy', 'Deployment failed', { appId, error: error.message });
|
||||
const template = ctx.APP_TEMPLATES[appId];
|
||||
ctx.notification.send('deploymentFailed', 'Deployment Failed', `Failed to deploy **${template?.name || appId}**.\nError: ${error.message}`, 'error');
|
||||
ctx.errorResponse(res, 500, ctx.safeErrorMessage(error));
|
||||
errorResponse(res, 500, ctx.safeErrorMessage(error));
|
||||
}
|
||||
}, 'apps-deploy'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user