Two cleanups in one pass for the v1.14.0 'works on any platform' theme: 1. Response helpers — merged src/utils/responses.js and the root-level response-helpers.js into a single module at src/utils/responses.js. The old module had a richer set (created, noContent, validationError, unauthorized, forbidden, notFound, conflict) and is now re-exported from the new location. Updated 15 routes to import from src/utils/responses and deleted the root response-helpers.js. 2. Error logger — error-handler.js now uses the unified src/utils/logging.js#logError (same one src/app.js uses), so all errors go to one log file with one rotation policy. Removed the dead asyncHandler export (the real one is in src/utils/async-handler.js and is used everywhere). Deleted the legacy error-logger.js. Both are invisible to users — same HTTP response shapes, same log file path, same error format. Internal-only refactor.
165 lines
5.6 KiB
JavaScript
165 lines
5.6 KiB
JavaScript
/**
|
|
* Auto-Restart Policy Routes
|
|
*
|
|
* CRUD endpoints for per-container auto-restart policies.
|
|
* Also provides a dry-run test endpoint.
|
|
*
|
|
* @module routes/auto-restart
|
|
*/
|
|
|
|
const express = require('express');
|
|
const { success } = require('../src/utils/responses');
|
|
const { ValidationError, NotFoundError } = require('../errors');
|
|
|
|
/**
|
|
* Auto-restart route factory
|
|
*
|
|
* @param {Object} deps - Explicit dependencies
|
|
* @param {Object} deps.autoRestartManager - AutoRestartManager instance
|
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
|
* @param {Function} deps.logError - Error logging function
|
|
* @returns {express.Router}
|
|
*/
|
|
module.exports = function ({ autoRestartManager, asyncHandler, logError }) {
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* GET /auto-restart/policies
|
|
* List all configured auto-restart policies.
|
|
*/
|
|
router.get('/policies', asyncHandler(async (_req, res) => {
|
|
const policies = autoRestartManager.listPolicies();
|
|
success(res, { policies });
|
|
}, 'auto-restart-list'));
|
|
|
|
/**
|
|
* GET /auto-restart/policies/:serviceId
|
|
* Get the restart policy for a single service.
|
|
*/
|
|
router.get('/policies/:serviceId', asyncHandler(async (req, res) => {
|
|
const { serviceId } = req.params;
|
|
|
|
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
|
|
throw new ValidationError('Invalid service ID format');
|
|
}
|
|
|
|
const policy = autoRestartManager.getPolicy(serviceId);
|
|
if (!policy) {
|
|
throw new NotFoundError(`Auto-restart policy for "${serviceId}"`);
|
|
}
|
|
|
|
success(res, { policy });
|
|
}, 'auto-restart-get'));
|
|
|
|
/**
|
|
* POST /auto-restart/policies/:serviceId
|
|
* Create or update a restart policy.
|
|
*
|
|
* Body: { enabled, maxRetries, retryIntervalMs, windowMinutes }
|
|
*/
|
|
router.post('/policies/:serviceId', asyncHandler(async (req, res) => {
|
|
const { serviceId } = req.params;
|
|
|
|
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
|
|
throw new ValidationError('Invalid service ID format');
|
|
}
|
|
|
|
const { enabled, maxRetries, retryIntervalMs, windowMinutes } = req.body;
|
|
|
|
// Validate inputs
|
|
if (enabled !== undefined && typeof enabled !== 'boolean') {
|
|
throw new ValidationError('enabled must be a boolean');
|
|
}
|
|
if (maxRetries !== undefined) {
|
|
if (!Number.isInteger(maxRetries) || maxRetries < 0 || maxRetries > 100) {
|
|
throw new ValidationError('maxRetries must be an integer between 0 and 100');
|
|
}
|
|
}
|
|
if (retryIntervalMs !== undefined) {
|
|
if (!Number.isInteger(retryIntervalMs) || retryIntervalMs < 0 || retryIntervalMs > 3600000) {
|
|
throw new ValidationError('retryIntervalMs must be an integer between 0 and 3600000');
|
|
}
|
|
}
|
|
if (windowMinutes !== undefined) {
|
|
if (!Number.isInteger(windowMinutes) || windowMinutes < 0 || windowMinutes > 1440) {
|
|
throw new ValidationError('windowMinutes must be an integer between 0 and 1440');
|
|
}
|
|
}
|
|
|
|
const policy = await autoRestartManager.setPolicy(serviceId, {
|
|
...(enabled !== undefined && { enabled }),
|
|
...(maxRetries !== undefined && { maxRetries }),
|
|
...(retryIntervalMs !== undefined && { retryIntervalMs }),
|
|
...(windowMinutes !== undefined && { windowMinutes }),
|
|
});
|
|
|
|
success(res, { policy, message: `Policy ${serviceId} saved` });
|
|
}, 'auto-restart-set'));
|
|
|
|
/**
|
|
* DELETE /auto-restart/policies/:serviceId
|
|
* Remove a restart policy.
|
|
*/
|
|
router.delete('/policies/:serviceId', asyncHandler(async (req, res) => {
|
|
const { serviceId } = req.params;
|
|
|
|
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
|
|
throw new ValidationError('Invalid service ID format');
|
|
}
|
|
|
|
const removed = await autoRestartManager.removePolicy(serviceId);
|
|
if (!removed) {
|
|
throw new NotFoundError(`Auto-restart policy for "${serviceId}"`);
|
|
}
|
|
|
|
success(res, { message: `Policy for "${serviceId}" removed` });
|
|
}, 'auto-restart-delete'));
|
|
|
|
/**
|
|
* POST /auto-restart/policies/:serviceId/test
|
|
* Dry-run: simulate a restart attempt without actually restarting.
|
|
* Returns what *would* happen given the current policy state.
|
|
*/
|
|
router.post('/policies/:serviceId/test', asyncHandler(async (req, res) => {
|
|
const { serviceId } = req.params;
|
|
|
|
if (!serviceId || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
|
|
throw new ValidationError('Invalid service ID format');
|
|
}
|
|
|
|
const policy = autoRestartManager.getPolicy(serviceId);
|
|
if (!policy) {
|
|
throw new NotFoundError(`Auto-restart policy for "${serviceId}"`);
|
|
}
|
|
|
|
const now = Date.now();
|
|
const inCooldown = policy.cooldownUntil && now < policy.cooldownUntil;
|
|
const wouldRetry = !inCooldown && policy.currentRetries < policy.maxRetries;
|
|
const nextAttempt = policy.currentRetries + 1;
|
|
|
|
success(res, {
|
|
dryRun: true,
|
|
serviceId,
|
|
policy: {
|
|
enabled: policy.enabled,
|
|
currentRetries: policy.currentRetries,
|
|
maxRetries: policy.maxRetries,
|
|
cooldownUntil: policy.cooldownUntil,
|
|
inCooldown,
|
|
},
|
|
wouldRestart: policy.enabled && wouldRetry,
|
|
wouldMaxOut: !wouldRetry && !inCooldown,
|
|
nextAttempt: wouldRetry ? nextAttempt : null,
|
|
message: !policy.enabled
|
|
? 'Policy is disabled — no restart would occur'
|
|
: inCooldown
|
|
? `In cooldown until ${new Date(policy.cooldownUntil).toISOString()} — would skip`
|
|
: wouldRetry
|
|
? `Would attempt restart ${nextAttempt}/${policy.maxRetries}`
|
|
: `Max retries (${policy.maxRetries}) already reached — would enter cooldown`,
|
|
});
|
|
}, 'auto-restart-test'));
|
|
|
|
return router;
|
|
};
|