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

477 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
};