feat: K8s 部署清单、Token 接入加固与日志/设置页优化

- kubernetes/: 30Gi PVC、单副本 Recreate、Secret/Service/NodePort/Ingress 清单与部署文档
- 数据安全: kubeconfig/token 统一加密存储,启动迁移历史明文,DB 0600
- Token 接入: 命名空间级最小权限提示、长期 Token、自签证书 TLS 兼容
- 部署: 命名空间级 Token 下跳过 Namespace patch/枚举,节点信息容错
- 日志页: namespace → app → pod 选择顺序与作用域收窄
- 设置页: 拆分为通用/告警/LDAP/镜像源四个 tab
- 弹框: 统一 max-h 滚动与长命令换行,修复内容溢出
This commit is contained in:
logan committed 2026-10-25 23:44:07 +08:00
1 parent 2d73661dd1
commit 9f7c522157
50 files changed
+1346 -149

No files matched your search

+56 -17
View File
@@ -6,6 +6,7 @@ import (
"opsxloki/pkg/crypto"
"os"
"path/filepath"
"strings"
"time"
"opsxloki/config"
@@ -32,6 +33,39 @@ func SaveKubeConfig(name, content string) (string, error) {
return path, nil
}
// MigrateClusterKubeconfigs 启动时把历史明文 kubeconfig 加密回 DB,并删除明文文件。
// 幂等:已加密的直接跳过。
func MigrateClusterKubeconfigs() {
db := repository.GetDB()
if db == nil {
return
}
kubeDir := filepath.Clean(config.GetConfig().KubeDir)
db.Lock()
changed := false
for i := range db.Clusters {
c := &db.Clusters[i]
if c.KubeConfig == "" || c.IsKubeconfigEncrypted() {
continue
}
if err := c.EncryptAndSetKubeconfig(c.KubeConfig); err != nil {
continue
}
if c.KubeConfigPath != "" {
p := filepath.Clean(c.KubeConfigPath)
if strings.HasPrefix(p, kubeDir+string(os.PathSeparator)) {
_ = os.Remove(p)
}
c.KubeConfigPath = ""
}
changed = true
}
db.Unlock()
if changed {
_ = db.RequestSaveSync()
}
}
// Create 创建集群。token 接入时自动构造等价 kubeconfig,统一存储,使部署模块同样可用。
func (s *ClusterService) Create(name, description, configType, apiServer, kubeconfig, token, contextName string) (*models.Cluster, error) {
if configType == "" {
@@ -40,33 +74,32 @@ func (s *ClusterService) Create(name, description, configType, apiServer, kubeco
finalKubeconfig := kubeconfig
if configType == "token" {
// 安全默认:不嵌入 CA、不跳过 TLS 验证,依赖系统 CA 池。
// 自签证书的集群请改用 kubeconfig 模式(其内含 CA 证书)。
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token, "", false)
// Token 模式通常用于内网/自签证书集群:默认跳过 TLS 校验。
// 如需严格校验,请使用 kubeconfig 模式(其中包含 CA 证书)。
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token, "", true) // Token 模式默认跳过 TLS 校验,兼容内网自签证书
}
// 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取
if apiServer == "" {
apiServer = ExtractAPIServer(finalKubeconfig)
}
path, err := SaveKubeConfig(name, finalKubeconfig)
if err != nil {
return nil, fmt.Errorf("kubeconfig 保存失败: %w", err)
}
// 安全加固:kubeconfig 只加密存 DB,不再落明文文件
cluster := models.Cluster{
ID: repository.GetDB().NextID("clusters"),
Name: name,
Description: description,
ConfigType: configType,
APIServer: apiServer,
KubeConfig: finalKubeconfig,
KubeConfigPath: path,
KubeConfig: "",
KubeConfigPath: "",
Context: contextName,
Status: "pending",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := cluster.EncryptAndSetKubeconfig(finalKubeconfig); err != nil {
return nil, fmt.Errorf("kubeconfig 加密失败: %w", err)
}
db := repository.GetDB()
db.Lock()
@@ -144,18 +177,24 @@ func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
if v, ok := updates["apiServer"].(string); ok {
db.Clusters[i].APIServer = v
}
// kubeconfig 编辑约定:非空才更新(留空=保留旧密文)
// 凭据:kubeconfig 或 token 二选一,非空才更新,统一加密存 DB
if v, ok := updates["kubeconfig"].(string); ok && v != "" {
if enc, err := crypto.EncryptField(v); err == nil {
db.Clusters[i].KubeConfig = enc
db.Clusters[i].KubeConfigPath = ""
}
}
// 凭据字段:空值不覆盖,避免编辑其它项时清空
if v, ok := updates["kubeconfig"].(string); ok && v != "" {
db.Clusters[i].KubeConfig = v
if db.Clusters[i].KubeConfigPath != "" {
_ = os.WriteFile(db.Clusters[i].KubeConfigPath, []byte(v), 0600)
} else if tok, ok := updates["token"].(string); ok && tok != "" {
apiServer := db.Clusters[i].APIServer
if v, ok := updates["apiServer"].(string); ok && v != "" {
apiServer = v
}
if apiServer == "" {
return fmt.Errorf("Token 接入需要 API Server")
}
kc := BuildKubeconfigFromToken(apiServer, tok, "", true) // Token 模式默认跳过 TLS 校验
if enc, err := crypto.EncryptField(kc); err == nil {
db.Clusters[i].KubeConfig = enc
db.Clusters[i].KubeConfigPath = ""
}
}
db.Clusters[i].UpdatedAt = time.Now()