Add container exec and rename support

This commit is contained in:
Andreas Claesson 2025-11-10 19:52:56 +01:00
parent 43e27eb713
commit 10a5a8e216
2 changed files with 127 additions and 2 deletions

View File

@ -89,6 +89,94 @@ async function getContainerLogs(id) {
return logs.toString(); 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 = { module.exports = {
listContainers, listContainers,
getContainerStats, getContainerStats,
@ -97,5 +185,7 @@ module.exports = {
restartContainer, restartContainer,
createContainer, createContainer,
removeContainer, removeContainer,
getContainerLogs getContainerLogs,
execCommand,
renameContainer
}; };

View File

@ -98,4 +98,39 @@ router.get('/:id/logs', async (req, res) => {
} }
}); });
// 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; module.exports = router;