From 10a5a8e216f440a4489faf2df106808c96aa27e2 Mon Sep 17 00:00:00 2001 From: Andreas Claesson Date: Mon, 10 Nov 2025 19:52:56 +0100 Subject: [PATCH] Add container exec and rename support --- docker/containerService.js | 92 +++++++++++++++++++++++++++++++++++++- routes/containers.js | 37 ++++++++++++++- 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/docker/containerService.js b/docker/containerService.js index 557da2b..27a0fac 100644 --- a/docker/containerService.js +++ b/docker/containerService.js @@ -89,6 +89,94 @@ async function getContainerLogs(id) { return logs.toString(); } +async function execCommand(id, command) { + const container = docker.getContainer(id); + + // Check if container is running + const info = await container.inspect(); + if (!info.State.Running) { + throw new Error('Container is not running'); + } + + // Create exec instance + const exec = await container.exec({ + Cmd: ['/bin/sh', '-c', command], + AttachStdout: true, + AttachStderr: true, + }); + + // Start exec and capture output + return new Promise((resolve, reject) => { + exec.start({ hijack: true, stdin: false }, (err, stream) => { + if (err) { + return reject(err); + } + + let output = ''; + let errorOutput = ''; + + // Docker multiplexes stdout and stderr into a single stream + // Each frame has an 8-byte header + stream.on('data', (chunk) => { + // Parse Docker stream format + let offset = 0; + while (offset < chunk.length) { + if (chunk.length - offset < 8) break; + + const header = chunk.slice(offset, offset + 8); + const streamType = header[0]; // 1=stdout, 2=stderr + const size = header.readUInt32BE(4); + + offset += 8; + + if (chunk.length - offset < size) break; + + const payload = chunk.slice(offset, offset + size).toString('utf8'); + + if (streamType === 1) { + output += payload; + } else if (streamType === 2) { + errorOutput += payload; + } + + offset += size; + } + }); + + stream.on('end', async () => { + try { + const execInfo = await exec.inspect(); + console.log(chalk.blue('Command executed:'), chalk.cyan(command), chalk.yellow('Exit code:'), execInfo.ExitCode); + + resolve({ + output: (output + errorOutput).trim(), + exitCode: execInfo.ExitCode, + error: execInfo.ExitCode !== 0 ? errorOutput.trim() : null + }); + } catch (inspectError) { + reject(inspectError); + } + }); + + stream.on('error', (streamError) => { + reject(streamError); + }); + + // Set timeout (30 seconds) + setTimeout(() => { + stream.destroy(); + reject(new Error('Command execution timeout')); + }, 30000); + }); + }); +} + +async function renameContainer(id, newName) { + const container = docker.getContainer(id); + await container.rename({ name: newName }); + console.log(chalk.yellow('Container renamed:'), chalk.cyan(id), chalk.green('→'), chalk.cyan(newName)); +} + module.exports = { listContainers, getContainerStats, @@ -97,5 +185,7 @@ module.exports = { restartContainer, createContainer, removeContainer, - getContainerLogs + getContainerLogs, + execCommand, + renameContainer }; diff --git a/routes/containers.js b/routes/containers.js index 3bf34a0..e1ea5da 100644 --- a/routes/containers.js +++ b/routes/containers.js @@ -98,4 +98,39 @@ router.get('/:id/logs', async (req, res) => { } }); -module.exports = router; +// Execute command in container +router.post('/:id/exec', async (req, res) => { + try { + const { command } = req.body; + + if (!command) { + return res.status(400).json({ error: 'Command is required' }); + } + + const result = await containerService.execCommand(req.params.id, command); + res.json(result); + } catch (error) { + res.status(500).json({ + error: error.message || 'Failed to execute command', + exitCode: 1 + }); + } +}); + +// Rename container +router.post('/:id/rename', async (req, res) => { + try { + const { name } = req.query; + + if (!name) { + return res.status(400).json({ error: 'New name is required' }); + } + + await containerService.renameContainer(req.params.id, name); + res.json({ message: 'Container renamed successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to rename container' }); + } +}); + +module.exports = router; \ No newline at end of file