Files
LokiStack/backend/services/cluster.go
T
logan 2d73661dd1 feat: complete Loki query, insights, settings and deployment workflows
Add the Loki query UI, access policies, ingress insights, alert and registry settings, and their backend APIs. Include the related Kubernetes deployment and collector updates required by the integrated application.

Validated with go test ./..., npm run build, and npm run lint.
2026-10-25 21:28:32 +08:00

208 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"fmt"
"opsxloki/pkg/crypto"
"os"
"path/filepath"
"time"
"opsxloki/config"
"opsxloki/models"
"opsxloki/repository"
"opsxloki/utils"
)
type ClusterService struct{}
func NewClusterService() *ClusterService {
return &ClusterService{}
}
func SaveKubeConfig(name, content string) (string, error) {
dir := config.GetConfig().KubeDir
if err := os.MkdirAll(dir, 0700); err != nil {
return "", err
}
path := filepath.Join(dir, fmt.Sprintf("%s.yaml", name))
if err := os.WriteFile(path, []byte(content), 0600); err != nil {
return "", err
}
return path, nil
}
// Create 创建集群。token 接入时自动构造等价 kubeconfig,统一存储,使部署模块同样可用。
func (s *ClusterService) Create(name, description, configType, apiServer, kubeconfig, token, contextName string) (*models.Cluster, error) {
if configType == "" {
configType = "kubeconfig"
}
finalKubeconfig := kubeconfig
if configType == "token" {
// 安全默认:不嵌入 CA、不跳过 TLS 验证,依赖系统 CA 池。
// 自签证书的集群请改用 kubeconfig 模式(其内含 CA 证书)。
finalKubeconfig = BuildKubeconfigFromToken(apiServer, token, "", false)
}
// 展示用 APIServer:token 模式取入参,kubeconfig 模式从内容提取
if apiServer == "" {
apiServer = ExtractAPIServer(finalKubeconfig)
}
path, err := SaveKubeConfig(name, finalKubeconfig)
if err != nil {
return nil, fmt.Errorf("kubeconfig 保存失败: %w", err)
}
cluster := models.Cluster{
ID: repository.GetDB().NextID("clusters"),
Name: name,
Description: description,
ConfigType: configType,
APIServer: apiServer,
KubeConfig: finalKubeconfig,
KubeConfigPath: path,
Context: contextName,
Status: "pending",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
db := repository.GetDB()
db.Lock()
db.Clusters = append(db.Clusters, cluster)
db.Unlock()
db.RequestSave()
go s.Validate(cluster.ID)
return &cluster, nil
}
func (s *ClusterService) Get(id uint) (*models.Cluster, error) {
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
for i := range db.Clusters {
if db.Clusters[i].ID == id {
c := db.Clusters[i]
return &c, nil
}
}
return nil, utils.ErrNotFound
}
func (s *ClusterService) List(page, pageSize int) ([]models.Cluster, int64, error) {
db := repository.GetDB()
db.RLock()
defer db.RUnlock()
total := int64(len(db.Clusters))
start := (page - 1) * pageSize
if start < 0 || start >= len(db.Clusters) {
return []models.Cluster{}, total, nil
}
end := start + pageSize
if end > len(db.Clusters) {
end = len(db.Clusters)
}
result := make([]models.Cluster, end-start)
copy(result, db.Clusters[start:end])
return result, total, nil
}
func (s *ClusterService) Update(id uint, updates map[string]interface{}) error {
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i := range db.Clusters {
if db.Clusters[i].ID == id {
if v, ok := updates["name"].(string); ok && v != "" {
db.Clusters[i].Name = v
}
if v, ok := updates["description"].(string); ok {
db.Clusters[i].Description = v
}
if v, ok := updates["status"].(string); ok && v != "" {
db.Clusters[i].Status = v
}
if v, ok := updates["version"].(string); ok {
db.Clusters[i].Version = v
}
if v, ok := updates["nodesCount"].(int); ok {
db.Clusters[i].NodesCount = v
}
if v, ok := updates["context"].(string); ok {
db.Clusters[i].Context = v
}
if v, ok := updates["lastConnected"].(time.Time); ok {
db.Clusters[i].LastConnected = v
}
// 接入方式与展示字段
if v, ok := updates["configType"].(string); ok && v != "" {
db.Clusters[i].ConfigType = v
}
if v, ok := updates["apiServer"].(string); ok {
db.Clusters[i].APIServer = v
}
// kubeconfig 编辑约定:非空才更新(留空=保留旧密文)
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)
}
}
db.Clusters[i].UpdatedAt = time.Now()
db.RequestSave()
return nil
}
}
return utils.ErrNotFound
}
func (s *ClusterService) Delete(id uint) error {
db := repository.GetDB()
db.Lock()
defer db.Unlock()
for i, c := range db.Clusters {
if c.ID == id {
if c.KubeConfigPath != "" {
_ = os.Remove(c.KubeConfigPath)
}
db.Clusters = append(db.Clusters[:i], db.Clusters[i+1:]...)
db.RequestSave()
return nil
}
}
return utils.ErrNotFound
}
func (s *ClusterService) Validate(id uint) {
cluster, err := s.Get(id)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info, err := GetClusterInfo(ctx, cluster)
if err != nil {
s.Update(id, map[string]interface{}{"status": "error"})
return
}
s.Update(id, map[string]interface{}{
"status": "active",
"version": info.Version,
"nodesCount": len(info.Nodes),
"lastConnected": time.Now(),
"context": info.CurrentCtx,
})
}