main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d6f27545ad |
fix(sqlite): DSN 加 busy_timeout/WAL/foreign_keys + 连接池上限,避免 database is locked
=== 背景:这是 opsxsql 的「死锁类」风险 ===
LokiStack 是自研 JsonDB + sync.RWMutex,问题是**锁重入死锁**(已由上游 2e97c04 修复)。
opsxsql 用 SQLite + GORM,风险形式不同但后果相似:
裸 DSN(不带任何 pragma)+ 默认 journal 模式 + 无连接池上限
→ 写事务与读事务互相阻塞,并发写直接返回 "database is locked"(SQLITE_BUSY),
表现为「点保存失败 / 偶发 500」,而且不会挂住所以更难定位。
静态扫描结论:opsxsql 33 个 Go 文件无 RWMutex 重入问题(0 处),
唯一 RWMutex 在 internal/ghost 且用法正确。日志中 0 次 SQLITE_BUSY
(并发压力尚未到临界点,属**潜在**风险)。
=== 修改 1:DSN pragma ===
?_busy_timeout=5000 遇锁等 5 秒而不是立刻失败
&_journal_mode=WAL 读写不互斥,写不阻塞读
&_synchronous=NORMAL WAL 下常规安全级别
&_foreign_keys=on SQLite 默认关闭外键,容易埋脏数据
仅在 DBPath 不含 "?" 时追加,保留运维自定义 DSN 的能力。
=== 修改 2:连接池上限 4 ===
SQLite 同时只允许一个写者,连接开太多只会加剧锁竞争。
=== 修改 3:ghost.go 回调移出锁(防御性)===
原来 cb(taskID, result.Output) 在持有 Manager.mu 写锁时执行;
若调用方在回调里查任务状态(GetTaskStatus 需要 RLock)会永久死锁。
当前所有调用方都传 nil,属潜在问题,一并修掉。
=== 实测(46a97a5c11b7,ns=sql)===
Pod 1/1 Running,重启 0
/app/data 出现 sqlite3.db-wal + sqlite3.db-shm → WAL 已生效
日志 0 次 database is locked / SQLITE_BUSY
|
||
|
|
9dcb39e971 |
feat(k8s): install.sh 三级部署预检——kubectl 缺失/连不上集群时给出可执行的具体修法
用户反馈:部署时报错只说「未找到 kubectl,请先安装」「kubectl 无法连接集群,
请检查 kubeconfig」,对新人等于没说 —— 不知道是没装工具、网络不通、凭据过期
还是 RBAC 没权限,也不知道下一步查什么。
原实现只有两行:
command -v kubectl || die "未找到 kubectl,请先安装"
kubectl version --request-timeout=5s || die "kubectl 无法连接集群,请检查 kubeconfig"
改为三级预检 + 一级软检查:
【0.1 kubectl 是否存在】
按 CentOS/TencentOS、Ubuntu/Debian、macOS 分别给安装命令;
额外给「机器没有外网」的替代路径(控制台下载 / 内网跳板机 / 只 scp 二进制)。
【0.2 kubeconfig 文件是否存在】← 新增,堵一个误判
KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并退回默认规则,
最终报出 "Authentication required" / "connection refused" 这类误导性错误,
让人白查凭据和网络。现在单独判一次,直接给出查找路径 + 各云控制台下载入口。
支持 KUBECONFIG 用 : 分隔多路径,任一存在即通过。
【0.3 能否连上 API Server】按错误类型分类,各给专属修法
· context 不存在 → get-contexts / use-context / 重新下载
· 401 认证失败 → 云厂商 token 有效期几小时,重新下载;自建查客户端证书
· 403 RBAC 拒绝 → auth whoami / auth can-i --list,列出部署所需的具体资源
· x509 证书问题 → 确认证书过期;可信内网可临时 insecure-skip-tls-verify
· 网络不通 → curl -k <API>/version、VPN/专线、安全组放行 6443、换跳板机
· 其他 → 原样打印 kubectl 错误 + 通用排查清单
【0.4 部署权限软检查】auth can-i 非 yes 时只告警不阻断
(首次部署时目标命名空间可能还不存在,硬拦会误伤正常流程)
失败时统一打印环境信息(kubeconfig 路径 / context / API Server / 当前用户),
一眼看出连的是哪个集群。连接超时可用 KUBECTL_TIMEOUT=15s 放宽(默认 8s)。
README 同步新增「前置条件」章节:预检项对照表 + 错误分类表 + 失败输出示例。
=== 验证 ===
从真实 install.sh 抽出预检段,用假 kubectl 精确模拟 8 种场景(两个脚本各跑一遍):
A 没有 kubectl → 退出 1,打印各系统安装命令 ✅
B API Server 超时 → 退出 1,提示 VPN/专线/安全组 ✅
C 没有 context → 退出 1,提示 get-contexts/use-context ✅
D 401 凭据过期 → 退出 1,提示重新下载 kubeconfig ✅
E 403 权限不足 → 退出 1,列出所需资源权限 ✅
F x509 证书过期 → 退出 1,提示证书/insecure 排障 ✅
G 正常环境(真实集群) → 通过预检,退出 0 ✅
H kubeconfig 文件不存在 → 退出 1,提示下载位置(不误报成 401)✅
结果:阻断 7/7、正常通过 1/1;bash -n 语法检查通过
⚠️ 踩坑记录:kubeconfig 多路径判断最初写成
while IFS= read -r _p; do ...; done < <(printf "%s" "$PATHS" | tr ":" "\n")
printf 输出没有结尾换行 → read 带着数据返回非 0(EOF)→ 循环体一次都不执行,
导致所有场景都误报「找不到 kubeconfig」。改用 IFS=":" + for 拆分。
|
||
|
|
0c57e18f19 |
feat(log): Gin 改结构化 JSON 访问日志 + 保留期自动清理(含 VACUUM 回收)
=== 一、审计结论(用户问:gin 日志是否标准、会不会打爆磁盘)===
现状是标准的:gin.Default() 写 stdout,代码里没有任何
gin.DefaultWriter / log.SetOutput / lumberjack / os.OpenFile 写日志文件;
容器内 /var/log 是空的,find / -name "*.log" 无结果;
kubelet containerLogMaxSize=10Mi / containerLogMaxFiles=5 自动轮转,
实测 /var/log 只占 120G 磁盘的 0.8%。
但发现两个真问题:
1. 访问日志是 gin 默认文本格式,Loki 里没法按字段过滤
2. 审计日志/查询历史等只配了保留天数,**没有任何自动清理** ——
只有手动按钮,表会一直涨,SQLite 文件删了也不缩
=== 二、结构化 JSON 访问日志(middleware/accesslog.go)===
gin.Default() → gin.New() + AccessLog() + gin.Recovery() + CORSMiddleware
- 一行一个 JSON:ts/level/type/method/path/status/latency_ms/bytes/ip/ua
- 自动分级:5xx=error / 4xx=warn / 其余=info
- 鉴权后带 user_id / username,便于审计
- 健康检查与 /assets 静音(K8s 探针每 10s 一次,会淹没真实请求)
- Recovery 显式加回(gin.Default 里包含它,漏了 panic 会打挂进程)
⚠️ 关键细节:Go 的 map 序列化按 key 字母序,ts 会排到最后,
一行以 {"bytes":... 开头根本看不出时间。LogJSON 手工拼装,
把 ts/level/type/msg 固定在最前,其余排序输出。
=== 三、保留期自动清理(handler/cleanup.go)===
启动 2 分钟后跑一次,之后每 24h 一次;cleanup_auto_enabled=false 可关。
覆盖 8 张会无限增长的表(原来只管 4 张):
audit_logs / query_histories / slow_logs / tickets
+ operation_logs / workflow_execution_logs / scheduled_task_logs(新增)
+ refresh_tokens(按 expires_at < now,过期不清理会每次登录加一行)
设计取舍:
- 每批删 5000 行、批间 sleep 50ms:SQLite 单写者,一条 DELETE 全删会把库锁死
- 单表单轮最多 20 万行,剩下的留给下一轮
- deleted 后看 freelist/page_count,>25% 才 VACUUM(DELETE 不还磁盘,必须 VACUUM)
- 保留天数 <=0 视为不清理,避免误配把数据全删
手动接口同步改造:/settings/cleanup/{preview,run} 复用同一套逻辑,
返回结构改为 {deleted, total, vacuumed, freed_bytes, cost_ms}。
新增系统设置(seed 自动补,已实测 8 个 key 全部落库):
operation_log_retention_days=365 workflow_log_retention_days=180
scheduled_log_retention_days=180 cleanup_auto_enabled=true
=== 四、实测 ===
访问日志:{"ts":"2026-10-27T14:46:39.58Z","level":"info","type":"access","bytes":668,"ip":"...","latency_ms":78.6,"method":"POST","path":"/api/v1/auth/login","status":200,...}
health 日志行数: 0 ✅ [GIN] 文本行数: 0 ✅ 非 JSON 行: 4(仅启动)
cleanup preview: 8 张表的 count/days ✅
cleanup run: {"cost_ms":1,"deleted":{},"freed_bytes":0,"vacuumed":false,"total":0} ✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:30593de3fc09
|
||
|
|
4c2c5e182a |
feat(redis-query): 查询页 Redis 结果面板补导出按钮(CSV / JSON / 导出全部)
问题:上一轮把导出按钮只加在了「键浏览页」(RedisDetailPage → RedisKeyDetailPanel),
用户在「查询页」(/queries → RedisKeyValuePanel)看结果,那边一个导出按钮都没有。
两个面板是不同组件,只改一个另一个不会生效:
/queries → features/query/components/RedisKeyValuePanel.tsx ← 本次补
/redis(键浏览) → features/redis/components/RedisKeyDetailPanel.tsx ← 上一轮已加
改动:
1. RedisKeyValuePanel 新增 instanceId / db 两个 props(导出需要)
2. 组件内新增导出逻辑:
- buildPageText() 把当前已加载值转 CSV / JSON(纯前端,不发请求)
- exportCurrent() 导出当前页,瞬时完成
- exportAll() 申请一次性票据 → 浏览器原生下载,服务端流式导出全部
3. 选中 key 的头部右侧新增按钮组:CSV / JSON / 导出全部
(搜索框宽度 64 → 56,给按钮腾位置)
4. QueriesPage 传入 instanceId={Number(instanceId)} db={Number(database||0)}
实测:
分页详情 type=zset total=83 本页=100条内 ✅
票据获取 ✅
无 Authorization 的浏览器式下载 HTTP 200,CSV 84 行 ✅
bundle: QueriesPage-*.js 含「导出全部」×1、「Redis 键值可视化」×1 ✅
RedisDetailPage-*.js 含「导出全部」×1(键浏览页仍在)✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:7d72f2a96875
|
||
|
|
71d5afff03 |
fix(query): 结尾分号导致 LIMIT 拼接成两条语句(MySQL 1064)+ SQL 编辑器光标异常与无法滚动
=== 一、点表生成的语句带分号 → 查询必报 1064 ===
用户报错:
Error 1064 (42000): You have an error in your SQL syntax; ...
near 'LIMIT 100 OFFSET 0' at line 2
复现语句(点表自动生成的原文,天然带分号):
SELECT * FROM `cex_activity`.`concert_goods_language`;
根因在 instance_detail.go 的自动补 LIMIT:
sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
原语句已以 ';' 结尾 → 拼完变成
SELECT * FROM ...; LIMIT 100 OFFSET 0
→ 两条语句,报错位置就是第 2 行的 LIMIT(与报错文案完全吻合)。
修复:新增 normalizeSQLForExec(),去掉首尾空白并反复剥掉结尾分号,
在两个「会拼 LIMIT」的入口统一调用:
① executeSQLQuery(查询)—— 归一化后再生效 COUNT(*) 统计与 LIMIT 注入
② exportSQLCSV(导出 CSV)—— 同样先归一化再拼 LIMIT
顺带收益:COUNT(*) 统计以前也会带上分号(虽然 MySQL 容忍),现在干净了。
安全边界未被放松:多语句仍被 isReadOnlySQL 拦住
SELECT 1; SELECT 2 → 拒绝「不允许一次执行多条语句」✅
SELECT 1; LIMIT 5 → 拒绝 ✅(这是一次性两条语句,不是尾分号)
双分号 ;; → 归一化后正常执行 ✅
=== 二、SQL / Redis / Mongo 编辑器:光标异常 + 只有 13 行且滚不动 ===
用户反馈:
「默认还是有一个很长的竖线 一闪一闪,需要回车后第 2 行才正常」
「输入框下面还有空白,最后只有 13 行可以输入,至少需要加一个滚动」
根因 1(滚不动,13 行):appTheme 的 '&' 没有 height:100%。
.cm-editor 会随内容无限增高,而外层 div 固定 200px 且裁切,
CodeMirror 内部的 .cm-scroller 永远拿不到受限高度 → 不产生滚动条。
13 行 = 200px ÷ 15px 行高,正好对上。
根因 2(光标拉长):appTheme 在 extensions 里排在 oneDark 【之前】。
CodeMirror theme 是后者覆盖前者,oneDark 的默认行高/内边距盖掉了我们的
line-height,首次测量拿到的行高不对 → 空文档时光标高度算成好几行。
修复:
① appTheme '&' 增加 height:'100%';外层容器 style 改为
height + minHeight:120px + resize:vertical + overflow:hidden
(.cm-scroller 立刻有确定高度 → 内部滚动生效;右下角还能拖动拉高)
② appTheme 移到 oneDark 之后(顺序即优先级)
③ 行高统一写成 1.55em,并在 .cm-scroller / .cm-content / .cm-line /
.cm-gutters 四处保持一致(CM 靠读 .cm-content 的 line-height 算光标高度)
④ 挂载后 requestMeasure() + 下一帧再测一次 + document.fonts.ready 后再测一次,
修掉「首帧测量错乱」这一大类问题
⑤ SqlEditorSection 默认高度 200px → 240px(约 15 行可见)
注意:仍然不要给 .cm-cursor 写死 height / marginTop ——
CM 自己按行高算,写死会溢出到相邻行(历史上已经踩过一次)。
=== 三、实测 ===
表本身为空:cex_activity.concert_goods_language COUNT(*)=0
→ SELECT * 返回 0 行是正确结果,不是故障
分号归一化:带分号 / 不带分号 / 双分号 → rows=100 total=17447 完全一致,无 1064 ✅
安全边界:两条语句、分号后接 LIMIT 均被拒 ✅
编辑器产物:SqlEditor-*.js 内含 1.55em 与 resize:"vertical" ✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:72a00dda2419
|
||
|
|
7dfbc0b680 |
feat(redis): 大 key 分页 + 流式导出(含票据鉴权)+ 查询行数上限默认 5000
=== 一、问题:大 key 会把内存打爆,且查询结果没有导出 ===
RedisGetKeyDetail 原来对每种类型都是全量读取:
string → GET list → LRANGE 0 -1 set → SMEMBERS
zset → ZRANGE 0 -1 hash → HGETALL
然后整包序列化返回。像 PERP:market:* 这类盘口/kline key,
一个 key 几十万条就会:Go 侧整值入内存 → JSON 序列化翻倍 → 浏览器渲染卡死。
=== 二、后端:键详情分页(统一令牌,兼容四种类型)===
GET /redis/:id/keys/:key?db=&count=&token=
count 默认 200,硬上限 2000
token 语义按类型区分,前端无感知:
list / zset → 十进制 offset(LRANGE / ZRANGE 区间)
hash / set → Redis SCAN 游标(HSCAN / SSCAN)
string → 忽略
返回 total / count / nextToken / truncated / previewCap
string 特殊处理:不能分页,改为预览上限 256KB
- 用 STRLEN 拿真实大小(O(1))
- 超过上限时用 GETRANGE 只取前 256KB,truncated=true
- 完整内容只能靠导出
新增 wrapper 方法:HScan / ZScan / HLen / ZCard / StrLen / GetRange
(HGETALL、SMEMBERS、ZRANGE 0 -1 一律不再用于详情读取)
=== 三、后端:流式导出(两段式票据,避免令牌进 URL 日志)===
① POST /redis/:id/keys/:key/export-ticket (走 Bearer 鉴权)
→ HMAC-SHA256(JWTSecret, instance|db|key|exp) 生成票据,2 分钟过期,
绑定具体 instance/db/key,不能跨 key 复用
② GET /redis/:id/keys/:key/export?db=&format=csv|jsonl&ticket=
→ 注册在鉴权组【之外】,靠票据校验
为什么这么做:浏览器原生下载(<a download>/window.open)带不了
Authorization 头;若前端用 axios 取 blob,整份导出会进浏览器内存,
与「大 key 不爆内存」的目标相悖。
导出实现全程流式(服务端内存 = 单批 + 64KB 缓冲):
string → GETRANGE 256KB 分块
list → LRANGE 每批 1000
zset → ZRANGE WITHSCORES 每批 1000
hash → HSCAN 游标每批 1000
set → SSCAN 游标每批 1000
单次上限 200 万行;CSV 带 UTF-8 BOM(Excel 直接打开不乱码);
列顺:hash(field,value) / list(index,value) / set(member) / zset(rank,member,score)
=== 四、前端:详情面板加分页条 + 4 个导出按钮 ===
RedisKeyDetailPanel:
- 「共 N 条 · 第 X 页 · 每页 [100/200/500/1000/2000]」+ 上一页/下一页
- SCAN 游标不可逆,上一页用令牌栈回放
- string 超预览上限时显示黄色提示「仅预览前 256KB,共 N 字节」
- 导出四档:当前页 CSV / 当前页 JSON(纯前端,瞬时)
导出全部 CSV / 导出全部 JSONL(服务端流式,边读边写)
顺带修掉头注释里「已知限制:大值不分页」的过时说明。
=== 五、查询行数上限:默认 1000 → 5000,并修掉硬编码 ===
用户要求:「默认设置里面有查询最大上限吧 比如 mysql pg 查询最大Limit限制 5000」
发现两个问题:
1. seed 默认值只有 1000(main.go query_limit_max)
2. ExecuteQuery 里还有一段硬编码:
if input.Limit <= 0 { input.Limit = maxLimit; if input.Limit > 1000 { input.Limit = 1000 } }
→ 管理员把设置改成 5000 也白搭,前端不传 limit 时永远被压回 1000
修复:
- seed 默认值 1000 → 5000(Value + DefaultValue)
- getSettingInt("query_limit_max", 1000) → 5000
- 删掉硬编码 1000 封顶,唯一口径就是设置值
- 线上已有记录的 value 已通过 PUT /settings 改成 5000(seed 只在缺失时插入,不会自动更新旧值)
对 MySQL / PostgreSQL / Redis / MongoDB 统一生效(ExecuteQuery 按 db_type 分发,
limit 在入口处统一 clamp)。
=== 六、实测结果(线上)===
分页:
zset PERP:market:kline:symbol_kline_12h_AAVEUSDT_PERP_MARK
total=83,count=5 → 第1页 5 条 nextToken=5 → 第2页 5 条 nextToken=10,数据不同 ✅
hash ST:market:ticker:symbol_ticker
total=27,count=3 → 第1页 3 字段 nextToken=24 → 第2页 3 字段(完全不同)✅
string PERP:market:depth:symbol_depth_BTCUSDT_PERP
STRLEN=14192 → 导出 14192 字节,完全一致 ✅(实时 key 两次采样有差异属正常)
导出:
zset CSV 200 / 30970 字节 / 84 行(header+83)✅
zset JSONL 200 / 84 行 ✅
string CSV 200 / 分块 GETRANGE 拼接完整 ✅
无票据 → 403 ✅ 伪造票据 → 403 ✅
查询上限:
limit=99999 → 5000 行 ✅ limit=0(默认)→ 5000 行 ✅
SQL 导出 CSV(原已存在 ExportQueryCSV,支持 mysql/pg/redis/mongo)200 / 101 行 ✅
镜像:lghost/opsxsql:latest + v0.1 = sha256:68fc996681c6
|
||
|
|
36724bba94 |
fix(queries): MySQL 实例误调 Redis 接口 500 + 点表生成 SELECT + 光标多行闪烁
=== 一、/queries 打开 MySQL 实例报 500 ===
现象:GET /api/v1/redis/5/dbs → 500
后端日志 redis: invalid reply: "J\x00\x00\x00\n"
("J\x00\x00\x00" 是 MySQL 握手包,说明用 Redis 协议连了 3306)
根因:SchemaSidebar 是 mysql/redis 共用的组件,其中
const redisDbs = useRedisDbs(Number(instanceId));
是无条件调用(React hooks 不能条件调用),而 useRedisDbs 内部
只看 instanceId 就请求 /redis/{id}/dbs →
MySQL 实例也发 Redis 请求 → 后端用 Redis 协议连 MySQL → 500。
修复(前后端双保险):
1. useRedisDbs 增加 enabled 参数(默认 true),false 时不发请求并复位 [0]/false
2. SchemaSidebar 传 isRedis:useRedisDbs(Number(instanceId), isRedis)
3. redis.go getRedisClient 前置校验 DBType != "redis" 直接返回
4. resp 包新增 UserFacingError / UserFacing(),InternalErr 识别后返回
400 + 可读原因(不再把所有错误笼统包装成 500 "服务器内部错误")
验证:
GET /redis/5/dbs(MySQL)→ 400
{"code":"bad_request","detail":"实例「pre-mysql-只读」的类型是 mysql,
不是 redis。Redis 接口只适用于 redis 实例;MySQL/PostgreSQL 请使用
/api/v1/instances/5/databases 等对应接口"}
GET /instances/5/databases → 200
GET /redis/4/dbs(Redis 集群)→ 200 {"cluster":true,"dbs":[0]}
=== 二、左侧栏点表不生成 SELECT ===
原交互:单击=展开/折叠,双击=执行 SELECT,Alt+点击=插入表名。
需求:单击表 → 在右侧编辑器生成 SELECT * FROM <db>.<table>
修复:
1. SchemaSidebar 表节点:箭头图标包 data-chevron 单独触发展开/折叠;
点表名区域 → onInsert(previewSelectSql(...) + ';\n')
Alt+点击仍为只插表名;双击仍为直接执行
2. QueriesPage onInsert:生成的文本以 SELECT 开头时「替换」编辑器内容
(原来一律追加,会和上一条语句拼成非法 SQL);表名/字段名仍追加
=== 三、SQL 编辑器光标同时在多行闪烁 ===
根因:.cm-cursor 强制了 height:1.25em !important + marginTop:0.15em,
而 .cm-line 行高是 1.55em,光标高度溢出到相邻行,视觉上像两行同时闪。
修复:光标样式只保留颜色与 1.5px 宽度,height/marginTop 交回 CodeMirror 自行计算。
镜像:lghost/opsxsql:latest + v0.1
|
||
|
|
0427b87412 |
fix(instance): access_mode 值 read/readonly 不一致导致「读」实例被过滤
现象:instance-groups 里给 MySQL 实例授予「只读查询」权限后,
/queries 页面仍然选不到该实例,admin 也无法访问。
根因(枚举值前后端不一致):
前端 CreateInstanceForm「读」按钮写入 access_mode = "read"
后端 AccessibleInstances 只匹配 ["readwrite", "readonly"]
→ "read" 永远不命中,选了「读」的实例(MySQL)被过滤掉;
Redis 实例是 "readwrite" 所以恰好能查,掩盖了该 bug。
修复:后端 readonly 过滤增加 "read",兼容两种历史写法:
[]string{"readwrite", "readonly", "read"}
写过滤仍为 ["readwrite", "write"]("read" 实例正确地不能写)
验证(admin):
GET /instances/accessible?access_type=readonly → 5 个(含 MySQL id=5, access_mode=read)
GET /instances/accessible?access_type=write → 4 个(MySQL 正确排除)
镜像:lghost/opsxsql:latest + v0.1 = sha256:6b1a7121da71
|
||
|
|
f19a84f37e |
fix(redis): 集群拓扑解析错误,跨机集群节点被强制指向种子机
【现象】Redis 集群实例查 keys 报 500:
Redis cluster connection failed: dial tcp 172.19.16.16:6005: connect: connection refused
【根因】handler/redis.go 解析 CLUSTER SLOTS:
nodeAddr := slot.Nodes[0].Addr // Redis 上报的真实地址 172.19.16.30:6005
if idx := strings.LastIndex(nodeAddr, ":"); idx >= 0 {
nodeAddr = instance.Host + nodeAddr[idx:] // 强制改写成 种子机主机 + 原端口
}
该逻辑假设「集群所有节点在同一台机器、只是端口不同」。
对跨机集群(16/30/59 三台)会把所有节点地址破坏成
172.19.16.16:6002 / :6003 / :6005 —— 这些端口在 16 上根本不存在 → refused。
【修复】
仅当 Redis 上报空 IP / 127.0.0.1 / 0.0.0.0 时才用配置 host 兜底(NAT/端口映射场景),
其余情况一律保留 Redis 上报的真实主机地址。
【附带数据修复(非代码)】
实例 5 host: pre-mysql.astx.club → 172.19.16.2
原因:pre-mysql.astx.club 公网 DNS 解析到 43.135.53.180(另一环境 MySQL),
认证失败 → 前端数据库下拉为空、无法选择数据库。
【验证】
GET /api/v1/redis/4/keys?db=0&pattern=* → 200
{"cluster":true,"keys":[{"key":"24H_rate_limit_10001_...","type":"string","ttl":42901000},
{"key":"CEX:FOLLOW:FOLLOWERS:TRADFI:97127836","type":"hash"},...]}
GET /api/v1/redis/4/dbs → 200 {"cluster":true,"dbs":[0]}
GET /api/v1/instances/5/databases → 200 返回 25 个库
["cex_activity","cex_admin","cex_agent",...,"cex_user","cex_xxl_job"]
镜像 sha256:165992ef79d5
|
||
|
|
6b81dbbd4a |
feat(ldap): LDAP 改为只读接入,彻底移除账号验证与批量导入写入功能
按需求(sql.astx.club/settings):
1. 移除「测试账号验证」按钮
- LdapConfigForm.tsx 删按钮 + onOpenAccountTest prop
- 不再渲染 LdapAccountTestDialog
2. 移除批量导入的 LDAP 写操作(BatchImportModal.tsx)
- TABS 去掉「导入到 LDAP」「从 LDAP 导入」
- 删除对应两个 JSX 代码块(tab === 'to-ldap' / 'from-ldap')
- 删除「创建 OU」子弹窗
- 只保留「本地导入」(CSV 建本地账号,不触碰 LDAP 目录)
3. 移除 LDAP 账号管理面板(含危险写操作)
- 不再渲染 LdapManagePanel(新建 OU / 添加用户 / 删除用户)
- 删除 LdapCreateOuDialog / LdapAddUserDialog / ConfirmDialog 引用
- LdapConfigPage.tsx 421 → 340 行;BatchImportModal.tsx 557 → 329 行
4. 保留(只读用途)
- LDAP 配置表单(服务器 / 绑定 / 搜索 / 属性映射)
- 「测试连接」按钮(验证配置可用,不写 LDAP 目录)
设计原则:LDAP 仅用于域账号登录认证(只读 bind + search),
目录维护请到域控侧操作。
验证(前端 bundle grep,全部归零):
测试账号验证 0 | 导入到 LDAP 0 | 从 LDAP 导入 0
新建 OU 0 | 创建 OU 0 | 危险操作 0 | 此操作不可逆 0
保留:
测试连接 6 | 本地导入 1
镜像 sha256:3970db141d23
/health → {"backend":"go","status":"ok","version":"0.4.0"}
http://sql.astx.club/settings → HTTP 200
|
||
|
|
e8355e5d5f |
feat(ldap): LDAP 改为只读接入,移除账号验证测试与批量导入写操作
按需求调整 settings 页 LDAP 区域:
1. 移除「测试账号验证」按钮
- LdapConfigForm.tsx 删除按钮 + onOpenAccountTest prop
- LdapConfigPage.tsx 不再渲染 LdapAccountTestDialog
2. 移除批量导入的 LDAP 写操作
- BatchImportModal.tsx 的 TABS 去掉「导入到 LDAP」「从 LDAP 导入」
- 只保留「本地导入」(CSV 建本地账号,不触碰 LDAP 目录)
3. 移除 LDAP 账号管理面板(危险写操作)
- 不再渲染 LdapManagePanel(新建 OU / 添加用户 / 删除用户)
- 不再渲染 LdapCreateOuDialog / LdapAddUserDialog
- 面板含「此操作不可逆」删除功能,与只读接入原则冲突
4. 保留(只读用途)
- LDAP 配置表单(服务器/绑定/搜索/属性映射)
- 「测试连接」按钮(验证配置是否可用,不写 LDAP)
设计原则:本系统 LDAP 仅用于域账号登录认证(只读 bind + search),
目录维护请在域控侧操作。
验证(前端 bundle 实测):
测试账号验证: 0 导入到 LDAP: 0 从 LDAP 导入: 0
新建 OU: 0 危险操作: 0 此操作不可逆: 0
测试连接: 6(保留) 本地导入: 1(保留)
/health → {"backend":"go","status":"ok","version":"0.4.0"}
http://sql.astx.club/settings → HTTP 200
|
||
|
|
d584b64813 |
feat(security): 默认账号随机初始密码(不再硬编码 admin123)
参考 lokistack 的实现:
1. 首次启动生成 16 位随机密码(大小写+数字,排除 0/O/1/l/I 易混淆字符)
- 4 个默认账号共用:admin / dba / auditor / developer
- 写入数据目录 initial-admin-password.txt(0600)
- 打印到启动日志(含账号列表、密码、文件路径、docker/kubectl 获取命令)
2. 支持 ADMIN_INIT_PASSWORD 环境变量覆盖(自动化部署 / 需要固定密码时)
3. 生成失败直接 log.Fatalf 退出,不降级到公开默认口令
4. 批量导入用户 BatchImportUsers:空密码不再默认 admin123,
改为随机 14 位(新增 genPassword),保证每个导入用户密码不可预测
5. 幂等:已存在的账号不重建、不改密码;只有缺失的账号才创建
6. imagePullPolicy: IfNotPresent → Always
原因:latest tag 配 IfNotPresent 会导致节点一直复用缓存的旧镜像,
新 push 的镜像不生效(本次实测踩到:Pod imageID 仍是旧 digest)
7. 文档
- README 新增「首次部署获取默认账号密码」章节(含 4 账号角色表 + 3 种获取方式)
- docker-compose.yml 加 ADMIN_INIT_PASSWORD 环境变量
- kubernetes/deployment.yaml 加 env 注释
验证(清空 DB 后全新 seed):
镜像 sha256:eaf0affd8d93
/app/data/initial-admin-password.txt 199B 权限 0600
内容含 "适用账号: [admin dba auditor developer]" + 随机密码
启动日志完整打印获取方式
登录实测:admin / dba / developer 用新密码均返回 access_token;
admin123 登录失败(已失效)
二进制 strings:admin123 命中 0
|
||
|
|
396df57c9c |
build+deploy: 编译加 -trimpath,镜像改 lghost/opsxsql,K8s 部署到 sql 命名空间
1. -trimpath(抹除源码绝对路径)
- Dockerfile: go build 加 -trimpath
- run.sh: 两处 go build 加 -trimpath
效果:二进制内 /app/backend/ 绝对路径命中 66 → 0(module 相对路径 sql-platform/ 仍保留,
那是 Go 的正常行为,-trimpath 只去绝对前缀)
2. 镜像
- kubernetes/deployment.yaml: opsxsql:latest → lghost/opsxsql:latest
- kubernetes/install.sh: 默认 IMAGE 改 lghost/opsxsql:latest
- 已构建并推送:lghost/opsxsql:latest (digest 70562a408cae, 25.2MB)
3. K8s 部署验证(namespace: sql)
- Deployment opsxsql 1/1 Running,0 重启
- Pod: opsxsql-695444d6b9-chrzp (node 172.19.0.161)
- Service opsxsql.sql.svc.cluster.local:80
- PVC opsxsql-data Bound 10Gi (cbs)
- Secret opsxsql-secrets 自动生成(SECRET_KEY / JWT_SECRET)
- /health → {"backend":"go","status":"ok","version":"0.4.0"}
- 首页正常,seed 完成(26 条审核规则 + LDAP 默认配置)
部署命令:bash kubernetes/install.sh
|