Go(Gin) + React(Vite + shadcn/ui + Tailwind) 前后端分离,全中文 UI - 集群管理:Kubeconfig / Token 双接入,kubectl + helm 部署 - 主机管理:SSH 密钥 / 密码双认证,CPU/内存/磁盘使用率监控,SSH 错误中文友好提示 - 存储配置、部署管理、系统设置 - Docker 镜像内置 kubectl + helm,开箱即用 - run.sh 一键启停(start/stop/restart/status) Co-Authored-By: Claude <noreply@anthropic.com>
348 lines
8.3 KiB
Go
348 lines
8.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"lokideploy/models"
|
|
"lokideploy/utils"
|
|
)
|
|
|
|
type DeploymentService struct{}
|
|
|
|
func NewDeploymentService() *DeploymentService {
|
|
return &DeploymentService{}
|
|
}
|
|
|
|
type CreateDeploymentRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
Description string `json:"description"`
|
|
Version string `json:"version"`
|
|
Mode string `json:"mode" binding:"required"`
|
|
ClusterID *uint `json:"clusterID"`
|
|
HostID *uint `json:"hostID"`
|
|
Namespace string `json:"namespace"`
|
|
StorageConfigID uint `json:"storageConfigID"`
|
|
AuthEnabled bool `json:"authEnabled"`
|
|
RetentionDays int `json:"retentionDays"`
|
|
Replicas int `json:"replicas"`
|
|
ResourcesCPU string `json:"resourcesCPU"`
|
|
ResourcesMem string `json:"resourcesMem"`
|
|
StorageSize string `json:"storageSize"`
|
|
}
|
|
|
|
func (s *DeploymentService) Create(req *CreateDeploymentRequest) (*models.LokiDeployment, error) {
|
|
db := models.GetDB()
|
|
d := models.LokiDeployment{
|
|
ID: db.NextID("deployments"),
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
Version: req.Version,
|
|
Mode: req.Mode,
|
|
ClusterID: req.ClusterID,
|
|
HostID: req.HostID,
|
|
Namespace: req.Namespace,
|
|
StorageConfigID: req.StorageConfigID,
|
|
AuthEnabled: req.AuthEnabled,
|
|
RetentionDays: req.RetentionDays,
|
|
Replicas: req.Replicas,
|
|
ResourcesCPU: req.ResourcesCPU,
|
|
ResourcesMem: req.ResourcesMem,
|
|
StorageSize: req.StorageSize,
|
|
Status: "pending",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
d.BeforeCreate()
|
|
|
|
// Load relations
|
|
if d.ClusterID != nil && *d.ClusterID > 0 {
|
|
for i := range db.Clusters {
|
|
if db.Clusters[i].ID == *d.ClusterID {
|
|
c := db.Clusters[i]
|
|
d.Cluster = &c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if d.HostID != nil && *d.HostID > 0 {
|
|
for i := range db.Hosts {
|
|
if db.Hosts[i].ID == *d.HostID {
|
|
h := db.Hosts[i]
|
|
d.Host = &h
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if d.StorageConfigID > 0 {
|
|
for i := range db.Storages {
|
|
if db.Storages[i].ID == d.StorageConfigID {
|
|
st := db.Storages[i]
|
|
d.StorageConfig = &st
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
db.Lock()
|
|
db.Deployments = append(db.Deployments, d)
|
|
db.Unlock()
|
|
go db.Save()
|
|
|
|
return &d, nil
|
|
}
|
|
|
|
func (s *DeploymentService) Get(id uint) (*models.LokiDeployment, error) {
|
|
db := models.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
for i := range db.Deployments {
|
|
if db.Deployments[i].ID == id {
|
|
d := db.Deployments[i]
|
|
s.loadRelations(&d)
|
|
return &d, nil
|
|
}
|
|
}
|
|
return nil, utils.ErrNotFound
|
|
}
|
|
|
|
func (s *DeploymentService) List(page, pageSize int) ([]models.LokiDeployment, int64, error) {
|
|
db := models.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
|
|
total := int64(len(db.Deployments))
|
|
start := (page - 1) * pageSize
|
|
if start < 0 || start >= len(db.Deployments) {
|
|
return []models.LokiDeployment{}, total, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > len(db.Deployments) {
|
|
end = len(db.Deployments)
|
|
}
|
|
result := make([]models.LokiDeployment, end-start)
|
|
copy(result, db.Deployments[start:end])
|
|
for i := range result {
|
|
s.loadRelations(&result[i])
|
|
}
|
|
return result, total, nil
|
|
}
|
|
|
|
func (s *DeploymentService) Update(id uint, updates map[string]interface{}) error {
|
|
db := models.GetDB()
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i := range db.Deployments {
|
|
if db.Deployments[i].ID == id {
|
|
if v, ok := updates["status"].(string); ok {
|
|
db.Deployments[i].Status = v
|
|
}
|
|
if v, ok := updates["statusMessage"].(string); ok {
|
|
db.Deployments[i].StatusMessage = v
|
|
}
|
|
if v, ok := updates["endpoint"].(string); ok {
|
|
db.Deployments[i].Endpoint = v
|
|
}
|
|
db.Deployments[i].UpdatedAt = time.Now()
|
|
go db.Save()
|
|
return nil
|
|
}
|
|
}
|
|
return utils.ErrNotFound
|
|
}
|
|
|
|
func (s *DeploymentService) Delete(id uint) error {
|
|
db := models.GetDB()
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i, d := range db.Deployments {
|
|
if d.ID == id {
|
|
db.Deployments = append(db.Deployments[:i], db.Deployments[i+1:]...)
|
|
go db.Save()
|
|
return nil
|
|
}
|
|
}
|
|
return utils.ErrNotFound
|
|
}
|
|
|
|
func (s *DeploymentService) StartDeploy(id uint) error {
|
|
deployment, err := s.Get(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if deployment.Status == "deploying" {
|
|
return fmt.Errorf("部署正在进行中,请勿重复操作")
|
|
}
|
|
s.Update(id, map[string]interface{}{"status": "deploying"})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
|
defer cancel()
|
|
|
|
logFunc := func(level, message, detail string) {
|
|
s.AddLog(id, level, message, detail)
|
|
}
|
|
|
|
deployer := NewLokiDeployer(deployment, logFunc)
|
|
go func() {
|
|
err := deployer.Deploy(ctx)
|
|
if err != nil {
|
|
s.Update(id, map[string]interface{}{
|
|
"status": "failed",
|
|
"statusMessage": fmt.Sprintf("Deployment failed: %v", err),
|
|
})
|
|
s.AddLog(id, "error", fmt.Sprintf("Deployment failed: %v", err), "")
|
|
} else {
|
|
s.Update(id, map[string]interface{}{
|
|
"status": "running",
|
|
"statusMessage": "Deployment completed successfully",
|
|
"endpoint": s.getEndpoint(deployment),
|
|
})
|
|
s.AddLog(id, "success", "Deployment completed successfully", "")
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (s *DeploymentService) StopDeployment(id uint) error {
|
|
deployment, err := s.Get(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if deployment.Status != "running" && deployment.Status != "deploying" {
|
|
return fmt.Errorf("部署未在运行,无法停止")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
CleanupLoki(ctx, deployment)
|
|
return s.Update(id, map[string]interface{}{
|
|
"status": "stopped",
|
|
"statusMessage": "Deployment stopped by user",
|
|
})
|
|
}
|
|
|
|
func (s *DeploymentService) RestartDeployment(id uint) error {
|
|
if err := s.StopDeployment(id); err != nil {
|
|
return err
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
return s.StartDeploy(id)
|
|
}
|
|
|
|
func (s *DeploymentService) AddLog(deploymentID uint, level, message, detail string) {
|
|
db := models.GetDB()
|
|
log := models.DeploymentLog{
|
|
ID: db.NextID("logs"),
|
|
DeploymentID: deploymentID,
|
|
Level: level,
|
|
Message: message,
|
|
Detail: detail,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
db.Lock()
|
|
db.Logs = append(db.Logs, log)
|
|
db.Unlock()
|
|
go db.Save()
|
|
}
|
|
|
|
func (s *DeploymentService) GetLogs(deploymentID uint, page, pageSize int) ([]models.DeploymentLog, int64, error) {
|
|
db := models.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
|
|
var filtered []models.DeploymentLog
|
|
for i := len(db.Logs) - 1; i >= 0; i-- {
|
|
if db.Logs[i].DeploymentID == deploymentID {
|
|
filtered = append(filtered, db.Logs[i])
|
|
}
|
|
}
|
|
|
|
total := int64(len(filtered))
|
|
start := (page - 1) * pageSize
|
|
if start >= len(filtered) {
|
|
return []models.DeploymentLog{}, total, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > len(filtered) {
|
|
end = len(filtered)
|
|
}
|
|
return filtered[start:end], total, nil
|
|
}
|
|
|
|
func (s *DeploymentService) GetStats() (map[string]interface{}, error) {
|
|
db := models.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
|
|
var running, failed, pending, stopped int64
|
|
for _, d := range db.Deployments {
|
|
switch d.Status {
|
|
case "running":
|
|
running++
|
|
case "failed":
|
|
failed++
|
|
case "pending":
|
|
pending++
|
|
case "stopped":
|
|
stopped++
|
|
}
|
|
}
|
|
|
|
return map[string]interface{}{
|
|
"totalDeployments": int64(len(db.Deployments)),
|
|
"running": running,
|
|
"failed": failed,
|
|
"pending": pending,
|
|
"stopped": stopped,
|
|
"clusterCount": int64(len(db.Clusters)),
|
|
"hostCount": int64(len(db.Hosts)),
|
|
}, nil
|
|
}
|
|
|
|
func (s *DeploymentService) loadRelations(d *models.LokiDeployment) {
|
|
db := models.GetDB()
|
|
if d.ClusterID != nil && *d.ClusterID > 0 {
|
|
for i := range db.Clusters {
|
|
if db.Clusters[i].ID == *d.ClusterID {
|
|
c := db.Clusters[i]
|
|
d.Cluster = &c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if d.HostID != nil && *d.HostID > 0 {
|
|
for i := range db.Hosts {
|
|
if db.Hosts[i].ID == *d.HostID {
|
|
h := db.Hosts[i]
|
|
d.Host = &h
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if d.StorageConfigID > 0 {
|
|
for i := range db.Storages {
|
|
if db.Storages[i].ID == d.StorageConfigID {
|
|
st := db.Storages[i]
|
|
d.StorageConfig = &st
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *DeploymentService) getEndpoint(d *models.LokiDeployment) string {
|
|
switch d.Mode {
|
|
case "k8s-helm", "k8s-yaml":
|
|
ns := d.Namespace
|
|
if ns == "" {
|
|
ns = "loki"
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
return ""
|
|
}
|