feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏

- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
This commit is contained in:
opsx committed 2026-10-27 17:11:38 +08:00
1 parent 02d1bd87a7
commit f3733f0bfc
201 files changed
+5438 -2388

No files matched your search

+2 -2
View File
@@ -11,8 +11,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
const alertCheckInterval = time.Minute
+28 -5
View File
@@ -2,11 +2,11 @@ package services
import (
"fmt"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/pkg/jwt"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/pkg/jwt"
"lokistack/repository"
"lokistack/utils"
)
type AuthService struct{}
@@ -182,6 +182,29 @@ func (s *AuthService) ListUsers(page, pageSize int) ([]models.User, int64) {
return users, total
}
// BatchDeleteUsers 批量删除用户;逐个应用 DeleteUser 守卫(不能删自己/最后一个 admin),
// 返回成功数与失败明细(id + 原因),单个失败不阻断其余。
func (s *AuthService) BatchDeleteUsers(ids []uint, requesterID uint) (int, []map[string]interface{}) {
seen := make(map[uint]bool, len(ids))
deleted := 0
failures := make([]map[string]interface{}, 0)
for _, id := range ids {
if seen[id] {
continue
}
seen[id] = true
if err := s.DeleteUser(id, requesterID); err != nil {
failures = append(failures, map[string]interface{}{
"id": id,
"reason": err.Error(),
})
continue
}
deleted++
}
return deleted, failures
}
// DeleteUser 删除用户;不能删除自己,不能删除最后一个管理员
func (s *AuthService) DeleteUser(id, requesterID uint) error {
db := repository.GetDB()
+5 -5
View File
@@ -3,16 +3,16 @@ package services
import (
"context"
"fmt"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
"os"
"path/filepath"
"strings"
"time"
"opsxloki/config"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/config"
"lokistack/models"
"lokistack/repository"
"lokistack/utils"
)
type ClusterService struct{}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"opsxloki/models"
"lokistack/models"
"sort"
"strings"
"time"
+1 -1
View File
@@ -9,7 +9,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"opsxloki/models"
"lokistack/models"
)
type ClusterDeploymentOption struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"opsxloki/models"
"lokistack/models"
"testing"
)
+12 -5
View File
@@ -6,9 +6,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/repository"
"lokistack/utils"
)
type CollectorService struct{}
@@ -75,7 +75,14 @@ func (s *CollectorService) generateConfigSnapshot(c *models.Collector) string {
case "host-promtail":
return PromtailConfig(endpoint, c.LogPath, "host", c.Labels)
case "k8s-daemonset":
return PromtailHelmValues(endpoint, clusterName, c.Labels)
ns := c.Namespace
if ns == "" && c.LokiDeployment != nil {
ns = c.LokiDeployment.Namespace
}
if ns == "" {
ns = "log"
}
return PromtailDaemonsetYAML(ns, endpoint, clusterName, c.Labels, c.TargetNamespaces, c.AppLabelSource, c.AppInclude)
case "k8s-fluentbit-sidecar":
host, port := parseLokiURL(endpoint)
app := c.TargetDeployment
@@ -101,7 +108,7 @@ func (s *CollectorService) Create(req *CreateCollectorRequest) (*models.Collecto
return nil, fmt.Errorf("主机采集器需要选择目标主机")
}
if (req.Type == "k8s-daemonset" || req.Type == "k8s-fluentbit-sidecar") && (req.ClusterID == nil || *req.ClusterID == 0) {
return nil, fmt.Errorf("K8s 采集器需要选择目标集群")
return nil, fmt.Errorf("Kubernetes 采集器需要选择目标集群")
}
if req.Type == "k8s-fluentbit-sidecar" && req.TargetDeployment == "" {
return nil, fmt.Errorf("Sidecar 采集器需要填写目标 Deployment 名称")
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"opsxloki/models"
"lokistack/models"
)
// CollectorDeployer 采集器部署器,按 type 分发到三种安装/卸载流程
@@ -178,7 +178,7 @@ func (d *CollectorDeployer) uninstallHost(ctx context.Context) error {
return nil
}
// ===== k8s-daemonset:helm 安装 Promtail DaemonSet =====
// ===== k8s-daemonset:以原生清单安装 Promtail DaemonSet =====
func (d *CollectorDeployer) installDaemonset(ctx context.Context, endpoint string) error {
cluster := d.collector.Cluster
@@ -254,7 +254,7 @@ func (d *CollectorDeployer) uninstallDaemonset(ctx context.Context) error {
ns = "log"
}
// clusterrole / clusterrolebinding 用 loki-promtail;
// 旧版本曾用 opsxloki-promtail,这里一并清理以免升级后留下孤儿资源。
// 数组末两项是历史遗留命名,一并清理避免升级后留下孤儿资源。
for _, item := range [][2]string{
{"daemonset", "promtail"}, {"configmap", "promtail"}, {"serviceaccount", "promtail"},
{"clusterrole", "loki-promtail"}, {"clusterrolebinding", "loki-promtail"},
+3 -114
View File
@@ -83,120 +83,9 @@ scrape_configs:
return strings.Replace(conf, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1)
}
// PromtailHelmValues DaemonSet 模式 Promtail Helm values(生产级)
// 包含:cri解析、kubernetes标签、批处理配置、资源限制
func PromtailHelmValues(lokiEndpoint, cluster, labelsJSON string) string {
values := fmt.Sprintf(`config:
clients:
- url: %s/loki/api/v1/push
batchwait: 1s
batchsize: 1048576
timeout: 10s
backoff_config:
min_period: 500ms
max_period: 5m
max_retries: 10
external_labels:
__EXT_LABELS__
positions:
filename: /run/promtail/positions.yaml
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
- cri: {}
- match:
selector: '{app!=""}'
stages:
- docker: {}
- match:
selector: '{app!=""}'
stages:
- labels:
app:
namespace:
pod:
container:
- limit:
rate: 1000
burst: 10000
drop: true
relabel_configs:
- source_labels: [__meta_kubernetes_pod_phase]
regex: (Succeeded|Failed)
action: drop
- source_labels: [__meta_kubernetes_pod_controller_name]
regex: ([0-9a-z-.]+?)(-[0-9a-f]{8,10})?
action: replace
target_label: __tmp_controller_name
- source_labels:
- __meta_kubernetes_pod_label_app_kubernetes_io_name
- __meta_kubernetes_pod_label_app
- __tmp_controller_name
- __meta_kubernetes_pod_name
regex: ^;*([^;]+)(;.*)?$
action: replace
target_label: app
- source_labels: [__meta_kubernetes_pod_annotation_kubernetes_io_config_hash]
regex: .+
action: replace
target_label: config_hash
- action: replace
source_labels: ['__meta_kubernetes_pod_name']
target_label: 'pod'
- action: replace
source_labels: ['__meta_kubernetes_namespace']
target_label: 'namespace'
- action: replace
source_labels: ['__meta_kubernetes_pod_container_name']
target_label: 'container'
- action: replace
source_labels: ['__meta_kubernetes_pod_node_name']
target_label: 'node_name'
- replacement: /var/log/pods/*$1/*.log
separator: /
source_labels: [__meta_kubernetes_pod_uid, __meta_kubernetes_pod_container_name]
target_label: __path__
daemonset:
enabled: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
extraVolumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
- name: promtail-run
hostPath:
path: /run/promtail
extraVolumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: promtail-run
mountPath: /run/promtail
`, lokiEndpoint)
return strings.Replace(values, "__EXT_LABELS__", promtailExternalLabels(cluster, labelsJSON), 1)
}
// PromtailDaemonsetYAML DaemonSet 模式纯 YAML 安装(SA/RBAC/ConfigMap/DaemonSet),
// 取代 helm 安装 —— 后端无需安装 helm 二进制。
// relabel 体系与主流 helm chart 对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
// 经 Kubernetes API 直接提交,后端无需任何额外二进制。
// relabel 体系与主流 Promtail 部署惯例对齐:namespace/pod/container/node_name + __path__ 按 pod uid 精确匹配。
// sanitizePromLabel 把 K8s 标签名转成 Prometheus/Promtail 的 meta label 片段。
//
// k8s.kuboard.cn/name → k8s_kuboard_cn_name
@@ -365,7 +254,7 @@ data:
# 4) pod name ← 最后兜底,保证 app 永不为空
#
# 不使用 k8s.kuboard.cn/name 之类的平台私有标签:那只是 Kuboard 管理的集群才有,
# 换 Rancher/Lens/Helm/kubectl 部署的集群就不存在,会破坏通用性。
# 换其他方式部署的集群就不存在,会破坏通用性。
# 如集群确实有私有规范标签,可在「采集器 → 编辑」里配置 AppLabelSource,
# 它会作为最高优先级插到下面列表最前(其余仍兜底)。
- source_labels:
+9 -14
View File
@@ -9,10 +9,10 @@ import (
"sync"
"time"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/seed"
"opsxloki/utils"
"lokistack/models"
"lokistack/repository"
"lokistack/seed"
"lokistack/utils"
)
// LokiVersionList 支持的 Loki 版本列表(按推荐程度排序)
@@ -569,19 +569,14 @@ func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
switch d.Mode {
case "k8s-helm", "k8s-yaml":
case "k8s-yaml":
ns := d.Namespace
if ns == "" {
ns = "loki"
ns = "log"
}
// k8s-yaml manifest 固定创建名为 loki 的 Service;
// k8s-helm (grafana/loki) 以 release 名(=部署名,默认 loki)暴露 Service。
// k8s-yaml manifest 固定创建名为 loki 的 Service。
// 注意:不使用 loki-gateway,gateway 仅在 Distributed+gateway 启用时存在。
svc := "loki"
if d.Mode == "k8s-helm" && d.Name != "" {
svc = d.Name
}
return fmt.Sprintf("http://%s.%s.svc.cluster.local:3100", svc, ns)
return fmt.Sprintf("http://loki.%s.svc.cluster.local:3100", ns)
case "host-binary":
if d.Host != nil {
return fmt.Sprintf("http://%s:3100", d.Host.HostIP)
@@ -720,7 +715,7 @@ func (s *DeploymentService) recordGrafanaEndpoint(d *models.LokiDeployment) {
}
var endpoint string
switch d.Mode {
case "k8s-helm", "k8s-yaml":
case "k8s-yaml":
if d.Cluster == nil {
s.AddLog(d.ID, "warn", "Grafana 地址记录跳过:未关联集群", "")
return
+5 -1
View File
@@ -68,7 +68,11 @@ func (s *DeploymentService) GetLiveConfig(ctx context.Context, id uint) (*LiveLo
location = dockerConfigPath(d.ID)
output, _, err := client.Execute(ctx, "cat "+location)
if err != nil {
return nil, fmt.Errorf("无法读取主机上的 Loki 配置")
// 历史版本路径兜底(重部署一次后目录即迁移到新命名)
location = legacyDockerConfigDir(d.ID) + "/loki.yaml"
if output, _, err = client.Execute(ctx, "cat "+location); err != nil {
return nil, fmt.Errorf("无法读取主机上的 Loki 配置")
}
}
raw = output
default:
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"testing"
"gopkg.in/yaml.v3"
"opsxloki/models"
"lokistack/models"
)
func TestRedactLokiYAML(t *testing.T) {
@@ -3,7 +3,7 @@ package services
import (
"testing"
"opsxloki/repository"
"lokistack/repository"
)
func TestParseRetentionYAML(t *testing.T) {
+4 -20
View File
@@ -5,10 +5,10 @@ import (
"fmt"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
)
type HostService struct{}
@@ -190,19 +190,3 @@ func (s *HostService) Validate(id uint) {
s.Update(id, map[string]interface{}{"status": "active"})
}
}
func (s *HostService) ExecuteCommand(id uint, command string) (string, string, error) {
host, err := s.Get(id)
if err != nil {
return "", "", err
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
client, err := NewSSHClientWithAuth(HostSSHAuth(host))
if err != nil {
return "", "", err
}
defer client.Close()
return client.Execute(ctx, command)
}
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"fmt"
"strings"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
const (
+1 -1
View File
@@ -9,7 +9,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"opsxloki/models"
"lokistack/models"
)
const ingressNamespace = "ingress-nginx"
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"regexp"
"time"
"opsxloki/models"
"lokistack/models"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@@ -54,7 +54,7 @@ func clusterRESTConfig(cluster *models.Cluster, timeout time.Duration) (*rest.Co
return nil, fmt.Errorf("构造 Kubernetes 客户端失败: %w", err)
}
result.Timeout = timeout
result.UserAgent = "LokiDeploy"
result.UserAgent = "lokistack"
return result, nil
}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
kcScheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/remotecommand"
"opsxloki/models"
"lokistack/models"
)
func ensureNamespace(ctx context.Context, cluster *models.Cluster, namespace string) error {
+5 -5
View File
@@ -1,6 +1,6 @@
// Package services — LDAP 接入:配置存储(SystemSetting 表 JSON)、连接测试、
// 用户搜索预览、批量导入与登录验证共用同一套连接/搜索代码。
// 设计参照 opsx-web-lite(has_password 脱敏/空密码保留旧值/批量导入冲突策略)。
// 约定:has_password 脱敏、空密码保留旧值、批量导入冲突策略。
package services
import (
@@ -10,10 +10,10 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
ldap "github.com/go-ldap/ldap/v3"
)
+4 -4
View File
@@ -3,9 +3,9 @@ package services
import (
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
// LDAPImportUser 批量导入请求中的用户条目(数据来自预览接口)
@@ -16,7 +16,7 @@ type LDAPImportUser struct {
Email string `json:"email"`
}
// LDAPImportResult 导入结果(对齐 opsx-web-lite 契约)
// LDAPImportResult 导入结果(与前端展示契约对齐)
type LDAPImportResult struct {
SuccessCount int `json:"successCount"`
SkippedCount int `json:"skippedCount"`
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
)
func ListLogPolicyGroups() []models.LogPolicyGroup {
+2 -2
View File
@@ -17,8 +17,8 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/repository"
"lokistack/models"
"lokistack/repository"
"k8s.io/client-go/rest"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package services
import (
"opsxloki/models"
"lokistack/models"
"strings"
"testing"
)
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
// ==================== 数据源 CRUD ====================
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"fmt"
"text/template"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
const lokiConfigTemplate = `auth_enabled: {{ .AuthEnabled }}
+4 -4
View File
@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"opsxloki/models"
"opsxloki/pkg/crypto"
"lokistack/models"
"lokistack/pkg/crypto"
)
// LokiDeployer 负责 Loki 的 Kubernetes、主机 Docker 及历史部署模式。
@@ -169,7 +169,7 @@ WantedBy=multi-user.target
return nil
}
// ==================== Helm Values 生成 ====================
// ==================== 部署参数与版本校验 ====================
func (d *LokiDeployer) effectiveVersion() string {
if d.deployment.Version != "" {
@@ -226,7 +226,7 @@ func CleanupLoki(ctx context.Context, deployment *models.LokiDeployment) {
_ = deleteK8sResource(ctx, deployment.Cluster, item[0], ns, item[1])
}
}
// 历史 Helm release 不由此版本自动删除,避免误删旧数据。
// 仅清理本产品创建的资源;其他途径部署的遗留资源不自动删除,避免误删数据。
if deployment.Mode == "host-binary" && deployment.Host != nil {
host := deployment.Host
+28 -8
View File
@@ -7,13 +7,19 @@ import (
"strings"
"time"
"opsxloki/models"
"lokistack/models"
)
func dockerContainerName(id uint) string { return fmt.Sprintf("lokideploy-loki-%d", id) }
func dockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokideploy/loki-%d", id) }
func dockerContainerName(id uint) string { return fmt.Sprintf("lokistack-loki-%d", id) }
func dockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokistack/loki-%d", id) }
func dockerConfigPath(id uint) string { return dockerConfigDir(id) + "/loki.yaml" }
func dockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokideploy/loki-%d", id) }
func dockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokistack/loki-%d", id) }
// 历史命名的容器与目录 — 仅用于迁移与清理,
// 避免已部署实例在升级后留下孤儿容器/目录。
func legacyDockerContainerName(id uint) string { return fmt.Sprintf("lokideploy-loki-%d", id) }
func legacyDockerConfigDir(id uint) string { return fmt.Sprintf("/etc/lokideploy/loki-%d", id) }
func legacyDockerDataDir(id uint) string { return fmt.Sprintf("/var/lib/lokideploy/loki-%d", id) }
func dockerLokiImage(version string) (string, error) {
if version == "" {
@@ -39,6 +45,8 @@ func stopDockerLoki(ctx context.Context, deployment *models.LokiDeployment) erro
if _, stderr, err := client.Execute(ctx, "docker stop "+dockerContainerName(deployment.ID)); err != nil {
return fmt.Errorf("停止 Loki 容器失败: %v: %s", err, strings.TrimSpace(stderr))
}
// 历史命名的同名容器一并停掉(可能不存在,尽力而为)
client.Execute(ctx, "docker stop "+legacyDockerContainerName(deployment.ID)+" >/dev/null 2>&1 || true")
return nil
}
@@ -52,11 +60,13 @@ func deleteDockerLoki(ctx context.Context, deployment *models.LokiDeployment) er
}
defer client.Close()
name := dockerContainerName(deployment.ID)
legacyName := legacyDockerContainerName(deployment.ID)
if _, _, err := client.Execute(ctx, "docker info >/dev/null 2>&1"); err != nil {
return fmt.Errorf("目标主机 Docker 不可用,无法安全清理部署")
}
// 仅删除该部署的容器和配置;日志数据目录保留,以便人工恢复。
cmd := "if docker container inspect " + name + " >/dev/null 2>&1; then docker rm -f " + name + " >/dev/null || exit 1; fi; rm -rf -- " + dockerConfigDir(deployment.ID)
// 仅删除该部署的容器和配置(含历史命名);日志数据目录保留,以便人工恢复。
cmd := "for n in " + name + " " + legacyName + "; do if docker container inspect $n >/dev/null 2>&1; then docker rm -f $n >/dev/null || exit 1; fi; done; rm -rf -- " +
dockerConfigDir(deployment.ID) + " " + legacyDockerConfigDir(deployment.ID)
if _, stderr, err := client.Execute(ctx, cmd); err != nil {
return fmt.Errorf("清理 Loki 容器与配置失败: %v: %s", err, strings.TrimSpace(stderr))
}
@@ -94,6 +104,16 @@ func (d *LokiDeployer) deployDocker(ctx context.Context) error {
name := dockerContainerName(deployment.ID)
configDir := dockerConfigDir(deployment.ID)
dataDir := dockerDataDir(deployment.ID)
// 历史命名目录原子迁移到新命名;存在才迁,不覆盖已有数据
migrate := fmt.Sprintf(
"if [ -d %s ] && [ ! -d %s ]; then mkdir -p /etc/lokistack && mv %s %s; fi; "+
"if [ -d %s ] && [ ! -d %s ]; then mkdir -p /var/lib/lokistack && mv %s %s; fi",
legacyDockerConfigDir(deployment.ID), configDir, legacyDockerConfigDir(deployment.ID), configDir,
legacyDockerDataDir(deployment.ID), dataDir, legacyDockerDataDir(deployment.ID), dataDir,
)
if _, stderr, err := client.Execute(ctx, migrate); err != nil {
return fmt.Errorf("迁移历史部署目录失败(可手动执行: %s): %v: %s", migrate, err, strings.TrimSpace(stderr))
}
if _, _, err := client.Execute(ctx, "mkdir -p "+configDir+" "+dataDir+" && chmod 755 "+configDir+" && chown 10001:10001 "+dataDir); err != nil {
return fmt.Errorf("创建主机持久化目录失败: %w", err)
}
@@ -118,8 +138,8 @@ func (d *LokiDeployer) deployDocker(ctx context.Context) error {
if _, stderr, err := client.Execute(ctx, "docker pull "+image); err != nil {
return fmt.Errorf("拉取 Loki 镜像失败: %v: %s", err, strings.TrimSpace(stderr))
}
// 同一部署 ID 的重部署只替换容器,配置和日志数据目录保留。
client.Execute(ctx, "docker rm -f "+name+" >/dev/null 2>&1 || true")
// 同一部署 ID 的重部署只替换容器(含历史命名),配置和日志数据目录保留。
client.Execute(ctx, "docker rm -f "+name+" "+legacyDockerContainerName(deployment.ID)+" >/dev/null 2>&1 || true")
port := deployment.ListenPort
if port == 0 {
port = 3100
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"opsxloki/models"
"lokistack/models"
)
func TestDockerLokiConfigUsesEnvironmentForStorageCredentials(t *testing.T) {
@@ -24,7 +24,7 @@ func TestDockerLokiConfigUsesEnvironmentForStorageCredentials(t *testing.T) {
t.Errorf("config missing %q", expected)
}
}
if got := dockerContainerName(deployment.ID); got != "lokideploy-loki-7" {
if got := dockerContainerName(deployment.ID); got != "lokistack-loki-7" {
t.Fatalf("unexpected container name: %s", got)
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"encoding/base64"
"fmt"
"opsxloki/pkg/crypto"
"lokistack/pkg/crypto"
)
func (d *LokiDeployer) generateManifest(ns string) string {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"opsxloki/models"
"lokistack/models"
)
// TestObjectStoreConfigs 验证 OSS / COS / OBS 生成的 Loki 配置:
+5 -5
View File
@@ -1,6 +1,6 @@
package services
// 飞书/Lark 群机器人通知 — 卡片消息(参照 opsx-sec notifier 与运维告警脚本的卡片结构),
// 飞书/Lark 群机器人通知 — 卡片消息,
// webhook 存 SystemSetting(AES 加密),对外只回 hasWebhook。
import (
"bytes"
@@ -13,9 +13,9 @@ import (
"strings"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
const (
@@ -116,7 +116,7 @@ func SendLarkCard(title, color string, lines []string) error {
},
"elements": []interface{}{
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "opsx-loki 日志告警"}}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "LokiStack 日志告警"}}},
},
},
}
+4 -4
View File
@@ -12,9 +12,9 @@ import (
"net/http"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
)
const channelHTTPTimeout = 10 * time.Second
@@ -182,7 +182,7 @@ func SendLarkCardToChannel(channelID uint, title, color string, lines []string)
},
"elements": []interface{}{
map[string]interface{}{"tag": "div", "text": map[string]string{"tag": "lark_md", "content": content}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "opsx-loki 日志告警"}}},
map[string]interface{}{"tag": "note", "elements": []interface{}{map[string]string{"tag": "plain_text", "content": "LokiStack 日志告警"}}},
},
},
}
+3 -3
View File
@@ -14,9 +14,9 @@ import (
"strings"
"time"
"opsxloki/config"
"opsxloki/models"
"opsxloki/pkg/crypto"
"lokistack/config"
"lokistack/models"
"lokistack/pkg/crypto"
"golang.org/x/crypto/ssh"
)
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"fmt"
"time"
"opsxloki/models"
"opsxloki/pkg/crypto"
"opsxloki/repository"
"opsxloki/utils"
"lokistack/models"
"lokistack/pkg/crypto"
"lokistack/repository"
"lokistack/utils"
)
type StorageService struct{}
+1 -1
View File
@@ -3,7 +3,7 @@ package services
import (
"testing"
"opsxloki/repository"
"lokistack/repository"
)
func TestStorageUpdateReplacesBothCredentials(t *testing.T) {