基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。 前端(Gin+React+shadcn/ui): - 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、 反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放 - 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info) - 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环) - 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史 - 按用户+标签策略的查询授权(服务端强制合并策略) 后端(Go+Gin+JSON DB): - 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流 - LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录 - 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建 - Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、 凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入 - DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签 - 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页 - 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录 修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、 健康检查 CreatedAt 零值等 10+ 存量 bug。 数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移; HKDF 加密前缀保留(已加密凭证不受影响)。
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
|
|
"opsxloki/models"
|
|
)
|
|
|
|
// TestConcurrentSaveNoDataLoss 是并发写安全(B1/B2)的回归测试:
|
|
// N 个 goroutine 并发修改 db 并各自 RequestSave(经防抖 channel 序列化),
|
|
// 随后同步刷盘,直接读取磁盘文件校验写入数量与 ID 唯一性——
|
|
// 证明 channel 序列化 + 原子重命名写入不会发生"后写覆盖先写"的丢更新。
|
|
func TestConcurrentSaveNoDataLoss(t *testing.T) {
|
|
const N = 50
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < N; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
db := GetDB()
|
|
// NextID 内部自行加锁;切勿在持有 db.Lock() 时调用(mu 不可重入,会自死锁)
|
|
id := db.NextID("hosts")
|
|
db.Lock()
|
|
db.Hosts = append(db.Hosts, models.Host{ID: id, Name: "concurrent-host"})
|
|
db.Unlock()
|
|
db.RequestSave()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
// 同步刷盘一次(绕过 500ms 防抖),确保最新内存状态落盘
|
|
if err := GetDB().RequestSaveSync(); err != nil {
|
|
t.Fatalf("RequestSaveSync: %v", err)
|
|
}
|
|
|
|
data, err := os.ReadFile(dbPath())
|
|
if err != nil {
|
|
t.Fatalf("read db file: %v", err)
|
|
}
|
|
var disk JsonDB
|
|
if err := json.Unmarshal(data, &disk); err != nil {
|
|
t.Fatalf("unmarshal db file: %v", err)
|
|
}
|
|
if len(disk.Hosts) != N {
|
|
t.Fatalf("DATA LOSS: disk has %d hosts, want %d (concurrent saves lost writes)", len(disk.Hosts), N)
|
|
}
|
|
|
|
// ID 唯一性:NextID 在 mutex 保护下递增,不应出现重复
|
|
seen := make(map[uint]bool, N)
|
|
for _, h := range disk.Hosts {
|
|
if seen[h.ID] {
|
|
t.Fatalf("duplicate host ID %d (NextID race)", h.ID)
|
|
}
|
|
seen[h.ID] = true
|
|
}
|
|
}
|
|
|
|
// TestRequestSaveDebounce 校验防抖行为:短时间内多次 RequestSave 不会撑爆 channel。
|
|
func TestRequestSaveDebounce(t *testing.T) {
|
|
db := GetDB()
|
|
startLen := len(db.Hosts)
|
|
// 连续触发 100 次防抖保存(channel 容量 10,防抖应合并而非阻塞)
|
|
for i := 0; i < 100; i++ {
|
|
id := db.NextID("hosts") // 自行加锁,勿在 db.Lock() 内调用
|
|
db.Lock()
|
|
db.Hosts = append(db.Hosts, models.Host{ID: id, Name: "debounce"})
|
|
db.Unlock()
|
|
db.RequestSave()
|
|
}
|
|
if err := db.RequestSaveSync(); err != nil {
|
|
t.Fatalf("RequestSaveSync after debounce: %v", err)
|
|
}
|
|
data, _ := os.ReadFile(dbPath())
|
|
var disk JsonDB
|
|
if err := json.Unmarshal(data, &disk); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
// 100 次追加 + 之前测试的 N 个 host,必须全部落盘
|
|
if len(disk.Hosts) != startLen+100 {
|
|
t.Fatalf("debounce lost writes: disk has %d hosts, want %d", len(disk.Hosts), startLen+100)
|
|
}
|
|
}
|