mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
477 lines
16 KiB
JavaScript
477 lines
16 KiB
JavaScript
/**
|
||
* Docker服务模块 - 处理Docker容器管理
|
||
*/
|
||
const Docker = require('dockerode');
|
||
const logger = require('../logger');
|
||
|
||
let docker = null;
|
||
|
||
async function initDockerConnection() {
|
||
if (docker) return docker;
|
||
|
||
try {
|
||
// 兼容MacOS的Docker socket路径
|
||
const options = process.platform === 'darwin'
|
||
? { socketPath: '/var/run/docker.sock' }
|
||
: null;
|
||
|
||
docker = new Docker(options);
|
||
await docker.ping();
|
||
logger.success('成功连接到Docker守护进程');
|
||
return docker;
|
||
} catch (error) {
|
||
logger.error('Docker连接失败:', error.message);
|
||
return null; // 返回null而不是抛出错误
|
||
}
|
||
}
|
||
|
||
// 获取Docker连接
|
||
async function getDockerConnection() {
|
||
if (!docker) {
|
||
docker = await initDockerConnection();
|
||
}
|
||
return docker;
|
||
}
|
||
|
||
// 修改其他Docker相关方法,添加更友好的错误处理
|
||
async function getContainersStatus() {
|
||
const docker = await initDockerConnection();
|
||
if (!docker) {
|
||
logger.warn('[getContainersStatus] Cannot connect to Docker daemon, returning error indicator.');
|
||
// 返回带有特殊错误标记的数组,前端可以通过这个标记识别 Docker 不可用
|
||
return [{
|
||
id: 'n/a',
|
||
name: 'Docker 服务不可用',
|
||
image: 'n/a',
|
||
state: 'error',
|
||
status: 'Docker 服务未运行或无法连接',
|
||
error: 'DOCKER_UNAVAILABLE', // 特殊错误标记
|
||
cpu: 'N/A',
|
||
memory: 'N/A',
|
||
created: new Date().toLocaleString()
|
||
}];
|
||
}
|
||
|
||
let containers = [];
|
||
try {
|
||
containers = await docker.listContainers({ all: true });
|
||
logger.info(`[getContainersStatus] Found ${containers.length} containers.`);
|
||
} catch (listError) {
|
||
logger.error('[getContainersStatus] Error listing containers:', listError.message || listError);
|
||
// 使用同样的错误标记模式
|
||
return [{
|
||
id: 'n/a',
|
||
name: '容器列表获取失败',
|
||
image: 'n/a',
|
||
state: 'error',
|
||
status: `获取容器列表失败: ${listError.message}`,
|
||
error: 'CONTAINER_LIST_ERROR',
|
||
cpu: 'N/A',
|
||
memory: 'N/A',
|
||
created: new Date().toLocaleString()
|
||
}];
|
||
}
|
||
|
||
const containerPromises = containers.map(async (container) => {
|
||
try {
|
||
const containerInspectInfo = await docker.getContainer(container.Id).inspect();
|
||
|
||
let stats = {};
|
||
let cpuUsage = 'N/A';
|
||
let memoryUsage = 'N/A';
|
||
|
||
// 仅在容器运行时尝试获取 stats
|
||
if (containerInspectInfo.State.Running) {
|
||
try {
|
||
stats = await docker.getContainer(container.Id).stats({ stream: false });
|
||
|
||
// Safely calculate CPU usage
|
||
if (stats.precpu_stats && stats.cpu_stats && stats.cpu_stats.cpu_usage && stats.precpu_stats.cpu_usage && stats.cpu_stats.system_cpu_usage && stats.precpu_stats.system_cpu_usage) {
|
||
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||
if (systemDelta > 0 && stats.cpu_stats.online_cpus > 0) {
|
||
cpuUsage = ((cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100.0).toFixed(2) + '%';
|
||
} else {
|
||
cpuUsage = '0.00%'; // Handle division by zero or no change
|
||
}
|
||
} else {
|
||
logger.warn(`[getContainersStatus] Incomplete CPU stats for container ${container.Id}`);
|
||
}
|
||
|
||
// Safely calculate Memory usage
|
||
if (stats.memory_stats && stats.memory_stats.usage && stats.memory_stats.limit) {
|
||
const memoryLimit = stats.memory_stats.limit;
|
||
if (memoryLimit > 0) {
|
||
memoryUsage = ((stats.memory_stats.usage / memoryLimit) * 100.0).toFixed(2) + '%';
|
||
} else {
|
||
memoryUsage = '0.00%'; // Handle division by zero (unlikely)
|
||
}
|
||
} else {
|
||
logger.warn(`[getContainersStatus] Incomplete Memory stats for container ${container.Id}`);
|
||
}
|
||
|
||
} catch (statsError) {
|
||
logger.warn(`[getContainersStatus] Failed to get stats for running container ${container.Id}: ${statsError.message}`);
|
||
// 保留 N/A 值
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: container.Id.slice(0, 12),
|
||
name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : (containerInspectInfo.Name ? containerInspectInfo.Name.replace(/^\//, '') : 'N/A'),
|
||
image: container.Image || 'N/A',
|
||
state: containerInspectInfo.State.Status || container.State || 'N/A',
|
||
status: container.Status || 'N/A',
|
||
cpu: cpuUsage,
|
||
memory: memoryUsage,
|
||
created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
|
||
};
|
||
} catch(err) {
|
||
logger.warn(`[getContainersStatus] Failed to get inspect info for container ${container.Id}: ${err.message}`);
|
||
// 返回一个包含错误信息的对象,而不是让 Promise.all 失败
|
||
return {
|
||
id: container.Id ? container.Id.slice(0, 12) : 'Unknown ID',
|
||
name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : 'Unknown Name',
|
||
image: container.Image || 'Unknown Image',
|
||
state: 'error',
|
||
status: `Error: ${err.message}`,
|
||
cpu: 'N/A',
|
||
memory: 'N/A',
|
||
created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
|
||
};
|
||
}
|
||
});
|
||
|
||
// 等待所有容器信息处理完成
|
||
const results = await Promise.all(containerPromises);
|
||
// 可以选择过滤掉完全失败的结果(虽然上面已经处理了)
|
||
// return results.filter(r => r.state !== 'error');
|
||
return results; // 返回所有结果,包括有错误的
|
||
}
|
||
|
||
// 获取单个容器状态
|
||
async function getContainerStatus(id) {
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
const container = docker.getContainer(id);
|
||
const containerInfo = await container.inspect();
|
||
return { state: containerInfo.State.Status };
|
||
}
|
||
|
||
// 重启容器
|
||
async function restartContainer(id) {
|
||
logger.info(`Attempting to restart container ${id}`);
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
logger.error(`[restartContainer ${id}] Cannot connect to Docker daemon.`);
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
try {
|
||
const container = docker.getContainer(id);
|
||
await container.restart();
|
||
logger.success(`Container ${id} restarted successfully.`);
|
||
return { success: true };
|
||
} catch (error) {
|
||
logger.error(`[restartContainer ${id}] Error restarting container:`, error.message || error);
|
||
// 检查是否是容器不存在的错误
|
||
if (error.statusCode === 404) {
|
||
throw new Error(`容器 ${id} 不存在`);
|
||
}
|
||
// 可以根据需要添加其他错误类型的检查
|
||
throw new Error(`重启容器失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 停止容器
|
||
async function stopContainer(id) {
|
||
logger.info(`Attempting to stop container ${id}`);
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
logger.error(`[stopContainer ${id}] Cannot connect to Docker daemon.`);
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
try {
|
||
const container = docker.getContainer(id);
|
||
await container.stop();
|
||
logger.success(`Container ${id} stopped successfully.`);
|
||
return { success: true };
|
||
} catch (error) {
|
||
logger.error(`[stopContainer ${id}] Error stopping container:`, error.message || error);
|
||
// 检查是否是容器不存在或已停止的错误
|
||
if (error.statusCode === 404) {
|
||
throw new Error(`容器 ${id} 不存在`);
|
||
} else if (error.statusCode === 304) {
|
||
logger.warn(`[stopContainer ${id}] Container already stopped.`);
|
||
return { success: true, message: '容器已停止' }; // 认为已停止也是成功
|
||
}
|
||
throw new Error(`停止容器失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 删除容器
|
||
async function deleteContainer(id) {
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
const container = docker.getContainer(id);
|
||
|
||
// 首先停止容器(如果正在运行)
|
||
try {
|
||
await container.stop();
|
||
} catch (stopError) {
|
||
logger.info('Container may already be stopped:', stopError.message);
|
||
}
|
||
|
||
// 然后删除容器
|
||
await container.remove();
|
||
return { success: true, message: '容器已成功删除' };
|
||
}
|
||
|
||
// 更新容器
|
||
async function updateContainer(id, tag) {
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
// 获取容器信息
|
||
const container = docker.getContainer(id);
|
||
const containerInfo = await container.inspect();
|
||
const currentImage = containerInfo.Config.Image;
|
||
const [imageName] = currentImage.split(':');
|
||
const newImage = `${imageName}:${tag}`;
|
||
const containerName = containerInfo.Name.slice(1); // 去掉开头的 '/'
|
||
|
||
logger.info(`Updating container ${id} from ${currentImage} to ${newImage}`);
|
||
|
||
// 拉取新镜像
|
||
logger.info(`Pulling new image: ${newImage}`);
|
||
await new Promise((resolve, reject) => {
|
||
docker.pull(newImage, (err, stream) => {
|
||
if (err) return reject(err);
|
||
docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output));
|
||
});
|
||
});
|
||
|
||
// 停止旧容器
|
||
logger.info('Stopping old container');
|
||
await container.stop();
|
||
|
||
// 删除旧容器
|
||
logger.info('Removing old container');
|
||
await container.remove();
|
||
|
||
// 创建新容器
|
||
logger.info('Creating new container');
|
||
const newContainerConfig = {
|
||
...containerInfo.Config,
|
||
Image: newImage,
|
||
HostConfig: containerInfo.HostConfig,
|
||
NetworkingConfig: {
|
||
EndpointsConfig: containerInfo.NetworkSettings.Networks
|
||
}
|
||
};
|
||
|
||
const newContainer = await docker.createContainer({
|
||
...newContainerConfig,
|
||
name: containerName
|
||
});
|
||
|
||
// 启动新容器
|
||
logger.info('Starting new container');
|
||
await newContainer.start();
|
||
|
||
logger.success('Container update completed successfully');
|
||
return { success: true };
|
||
}
|
||
|
||
// 获取容器日志
|
||
async function getContainerLogs(id, options = {}) {
|
||
logger.info(`Attempting to get logs for container ${id} with options:`, options);
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
logger.error(`[getContainerLogs ${id}] Cannot connect to Docker daemon.`);
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
try {
|
||
const container = docker.getContainer(id);
|
||
const logOptions = {
|
||
stdout: true,
|
||
stderr: true,
|
||
tail: options.tail || 100,
|
||
follow: options.follow || false
|
||
};
|
||
|
||
// 修复日志获取方式
|
||
if (!options.follow) {
|
||
// 对于非流式日志,直接等待返回
|
||
try {
|
||
const logs = await container.logs(logOptions);
|
||
|
||
// 如果logs是Buffer或字符串,直接处理
|
||
if (Buffer.isBuffer(logs) || typeof logs === 'string') {
|
||
// 清理ANSI转义码
|
||
const cleanedLogs = logs.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
|
||
logger.success(`Successfully retrieved logs for container ${id}`);
|
||
return cleanedLogs;
|
||
}
|
||
// 如果logs是流,转换为字符串
|
||
else if (typeof logs === 'object' && logs !== null) {
|
||
return new Promise((resolve, reject) => {
|
||
let allLogs = '';
|
||
|
||
// 处理数据事件
|
||
if (typeof logs.on === 'function') {
|
||
logs.on('data', chunk => {
|
||
allLogs += chunk.toString('utf8');
|
||
});
|
||
|
||
logs.on('end', () => {
|
||
const cleanedLogs = allLogs.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
|
||
logger.success(`Successfully retrieved logs for container ${id}`);
|
||
resolve(cleanedLogs);
|
||
});
|
||
|
||
logs.on('error', err => {
|
||
logger.error(`[getContainerLogs ${id}] Error reading log stream:`, err.message || err);
|
||
reject(new Error(`读取日志流失败: ${err.message}`));
|
||
});
|
||
} else {
|
||
// 如果不是标准流但返回了对象,尝试转换为字符串
|
||
logger.warn(`[getContainerLogs ${id}] Logs object does not have stream methods, trying to convert`);
|
||
try {
|
||
const logStr = logs.toString();
|
||
const cleanedLogs = logStr.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
|
||
resolve(cleanedLogs);
|
||
} catch (convErr) {
|
||
logger.error(`[getContainerLogs ${id}] Failed to convert logs to string:`, convErr);
|
||
reject(new Error('日志格式转换失败'));
|
||
}
|
||
}
|
||
});
|
||
} else {
|
||
logger.error(`[getContainerLogs ${id}] Unexpected logs response type:`, typeof logs);
|
||
throw new Error('日志响应格式错误');
|
||
}
|
||
} catch (logError) {
|
||
logger.error(`[getContainerLogs ${id}] Error getting logs:`, logError);
|
||
throw logError;
|
||
}
|
||
} else {
|
||
// 对于流式日志,调整方式
|
||
logger.info(`[getContainerLogs ${id}] Returning log stream for follow=true`);
|
||
const stream = await container.logs(logOptions);
|
||
return stream; // 直接返回流对象
|
||
}
|
||
} catch (error) {
|
||
logger.error(`[getContainerLogs ${id}] Error getting container logs:`, error.message || error);
|
||
if (error.statusCode === 404) {
|
||
throw new Error(`容器 ${id} 不存在`);
|
||
}
|
||
throw new Error(`获取日志失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// 获取已停止的容器
|
||
async function getStoppedContainers() {
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
const containers = await docker.listContainers({
|
||
all: true,
|
||
filters: { status: ['exited', 'dead', 'created'] }
|
||
});
|
||
|
||
return containers.map(container => ({
|
||
id: container.Id.slice(0, 12),
|
||
name: container.Names[0].replace(/^\//, ''),
|
||
status: container.State
|
||
}));
|
||
}
|
||
|
||
// 获取最近的Docker事件
|
||
async function getRecentEvents(limit = 10) {
|
||
const docker = await getDockerConnection();
|
||
if (!docker) {
|
||
throw new Error('无法连接到 Docker 守护进程');
|
||
}
|
||
|
||
// 注意:Dockerode的getEvents API可能不支持历史事件查询
|
||
// 以下代码是模拟生成最近事件,实际应用中可能需要其他方式实现
|
||
|
||
try {
|
||
const containers = await docker.listContainers({
|
||
all: true,
|
||
limit: limit,
|
||
filters: { status: ['exited', 'created', 'running', 'restarting'] }
|
||
});
|
||
|
||
// 从容器状态转换为事件
|
||
const events = containers.map(container => {
|
||
let action, status;
|
||
|
||
switch(container.State) {
|
||
case 'running':
|
||
action = 'start';
|
||
status = '运行中';
|
||
break;
|
||
case 'exited':
|
||
action = 'die';
|
||
status = '已停止';
|
||
break;
|
||
case 'created':
|
||
action = 'create';
|
||
status = '已创建';
|
||
break;
|
||
case 'restarting':
|
||
action = 'restart';
|
||
status = '重启中';
|
||
break;
|
||
default:
|
||
action = 'update';
|
||
status = container.Status;
|
||
}
|
||
|
||
return {
|
||
time: container.Created,
|
||
Action: action,
|
||
status: status,
|
||
Actor: {
|
||
Attributes: {
|
||
name: container.Names[0].replace(/^\//, '')
|
||
}
|
||
}
|
||
};
|
||
});
|
||
|
||
return events.sort((a, b) => b.time - a.time);
|
||
} catch (error) {
|
||
logger.error('获取Docker事件失败:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
module.exports = {
|
||
initDockerConnection,
|
||
getDockerConnection,
|
||
getContainersStatus,
|
||
getContainerStatus,
|
||
restartContainer,
|
||
stopContainer,
|
||
deleteContainer,
|
||
updateContainer,
|
||
getContainerLogs,
|
||
getStoppedContainers,
|
||
getRecentEvents
|
||
};
|