feat: opsx-loki 全面升级 — 前端重构/用户与LDAP/部署向导/日志查询

基于 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 加密前缀保留(已加密凭证不受影响)。
This commit is contained in:
logan committed 2026-10-24 13:20:12 +08:00
1 parent 327e282356
commit f9c6c1d826
204 files changed
+14107 -9103

No files matched your search

+100 -30
View File
@@ -7,7 +7,7 @@ import (
"os/exec"
"strings"
"lokideploy/models"
"opsxloki/models"
)
// CollectorDeployer 采集器部署器,按 type 分发到三种安装/卸载流程
@@ -97,11 +97,7 @@ func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) er
if host == nil {
return fmt.Errorf("主机采集器缺少目标主机")
}
password, err := DecryptCred(host.Password)
if err != nil {
return err
}
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
client, err := NewSSHClientWithAuth(HostSSHAuth(host))
if err != nil {
return err
}
@@ -145,7 +141,7 @@ func (d *CollectorDeployer) installHost(ctx context.Context, endpoint string) er
if logPath == "" {
logPath = "/var/log/*.log"
}
conf := PromtailConfig(endpoint, logPath)
conf := PromtailConfig(endpoint, logPath, "host", d.collector.Labels)
d.log("info", "上传 Promtail 配置", "")
if _, _, err := client.Execute(ctx, fmt.Sprintf("cat > /etc/promtail/promtail.yaml << 'EOF'\n%s\nEOF", conf)); err != nil {
return fmt.Errorf("上传配置失败: %w", err)
@@ -191,8 +187,7 @@ func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
if host == nil {
return nil
}
password, _ := DecryptCred(host.Password)
client, err := NewSSHClient(host.HostIP, host.Port, host.User, password)
client, err := NewSSHClientWithAuth(HostSSHAuth(host))
if err != nil {
return err
}
@@ -209,7 +204,7 @@ func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint string) error {
cluster := d.collector.Cluster
if cluster == nil {
return fmt.Errorf("DaemonSet 采集器缺少目标集群")
return fmt.Errorf("集群采集器缺少目标集群")
}
env, cleanup, err := kubeconfigEnv(cluster)
if err != nil {
@@ -217,37 +212,106 @@ func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint strin
}
defer cleanup()
// 默认部署到关联 Loki 的命名空间(统一 log 命名空间策略)
ns := d.collector.Namespace
if ns == "" && d.loki != nil {
ns = d.loki.Namespace
}
if ns == "" {
ns = "logging"
ns = "log"
}
values := PromtailHelmValues(endpoint)
valuesFile, valuesCleanup, err := writeTempFile("promtail-values-*.yaml", values, 0600)
clusterName := "unknown"
if cluster.Name != "" {
clusterName = cluster.Name
}
// 幂等:先清掉同名残留(如历史失败安装留下的无注解资源)
del := exec.CommandContext(ctx, "kubectl", "delete",
"daemonset/promtail", "configmap/promtail", "serviceaccount/promtail",
"-n", ns, "--ignore-not-found=true", "--wait=false")
del.Env = env
del.CombinedOutput()
// 纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),不依赖后端安装 helm
manifest := PromtailDaemonsetYAML(ns, endpoint, clusterName, d.collector.Labels)
if err := d.applyYAML(ctx, env, manifest, "promtail"); err != nil {
return err
}
d.log("info", fmt.Sprintf("等待 DaemonSet promtail 就绪(命名空间 %s)...", ns), "")
waitCmd := exec.CommandContext(ctx, "kubectl", "rollout", "status", "daemonset/promtail",
"-n", ns, "--timeout=5m")
waitCmd.Env = env
waitOut, _ := waitCmd.CombinedOutput()
d.log("info", string(waitOut), "")
// DNS 自愈:CoreDNS 异常的集群上服务名不可解析,回退 Loki ClusterIP 重建配置
svcURL := endpoint
if strings.Contains(endpoint, ".svc") {
probe := exec.CommandContext(ctx, "kubectl", "exec", "-n", ns,
"daemonset/promtail", "--", "wget", "-q", "-O", "/dev/null", "-T", "5", endpoint+"/ready")
probe.Env = env
if pout, perr := probe.CombinedOutput(); perr != nil {
d.log("warn", fmt.Sprintf("promtail 无法解析 Loki 服务名(%s),回退 ClusterIP...", strings.TrimSpace(string(pout))), "")
ip := d.lokiClusterIP(ctx, env, endpoint)
if ip != "" {
// 只替换 host 部分,保留 scheme/port/path: http://loki.ns.svc:3100/... → http://IP:3100/...
afterScheme := strings.SplitN(endpoint, "//", 2)
if len(afterScheme) == 2 {
rest := afterScheme[1]
host := rest
if i := strings.IndexAny(rest, ":/"); i >= 0 {
host = rest[:i]
}
fallback := afterScheme[0] + "//" + strings.Replace(rest, host, ip, 1)
if err := d.applyYAML(ctx, env, PromtailDaemonsetYAML(ns, fallback, clusterName, d.collector.Labels), "promtail"); err == nil {
rs := exec.CommandContext(ctx, "kubectl", "rollout", "restart", "daemonset/promtail", "-n", ns)
rs.Env = env
rs.CombinedOutput()
d.log("info", fmt.Sprintf("promtail 推送地址已回退为 ClusterIP: %s", fallback), "")
svcURL = fallback
}
}
}
}
}
d.collector.Config = svcURL
d.log("success", fmt.Sprintf("Promtail DaemonSet 已部署到 %s(推送至 %s)", ns, svcURL), "")
return nil
}
// applyYAML kubectl apply 一个 manifest(临时文件)
func (d *CollectorDeployer) applyYAML(ctx context.Context, env []string, manifest, label string) error {
f, cleanup, err := writeTempFile("collector-*.yaml", manifest, 0600)
if err != nil {
return err
}
defer valuesCleanup()
d.log("info", fmt.Sprintf("helm install promtail(命名空间 %s)", ns), "")
// 添加 grafana repo(幂等)
repo := exec.CommandContext(ctx, "helm", "repo", "add", "grafana", "https://grafana.github.io/helm-charts")
repo.Env = env
repo.CombinedOutput()
upd := exec.CommandContext(ctx, "helm", "repo", "update")
upd.Env = env
upd.CombinedOutput()
cmd := exec.CommandContext(ctx, "helm", "upgrade", "--install", "promtail", "grafana/promtail",
"-f", valuesFile, "--namespace", ns, "--create-namespace", "--wait", "--timeout", "5m")
defer cleanup()
cmd := exec.CommandContext(ctx, "kubectl", "apply", "-f", f)
cmd.Env = env
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("helm 安装 Promtail 失败: %w\n%s", err, string(out))
d.log("error", fmt.Sprintf("kubectl apply %s failed: %v", label, err), string(out))
return fmt.Errorf("kubectl apply %s failed: %w\n%s", label, err, string(out))
}
return nil
}
// lokiClusterIP 从 endpoint 的命名空间取 loki Service 的 ClusterIP(不经 DNS)
func (d *CollectorDeployer) lokiClusterIP(ctx context.Context, env []string, endpoint string) string {
ns := "log"
if d.loki != nil && d.loki.Namespace != "" {
ns = d.loki.Namespace
}
cmd := exec.CommandContext(ctx, "kubectl", "get", "svc", "loki", "-n", ns,
"-o", "jsonpath={.spec.clusterIP}")
cmd.Env = env
out, _ := cmd.CombinedOutput()
return strings.TrimSpace(string(out))
}
func (d *CollectorDeployer) uninstallDaemonset(ctx context.Context) error {
cluster := d.collector.Cluster
if cluster == nil {
@@ -259,10 +323,16 @@ func (d *CollectorDeployer) uninstallDaemonset(ctx context.Context) error {
}
defer cleanup()
ns := d.collector.Namespace
if ns == "" {
ns = "logging"
if ns == "" && d.loki != nil {
ns = d.loki.Namespace
}
cmd := exec.CommandContext(ctx, "helm", "uninstall", "promtail", "--namespace", ns)
if ns == "" {
ns = "log"
}
cmd := exec.CommandContext(ctx, "kubectl", "delete",
"daemonset/promtail", "configmap/promtail", "serviceaccount/promtail",
"clusterrole/opsxloki-promtail", "clusterrolebinding/opsxloki-promtail",
"-n", ns, "--ignore-not-found=true")
cmd.Env = env
cmd.CombinedOutput()
return nil