Phase 3 (WIP): Refactor containers & monitoring routes
- routes/containers.js: Explicit deps (docker, asyncHandler, log) - routes/monitoring.js: Explicit deps (docker, resourceMonitor) - Pattern established: factory function with destructured deps
This commit is contained in:
@@ -1,14 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Container management routes
|
||||||
|
* Refactored to use explicit dependencies instead of ctx god object
|
||||||
|
*/
|
||||||
|
|
||||||
const express = require('express');
|
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 asyncHandler = require('../src/utils/async-handler');
|
||||||
|
const log = require('../src/utils/logger');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
module.exports = function({ docker }) {
|
||||||
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 +28,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' });
|
res.json({ success: true, 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' });
|
res.json({ success: true, 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' });
|
res.json({ success: true, 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 +58,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;
|
||||||
@@ -75,7 +82,7 @@ module.exports = function(ctx) {
|
|||||||
CapAdd: hostConfig.CapAdd,
|
CapAdd: hostConfig.CapAdd,
|
||||||
CapDrop: hostConfig.CapDrop,
|
CapDrop: hostConfig.CapDrop,
|
||||||
Devices: hostConfig.Devices,
|
Devices: hostConfig.Devices,
|
||||||
LogConfig: DOCKER.LOG_CONFIG, // Ensure log rotation on updated containers
|
LogConfig: DOCKER.LOG_CONFIG,
|
||||||
},
|
},
|
||||||
NetworkingConfig: {},
|
NetworkingConfig: {},
|
||||||
};
|
};
|
||||||
@@ -89,40 +96,45 @@ 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
|
||||||
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
|
log.error('docker', 'Failed to start new container', { containerName, error: startError.message });
|
||||||
ctx.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 */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw startError;
|
throw startError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newContainerInfo = await newContainer.inspect();
|
const newContainerInfo = await newContainer.inspect();
|
||||||
|
|
||||||
// Prune dangling images after update
|
// Prune dangling images
|
||||||
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({
|
res.json({
|
||||||
@@ -132,27 +144,27 @@ module.exports = function(ctx) {
|
|||||||
});
|
});
|
||||||
}, 'container-update'));
|
}, 'container-update'));
|
||||||
|
|
||||||
// Check for available updates (compares local and remote image digests)
|
// Check for available updates
|
||||||
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.status === 'Downloading' || e.status === 'Download complete',
|
(e) => 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,7 +172,7 @@ 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({
|
res.json({
|
||||||
@@ -172,7 +184,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,
|
||||||
@@ -184,20 +196,20 @@ module.exports = function(ctx) {
|
|||||||
}, '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' });
|
res.json({ success: true, 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.Labels && container.Labels['sami.managed'] === 'true',
|
(container) => container.Labels && container.Labels['sami.managed'] === 'true',
|
||||||
);
|
);
|
||||||
|
|
||||||
const discoveredContainers = samiContainers.map(container => ({
|
const discoveredContainers = samiContainers.map((container) => ({
|
||||||
id: container.Id,
|
id: container.Id,
|
||||||
name: container.Names[0].replace('/', ''),
|
name: container.Names[0].replace('/', ''),
|
||||||
image: container.Image,
|
image: container.Image,
|
||||||
@@ -210,7 +222,11 @@ 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 }) });
|
res.json({
|
||||||
|
success: true,
|
||||||
|
containers: result.data,
|
||||||
|
...(result.pagination && { pagination: result.pagination }),
|
||||||
|
});
|
||||||
}, 'containers-discover'));
|
}, 'containers-discover'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|||||||
217
dashcaddy-api/routes/containers.old.js
Normal file
217
dashcaddy-api/routes/containers.old.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { DOCKER } = require('../constants');
|
||||||
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
|
const { NotFoundError } = require('../errors');
|
||||||
|
|
||||||
|
module.exports = function(ctx) {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Helper: verify container exists before operating on it
|
||||||
|
async function getVerifiedContainer(id) {
|
||||||
|
const container = ctx.docker.client.getContainer(id);
|
||||||
|
try {
|
||||||
|
await container.inspect();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.statusCode === 404 || (err.message && err.message.includes('no such container'))) {
|
||||||
|
throw new NotFoundError(`Container ${id}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start container
|
||||||
|
router.post('/:id/start', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
|
await container.start();
|
||||||
|
res.json({ success: true, message: 'Container started' });
|
||||||
|
}, 'container-start'));
|
||||||
|
|
||||||
|
// Stop container
|
||||||
|
router.post('/:id/stop', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
|
await container.stop();
|
||||||
|
res.json({ success: true, message: 'Container stopped' });
|
||||||
|
}, 'container-stop'));
|
||||||
|
|
||||||
|
// Restart container
|
||||||
|
router.post('/:id/restart', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
|
await container.restart();
|
||||||
|
res.json({ success: true, message: 'Container restarted' });
|
||||||
|
}, 'container-restart'));
|
||||||
|
|
||||||
|
// Update container to latest image version
|
||||||
|
router.post('/:id/update', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const containerId = req.params.id;
|
||||||
|
const container = await getVerifiedContainer(containerId);
|
||||||
|
|
||||||
|
// Get container info
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
const imageName = containerInfo.Config.Image;
|
||||||
|
const containerName = containerInfo.Name.replace(/^\//, '');
|
||||||
|
|
||||||
|
ctx.log.info('docker', 'Updating container', { containerName, imageName });
|
||||||
|
|
||||||
|
// Pull the latest image
|
||||||
|
ctx.log.info('docker', `Pulling latest image: ${imageName}`);
|
||||||
|
await ctx.docker.pull(imageName);
|
||||||
|
|
||||||
|
// Get current container config for recreation
|
||||||
|
const hostConfig = containerInfo.HostConfig;
|
||||||
|
const config = {
|
||||||
|
Image: imageName,
|
||||||
|
name: containerName,
|
||||||
|
Env: containerInfo.Config.Env,
|
||||||
|
ExposedPorts: containerInfo.Config.ExposedPorts,
|
||||||
|
Labels: containerInfo.Config.Labels,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: hostConfig.Binds,
|
||||||
|
PortBindings: hostConfig.PortBindings,
|
||||||
|
RestartPolicy: hostConfig.RestartPolicy,
|
||||||
|
NetworkMode: hostConfig.NetworkMode,
|
||||||
|
ExtraHosts: hostConfig.ExtraHosts,
|
||||||
|
Privileged: hostConfig.Privileged,
|
||||||
|
CapAdd: hostConfig.CapAdd,
|
||||||
|
CapDrop: hostConfig.CapDrop,
|
||||||
|
Devices: hostConfig.Devices,
|
||||||
|
LogConfig: DOCKER.LOG_CONFIG, // Ensure log rotation on updated containers
|
||||||
|
},
|
||||||
|
NetworkingConfig: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get network settings if using a custom network
|
||||||
|
if (hostConfig.NetworkMode && !['bridge', 'host', 'none'].includes(hostConfig.NetworkMode)) {
|
||||||
|
const networkName = hostConfig.NetworkMode;
|
||||||
|
config.NetworkingConfig.EndpointsConfig = {
|
||||||
|
[networkName]: containerInfo.NetworkSettings.Networks[networkName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop and remove old container
|
||||||
|
ctx.log.info('docker', 'Stopping container', { containerName });
|
||||||
|
await container.stop().catch(() => {}); // Ignore if already stopped
|
||||||
|
ctx.log.info('docker', 'Removing container', { containerName });
|
||||||
|
await container.remove();
|
||||||
|
|
||||||
|
// Wait for port release (Windows/Docker Desktop can be slow to free ports)
|
||||||
|
await new Promise(r => setTimeout(r, 3000));
|
||||||
|
|
||||||
|
// Create and start new container
|
||||||
|
ctx.log.info('docker', 'Creating new container', { containerName });
|
||||||
|
let newContainer;
|
||||||
|
try {
|
||||||
|
newContainer = await ctx.docker.client.createContainer(config);
|
||||||
|
ctx.log.info('docker', 'Starting container', { containerName });
|
||||||
|
await newContainer.start();
|
||||||
|
} catch (startError) {
|
||||||
|
// 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 });
|
||||||
|
if (newContainer) {
|
||||||
|
try { await newContainer.remove({ force: true }); } catch (e) { /* already gone */ }
|
||||||
|
}
|
||||||
|
throw startError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContainerInfo = await newContainer.inspect();
|
||||||
|
|
||||||
|
// Prune dangling images after update
|
||||||
|
try {
|
||||||
|
const pruneResult = await ctx.docker.client.pruneImages({ filters: { dangling: { true: true } } });
|
||||||
|
if (pruneResult.SpaceReclaimed > 0) {
|
||||||
|
ctx.log.info('docker', 'Pruned dangling images after update', { spaceReclaimed: `${Math.round(pruneResult.SpaceReclaimed / 1024 / 1024) }MB` });
|
||||||
|
}
|
||||||
|
} catch (pruneErr) {
|
||||||
|
ctx.log.debug('docker', 'Image prune after update failed', { error: pruneErr.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Container ${containerName} updated successfully`,
|
||||||
|
newContainerId: newContainerInfo.Id,
|
||||||
|
});
|
||||||
|
}, 'container-update'));
|
||||||
|
|
||||||
|
// Check for available updates (compares local and remote image digests)
|
||||||
|
router.get('/:id/check-update', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const containerId = req.params.id;
|
||||||
|
const container = await getVerifiedContainer(containerId);
|
||||||
|
const containerInfo = await container.inspect();
|
||||||
|
const imageName = containerInfo.Config.Image;
|
||||||
|
|
||||||
|
const localImage = ctx.docker.client.getImage(containerInfo.Image);
|
||||||
|
const localImageInfo = await localImage.inspect();
|
||||||
|
const localDigest = localImageInfo.RepoDigests?.[0] || null;
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
try {
|
||||||
|
const pullStream = await ctx.docker.pull(imageName);
|
||||||
|
|
||||||
|
const downloadedLayers = pullStream.filter(e =>
|
||||||
|
e.status === 'Downloading' || e.status === 'Download complete',
|
||||||
|
);
|
||||||
|
updateAvailable = downloadedLayers.length > 0;
|
||||||
|
|
||||||
|
const newImage = ctx.docker.client.getImage(imageName);
|
||||||
|
const newImageInfo = await newImage.inspect();
|
||||||
|
const newDigest = newImageInfo.RepoDigests?.[0] || null;
|
||||||
|
|
||||||
|
if (localDigest && newDigest && localDigest !== newDigest) {
|
||||||
|
updateAvailable = true;
|
||||||
|
}
|
||||||
|
} catch (pullError) {
|
||||||
|
ctx.log.debug('docker', 'Could not check for updates', { error: pullError.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
imageName,
|
||||||
|
updateAvailable,
|
||||||
|
currentDigest: localDigest,
|
||||||
|
});
|
||||||
|
}, 'container-check-update'));
|
||||||
|
|
||||||
|
// Get container logs
|
||||||
|
router.get('/:id/logs', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
|
const logs = await container.logs({
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
tail: 100,
|
||||||
|
timestamps: true,
|
||||||
|
});
|
||||||
|
res.json({ success: true, logs: logs.toString() });
|
||||||
|
}, 'container-logs'));
|
||||||
|
|
||||||
|
// Delete container
|
||||||
|
router.delete('/:id', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const container = await getVerifiedContainer(req.params.id);
|
||||||
|
await container.remove({ force: true });
|
||||||
|
res.json({ success: true, message: 'Container removed' });
|
||||||
|
}, 'container-delete'));
|
||||||
|
|
||||||
|
// Discover running containers
|
||||||
|
router.get('/discover', ctx.asyncHandler(async (req, res) => {
|
||||||
|
const containers = await ctx.docker.client.listContainers({ all: true });
|
||||||
|
const samiContainers = containers.filter(container =>
|
||||||
|
container.Labels && container.Labels['sami.managed'] === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
|
const discoveredContainers = samiContainers.map(container => ({
|
||||||
|
id: container.Id,
|
||||||
|
name: container.Names[0].replace('/', ''),
|
||||||
|
image: container.Image,
|
||||||
|
state: container.State,
|
||||||
|
status: container.Status,
|
||||||
|
appTemplate: container.Labels['sami.app'],
|
||||||
|
subdomain: container.Labels['sami.subdomain'],
|
||||||
|
ports: container.Ports,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const paginationParams = parsePaginationParams(req.query);
|
||||||
|
const result = paginate(discoveredContainers, paginationParams);
|
||||||
|
res.json({ success: true, containers: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
||||||
|
}, 'containers-discover'));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
const express = require('express');
|
/**
|
||||||
|
* Monitoring and stats routes
|
||||||
|
* Refactored to use explicit dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
const express = require('express');
|
||||||
|
const asyncHandler = require('../src/utils/async-handler');
|
||||||
|
|
||||||
|
module.exports = function({ docker, resourceMonitor }) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ===== RESOURCE MONITORING ENDPOINTS =====
|
// ===== RESOURCE MONITORING ENDPOINTS =====
|
||||||
|
|
||||||
// Get all container stats (from resource monitor module)
|
router.get('/monitoring/stats', asyncHandler(async (req, res) => {
|
||||||
router.get('/monitoring/stats', ctx.asyncHandler(async (req, res) => {
|
const stats = resourceMonitor.getAllStats();
|
||||||
const stats = ctx.resourceMonitor.getAllStats();
|
|
||||||
res.json({ success: true, stats });
|
res.json({ success: true, stats });
|
||||||
}, 'monitoring-stats'));
|
}, 'monitoring-stats'));
|
||||||
|
|
||||||
// Get stats for specific container
|
router.get('/monitoring/stats/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.get('/monitoring/stats/:containerId', ctx.asyncHandler(async (req, res) => {
|
const stats = resourceMonitor.getCurrentStats(req.params.containerId);
|
||||||
const stats = ctx.resourceMonitor.getCurrentStats(req.params.containerId);
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
const { NotFoundError } = require('../errors');
|
const { NotFoundError } = require('../errors');
|
||||||
throw new NotFoundError('Container');
|
throw new NotFoundError('Container');
|
||||||
@@ -21,17 +25,15 @@ module.exports = function(ctx) {
|
|||||||
res.json({ success: true, stats });
|
res.json({ success: true, stats });
|
||||||
}, 'monitoring-stats-container'));
|
}, 'monitoring-stats-container'));
|
||||||
|
|
||||||
// Get historical stats
|
router.get('/monitoring/history/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.get('/monitoring/history/:containerId', ctx.asyncHandler(async (req, res) => {
|
|
||||||
const hours = parseInt(req.query.hours) || 24;
|
const hours = parseInt(req.query.hours) || 24;
|
||||||
const history = ctx.resourceMonitor.getHistoricalStats(req.params.containerId, hours);
|
const history = resourceMonitor.getHistoricalStats(req.params.containerId, hours);
|
||||||
res.json({ success: true, history, hours });
|
res.json({ success: true, history, hours });
|
||||||
}, 'monitoring-history'));
|
}, 'monitoring-history'));
|
||||||
|
|
||||||
// Get aggregated stats
|
router.get('/monitoring/aggregated/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.get('/monitoring/aggregated/:containerId', ctx.asyncHandler(async (req, res) => {
|
|
||||||
const hours = parseInt(req.query.hours) || 24;
|
const hours = parseInt(req.query.hours) || 24;
|
||||||
const aggregated = ctx.resourceMonitor.getAggregatedStats(req.params.containerId, hours);
|
const aggregated = resourceMonitor.getAggregatedStats(req.params.containerId, hours);
|
||||||
if (!aggregated) {
|
if (!aggregated) {
|
||||||
const { NotFoundError } = require('../errors');
|
const { NotFoundError } = require('../errors');
|
||||||
throw new NotFoundError('Monitoring data');
|
throw new NotFoundError('Monitoring data');
|
||||||
@@ -39,49 +41,42 @@ module.exports = function(ctx) {
|
|||||||
res.json({ success: true, aggregated, hours });
|
res.json({ success: true, aggregated, hours });
|
||||||
}, 'monitoring-aggregated'));
|
}, 'monitoring-aggregated'));
|
||||||
|
|
||||||
// Configure alerts
|
router.post('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.post('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => {
|
resourceMonitor.setAlertConfig(req.params.containerId, req.body);
|
||||||
ctx.resourceMonitor.setAlertConfig(req.params.containerId, req.body);
|
|
||||||
res.json({ success: true, message: 'Alert configuration saved' });
|
res.json({ success: true, message: 'Alert configuration saved' });
|
||||||
}, 'monitoring-alerts-set'));
|
}, 'monitoring-alerts-set'));
|
||||||
|
|
||||||
// Get alert configuration
|
router.get('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.get('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => {
|
const config = resourceMonitor.getAlertConfig(req.params.containerId);
|
||||||
const config = ctx.resourceMonitor.getAlertConfig(req.params.containerId);
|
|
||||||
res.json({ success: true, config: config || {} });
|
res.json({ success: true, config: config || {} });
|
||||||
}, 'monitoring-alerts-get'));
|
}, 'monitoring-alerts-get'));
|
||||||
|
|
||||||
// Delete alert configuration
|
router.delete('/monitoring/alerts/:containerId', asyncHandler(async (req, res) => {
|
||||||
router.delete('/monitoring/alerts/:containerId', ctx.asyncHandler(async (req, res) => {
|
resourceMonitor.removeAlertConfig(req.params.containerId);
|
||||||
ctx.resourceMonitor.removeAlertConfig(req.params.containerId);
|
|
||||||
res.json({ success: true, message: 'Alert configuration removed' });
|
res.json({ success: true, message: 'Alert configuration removed' });
|
||||||
}, 'monitoring-alerts-delete'));
|
}, 'monitoring-alerts-delete'));
|
||||||
|
|
||||||
// ===== CONTAINER STATS ENDPOINTS (legacy /stats/) =====
|
// ===== CONTAINER STATS ENDPOINTS (legacy /stats/) =====
|
||||||
|
|
||||||
// Get all container stats (live Docker stats)
|
router.get('/stats/containers', asyncHandler(async (req, res) => {
|
||||||
router.get('/stats/containers', ctx.asyncHandler(async (req, res) => {
|
const containers = await docker.client.listContainers({ all: false });
|
||||||
const containers = await ctx.docker.client.listContainers({ all: false });
|
|
||||||
const stats = [];
|
const stats = [];
|
||||||
|
|
||||||
for (const containerInfo of containers) {
|
for (const containerInfo of containers) {
|
||||||
try {
|
try {
|
||||||
const container = ctx.docker.client.getContainer(containerInfo.Id);
|
const container = docker.client.getContainer(containerInfo.Id);
|
||||||
const containerStats = await container.stats({ stream: false });
|
const containerStats = await container.stats({ stream: false });
|
||||||
|
|
||||||
// Calculate CPU percentage
|
|
||||||
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
||||||
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
||||||
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
||||||
(containerStats.precpu_stats.system_cpu_usage || 0);
|
(containerStats.precpu_stats.system_cpu_usage || 0);
|
||||||
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
||||||
|
|
||||||
// Calculate memory usage
|
|
||||||
const memUsage = containerStats.memory_stats.usage || 0;
|
const memUsage = containerStats.memory_stats.usage || 0;
|
||||||
const memLimit = containerStats.memory_stats.limit || 1;
|
const memLimit = containerStats.memory_stats.limit || 1;
|
||||||
const memPercent = (memUsage / memLimit) * 100;
|
const memPercent = (memUsage / memLimit) * 100;
|
||||||
|
|
||||||
// Network stats
|
|
||||||
let netRx = 0, netTx = 0;
|
let netRx = 0, netTx = 0;
|
||||||
if (containerStats.networks) {
|
if (containerStats.networks) {
|
||||||
for (const net of Object.values(containerStats.networks)) {
|
for (const net of Object.values(containerStats.networks)) {
|
||||||
@@ -95,21 +90,15 @@ module.exports = function(ctx) {
|
|||||||
name: containerInfo.Names[0]?.replace(/^\//, '') || 'unknown',
|
name: containerInfo.Names[0]?.replace(/^\//, '') || 'unknown',
|
||||||
image: containerInfo.Image,
|
image: containerInfo.Image,
|
||||||
status: containerInfo.State,
|
status: containerInfo.State,
|
||||||
cpu: {
|
cpu: { percent: Math.round(cpuPercent * 100) / 100 },
|
||||||
percent: Math.round(cpuPercent * 100) / 100,
|
|
||||||
},
|
|
||||||
memory: {
|
memory: {
|
||||||
used: memUsage,
|
used: memUsage,
|
||||||
limit: memLimit,
|
limit: memLimit,
|
||||||
percent: Math.round(memPercent * 100) / 100,
|
percent: Math.round(memPercent * 100) / 100,
|
||||||
},
|
},
|
||||||
network: {
|
network: { rx: netRx, tx: netTx },
|
||||||
rx: netRx,
|
|
||||||
tx: netTx,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Skip containers we can't get stats for
|
|
||||||
console.log(`Could not get stats for ${containerInfo.Names[0]}:`, e.message);
|
console.log(`Could not get stats for ${containerInfo.Names[0]}:`, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,24 +106,20 @@ module.exports = function(ctx) {
|
|||||||
res.json({ success: true, stats, timestamp: new Date().toISOString() });
|
res.json({ success: true, stats, timestamp: new Date().toISOString() });
|
||||||
}, 'stats-containers'));
|
}, 'stats-containers'));
|
||||||
|
|
||||||
// Get single container stats
|
router.get('/stats/container/:id', asyncHandler(async (req, res) => {
|
||||||
router.get('/stats/container/:id', ctx.asyncHandler(async (req, res) => {
|
const container = docker.client.getContainer(req.params.id);
|
||||||
const container = ctx.docker.client.getContainer(req.params.id);
|
|
||||||
const containerStats = await container.stats({ stream: false });
|
const containerStats = await container.stats({ stream: false });
|
||||||
const info = await container.inspect();
|
const info = await container.inspect();
|
||||||
|
|
||||||
// Calculate CPU percentage
|
|
||||||
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
const cpuDelta = containerStats.cpu_stats.cpu_usage.total_usage -
|
||||||
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
(containerStats.precpu_stats.cpu_usage?.total_usage || 0);
|
||||||
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
const systemDelta = containerStats.cpu_stats.system_cpu_usage -
|
||||||
(containerStats.precpu_stats.system_cpu_usage || 0);
|
(containerStats.precpu_stats.system_cpu_usage || 0);
|
||||||
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * (containerStats.cpu_stats.online_cpus || 1) : 0;
|
||||||
|
|
||||||
// Memory
|
|
||||||
const memUsage = containerStats.memory_stats.usage || 0;
|
const memUsage = containerStats.memory_stats.usage || 0;
|
||||||
const memLimit = containerStats.memory_stats.limit || 1;
|
const memLimit = containerStats.memory_stats.limit || 1;
|
||||||
|
|
||||||
// Network
|
|
||||||
let netRx = 0, netTx = 0;
|
let netRx = 0, netTx = 0;
|
||||||
if (containerStats.networks) {
|
if (containerStats.networks) {
|
||||||
for (const net of Object.values(containerStats.networks)) {
|
for (const net of Object.values(containerStats.networks)) {
|
||||||
@@ -150,9 +135,7 @@ module.exports = function(ctx) {
|
|||||||
image: info.Config.Image,
|
image: info.Config.Image,
|
||||||
status: info.State.Status,
|
status: info.State.Status,
|
||||||
started: info.State.StartedAt,
|
started: info.State.StartedAt,
|
||||||
cpu: {
|
cpu: { percent: Math.round(cpuPercent * 100) / 100 },
|
||||||
percent: Math.round(cpuPercent * 100) / 100,
|
|
||||||
},
|
|
||||||
memory: {
|
memory: {
|
||||||
used: memUsage,
|
used: memUsage,
|
||||||
limit: memLimit,
|
limit: memLimit,
|
||||||
|
|||||||
Reference in New Issue
Block a user