dockerService.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. /**
  2. * Docker服务模块 - 处理Docker容器管理
  3. */
  4. const Docker = require('dockerode');
  5. const logger = require('../logger');
  6. let docker = null;
  7. async function initDockerConnection() {
  8. if (docker) return docker;
  9. try {
  10. // 兼容MacOS的Docker socket路径
  11. const options = process.platform === 'darwin'
  12. ? { socketPath: '/var/run/docker.sock' }
  13. : null;
  14. docker = new Docker(options);
  15. await docker.ping();
  16. logger.success('成功连接到Docker守护进程');
  17. return docker;
  18. } catch (error) {
  19. logger.error('Docker连接失败:', error.message);
  20. return null; // 返回null而不是抛出错误
  21. }
  22. }
  23. // 获取Docker连接
  24. async function getDockerConnection() {
  25. if (!docker) {
  26. docker = await initDockerConnection();
  27. }
  28. return docker;
  29. }
  30. // 修改其他Docker相关方法,添加更友好的错误处理
  31. async function getContainersStatus() {
  32. const docker = await initDockerConnection();
  33. if (!docker) {
  34. logger.warn('[getContainersStatus] Cannot connect to Docker daemon, returning error indicator.');
  35. // 返回带有特殊错误标记的数组,前端可以通过这个标记识别 Docker 不可用
  36. return [{
  37. id: 'n/a',
  38. name: 'Docker 服务不可用',
  39. image: 'n/a',
  40. state: 'error',
  41. status: 'Docker 服务未运行或无法连接',
  42. error: 'DOCKER_UNAVAILABLE', // 特殊错误标记
  43. cpu: 'N/A',
  44. memory: 'N/A',
  45. created: new Date().toLocaleString()
  46. }];
  47. }
  48. let containers = [];
  49. try {
  50. containers = await docker.listContainers({ all: true });
  51. logger.info(`[getContainersStatus] Found ${containers.length} containers.`);
  52. } catch (listError) {
  53. logger.error('[getContainersStatus] Error listing containers:', listError.message || listError);
  54. // 使用同样的错误标记模式
  55. return [{
  56. id: 'n/a',
  57. name: '容器列表获取失败',
  58. image: 'n/a',
  59. state: 'error',
  60. status: `获取容器列表失败: ${listError.message}`,
  61. error: 'CONTAINER_LIST_ERROR',
  62. cpu: 'N/A',
  63. memory: 'N/A',
  64. created: new Date().toLocaleString()
  65. }];
  66. }
  67. const containerPromises = containers.map(async (container) => {
  68. try {
  69. const containerInspectInfo = await docker.getContainer(container.Id).inspect();
  70. let stats = {};
  71. let cpuUsage = 'N/A';
  72. let memoryUsage = 'N/A';
  73. // 仅在容器运行时尝试获取 stats
  74. if (containerInspectInfo.State.Running) {
  75. try {
  76. stats = await docker.getContainer(container.Id).stats({ stream: false });
  77. // Safely calculate CPU usage
  78. 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) {
  79. const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
  80. const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
  81. if (systemDelta > 0 && stats.cpu_stats.online_cpus > 0) {
  82. cpuUsage = ((cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100.0).toFixed(2) + '%';
  83. } else {
  84. cpuUsage = '0.00%'; // Handle division by zero or no change
  85. }
  86. } else {
  87. logger.warn(`[getContainersStatus] Incomplete CPU stats for container ${container.Id}`);
  88. }
  89. // Safely calculate Memory usage
  90. if (stats.memory_stats && stats.memory_stats.usage && stats.memory_stats.limit) {
  91. const memoryLimit = stats.memory_stats.limit;
  92. if (memoryLimit > 0) {
  93. memoryUsage = ((stats.memory_stats.usage / memoryLimit) * 100.0).toFixed(2) + '%';
  94. } else {
  95. memoryUsage = '0.00%'; // Handle division by zero (unlikely)
  96. }
  97. } else {
  98. logger.warn(`[getContainersStatus] Incomplete Memory stats for container ${container.Id}`);
  99. }
  100. } catch (statsError) {
  101. logger.warn(`[getContainersStatus] Failed to get stats for running container ${container.Id}: ${statsError.message}`);
  102. // 保留 N/A 值
  103. }
  104. }
  105. return {
  106. id: container.Id.slice(0, 12),
  107. name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : (containerInspectInfo.Name ? containerInspectInfo.Name.replace(/^\//, '') : 'N/A'),
  108. image: container.Image || 'N/A',
  109. state: containerInspectInfo.State.Status || container.State || 'N/A',
  110. status: container.Status || 'N/A',
  111. cpu: cpuUsage,
  112. memory: memoryUsage,
  113. created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
  114. };
  115. } catch(err) {
  116. logger.warn(`[getContainersStatus] Failed to get inspect info for container ${container.Id}: ${err.message}`);
  117. // 返回一个包含错误信息的对象,而不是让 Promise.all 失败
  118. return {
  119. id: container.Id ? container.Id.slice(0, 12) : 'Unknown ID',
  120. name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : 'Unknown Name',
  121. image: container.Image || 'Unknown Image',
  122. state: 'error',
  123. status: `Error: ${err.message}`,
  124. cpu: 'N/A',
  125. memory: 'N/A',
  126. created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
  127. };
  128. }
  129. });
  130. // 等待所有容器信息处理完成
  131. const results = await Promise.all(containerPromises);
  132. // 可以选择过滤掉完全失败的结果(虽然上面已经处理了)
  133. // return results.filter(r => r.state !== 'error');
  134. return results; // 返回所有结果,包括有错误的
  135. }
  136. // 获取单个容器状态
  137. async function getContainerStatus(id) {
  138. const docker = await getDockerConnection();
  139. if (!docker) {
  140. throw new Error('无法连接到 Docker 守护进程');
  141. }
  142. const container = docker.getContainer(id);
  143. const containerInfo = await container.inspect();
  144. return { state: containerInfo.State.Status };
  145. }
  146. // 重启容器
  147. async function restartContainer(id) {
  148. logger.info(`Attempting to restart container ${id}`);
  149. const docker = await getDockerConnection();
  150. if (!docker) {
  151. logger.error(`[restartContainer ${id}] Cannot connect to Docker daemon.`);
  152. throw new Error('无法连接到 Docker 守护进程');
  153. }
  154. try {
  155. const container = docker.getContainer(id);
  156. await container.restart();
  157. logger.success(`Container ${id} restarted successfully.`);
  158. return { success: true };
  159. } catch (error) {
  160. logger.error(`[restartContainer ${id}] Error restarting container:`, error.message || error);
  161. // 检查是否是容器不存在的错误
  162. if (error.statusCode === 404) {
  163. throw new Error(`容器 ${id} 不存在`);
  164. }
  165. // 可以根据需要添加其他错误类型的检查
  166. throw new Error(`重启容器失败: ${error.message}`);
  167. }
  168. }
  169. // 停止容器
  170. async function stopContainer(id) {
  171. logger.info(`Attempting to stop container ${id}`);
  172. const docker = await getDockerConnection();
  173. if (!docker) {
  174. logger.error(`[stopContainer ${id}] Cannot connect to Docker daemon.`);
  175. throw new Error('无法连接到 Docker 守护进程');
  176. }
  177. try {
  178. const container = docker.getContainer(id);
  179. await container.stop();
  180. logger.success(`Container ${id} stopped successfully.`);
  181. return { success: true };
  182. } catch (error) {
  183. logger.error(`[stopContainer ${id}] Error stopping container:`, error.message || error);
  184. // 检查是否是容器不存在或已停止的错误
  185. if (error.statusCode === 404) {
  186. throw new Error(`容器 ${id} 不存在`);
  187. } else if (error.statusCode === 304) {
  188. logger.warn(`[stopContainer ${id}] Container already stopped.`);
  189. return { success: true, message: '容器已停止' }; // 认为已停止也是成功
  190. }
  191. throw new Error(`停止容器失败: ${error.message}`);
  192. }
  193. }
  194. // 删除容器
  195. async function deleteContainer(id) {
  196. const docker = await getDockerConnection();
  197. if (!docker) {
  198. throw new Error('无法连接到 Docker 守护进程');
  199. }
  200. const container = docker.getContainer(id);
  201. // 首先停止容器(如果正在运行)
  202. try {
  203. await container.stop();
  204. } catch (stopError) {
  205. logger.info('Container may already be stopped:', stopError.message);
  206. }
  207. // 然后删除容器
  208. await container.remove();
  209. return { success: true, message: '容器已成功删除' };
  210. }
  211. // 更新容器
  212. async function updateContainer(id, tag) {
  213. const docker = await getDockerConnection();
  214. if (!docker) {
  215. throw new Error('无法连接到 Docker 守护进程');
  216. }
  217. // 获取容器信息
  218. const container = docker.getContainer(id);
  219. const containerInfo = await container.inspect();
  220. const currentImage = containerInfo.Config.Image;
  221. const [imageName] = currentImage.split(':');
  222. const newImage = `${imageName}:${tag}`;
  223. const containerName = containerInfo.Name.slice(1); // 去掉开头的 '/'
  224. logger.info(`Updating container ${id} from ${currentImage} to ${newImage}`);
  225. // 拉取新镜像
  226. logger.info(`Pulling new image: ${newImage}`);
  227. await new Promise((resolve, reject) => {
  228. docker.pull(newImage, (err, stream) => {
  229. if (err) return reject(err);
  230. docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output));
  231. });
  232. });
  233. // 停止旧容器
  234. logger.info('Stopping old container');
  235. await container.stop();
  236. // 删除旧容器
  237. logger.info('Removing old container');
  238. await container.remove();
  239. // 创建新容器
  240. logger.info('Creating new container');
  241. const newContainerConfig = {
  242. ...containerInfo.Config,
  243. Image: newImage,
  244. HostConfig: containerInfo.HostConfig,
  245. NetworkingConfig: {
  246. EndpointsConfig: containerInfo.NetworkSettings.Networks
  247. }
  248. };
  249. const newContainer = await docker.createContainer({
  250. ...newContainerConfig,
  251. name: containerName
  252. });
  253. // 启动新容器
  254. logger.info('Starting new container');
  255. await newContainer.start();
  256. logger.success('Container update completed successfully');
  257. return { success: true };
  258. }
  259. // 获取容器日志
  260. async function getContainerLogs(id, options = {}) {
  261. logger.info(`Attempting to get logs for container ${id} with options:`, options);
  262. const docker = await getDockerConnection();
  263. if (!docker) {
  264. logger.error(`[getContainerLogs ${id}] Cannot connect to Docker daemon.`);
  265. throw new Error('无法连接到 Docker 守护进程');
  266. }
  267. try {
  268. const container = docker.getContainer(id);
  269. const logOptions = {
  270. stdout: true,
  271. stderr: true,
  272. tail: options.tail || 100,
  273. follow: options.follow || false
  274. };
  275. // 修复日志获取方式
  276. if (!options.follow) {
  277. // 对于非流式日志,直接等待返回
  278. try {
  279. const logs = await container.logs(logOptions);
  280. // 如果logs是Buffer或字符串,直接处理
  281. if (Buffer.isBuffer(logs) || typeof logs === 'string') {
  282. // 清理ANSI转义码
  283. const cleanedLogs = logs.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
  284. logger.success(`Successfully retrieved logs for container ${id}`);
  285. return cleanedLogs;
  286. }
  287. // 如果logs是流,转换为字符串
  288. else if (typeof logs === 'object' && logs !== null) {
  289. return new Promise((resolve, reject) => {
  290. let allLogs = '';
  291. // 处理数据事件
  292. if (typeof logs.on === 'function') {
  293. logs.on('data', chunk => {
  294. allLogs += chunk.toString('utf8');
  295. });
  296. logs.on('end', () => {
  297. const cleanedLogs = allLogs.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
  298. logger.success(`Successfully retrieved logs for container ${id}`);
  299. resolve(cleanedLogs);
  300. });
  301. logs.on('error', err => {
  302. logger.error(`[getContainerLogs ${id}] Error reading log stream:`, err.message || err);
  303. reject(new Error(`读取日志流失败: ${err.message}`));
  304. });
  305. } else {
  306. // 如果不是标准流但返回了对象,尝试转换为字符串
  307. logger.warn(`[getContainerLogs ${id}] Logs object does not have stream methods, trying to convert`);
  308. try {
  309. const logStr = logs.toString();
  310. const cleanedLogs = logStr.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
  311. resolve(cleanedLogs);
  312. } catch (convErr) {
  313. logger.error(`[getContainerLogs ${id}] Failed to convert logs to string:`, convErr);
  314. reject(new Error('日志格式转换失败'));
  315. }
  316. }
  317. });
  318. } else {
  319. logger.error(`[getContainerLogs ${id}] Unexpected logs response type:`, typeof logs);
  320. throw new Error('日志响应格式错误');
  321. }
  322. } catch (logError) {
  323. logger.error(`[getContainerLogs ${id}] Error getting logs:`, logError);
  324. throw logError;
  325. }
  326. } else {
  327. // 对于流式日志,调整方式
  328. logger.info(`[getContainerLogs ${id}] Returning log stream for follow=true`);
  329. const stream = await container.logs(logOptions);
  330. return stream; // 直接返回流对象
  331. }
  332. } catch (error) {
  333. logger.error(`[getContainerLogs ${id}] Error getting container logs:`, error.message || error);
  334. if (error.statusCode === 404) {
  335. throw new Error(`容器 ${id} 不存在`);
  336. }
  337. throw new Error(`获取日志失败: ${error.message}`);
  338. }
  339. }
  340. // 获取已停止的容器
  341. async function getStoppedContainers() {
  342. const docker = await getDockerConnection();
  343. if (!docker) {
  344. throw new Error('无法连接到 Docker 守护进程');
  345. }
  346. const containers = await docker.listContainers({
  347. all: true,
  348. filters: { status: ['exited', 'dead', 'created'] }
  349. });
  350. return containers.map(container => ({
  351. id: container.Id.slice(0, 12),
  352. name: container.Names[0].replace(/^\//, ''),
  353. status: container.State
  354. }));
  355. }
  356. // 获取最近的Docker事件
  357. async function getRecentEvents(limit = 10) {
  358. const docker = await getDockerConnection();
  359. if (!docker) {
  360. throw new Error('无法连接到 Docker 守护进程');
  361. }
  362. // 注意:Dockerode的getEvents API可能不支持历史事件查询
  363. // 以下代码是模拟生成最近事件,实际应用中可能需要其他方式实现
  364. try {
  365. const containers = await docker.listContainers({
  366. all: true,
  367. limit: limit,
  368. filters: { status: ['exited', 'created', 'running', 'restarting'] }
  369. });
  370. // 从容器状态转换为事件
  371. const events = containers.map(container => {
  372. let action, status;
  373. switch(container.State) {
  374. case 'running':
  375. action = 'start';
  376. status = '运行中';
  377. break;
  378. case 'exited':
  379. action = 'die';
  380. status = '已停止';
  381. break;
  382. case 'created':
  383. action = 'create';
  384. status = '已创建';
  385. break;
  386. case 'restarting':
  387. action = 'restart';
  388. status = '重启中';
  389. break;
  390. default:
  391. action = 'update';
  392. status = container.Status;
  393. }
  394. return {
  395. time: container.Created,
  396. Action: action,
  397. status: status,
  398. Actor: {
  399. Attributes: {
  400. name: container.Names[0].replace(/^\//, '')
  401. }
  402. }
  403. };
  404. });
  405. return events.sort((a, b) => b.time - a.time);
  406. } catch (error) {
  407. logger.error('获取Docker事件失败:', error);
  408. return [];
  409. }
  410. }
  411. module.exports = {
  412. initDockerConnection,
  413. getDockerConnection,
  414. getContainersStatus,
  415. getContainerStatus,
  416. restartContainer,
  417. stopContainer,
  418. deleteContainer,
  419. updateContainer,
  420. getContainerLogs,
  421. getStoppedContainers,
  422. getRecentEvents
  423. };