refactor: 前后端目录结构模块化 + v2 优化移植与缺陷修复

前端:
- src/srv/api → src/api,按域分文件夹(auth/cluster/host/storage/deployment/collector)
- 新增 http.ts 引擎(http.get/post/put/delete),域模块改用语义化调用
- index.html 增加 favicon(匹配应用 logo 的 emerald + Activity 脉冲)

后端分层增强(保留包名):
- 新增 repository/:从 models 拆出 JsonDB 持久化层(InitDB/GetDB/Shutdown/RequestSave)
- 新增 dto/:从 controllers 抽出各域请求契约(带 binding 校验)
- 新增 pkg/crypto、pkg/jwt:从 utils 拆出安全原语(外部 jwt 别名 jwtv5 避免冲突)
- models 只留领域实体 + sanitizer;utils 只留 gin/应用相关(response/errors)

v2 优化移植与缺陷修复:
- 移植密钥分离/SSH TOFU/并发写安全/Loki 生产级配置/Docker 加固
- 修复 deployment.go 并发丢更新、Helm values 重复键、EncryptField 哨兵、loki-gateway 端点等审计缺陷
- 实现真正的优雅关停(main.go http.Server + signal + repository.Shutdown)
- 修复登录契约:后端返回 accessToken,前端 auth.ts/Login.tsx 对齐
- 补 crypto/jwt/repository 单元测试(含并发写回归)

验证: go build/vet/test 全绿;npm run build 通过;登录→鉴权→优雅关停冒烟通过

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Logan.LiandClaude committed 2026-06-30 10:46:26 +08:00
1 parent fb8d10231a
commit 327e282356
93 files changed
+8433 -3089

No files matched your search

+415 -85
View File
@@ -1,133 +1,463 @@
# LokiDeploy - K8s Loki Deployment Platform
# LokiDeploy v2 — 安全的 K8s Loki 部署平台
A lightweight **Gin + React + JSON file storage** platform for deploying Grafana Loki log aggregation on Kubernetes clusters or bare-metal hosts via SSH.
一个轻量级的 **Gin + React + JSON 文件存储** 平台,用于通过 SSH 或 kubeconfig 把 Grafana Loki
日志聚合系统部署到 Kubernetes 集群或裸金属主机上。
## Features
> **v2 安全增强**:非 root 容器、运行时强制校验密钥、健康检查、资源限制、日志轮转、优雅关停。
>
> **面向 AI 编码代理**:本仓库的 `CLAUDE.md` 与 `AGENTS.md` 记录了架构约定与常用命令,供 Claude Code、
> Cursor、Aider 等工具参考。两份文件内容一致,修改其一请同步另一份。
- **K8s Cluster Management** - Add clusters via kubeconfig, auto-validate connectivity
- **SSH Host Management** - Password or key-based authentication
- **Storage Configuration** - S3, OSS, MinIO, RustFS backends
- **One-Click Loki Deploy** - Helm chart / YAML manifest / Binary
- **Deployment Logs** - Real-time log streaming
- **Web Terminal** - Browser-based remote command execution
- **Single Binary** - 13MB zero-dependency executable
- **Security** - Argon2 password hashing, AES credential encryption, JWT auth
## 功能特性
## Quick Start
- **K8s 集群管理** — 通过 kubeconfig 添加集群,自动校验连通性
- **SSH 主机管理** — 支持密码或密钥认证
- **存储配置** — 支持 S3、OSS、MinIO、RustFS 等后端
- **一键部署 Loki** — Helm Chart / YAML 清单 / 二进制 三种模式
- **部署日志** — 实时日志流
- **Web 终端** — 在浏览器中执行远程命令
- **单体二进制** — 约 13MB,零外部依赖
- **安全** — Argon2 密码哈希、AES 凭证加密、JWT 鉴权、非 root 容器
### Prerequisites
- Linux/macOS with `kubectl` and `helm` installed (for K8s deployment)
- `curl` or `wget` on target hosts
- Go 1.23+ (for building from source)
- Node.js 20+ (for building frontend)
---
### Option 1: Build & Run (Recommended)
## 快速开始
### 前置条件
- Docker Engine 24.0+ 与 Docker Compose v2+
- 宿主机上装有 `kubectl` 和 `helm`(用于向外部集群部署 K8s 资源)
- `openssl`(用于生成密钥)
### 第 1 步:生成密钥(必需)
启动前必须生成所需的安全密钥:
```bash
# 必需:JWT 签名密钥(建议 >= 32 字符)
export JWT_SECRET=$(openssl rand -hex 32)
# 必需:用于凭证存储的 AES 加密密钥(必须 >= 32 字节)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
# 可选:跨会话持久化
echo "export JWT_SECRET=$JWT_SECRET" >> ~/.bashrc
echo "export ENCRYPTION_KEY=$ENCRYPTION_KEY" >> ~/.bashrc
```
> **警告**:如果未设置 `JWT_SECRET` 或 `ENCRYPTION_KEY`,容器将 **拒绝启动**,以避免不安全的默认部署。
### 第 2 步:用 Docker Compose 启动(推荐)
```bash
git clone https://github.com/your/lokideploy.git
cd lokideploy
# Build everything
bash build.sh
# 设置密钥(见第 1 步)
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
# Start frontend (:3000) + backend (:8080)
./run.sh
# Open http://localhost:3000
# Default: admin / admin123
```
### Option 2: Docker Compose
```bash
cd lokideploy
# 启动
docker compose up -d
# Open http://localhost:8080
# 查看健康状态
docker compose ps
docker compose logs -f
# 打开 http://localhost:8080
# 默认账号:admin / admin123
```
### Option 3: Development Mode (Hot Reload)
### 第 3 步:验证健康状态
```bash
# Terminal 1 - Backend
cd backend
go run .
# 容器级健康检查(内部)
docker inspect --format='{{.State.Health.Status}}' lokideploy
# Terminal 2 - Frontend
# 期望输出:"healthy"
# 手动检查
curl http://localhost:8080/index.html
```
---
## 生产部署
### 方式 A:带密钥文件的 Docker Compose(推荐)
```bash
# 创建密钥文件
mkdir -p secrets
openssl rand -hex 32 > secrets/jwt_secret.txt
openssl rand -hex 32 > secrets/encryption_key.txt
chmod 600 secrets/*.txt
# 使用 docker-compose.prod.yml
docker compose -f docker-compose.prod.yml up -d
```
### 方式 B:Docker Swarm
```bash
# 初始化 swarm(如果尚未初始化)
docker swarm init
# 创建 secrets
echo "$(openssl rand -hex 32)" | docker secret create lokideploy_jwt -
echo "$(openssl rand -hex 32)" | docker secret create lokideploy_key -
# 部署
docker stack deploy -c docker-compose.prod.yml lokideploy
```
### 安全清单
- [ ] `JWT_SECRET` 至少 32 字符且为密码学随机
- [ ] `ENCRYPTION_KEY` 至少 32 字节且为密码学随机
- [ ] 密钥存放在 Docker Secrets 或外部保险柜中(而非 env 文件)
- [ ] `.env` 与 `secrets/` 已加入 `.gitignore`
- [ ] 启用了 `no-new-privileges:true`(docker-compose 默认启用)
- [ ] kubeconfig 以只读方式挂载(`:ro`)
- [ ] SSH 密钥以只读方式挂载(`:ro`)
- [ ] 已合理配置资源限制
- [ ] 健康检查通过
- [ ] 已配置日志轮转
---
## 从源码构建
```bash
# 构建前端 + 后端
bash build.sh
# 用本地脚本启动(开发)
./run.sh
```
### 开发模式(热重载)
```bash
# 终端 1 — 后端
cd backend
go run . # 监听 :8080
# 终端 2 — 前端
cd frontend
npm install
npm run dev
# Opens http://localhost:3000, proxies API to localhost:8080
npm run dev # 打开 http://localhost:3000,API 代理到 localhost:8080
```
## Architecture
> 直接 `go run` 启动后端前,必须先 `export JWT_SECRET` 与 `ENCRYPTION_KEY`,否则
> `backend/config/config.go` 会 `log.Fatal` 拒绝启动。`./run.sh` 会自动注入开发用默认值。
### 测试与 lint
```bash
cd backend && go test ./... # 所有 Go 测试(utils、repository)
cd backend && go test ./utils/ -run TestName -v # 运行单个测试
cd frontend && npm run lint # ESLint
```
---
## 架构
```
Frontend (React + TypeScript + Tailwind CSS) [Port 3000 dev / 8080 prod]
前端(React + TypeScript + Tailwind CSS) [开发 :3000 / 生产 :8080]
|
| HTTP/REST + JWT Bearer Token
v
Backend (Go + Gin) [Port 8080]
后端(Go + Gin) [:8080]
|
+---> JSON File DB (data/lokideploy.json)
+---> K8s CLI (kubectl / helm)
+---> SSH Client (golang.org/x/crypto/ssh)
+---> Loki Deployer (Helm Chart / YAML / Binary)
+---> JSON 文件数据库(data/lokideploy.json)
+---> K8s CLI(kubectl / helm)
+---> SSH 客户端(golang.org/x/crypto/ssh)
+---> Loki 部署器(Helm Chart / YAML / Binary)
```
## API Endpoints
### 后端分层
Base URL: `/api/v1`
请求流转:**router → middleware(JWT)→ controller → service → repository(JsonDB)**
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/auth/login` | No | Login, get JWT token |
| GET | `/auth/user` | Yes | Current user info |
| POST | `/auth/password` | Yes | Change password |
| GET/POST | `/clusters` | Yes | Cluster CRUD |
| POST | `/clusters/:id/validate` | Yes | Validate cluster |
| GET/POST | `/hosts` | Yes | Host CRUD |
| POST | `/hosts/:id/execute` | Yes | Remote command |
| GET/POST | `/storages` | Yes | Storage config CRUD |
| GET/POST | `/deployments` | Yes | Deployment CRUD |
| POST | `/deployments/:id/deploy` | Yes | Start deployment |
| POST | `/deployments/:id/stop` | Yes | Stop deployment |
| GET | `/deployments/:id/logs` | Yes | Deployment logs |
| GET | `/deployments/stats` | Yes | Statistics |
- `config/` — `GetConfig()` 单例,读取环境变量,密钥缺失或长度不足时直接 `log.Fatal`。
- `routers/router.go` — 所有路由注册在 `/api/v1`;注册 SPA 静态资源并用 `NoRoute` 回退到
`index.html` 支持前端路由。
- `controllers/` — 薄 Gin handler,每资源一个文件;绑定/校验入参、调用 service、用
`models.SanitizedJSON(...)` 包装返回。
- `services/` — 业务逻辑。部署流水线:
`DeploymentService.Deploy()` → `LokiDeployer.Deploy()`,按模式(`k8s-helm` / `k8s-yaml` /
`host-binary`)分发到 `kubectl`/`helm` 或 SSH。另有 `K8sClient`、`ssh`、`storage`、
`cluster_info` 与较新的 **collector(日志采集器)** 模块。
- `middleware/jwt.go` — `JWTAuth()`、`AdminAuth()`。
- `utils/` — `response.go`(响应封装)、`crypto.go`(Argon2 + AES-GCM)、`jwt.go`。
- `seed/` — 数据库为空时写入默认 admin 用户。
## Deployment Modes
### 数据层 — JSON 文件数据库
| Mode | Target | Description |
|------|--------|-------------|
| `k8s-helm` | K8s Cluster | Official Grafana Helm chart |
| `k8s-yaml` | K8s Cluster | Raw Kubernetes manifests |
| `host-binary` | Linux Host | Binary + systemd via SSH |
持久化是 **单个 JSON 文件** `$DATA_DIR/lokideploy.json`(默认 `./data/`),**没有 SQL 数据库**。
`repository.JsonDB` 是与文件对应的内存结构:
## Storage Backends
- 写操作由 `Lock()/RLock()` 保护,`RequestSave()` 触发 **防抖(500ms)原子写**(临时文件 →
`fsync` → rename),由单一串行化的 `saveWorker` goroutine 执行。常规写入用 `RequestSave()`,
不要在 worker 可能运行时直接 `Save()`。
- 优雅关停顺序很关键:停防抖定时器 → 关闭 worker 并等待 → 最后一次 `doSave`。顺序颠倒会丢更新。
- 因此应用 **只能单实例运行**,不可水平扩展。
- `NextID(table)` 为各集合分配单调递增的整数 ID。
| Type | Endpoint Example |
|------|-----------------|
> 注:`repository/db.go` 是当前在用的实现;`models/db.go` 是正在迁移、尚未清理的旧副本(仍会编译,
> 但已无人调用)。新增数据库代码请写到 `repository`。
### 响应封装与脱敏
- 所有 API 响应为 `{ code, message, data }`,`code == 0` 表示成功。
- **不要直接返回原始 model**,必须用 `models.SanitizedJSON(...)` 包装以剔除密码、kubeconfig、
私钥、token 等敏感字段。
### 凭证安全
- 密码用 **Argon2** 哈希;凭证用 **AES-GCM** 加密,AES 密钥由 `ENCRYPTION_KEY` 经 **HKDF** 派生。
- `ENCRYPTION_KEY` 必须 **>= 32 字节**(早期文档写 16,现在会导致启动失败)。
### 容器安全模型
```
+-------------------+ +---------------------+
| 宿主内核 | | 宿主文件系统 |
| | | ~/.kube (只读) |
| +-------------+ | | ~/.ssh (只读) |
| | 容器 | | | ./data (读写) |
| | (lokideploy | | +---------------------+
| | UID:1000) | |
| | no-new-privs| |
| | cap_drop:ALL| |
| +-------------+ |
+-------------------+
```
---
## API 接口
Base URL:`/api/v1`
| 方法 | 接口 | 鉴权 | 说明 |
|------|------|------|------|
| POST | `/auth/login` | 否 | 登录,获取 JWT token |
| GET | `/auth/user` | 是 | 当前用户信息 |
| POST | `/auth/password` | 是 | 修改密码 |
| GET/POST | `/clusters` | 是 | 集群增删改查 |
| POST | `/clusters/:id/validate` | 是 | 校验集群连通性 |
| GET/POST | `/hosts` | 是 | 主机增删改查 |
| POST | `/hosts/:id/execute` | 是 | 远程执行命令 |
| GET/POST | `/storages` | 是 | 存储配置增删改查 |
| GET/POST | `/deployments` | 是 | 部署增删改查 |
| POST | `/deployments/:id/deploy` | 是 | 启动部署 |
| POST | `/deployments/:id/stop` | 是 | 停止部署 |
| GET | `/deployments/:id/logs` | 是 | 部署日志 |
| GET | `/deployments/stats` | 是 | 统计信息 |
### 健康检查
容器对静态文件路径暴露健康检查:
```bash
# 容器级(内部)
wget --spider http://localhost:8080/index.html
# 宿主级
curl http://localhost:8080/index.html
# Docker inspect
docker inspect --format='{{.State.Health}}' lokideploy
```
健康检查参数:
- **间隔**:30 秒
- **超时**:5 秒
- **启动宽限期**:15 秒(允许启动)
- **重试次数**:3
---
## 部署模式
| 模式 | 目标 | 说明 |
|------|------|------|
| `k8s-helm` | K8s 集群 | 官方 Grafana Helm Chart |
| `k8s-yaml` | K8s 集群 | 原生 Kubernetes 清单 |
| `host-binary` | Linux 主机 | 通过 SSH 部署二进制 + systemd |
## 存储后端
| 类型 | Endpoint 示例 |
|------|---------------|
| S3 | `s3.amazonaws.com` |
| OSS | `oss-cn-hangzhou.aliyuncs.com` |
| MinIO | `http://minio:9000` |
| RustFS | `http://rustfs:8080` |
## Environment Variables
---
| Variable | Default | Description |
|----------|---------|-------------|
| `SERVER_PORT` | `8080` | HTTP server port |
| `DATA_DIR` | `./data` | Data storage directory |
| `JWT_SECRET` | `lokideploy-secret-key...` | JWT signing key |
## 环境变量
## Security
### 必需
- **Argon2** password hashing
- **AES-GCM** credential encryption
- **JWT** token authentication
- **RBAC** role-based access control
- API responses sanitized (no passwords, kubeconfig, or keys exposed)
| 变量 | 示例 | 说明 |
|------|------|------|
| `JWT_SECRET` | `$(openssl rand -hex 32)` | JWT 签名密钥(**必需**,>= 32 字符) |
| `ENCRYPTION_KEY` | `$(openssl rand -hex 32)` | AES 凭证加密密钥(**必需**,>= 32 字节) |
## License
### 可选
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `SERVER_PORT` | `8080` | HTTP 服务端口 |
| `DATA_DIR` | `/app/data` | 数据存储目录 |
| `KUBECONFIG` | `/home/lokideploy/.kube/config` | kubeconfig 文件路径 |
| `KUBECONFIG_PATH` | `~/.kube` | 宿主机上 kubeconfig 的挂载源路径 |
| `SSH_KEY_PATH` | `~/.ssh` | 宿主机上 SSH 密钥的挂载源路径 |
### Docker Secrets 支持
entrypoint 支持基于文件的 Docker Secrets 注入:
| 变量 | 文件变量 | 说明 |
|------|----------|------|
| `JWT_SECRET` | `JWT_SECRET_FILE` | 含 JWT 密钥的 secret 文件路径 |
| `ENCRYPTION_KEY` | `ENCRYPTION_KEY_FILE` | 含加密密钥的 secret 文件路径 |
配合 Docker Compose secrets 的示例:
```yaml
services:
lokideploy:
environment:
JWT_SECRET_FILE: /run/secrets/jwt_secret
ENCRYPTION_KEY_FILE: /run/secrets/encryption_key
secrets:
- jwt_secret
- encryption_key
secrets:
jwt_secret:
file: ./secrets/jwt_secret.txt
encryption_key:
file: ./secrets/encryption_key.txt
```
---
## 安全
### 鉴权与授权
- **Argon2** 密码哈希
- **AES-GCM** 凭证加密(密钥由 `ENCRYPTION_KEY` 派生)
- **JWT** token 鉴权
- **RBAC** 基于角色的访问控制
- API 响应经过脱敏(不暴露密码、kubeconfig、密钥)
### 容器安全
- **非 root 用户**:`lokideploy:1000`
- **禁止提权**:`no-new-privileges:true`
- **丢弃能力**:除必需外丢弃所有 capabilities
- **只读挂载**:kubeconfig 与 SSH 密钥只读挂载
- **资源限制**:CPU 与内存受限
- **日志轮转**:防止磁盘写满
### 密钥管理最佳实践
1. **绝不** 把密钥提交到版本控制
2. 生产环境使用 **Docker Secrets**(Swarm/Kubernetes)
3. 定期 **轮换密钥**:`openssl rand -hex 32`
4. 企业部署可使用密钥保险柜(HashiCorp Vault、AWS Secrets Manager 等)
5. 设置文件权限:对密钥文件执行 `chmod 600`
---
## 资源配置
### 默认限制(docker-compose.yml)
| 资源 | 上限 | 预留 |
|------|------|------|
| CPU | 2.0 核 | 0.25 核 |
| 内存 | 512 MB | 64 MB |
| PIDs | 100 | - |
### 按规模调整
```yaml
# 更大规模部署(10+ 集群)
deploy:
resources:
limits:
cpus: '4.0'
memory: 1G
reservations:
cpus: '1.0'
memory: 256M
```
---
## 故障排查
### 容器立即退出
```bash
# 检查密钥是否已设置
docker compose logs
# 期望错误信息:"Missing required security environment variables"
# 修复:
export JWT_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
docker compose up -d
```
### 健康检查失败
```bash
# 查看容器状态
docker inspect --format='{{.State.Health}}' lokideploy
# 查看日志
docker compose logs --tail=50
```
### kubeconfig 不可访问
```bash
# 确保宿主侧权限正确
chmod 600 ~/.kube/config
# 核对挂载路径
docker exec lokideploy ls -la /home/lokideploy/.kube/
```
---
## 许可证
MIT License
---
## 更新日志
### v2.0.0
- **安全**:非 root 容器用户(`lokideploy:1000`)
- **安全**:运行时强制校验 `JWT_SECRET`/`ENCRYPTION_KEY`
- **安全**:`no-new-privileges` 与 capabilities 丢弃
- **安全**:kubectl/helm 的 SHA256 校验和验证
- **运维**:健康检查端点
- **运维**:资源限制与预留
- **运维**:日志轮转(json-file 驱动)
- **运维**:优雅关停(SIGTERM)
- **运维**:Docker Secrets 支持
- **运维**:带 nginx 负载均衡的生产 compose 文件