feat: auto-restart policies, SSL monitoring, DNS propagation, dependency tracking, config drift detection
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Dependencies Route — REST API for service dependency tracking
|
||||
*
|
||||
* Endpoints:
|
||||
* GET /dependencies/graph Full dependency graph
|
||||
* GET /dependencies/validate Validate a proposed dep chain
|
||||
* GET /dependencies/:serviceId Direct deps for one service
|
||||
* GET /dependencies/:serviceId/chain Ordered restart chain
|
||||
* GET /dependencies/:serviceId/status Dependency health status
|
||||
* POST /dependencies/:serviceId Set dependencies
|
||||
* DELETE /dependencies/:serviceId Remove all dependencies
|
||||
* POST /dependencies/:serviceId/restart Restart with dependency chain
|
||||
*
|
||||
* @module routes/dependencies
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const { success, error: errorResponse } = require('../response-helpers');
|
||||
const { NotFoundError, ValidationError } = require('../errors');
|
||||
|
||||
/**
|
||||
* Dependencies route factory
|
||||
*
|
||||
* @param {Object} deps - Explicit dependencies
|
||||
* @param {Object} deps.dependencyManager - DependencyManager instance
|
||||
* @param {Object} deps.servicesStateManager - State manager for services.json
|
||||
* @param {Object} deps.docker - Docker client wrapper
|
||||
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||
* @param {Function} deps.logError - Error logging function
|
||||
* @param {Function} deps.resyncHealthChecker - Health checker resync function
|
||||
* @param {Object} deps.log - Logger instance
|
||||
* @returns {express.Router}
|
||||
*/
|
||||
module.exports = function({
|
||||
dependencyManager,
|
||||
servicesStateManager,
|
||||
docker,
|
||||
asyncHandler,
|
||||
logError,
|
||||
resyncHealthChecker,
|
||||
log,
|
||||
}) {
|
||||
const router = express.Router();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /dependencies/graph — Full dependency graph
|
||||
// -------------------------------------------------------------------------
|
||||
router.get('/graph', asyncHandler(async (req, res) => {
|
||||
const graph = await dependencyManager.getDependencyGraph();
|
||||
success(res, { graph });
|
||||
}, 'dep-graph'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /dependencies/validate — Validate a proposed dep chain (query params)
|
||||
// -------------------------------------------------------------------------
|
||||
router.get('/validate', asyncHandler(async (req, res) => {
|
||||
const { serviceId, dependsOn } = req.query;
|
||||
|
||||
if (!serviceId) {
|
||||
throw new ValidationError('serviceId query parameter is required');
|
||||
}
|
||||
|
||||
// dependsOn may be a comma-separated string or already an array
|
||||
let parsed;
|
||||
if (Array.isArray(dependsOn)) {
|
||||
parsed = dependsOn;
|
||||
} else if (typeof dependsOn === 'string' && dependsOn.length > 0) {
|
||||
parsed = dependsOn.split(',').map(s => s.trim()).filter(Boolean);
|
||||
} else {
|
||||
parsed = [];
|
||||
}
|
||||
|
||||
const result = await dependencyManager.validateDependencies(serviceId, parsed);
|
||||
success(res, result);
|
||||
}, 'dep-validate'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /dependencies/:serviceId — Direct deps for one service
|
||||
// -------------------------------------------------------------------------
|
||||
router.get('/:serviceId', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
const dependencies = await dependencyManager.getDependencies(serviceId);
|
||||
const dependents = await dependencyManager.getDependents(serviceId);
|
||||
|
||||
// Read the service's current dependsOn array
|
||||
const services = await servicesStateManager.read();
|
||||
const allServices = Array.isArray(services) ? services : (services.services || []);
|
||||
const service = allServices.find(s => s.id === serviceId);
|
||||
|
||||
if (!service) {
|
||||
throw new NotFoundError(`Service "${serviceId}"`);
|
||||
}
|
||||
|
||||
success(res, {
|
||||
serviceId,
|
||||
dependsOn: service.dependsOn || [],
|
||||
dependencies,
|
||||
dependents: dependents.map(d => ({ id: d.id, name: d.name })),
|
||||
});
|
||||
}, 'dep-get'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /dependencies/:serviceId/chain — Ordered restart chain
|
||||
// -------------------------------------------------------------------------
|
||||
router.get('/:serviceId/chain', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
const chain = await dependencyManager.getOrderedRestartChain(serviceId);
|
||||
success(res, { serviceId, chain });
|
||||
}, 'dep-chain'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// GET /dependencies/:serviceId/status — Dependency health status
|
||||
// -------------------------------------------------------------------------
|
||||
router.get('/:serviceId/status', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
const statuses = await dependencyManager.getDependencyStatus(serviceId);
|
||||
success(res, { serviceId, statuses });
|
||||
}, 'dep-status'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /dependencies/:serviceId — Set dependencies
|
||||
// -------------------------------------------------------------------------
|
||||
router.post('/:serviceId', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
const { dependsOn } = req.body;
|
||||
|
||||
if (!Array.isArray(dependsOn)) {
|
||||
throw new ValidationError('Request body must include dependsOn as an array of service IDs');
|
||||
}
|
||||
|
||||
// Validate first
|
||||
const validation = await dependencyManager.validateDependencies(serviceId, dependsOn);
|
||||
if (!validation.valid) {
|
||||
return errorResponse(res, validation.errors.join('; '), 400);
|
||||
}
|
||||
|
||||
// Update the service
|
||||
let found = false;
|
||||
await servicesStateManager.update(services => {
|
||||
const arr = Array.isArray(services) ? services : [];
|
||||
return arr.map(s => {
|
||||
if (s.id === serviceId) {
|
||||
found = true;
|
||||
return { ...s, dependsOn: dependsOn.slice() };
|
||||
}
|
||||
return s;
|
||||
});
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new NotFoundError(`Service "${serviceId}"`);
|
||||
}
|
||||
|
||||
log.info('dependency', 'Dependencies updated', { serviceId, dependsOn });
|
||||
|
||||
success(res, {
|
||||
message: `Dependencies updated for "${serviceId}"`,
|
||||
serviceId,
|
||||
dependsOn,
|
||||
});
|
||||
}, 'dep-set'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DELETE /dependencies/:serviceId — Remove all dependencies for a service
|
||||
// -------------------------------------------------------------------------
|
||||
router.delete('/:serviceId', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
|
||||
let found = false;
|
||||
await servicesStateManager.update(services => {
|
||||
const arr = Array.isArray(services) ? services : [];
|
||||
return arr.map(s => {
|
||||
if (s.id === serviceId) {
|
||||
found = true;
|
||||
const updated = { ...s };
|
||||
delete updated.dependsOn;
|
||||
return updated;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
throw new NotFoundError(`Service "${serviceId}"`);
|
||||
}
|
||||
|
||||
log.info('dependency', 'Dependencies removed', { serviceId });
|
||||
|
||||
success(res, {
|
||||
message: `All dependencies removed for "${serviceId}"`,
|
||||
serviceId,
|
||||
});
|
||||
}, 'dep-delete'));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// POST /dependencies/:serviceId/restart — Restart with dependency chain
|
||||
// -------------------------------------------------------------------------
|
||||
router.post('/:serviceId/restart', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
|
||||
// Verify the service exists
|
||||
const services = await servicesStateManager.read();
|
||||
const allServices = Array.isArray(services) ? services : (services.services || []);
|
||||
if (!allServices.find(s => s.id === serviceId)) {
|
||||
throw new NotFoundError(`Service "${serviceId}"`);
|
||||
}
|
||||
|
||||
// Get the chain first for the response (before async restart begins)
|
||||
let chain;
|
||||
try {
|
||||
chain = await dependencyManager.getOrderedRestartChain(serviceId);
|
||||
} catch (err) {
|
||||
return errorResponse(res, err.message, 400);
|
||||
}
|
||||
|
||||
// Respond immediately with the chain order
|
||||
success(res, {
|
||||
message: `Dependency restart initiated for "${serviceId}"`,
|
||||
serviceId,
|
||||
chain,
|
||||
});
|
||||
|
||||
// Run the restart chain asynchronously so the client doesn't block
|
||||
dependencyManager.restartWithDependencies(serviceId).catch(err => {
|
||||
if (log) {
|
||||
log.error('dependency', 'Async dependency restart failed', {
|
||||
serviceId,
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 'dep-restart'));
|
||||
|
||||
return router;
|
||||
};
|
||||
Reference in New Issue
Block a user