Compare commits

...
5 Commits
Author SHA1 Message Date
Hermes e361d9a328 fix: increase pull timeout to 300s, add missing environment:{} to portainer + uptime-kuma templates
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 17:05:28 -07:00
Hermes aa25bcc053 fix: always expose DC-prefixed errors to users in safeErrorMessage
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 17:02:13 -07:00
Hermes bda08b592e fix: idempotent Caddy subpath config, increase Docker pull timeout to 120s, extend health check to 60s
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- helpers.js: treat 'No changes to apply' as success (config already exists = idempotent)
- constants.js: Docker pull timeout 30s → 120s (large images need more time)
- deploy.js: health check 40s → 60s (some apps like filebrowser are slow to start)
2026-06-10 16:43:34 -07:00
Hermes 0e408974a0 fix: harden deploy error handling - guard against undefined errors, safeErrorMessage null check
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
- deploy.js: wrap logError/notification in try/catch so they never mask the original deploy error
- deploy.js: use optional chaining for error.message access
- logging.js: safeErrorMessage handles null/undefined error gracefully
2026-06-10 16:39:14 -07:00
Hermes f4b35dcc30 fix: correct apps route mount paths - mount all sub-routers at /apps prefix to match frontend API calls
CI / Test & Lint (push) Has been cancelled
CI / Security audit (push) Has been cancelled
2026-06-10 16:28:41 -07:00
6 changed files with 28 additions and 18 deletions
+4 -2
View File
@@ -342,7 +342,8 @@ const APP_TEMPLATES = {
volumes: [ volumes: [
"/var/run/docker.sock:/var/run/docker.sock", "/var/run/docker.sock:/var/run/docker.sock",
"/opt/portainer/data:/data" "/opt/portainer/data:/data"
] ],
environment: {}
}, },
subdomain: "portainer", subdomain: "portainer",
defaultPort: 9000, defaultPort: 9000,
@@ -393,7 +394,8 @@ const APP_TEMPLATES = {
docker: { docker: {
image: "louislam/uptime-kuma:latest", image: "louislam/uptime-kuma:latest",
ports: ["{{PORT}}:3001"], ports: ["{{PORT}}:3001"],
volumes: ["/opt/uptime-kuma:/app/data"] volumes: ["/opt/uptime-kuma:/app/data"],
environment: {}
}, },
subdomain: "uptime", subdomain: "uptime",
defaultPort: 3002, defaultPort: 3002,
+1 -1
View File
@@ -102,7 +102,7 @@ const DNS_RECORD_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV', 'PTR',
// ── Docker ────────────────────────────────────────────────────── // ── Docker ──────────────────────────────────────────────────────
const DOCKER = { const DOCKER = {
CONTAINER_PREFIX: 'sami-', CONTAINER_PREFIX: 'sami-',
TIMEOUT: 30000, // 30s — timeout for docker pull/create operations TIMEOUT: 300000, // 300s — timeout for docker pull/create operations
LOG_CONFIG: { LOG_CONFIG: {
Type: 'json-file', Type: 'json-file',
Config: { 'max-size': '10m', 'max-file': '3' } // 30MB max per container Config: { 'max-size': '10m', 'max-file': '3' } // 30MB max per container
+5 -4
View File
@@ -306,7 +306,7 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
} else { } else {
containerId = await deployContainer(appId, config, template); containerId = await deployContainer(appId, config, template);
log.info('deploy', 'Container deployed', { containerId }); log.info('deploy', 'Container deployed', { containerId });
await helpers.waitForHealthCheck(containerId, template.healthCheck, config.port || template.defaultPort); await helpers.waitForHealthCheck(containerId, template.healthCheck, config.port || template.defaultPort, 30);
log.info('deploy', 'Container is healthy', { containerId }); log.info('deploy', 'Container is healthy', { containerId });
} }
@@ -420,10 +420,11 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
res.json(response); res.json(response);
} catch (error) { } catch (error) {
await logError('app-deploy', error, { appId, config }); try { await logError('app-deploy', error, { appId, config }); } catch (_) { /* logError failure should not mask original error */ }
log.error('deploy', 'Deployment failed', { appId, error: error.message }); const msg = error?.message || String(error || 'Unknown error');
log.error('deploy', 'Deployment failed', { appId, error: msg });
const template = ctx.APP_TEMPLATES[appId]; const template = ctx.APP_TEMPLATES[appId];
ctx.notification.send('deploymentFailed', 'Deployment Failed', `Failed to deploy **${template?.name || appId}**.\nError: ${error.message}`, 'error'); try { ctx.notification.send('deploymentFailed', 'Deployment Failed', `Failed to deploy **${template?.name || appId}**.\nError: ${msg}`, 'error'); } catch (_) {}
errorResponse(res, 500, ctx.safeErrorMessage(error)); errorResponse(res, 500, ctx.safeErrorMessage(error));
} }
}, 'apps-deploy')); }, 'apps-deploy'));
+4 -1
View File
@@ -379,9 +379,12 @@ module.exports = function({ docker, caddy, credentialManager, servicesStateManag
return content.slice(0, endIdx) + injection + content.slice(endIdx); return content.slice(0, endIdx) + injection + content.slice(endIdx);
}); });
if (!result.success) { if (!result.success && result.error !== 'No changes to apply') {
throw new Error(`[DC-303] Failed to add subpath config for ${subdomain}: ${result.error}`); throw new Error(`[DC-303] Failed to add subpath config for ${subdomain}: ${result.error}`);
} }
if (result.error === 'No changes to apply') {
log.info('caddy', 'Subpath config already exists, reusing', { subdomain });
}
} }
/** Remove a subpath config block from between its markers in the Caddyfile. */ /** Remove a subpath config block from between its markers in the Caddyfile. */
+9 -9
View File
@@ -25,7 +25,6 @@ module.exports = function(ctx) {
asyncHandler: ctx.asyncHandler, asyncHandler: ctx.asyncHandler,
errorResponse: ctx.errorResponse, errorResponse: ctx.errorResponse,
log: ctx.log, log: ctx.log,
// Additional context properties needed by routes
APP_TEMPLATES: ctx.APP_TEMPLATES, APP_TEMPLATES: ctx.APP_TEMPLATES,
TEMPLATE_CATEGORIES: ctx.TEMPLATE_CATEGORIES, TEMPLATE_CATEGORIES: ctx.TEMPLATE_CATEGORIES,
DIFFICULTY_LEVELS: ctx.DIFFICULTY_LEVELS, DIFFICULTY_LEVELS: ctx.DIFFICULTY_LEVELS,
@@ -40,25 +39,26 @@ module.exports = function(ctx) {
ctx: ctx ctx: ctx
}; };
// Initialize helpers with dependencies (ctx is the Koa context)
const helpers = initHelpers({ ...deps, ctx }); const helpers = initHelpers({ ...deps, ctx });
// Mount sub-routes — pass full ctx so sub-routes can reference ctx.* properties
const subCtx = Object.assign({}, ctx, { helpers }); const subCtx = Object.assign({}, ctx, { helpers });
try { router.use('/', initDeploy(subCtx)); } // Mount sub-routers at their prefix paths.
// Sub-modules define routes at '/' (root of their sub-router).
// Final paths: /api/v1/apps/deploy, /api/v1/apps/remove, /api/v1/apps/templates, etc.
try { router.use('/apps', initDeploy(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] deploy routes init failed:', e.message, e.stack); } catch(e) { (ctx.log || console).error('[apps] deploy routes init failed:', e.message, e.stack); }
try { router.use('/', initRemoval(subCtx)); } try { router.use('/apps', initRemoval(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] removal routes init failed:', e.message, e.stack); } catch(e) { (ctx.log || console).error('[apps] removal routes init failed:', e.message, e.stack); }
try { router.use('/', initTemplates(subCtx)); } try { router.use('/apps', initTemplates(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] templates routes init failed:', e.message, e.stack); } catch(e) { (ctx.log || console).error('[apps] templates routes init failed:', e.message, e.stack); }
try { router.use('/', initRestore(Object.assign({}, subCtx, { backupManager: ctx.backupManager }))); } try { router.use('/apps', initRestore(Object.assign({}, subCtx, { backupManager: ctx.backupManager }))); }
catch(e) { (ctx.log || console).error('[apps] restore routes init failed:', e.message, e.stack); } catch(e) { (ctx.log || console).error('[apps] restore routes init failed:', e.message, e.stack); }
try { router.use('/', initCompose(subCtx)); } try { router.use('/apps', initCompose(subCtx)); }
catch(e) { (ctx.log || console).error('[apps] compose routes init failed:', e.message, e.stack); } catch(e) { (ctx.log || console).error('[apps] compose routes init failed:', e.message, e.stack); }
return router; return router;
+5 -1
View File
@@ -94,8 +94,12 @@ async function logError(ERROR_LOG_FILE, MAX_ERROR_LOG_SIZE, context, error, addi
* Return a safe error message without leaking internals * Return a safe error message without leaking internals
*/ */
function safeErrorMessage(error) { function safeErrorMessage(error) {
if (!error) return 'An internal error occurred';
const msg = error.message || String(error); const msg = error.message || String(error);
// Always expose DC-prefixed user-facing errors
if (/\[DC-\d+\]/.test(msg)) return msg;
// Detect port conflict errors // Detect port conflict errors
const portMatch = msg.match(/exposing port TCP [^:]*:(\d+)/); const portMatch = msg.match(/exposing port TCP [^:]*:(\d+)/);
if (portMatch || msg.includes('port is already allocated') || msg.includes('ports are not available')) { if (portMatch || msg.includes('port is already allocated') || msg.includes('ports are not available')) {
@@ -103,7 +107,7 @@ function safeErrorMessage(error) {
return `[DC-200] Port ${port} is already in use. Try a different port or stop the service using that port first.`; return `[DC-200] Port ${port} is already in use. Try a different port or stop the service using that port first.`;
} }
// Only expose short, user-facing messages // Only expose short, user-facing messages (no paths, stack traces, or internal details)
if (msg.length < 200 && !msg.includes('/') && !msg.includes('\\') && !msg.includes(' at ')) { if (msg.length < 200 && !msg.includes('/') && !msg.includes('\\') && !msg.includes(' at ')) {
return msg; return msg;
} }