feat: Add container management page operations to implement container restart, stop, and other operations.

This commit is contained in:
dqzboy
2024-08-27 23:17:40 +08:00
parent 5656c102c3
commit 55c52ba16d
6 changed files with 291 additions and 7 deletions

View File

@@ -149,6 +149,7 @@ docker logs -f [容器ID或名称]
- [x] 支持国内服务器一键部署解决国内环境无法安装Docker\Compose服务难题
- [x] 支持主流Linux发行版操作系统,例如Centos、Ubuntu、Rocky、Debian、Rhel等
- [x] 支持主流ARCH架构下部署包括linux/amd64、linux/arm64
- [x] 针对本项目单独开发等Docker Registry管理面板实现镜像搜索、广告展示、文档教程、容器管理等
## ✨ 教程
#### 配置Nginx反向代理
@@ -227,6 +228,14 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1
<td width="50%" align="center"><img src="https://github.com/dqzboy/Docker-Proxy/assets/42825450/0ddb041b-64f6-4d93-b5bf-85ad3b53d0e0?raw=true"></td>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c7e368ca-7f1a-4311-9a10-a5f4f06d86d8?raw=true"></td>
</tr>
<tr>
<td width="50%" align="center"><b>Docker官方镜像搜索</b></td>
<td width="50%" align="center"><b>Docker容器服务管理</b></td>
</tr>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/8569c5c4-4ce6-4cd4-8547-fa9816019049?raw=true"></td>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033?raw=true"></td>
</tr>
</table>
---

View File

@@ -127,6 +127,12 @@ docker logs -f [容器ID或名称]
</tr>
</table>
<table>
<tr>
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033"?raw=true"></td>
</tr>
</table>
---
## 🫶 赞助

View File

@@ -4,5 +4,7 @@ services:
container_name: hubcmd-ui
image: dqzboy/hubcmd-ui:latest
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- 30080:3000

View File

@@ -2,6 +2,8 @@
"dependencies": {
"axios": "^1.7.5",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dockerode": "^4.0.2",
"express": "^4.19.2",
"express-session": "^1.18.0",
"morgan": "^1.10.0"

View File

@@ -7,8 +7,27 @@ const bcrypt = require('bcrypt');
const crypto = require('crypto');
const logger = require('morgan'); // 引入 morgan 作为日志工具
const axios = require('axios'); // 用于发送 HTTP 请求
const Docker = require('dockerode');
const app = express();
const cors = require('cors');
let docker = null;
async function initDocker() {
if (docker === null) {
docker = new Docker();
try {
await docker.ping();
console.log('成功连接到 Docker 守护进程');
} catch (err) {
console.error('无法连接到 Docker 守护进程:', err);
docker = null;
}
}
return docker;
}
app.use(cors());
app.use(express.json());
app.use(express.static('web'));
app.use(bodyParser.urlencoded({ extended: true }));
@@ -124,7 +143,6 @@ async function readDocumentation() {
try {
await ensureDocumentationDir();
const files = await fs.readdir(DOCUMENTATION_DIR);
console.log('Files in documentation directory:', files); // 添加日志
const documents = await Promise.all(files.map(async file => {
const filePath = path.join(DOCUMENTATION_DIR, file);
@@ -139,7 +157,6 @@ async function readDocumentation() {
}));
const publishedDocuments = documents.filter(doc => doc.published);
console.log('Published documents:', publishedDocuments); // 添加日志
return publishedDocuments;
} catch (error) {
console.error('Error reading documentation:', error);
@@ -205,9 +222,11 @@ app.post('/api/change-password', async (req, res) => {
// 需要登录验证的中间件
function requireLogin(req, res, next) {
console.log('Session:', req.session); // 添加这行
if (req.session.user) {
next();
} else {
console.log('用户未登录'); // 添加这行
res.status(401).json({ error: 'Not logged in' });
}
}
@@ -310,7 +329,6 @@ app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res)
app.get('/api/documentation', async (req, res) => {
try {
const documents = await readDocumentation();
console.log('Sending documents:', documents); // 添加日志
res.json(documents);
} catch (error) {
console.error('Error in /api/documentation:', error);
@@ -379,11 +397,9 @@ app.get('/api/documentation-list', async (req, res) => {
app.get('/api/documentation/:id', async (req, res) => {
try {
const docId = req.params.id;
console.log('Fetching document with id:', docId); // 添加日志
const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
const content = await fs.readFile(docPath, 'utf8');
const doc = JSON.parse(content);
console.log('Sending document:', doc); // 添加日志
res.json(doc);
} catch (error) {
console.error('Error reading document:', error);
@@ -391,6 +407,95 @@ app.get('/api/documentation/:id', async (req, res) => {
}
});
// API端点来获取Docker容器状态
app.get('/api/docker-status', requireLogin, async (req, res) => {
try {
const docker = await initDocker();
if (!docker) {
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
}
const containers = await docker.listContainers({ all: true });
const containerStatus = await Promise.all(containers.map(async (container) => {
const containerInfo = await docker.getContainer(container.Id).inspect();
const stats = await docker.getContainer(container.Id).stats({ stream: false });
// 计算 CPU 使用率
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;
const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
// 计算内存使用率
const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
return {
id: container.Id.slice(0, 12),
name: container.Names[0].replace(/^\//, ''),
image: container.Image,
state: containerInfo.State.Status,
status: container.Status,
cpu: cpuUsage.toFixed(2) + '%',
memory: memoryUsage.toFixed(2) + '%',
created: new Date(container.Created * 1000).toLocaleString()
};
}));
res.json(containerStatus);
} catch (error) {
console.error('获取 Docker 状态时出错:', error);
res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
}
});
// API端点重启容器
app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
try {
const docker = await initDocker();
if (!docker) {
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
}
const container = docker.getContainer(req.params.id);
await container.restart();
res.json({ success: true });
} catch (error) {
console.error('重启容器失败:', error);
res.status(500).json({ error: '重启容器失败', details: error.message });
}
});
// API端点停止容器
app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
try {
const docker = await initDocker();
if (!docker) {
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
}
const container = docker.getContainer(req.params.id);
await container.stop();
res.json({ success: true });
} catch (error) {
console.error('停止容器失败:', error);
res.status(500).json({ error: '停止容器失败', details: error.message });
}
});
// API端点获取单个容器的状态
app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
try {
const docker = await initDocker();
if (!docker) {
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
}
const container = docker.getContainer(req.params.id);
const containerInfo = await container.inspect();
res.json({ state: containerInfo.State.Status });
} catch (error) {
console.error('获取容器状态失败:', error);
res.status(500).json({ error: '获取容器状态失败', details: error.message });
}
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {

View File

@@ -86,6 +86,7 @@
font-family: inherit;
font-size: inherit;
}
.action-btn:hover {
background-color: #0256b9;
}
@@ -400,6 +401,21 @@
max-width: 100%;
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 3px solid #0366d6;
animation: spin 1s linear infinite;
}
</style>
</head>
<body>
@@ -415,6 +431,7 @@
<li data-section="ad-management">广告管理</li>
<li data-section="documentation-management">文档管理</li>
<li data-section="password-change">修改密码</li>
<li data-section="docker-status">Docker 服务状态</li>
</ul>
</div>
<div class="content-area">
@@ -490,6 +507,7 @@
</div>
</div>
<!-- 修改密码部分 -->
<div id="password-change" class="content-section">
<h2 class="menu-label">修改密码</h2>
<label for="currentPassword">当前密码</label>
@@ -500,6 +518,28 @@
<span id="passwordStrength" style="color: red;"></span>
<button type="button" onclick="changePassword()">修改密码</button>
</div>
<!-- Docker服务状态 -->
<div id="docker-status" class="content-section">
<h1 class="admin-title">Docker 服务状态</h1>
<table id="dockerStatusTable">
<thead>
<tr>
<th>容器 ID</th>
<th>名称</th>
<th>镜像</th>
<th>状态</th>
<th>CPU</th>
<th>内存</th>
<th>创建时间</th>
</tr>
</thead>
<tbody id="dockerStatusTableBody">
<!-- Docker 容器状态将在这里动态添加 -->
</tbody>
</table>
<button type="button" onclick="refreshDockerStatus()">刷新状态</button>
</div>
</div>
</div>
</div>
@@ -1278,12 +1318,132 @@
}
}
async function loadDockerStatus() {
try {
const response = await fetch('/api/docker-status');
if (!response.ok) {
if (response.status === 503) {
throw new Error('无法连接到 Docker 守护进程');
}
throw new Error('Failed to fetch Docker status');
}
const containerStatus = await response.json();
renderDockerStatus(containerStatus);
} catch (error) {
console.error('Error loading Docker status:', error);
alert('加载 Docker 状态失败: ' + error.message);
// 清空状态表格并显示错误信息
const tbody = document.getElementById('dockerStatusTableBody');
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
}
}
function renderDockerStatus(containerStatus) {
const tbody = document.getElementById('dockerStatusTableBody');
tbody.innerHTML = '';
// 添加表头
const thead = document.getElementById('dockerStatusTable').getElementsByTagName('thead')[0];
thead.innerHTML = `
<tr>
<th>容器 ID</th>
<th>名称</th>
<th>镜像</th>
<th>状态</th>
<th>CPU</th>
<th>内存</th>
<th>创建时间</th>
<th>操作</th>
</tr>
`;
containerStatus.forEach(container => {
const row = `
<tr>
<td>${container.id}</td>
<td>${container.name}</td>
<td>${container.image}</td>
<td id="status-${container.id}">${container.state}</td>
<td>${container.cpu}</td>
<td>${container.memory}</td>
<td>${container.created}</td>
<td>
<button onclick="restartContainer('${container.id}')" class="action-btn">重启</button>
<button onclick="stopContainer('${container.id}')" class="action-btn">停止</button>
</td>
</tr>
`;
tbody.innerHTML += row;
});
}
async function restartContainer(id) {
if (confirm('确定要重启这个容器吗?')) {
try {
const statusCell = document.getElementById(`status-${id}`);
statusCell.innerHTML = '<div class="loading"></div>';
const response = await fetch(`/api/docker/restart/${id}`, { method: 'POST' });
if (response.ok) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒确保状态已更新
const newStatus = await getContainerStatus(id);
statusCell.textContent = newStatus;
} else {
throw new Error('重启失败');
}
} catch (error) {
console.error('重启容器失败:', error);
alert('重启容器失败: ' + error.message);
loadDockerStatus(); // 重新加载所有容器状态
}
}
}
async function stopContainer(id) {
if (confirm('确定要停止这个容器吗?')) {
try {
const statusCell = document.getElementById(`status-${id}`);
statusCell.innerHTML = '<div class="loading"></div>';
const response = await fetch(`/api/docker/stop/${id}`, { method: 'POST' });
if (response.ok) {
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒确保状态已更新
const newStatus = await getContainerStatus(id);
statusCell.textContent = newStatus;
} else {
throw new Error('停止失败');
}
} catch (error) {
console.error('停止容器失败:', error);
alert('停止容器失败: ' + error.message);
loadDockerStatus(); // 重新加载所有容器状态
}
}
}
async function getContainerStatus(id) {
const response = await fetch(`/api/docker/status/${id}`);
if (response.ok) {
const data = await response.json();
return data.state;
} else {
throw new Error('获取容器状态失败');
}
}
function refreshDockerStatus() {
loadDockerStatus();
}
document.addEventListener('DOMContentLoaded', function() {
const sidebarItems = document.querySelectorAll('.sidebar li');
const contentSections = document.querySelectorAll('.content-section');
if (isLoggedIn) {
initEditor();
}
if (isLoggedIn) {
loadDockerStatus();
}
function showSection(sectionId) {
contentSections.forEach(section => {
if (section.id === sectionId) {
@@ -1310,4 +1470,4 @@
});
</script>
</body>
</html>
</html>