Files
Docker-Proxy/hubcmdui/services/dockerService.js
T

522 lines
18 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 startContainer(id) {
logger.info(`Attempting to start container ${id}`);
const docker = await getDockerConnection();
if (!docker) {
logger.error(`[startContainer ${id}] Cannot connect to Docker daemon.`);
throw new Error('无法连接到 Docker 守护进程');
}
try {
const container = docker.getContainer(id);
await container.start();
logger.success(`Container ${id} started successfully.`);
return { success: true };
} catch (error) {
logger.error(`[startContainer ${id}] Error starting container:`, error.message || error);
// 检查是否是容器不存在的错误
if (error.statusCode === 404) {
throw new Error(`容器 ${id} 不存在`);
} else if (error.statusCode === 304) {
logger.warn(`[startContainer ${id}] Container already started.`);
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 守护进程');
}
try {
logger.info('正在获取已停止的容器...');
const containers = await docker.listContainers({
all: true,
filters: { status: ['exited', 'dead', 'created'] }
});
logger.info(`找到 ${containers.length} 个已停止的容器`);
// 记录每个容器的信息
containers.forEach(container => {
logger.info(`容器 ID: ${container.Id}, 名称: ${container.Names}, 镜像: ${container.Image}, 状态: ${container.State}`);
});
const result = containers.map(container => ({
id: container.Id.slice(0, 12),
name: container.Names[0].replace(/^\//, ''),
image: container.Image,
status: container.State
}));
logger.info('已转换容器信息: ' + JSON.stringify(result));
return result;
} catch (error) {
logger.error('获取已停止容器失败:', error);
throw error;
}
}
// 获取最近的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,
startContainer,
deleteContainer,
updateContainer,
getContainerLogs,
getStoppedContainers,
getRecentEvents
};