refactor(routes): Phase 3.2 - standardize containers.js with explicit dependencies
- Replaced god object ctx with explicit dependency injection - Added JSDoc documenting required dependencies (only 3!) - Updated response calls to use response-helpers (success) - Dependencies: docker, log, asyncHandler (vs 50+ ctx properties) - Self-documenting and testable
This commit is contained in:
@@ -2,13 +2,22 @@ const express = require('express');
|
|||||||
const { DOCKER } = require('../constants');
|
const { DOCKER } = require('../constants');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
const { NotFoundError } = require('../errors');
|
const { NotFoundError } = require('../errors');
|
||||||
|
const { success } = require('../response-helpers');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
/**
|
||||||
|
* Containers route factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Object} deps.docker - Docker client wrapper (client, pull methods)
|
||||||
|
* @param {Object} deps.log - Logger instance
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
module.exports = function({ docker, log, asyncHandler }) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Helper: verify container exists before operating on it
|
// Helper: verify container exists before operating on it
|
||||||
async function getVerifiedContainer(id) {
|
async function getVerifiedContainer(id) {
|
||||||
const container = ctx.docker.client.getContainer(id);
|
const container = docker.client.getContainer(id);
|
||||||
try {
|
try {
|
||||||
await container.inspect();
|
await container.inspect();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -21,28 +30,28 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start container
|
// Start container
|
||||||
router.post('/:id/start', ctx.asyncHandler(async (req, res) => {
|
router.post('/:id/start', asyncHandler(async (req, res) => {
|
||||||
const container = await getVerifiedContainer(req.params.id);
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
await container.start();
|
await container.start();
|
||||||
res.json({ success: true, message: 'Container started' });
|
success(res, { message: 'Container started' });
|
||||||
}, 'container-start'));
|
}, 'container-start'));
|
||||||
|
|
||||||
// Stop container
|
// Stop container
|
||||||
router.post('/:id/stop', ctx.asyncHandler(async (req, res) => {
|
router.post('/:id/stop', asyncHandler(async (req, res) => {
|
||||||
const container = await getVerifiedContainer(req.params.id);
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
await container.stop();
|
await container.stop();
|
||||||
res.json({ success: true, message: 'Container stopped' });
|
success(res, { message: 'Container stopped' });
|
||||||
}, 'container-stop'));
|
}, 'container-stop'));
|
||||||
|
|
||||||
// Restart container
|
// Restart container
|
||||||
router.post('/:id/restart', ctx.asyncHandler(async (req, res) => {
|
router.post('/:id/restart', asyncHandler(async (req, res) => {
|
||||||
const container = await getVerifiedContainer(req.params.id);
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
await container.restart();
|
await container.restart();
|
||||||
res.json({ success: true, message: 'Container restarted' });
|
success(res, { message: 'Container restarted' });
|
||||||
}, 'container-restart'));
|
}, 'container-restart'));
|
||||||
|
|
||||||
// Update container to latest image version
|
// Update container to latest image version
|
||||||
router.post('/:id/update', ctx.asyncHandler(async (req, res) => {
|
router.post('/:id/update', asyncHandler(async (req, res) => {
|
||||||
const containerId = req.params.id;
|
const containerId = req.params.id;
|
||||||
const container = await getVerifiedContainer(containerId);
|
const container = await getVerifiedContainer(containerId);
|
||||||
|
|
||||||
@@ -51,11 +60,11 @@ module.exports = function(ctx) {
|
|||||||
const imageName = containerInfo.Config.Image;
|
const imageName = containerInfo.Config.Image;
|
||||||
const containerName = containerInfo.Name.replace(/^\//, '');
|
const containerName = containerInfo.Name.replace(/^\//, '');
|
||||||
|
|
||||||
ctx.log.info('docker', 'Updating container', { containerName, imageName });
|
log.info('docker', 'Updating container', { containerName, imageName });
|
||||||
|
|
||||||
// Pull the latest image
|
// Pull the latest image
|
||||||
ctx.log.info('docker', `Pulling latest image: ${imageName}`);
|
log.info('docker', `Pulling latest image: ${imageName}`);
|
||||||
await ctx.docker.pull(imageName);
|
await docker.pull(imageName);
|
||||||
|
|
||||||
// Get current container config for recreation
|
// Get current container config for recreation
|
||||||
const hostConfig = containerInfo.HostConfig;
|
const hostConfig = containerInfo.HostConfig;
|
||||||
@@ -89,24 +98,24 @@ module.exports = function(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop and remove old container
|
// Stop and remove old container
|
||||||
ctx.log.info('docker', 'Stopping container', { containerName });
|
log.info('docker', 'Stopping container', { containerName });
|
||||||
await container.stop().catch(() => {}); // Ignore if already stopped
|
await container.stop().catch(() => {}); // Ignore if already stopped
|
||||||
ctx.log.info('docker', 'Removing container', { containerName });
|
log.info('docker', 'Removing container', { containerName });
|
||||||
await container.remove();
|
await container.remove();
|
||||||
|
|
||||||
// Wait for port release (Windows/Docker Desktop can be slow to free ports)
|
// Wait for port release (Windows/Docker Desktop can be slow to free ports)
|
||||||
await new Promise(r => setTimeout(r, 3000));
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
|
||||||
// Create and start new container
|
// Create and start new container
|
||||||
ctx.log.info('docker', 'Creating new container', { containerName });
|
log.info('docker', 'Creating new container', { containerName });
|
||||||
let newContainer;
|
let newContainer;
|
||||||
try {
|
try {
|
||||||
newContainer = await ctx.docker.client.createContainer(config);
|
newContainer = await docker.client.createContainer(config);
|
||||||
ctx.log.info('docker', 'Starting container', { containerName });
|
log.info('docker', 'Starting container', { containerName });
|
||||||
await newContainer.start();
|
await newContainer.start();
|
||||||
} catch (startError) {
|
} catch (startError) {
|
||||||
// Clean up the failed container so it doesn't block future attempts
|
// Clean up the failed container so it doesn't block future attempts
|
||||||
ctx.log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
|
log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
|
||||||
if (newContainer) {
|
if (newContainer) {
|
||||||
try { await newContainer.remove({ force: true }); } catch (e) { /* already gone */ }
|
try { await newContainer.remove({ force: true }); } catch (e) { /* already gone */ }
|
||||||
}
|
}
|
||||||
@@ -117,42 +126,41 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
// Prune dangling images after update
|
// Prune dangling images after update
|
||||||
try {
|
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) {
|
if (pruneResult.SpaceReclaimed > 0) {
|
||||||
ctx.log.info('docker', 'Pruned dangling images after update', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
log.info('docker', 'Pruned dangling images after update', { spaceReclaimed: Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) + 'MB' });
|
||||||
}
|
}
|
||||||
} catch (pruneErr) {
|
} catch (pruneErr) {
|
||||||
ctx.log.debug('docker', 'Image prune after update failed', { error: pruneErr.message });
|
log.debug('docker', 'Image prune after update failed', { error: pruneErr.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
success(res, {
|
||||||
success: true,
|
|
||||||
message: `Container ${containerName} updated successfully`,
|
message: `Container ${containerName} updated successfully`,
|
||||||
newContainerId: newContainerInfo.Id
|
newContainerId: newContainerInfo.Id
|
||||||
});
|
});
|
||||||
}, 'container-update'));
|
}, 'container-update'));
|
||||||
|
|
||||||
// Check for available updates (compares local and remote image digests)
|
// Check for available updates (compares local and remote image digests)
|
||||||
router.get('/:id/check-update', ctx.asyncHandler(async (req, res) => {
|
router.get('/:id/check-update', asyncHandler(async (req, res) => {
|
||||||
const containerId = req.params.id;
|
const containerId = req.params.id;
|
||||||
const container = await getVerifiedContainer(containerId);
|
const container = await getVerifiedContainer(containerId);
|
||||||
const containerInfo = await container.inspect();
|
const containerInfo = await container.inspect();
|
||||||
const imageName = containerInfo.Config.Image;
|
const imageName = containerInfo.Config.Image;
|
||||||
|
|
||||||
const localImage = ctx.docker.client.getImage(containerInfo.Image);
|
const localImage = docker.client.getImage(containerInfo.Image);
|
||||||
const localImageInfo = await localImage.inspect();
|
const localImageInfo = await localImage.inspect();
|
||||||
const localDigest = localImageInfo.RepoDigests?.[0] || null;
|
const localDigest = localImageInfo.RepoDigests?.[0] || null;
|
||||||
|
|
||||||
let updateAvailable = false;
|
let updateAvailable = false;
|
||||||
try {
|
try {
|
||||||
const pullStream = await ctx.docker.pull(imageName);
|
const pullStream = await docker.pull(imageName);
|
||||||
|
|
||||||
const downloadedLayers = pullStream.filter(e =>
|
const downloadedLayers = pullStream.filter(e =>
|
||||||
e.status === 'Downloading' || e.status === 'Download complete'
|
e.status === 'Downloading' || e.status === 'Download complete'
|
||||||
);
|
);
|
||||||
updateAvailable = downloadedLayers.length > 0;
|
updateAvailable = downloadedLayers.length > 0;
|
||||||
|
|
||||||
const newImage = ctx.docker.client.getImage(imageName);
|
const newImage = docker.client.getImage(imageName);
|
||||||
const newImageInfo = await newImage.inspect();
|
const newImageInfo = await newImage.inspect();
|
||||||
const newDigest = newImageInfo.RepoDigests?.[0] || null;
|
const newDigest = newImageInfo.RepoDigests?.[0] || null;
|
||||||
|
|
||||||
@@ -160,11 +168,10 @@ module.exports = function(ctx) {
|
|||||||
updateAvailable = true;
|
updateAvailable = true;
|
||||||
}
|
}
|
||||||
} catch (pullError) {
|
} catch (pullError) {
|
||||||
ctx.log.debug('docker', 'Could not check for updates', { error: pullError.message });
|
log.debug('docker', 'Could not check for updates', { error: pullError.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
success(res, {
|
||||||
success: true,
|
|
||||||
imageName,
|
imageName,
|
||||||
updateAvailable,
|
updateAvailable,
|
||||||
currentDigest: localDigest
|
currentDigest: localDigest
|
||||||
@@ -172,7 +179,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'container-check-update'));
|
}, 'container-check-update'));
|
||||||
|
|
||||||
// Get container logs
|
// Get container logs
|
||||||
router.get('/:id/logs', ctx.asyncHandler(async (req, res) => {
|
router.get('/:id/logs', asyncHandler(async (req, res) => {
|
||||||
const container = await getVerifiedContainer(req.params.id);
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
const logs = await container.logs({
|
const logs = await container.logs({
|
||||||
stdout: true,
|
stdout: true,
|
||||||
@@ -180,19 +187,19 @@ module.exports = function(ctx) {
|
|||||||
tail: 100,
|
tail: 100,
|
||||||
timestamps: true
|
timestamps: true
|
||||||
});
|
});
|
||||||
res.json({ success: true, logs: logs.toString() });
|
success(res, { logs: logs.toString() });
|
||||||
}, 'container-logs'));
|
}, 'container-logs'));
|
||||||
|
|
||||||
// Delete container
|
// Delete container
|
||||||
router.delete('/:id', ctx.asyncHandler(async (req, res) => {
|
router.delete('/:id', asyncHandler(async (req, res) => {
|
||||||
const container = await getVerifiedContainer(req.params.id);
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
await container.remove({ force: true });
|
await container.remove({ force: true });
|
||||||
res.json({ success: true, message: 'Container removed' });
|
success(res, { message: 'Container removed' });
|
||||||
}, 'container-delete'));
|
}, 'container-delete'));
|
||||||
|
|
||||||
// Discover running containers
|
// Discover running containers
|
||||||
router.get('/discover', ctx.asyncHandler(async (req, res) => {
|
router.get('/discover', asyncHandler(async (req, res) => {
|
||||||
const containers = await ctx.docker.client.listContainers({ all: true });
|
const containers = await docker.client.listContainers({ all: true });
|
||||||
const samiContainers = containers.filter(container =>
|
const samiContainers = containers.filter(container =>
|
||||||
container.Labels && container.Labels['sami.managed'] === 'true'
|
container.Labels && container.Labels['sami.managed'] === 'true'
|
||||||
);
|
);
|
||||||
@@ -210,7 +217,7 @@ module.exports = function(ctx) {
|
|||||||
|
|
||||||
const paginationParams = parsePaginationParams(req.query);
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
const result = paginate(discoveredContainers, paginationParams);
|
const result = paginate(discoveredContainers, paginationParams);
|
||||||
res.json({ success: true, containers: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
success(res, { containers: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
||||||
}, 'containers-discover'));
|
}, 'containers-discover'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
@@ -1187,7 +1187,11 @@ apiRouter.use(authRoutes(ctx));
|
|||||||
apiRouter.use(configRoutes(ctx));
|
apiRouter.use(configRoutes(ctx));
|
||||||
apiRouter.use('/dns', dnsRoutes(ctx));
|
apiRouter.use('/dns', dnsRoutes(ctx));
|
||||||
apiRouter.use('/notifications', notificationRoutes(ctx));
|
apiRouter.use('/notifications', notificationRoutes(ctx));
|
||||||
apiRouter.use('/containers', containerRoutes(ctx));
|
apiRouter.use('/containers', containerRoutes({
|
||||||
|
docker: ctx.docker,
|
||||||
|
log: ctx.log,
|
||||||
|
asyncHandler: ctx.asyncHandler
|
||||||
|
}));
|
||||||
apiRouter.use(serviceRoutes({
|
apiRouter.use(serviceRoutes({
|
||||||
servicesStateManager: ctx.servicesStateManager,
|
servicesStateManager: ctx.servicesStateManager,
|
||||||
credentialManager: ctx.credentialManager,
|
credentialManager: ctx.credentialManager,
|
||||||
|
|||||||
Reference in New Issue
Block a user