diff --git a/backend/services/cluster.go b/backend/services/cluster.go index e6b9009..ce3acc7 100644 --- a/backend/services/cluster.go +++ b/backend/services/cluster.go @@ -211,6 +211,11 @@ func (s *ClusterService) Delete(id uint) error { defer db.Unlock() for i, c := range db.Clusters { if c.ID == id { + for _, d := range db.Deployments { + if d.ClusterID != nil && *d.ClusterID == id { + return fmt.Errorf("仍有部署使用该集群,请先删除部署") + } + } if c.KubeConfigPath != "" { _ = os.Remove(c.KubeConfigPath) } diff --git a/backend/services/collector.go b/backend/services/collector.go index 75a043a..3a9637f 100644 --- a/backend/services/collector.go +++ b/backend/services/collector.go @@ -488,30 +488,59 @@ func (s *CollectorService) Update(id uint, updates map[string]interface{}) error return utils.ErrNotFound } +// UninstallCollectorsForDeployment 卸载并删除某部署下的所有采集器。 +// 卸载失败时保留记录,避免远端资源失去跟踪。 +func UninstallCollectorsForDeployment(ctx context.Context, deploymentID uint) { + db := repository.GetDB() + db.RLock() + var targets []models.Collector + for _, c := range db.Collectors { + if c.LokiDeploymentID == deploymentID { + targets = append(targets, c) + } + } + db.RUnlock() + + for i := range targets { + c := targets[i] + if err := NewCollectorDeployer(&c, c.LokiDeployment, nil).Uninstall(ctx); err != nil { + log.Printf("[deploy-delete] 采集器 %q 卸载失败,保留记录: %v", c.Name, err) + continue + } + db.Lock() + for j := range db.Collectors { + if db.Collectors[j].ID == c.ID { + db.Collectors = append(db.Collectors[:j], db.Collectors[j+1:]...) + break + } + } + db.RequestSave() + db.Unlock() + } +} + func (s *CollectorService) Delete(id uint) error { collector, err := s.Get(id) if err != nil { return utils.ErrNotFound } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + // 先卸载远端资源,成功后再删记录;卸载失败直接返回,避免「界面删了、远端还在」 + if err := NewCollectorDeployer(collector, collector.LokiDeployment, nil).Uninstall(ctx); err != nil { + return fmt.Errorf("卸载采集器失败,未删除记录: %w", err) + } db := repository.GetDB() db.Lock() + defer db.Unlock() for i, c := range db.Collectors { if c.ID == id { db.Collectors = append(db.Collectors[:i], db.Collectors[i+1:]...) - break + db.RequestSave() + return nil } } - db.Unlock() - db.RequestSave() - // 异步卸载(用拷贝的 collector,避免删除后取不到;失败留日志,远端不留孤儿容器也无提示) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer cancel() - if err := NewCollectorDeployer(collector, collector.LokiDeployment, nil).Uninstall(ctx); err != nil { - log.Printf("[collector] 删除后卸载 %q(目标 %s)失败: %v", collector.Name, collector.Name, err) - } - }() - return nil + return utils.ErrNotFound } // Install 异步部署采集器 diff --git a/backend/services/deployment.go b/backend/services/deployment.go index 67a1e4b..a4f4025 100644 --- a/backend/services/deployment.go +++ b/backend/services/deployment.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "log" "net" "regexp" "strconv" @@ -315,18 +316,39 @@ func (s *DeploymentService) Delete(id uint) error { if err != nil { return err } - if deployment.Mode == "host-docker" { - if deployment.Status == "deploying" { - return fmt.Errorf("部署正在进行中,请等待完成后删除") - } + if deployment.Status == "deploying" { + return fmt.Errorf("部署正在进行中,请等待完成后删除") + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // 先卸载该部署下的采集器:它们依赖这个 Loki 实例,留着会指向不存在的服务 + UninstallCollectorsForDeployment(ctx, id) + + switch deployment.Mode { + case "host-docker": if deployment.Status != "pending" { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() if err := deleteDockerLoki(ctx, deployment); err != nil { return err } } + case "k8s-yaml": + // 停止/重启走 CleanupLoki 时保留 PVC;这里是显式删除,连数据卷一起清理 + CleanupLoki(ctx, deployment) + if deployment.Cluster != nil { + ns := deployment.Namespace + if ns == "" { + ns = "log" + } + if err := deleteK8sResource(ctx, deployment.Cluster, "persistentvolumeclaim", ns, "loki-data"); err != nil { + log.Printf("[deploy-delete] 清理 PVC %s/loki-data 失败: %v", ns, err) + } + } + case "host-binary": + CleanupLoki(ctx, deployment) } + db := repository.GetDB() db.Lock() removedName := "" diff --git a/backend/services/host.go b/backend/services/host.go index d5b74ed..1a7c3c1 100644 --- a/backend/services/host.go +++ b/backend/services/host.go @@ -186,6 +186,16 @@ func (s *HostService) Delete(id uint) error { defer db.Unlock() for i, h := range db.Hosts { if h.ID == id { + for _, d := range db.Deployments { + if d.HostID != nil && *d.HostID == id { + return fmt.Errorf("仍有部署使用该主机,请先删除部署") + } + } + for _, c := range db.Collectors { + if c.HostID != nil && *c.HostID == id { + return fmt.Errorf("仍有采集器使用该主机,请先删除采集器") + } + } db.Hosts = append(db.Hosts[:i], db.Hosts[i+1:]...) db.RequestSave() return nil diff --git a/backend/services/storage.go b/backend/services/storage.go index 002e7a9..bc4dfaf 100644 --- a/backend/services/storage.go +++ b/backend/services/storage.go @@ -151,6 +151,11 @@ func (s *StorageService) Delete(id uint) error { defer db.Unlock() for i, st := range db.Storages { if st.ID == id { + for _, d := range db.Deployments { + if d.StorageConfigID == id { + return fmt.Errorf("仍有部署使用该存储配置,请先删除部署或改用其他存储") + } + } db.Storages = append(db.Storages[:i], db.Storages[i+1:]...) db.RequestSave() return nil diff --git a/docs/deployment/kubernetes.md b/docs/deployment/kubernetes.md index 18f91a3..f91e4c3 100644 --- a/docs/deployment/kubernetes.md +++ b/docs/deployment/kubernetes.md @@ -193,7 +193,7 @@ metadata: namespace: log rules: - apiGroups: [""] - resources: ["pods", "pods/log", "services", "configmaps", "secrets", "persistentvolumeclaims", "events", "serviceaccounts"] + resources: ["pods", "pods/log", "services", "services/proxy", "configmaps", "secrets", "persistentvolumeclaims", "events", "serviceaccounts"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["statefulsets", "daemonsets", "deployments"] diff --git a/frontend/src/pages/clusters/components/ClusterFormDialog.tsx b/frontend/src/pages/clusters/components/ClusterFormDialog.tsx index 593b4cd..84bbeb8 100644 --- a/frontend/src/pages/clusters/components/ClusterFormDialog.tsx +++ b/frontend/src/pages/clusters/components/ClusterFormDialog.tsx @@ -299,7 +299,7 @@ metadata: namespace: log rules: - apiGroups: [""] - resources: ["pods", "pods/log", "services", "configmaps", "secrets", "persistentvolumeclaims", "events", "serviceaccounts"] + resources: ["pods", "pods/log", "services", "services/proxy", "configmaps", "secrets", "persistentvolumeclaims", "events", "serviceaccounts"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["statefulsets", "daemonsets", "deployments"]