feat: complete E2E deployment flow with storage layered config and values template versioning

- Instance deployment: charts browser, deploy modal, instances list
- Values Template version management (create/history/rollback)
- Storage layered config (cluster > workspace > shared priority)
- Cluster credential decryptIfNeeded for mixed encrypted/plaintext kubeconfig
- YAML syntax validation (client-side + server-side warning)
- Frontend: charts, instances, storage, templates, admin pages
- Backend: storage service, instance service, cluster service, helm client
- Multi-Tenant Kubeconfig.md: added by user
This commit is contained in:
Ivan087
2026-04-30 16:31:00 +08:00
parent 985369d40f
commit 47849042a7
42 changed files with 2029 additions and 255 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strings"
@ -16,12 +17,13 @@ import (
// InstanceService Helm 实例管理领域服务
type InstanceService struct {
instanceRepo repository.InstanceRepository
clusterRepo repository.ClusterRepository
registryRepo repository.RegistryRepository
helmClient repository.HelmClient
ociClient repository.OCIClient
entryClient repository.InstanceEntryClient
instanceRepo repository.InstanceRepository
clusterRepo repository.ClusterRepository
registryRepo repository.RegistryRepository
helmClient repository.HelmClient
ociClient repository.OCIClient
entryClient repository.InstanceEntryClient
storageService *StorageService // for layered storage config resolution
}
// NewInstanceService 创建实例服务
@ -34,15 +36,21 @@ func NewInstanceService(
entryClient repository.InstanceEntryClient,
) *InstanceService {
return &InstanceService{
instanceRepo: instanceRepo,
clusterRepo: clusterRepo,
registryRepo: registryRepo,
helmClient: helmClient,
ociClient: ociClient,
entryClient: entryClient,
instanceRepo: instanceRepo,
clusterRepo: clusterRepo,
registryRepo: registryRepo,
helmClient: helmClient,
ociClient: ociClient,
entryClient: entryClient,
storageService: nil, // set via SetStorageService for layered storage
}
}
// SetStorageService 设置存储服务(用于分层存储配置解析)
func (s *InstanceService) SetStorageService(storageService *StorageService) {
s.storageService = storageService
}
const chartCacheDir = "/tmp/charts"
func (s *InstanceService) chartArchivePath(instance *entity.Instance) string {
@ -89,6 +97,20 @@ func (s *InstanceService) CreateInstance(ctx context.Context, instance *entity.I
return entity.ErrInstanceExists
}
// ===== 分层存储配置解析 =====
// Priority: workspace-level default > cluster-level default > shared default
if s.storageService != nil && instance.WorkspaceID != "" {
resolution, err := s.storageService.ResolveStorageConfig(ctx, instance.ClusterID, instance.WorkspaceID)
if err == nil && resolution != nil && resolution.Storage != nil {
// Merge resolved storage values into instance.Values
if instance.Values == nil {
instance.Values = make(map[string]interface{})
}
// User override takes highest priority (already set), so we only set if not already present
mergeStorageToValues(instance.Values, resolution.Storage)
}
}
instance.BeginOperation(entity.OperationInstall, "Preparing installation")
// 先写入数据库,记录 pending 状态
@ -104,7 +126,16 @@ func (s *InstanceService) CreateInstance(ctx context.Context, instance *entity.I
}
// 异步执行 Helm 安装并监控状态
go s.executeAndSyncInstall(context.Background(), instance.ID, cluster, registry, instance)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[goroutine-panic] instanceID=%s panic=%v", instance.ID, r)
}
}()
log.Printf("[goroutine-start] instanceID=%s name=%s cluster=%s", instance.ID, instance.Name, cluster.Name)
s.executeAndSyncInstall(context.Background(), instance.ID, cluster, registry, instance)
log.Printf("[goroutine-done] instanceID=%s", instance.ID)
}()
// 立即返回,状态同步由后台任务处理
return nil
@ -286,8 +317,10 @@ func (s *InstanceService) ListInstanceEntries(ctx context.Context, clusterID, in
// executeAndSyncInstall 异步执行安装并监控状态
func (s *InstanceService) executeAndSyncInstall(ctx context.Context, instanceID string, cluster *entity.Cluster, registry *entity.Registry, instance *entity.Instance) {
log.Printf("[install-start] instanceID=%s values=%v", instanceID, instance.Values)
// 执行 Helm 安装
if err := s.helmClient.Install(ctx, cluster, instance); err != nil {
log.Printf("[install-fail] instanceID=%s err=%v", instanceID, err)
// 更新实例状态为失败
instance, updateErr := s.instanceRepo.GetByID(ctx, instanceID)
if updateErr == nil && instance != nil {
@ -296,6 +329,7 @@ func (s *InstanceService) executeAndSyncInstall(ctx context.Context, instanceID
}
return
}
log.Printf("[install-ok] instanceID=%s revision=%d", instanceID, instance.Revision)
// 安装成功后,同步状态
s.syncInstanceStatus(ctx, instanceID, cluster, instance.Name, instance.Namespace, entity.OperationInstall)
@ -472,3 +506,48 @@ func (s *InstanceService) syncInstanceStatus(ctx context.Context, instanceID str
_ = s.instanceRepo.Update(ctx, instance)
}
}
// mergeStorageToValues 将存储配置 merge 到 Helm values
// 只覆盖 nil/空的字段,保留用户已设置的 values
func mergeStorageToValues(values map[string]interface{}, storage *entity.StorageBackend) {
if storage == nil || values == nil {
return
}
persistence := make(map[string]interface{})
switch storage.Type {
case entity.StorageTypeNFS:
if storage.Config.NFS != nil {
persistence["type"] = "nfs"
persistence["nfs"] = map[string]interface{}{
"server": storage.Config.NFS.Server,
"path": storage.Config.NFS.Path,
}
// Helm common chart labels
persistence["mountOptions"] = []string{"rw", "relatime", "vers=3"}
persistence["reclaimPolicy"] = "Retain"
}
case entity.StorageTypePV:
if storage.Config.PV != nil {
persistence["type"] = "persistentVolumeClaim"
persistence["storageClass"] = storage.Config.PV.StorageClassName
persistence["size"] = storage.Config.PV.Capacity
persistence["accessMode"] = storage.Config.PV.AccessModes
}
case entity.StorageTypeHostPath:
if storage.Config.HostPath != nil {
persistence["type"] = "hostPath"
persistence["hostPath"] = map[string]interface{}{
"path": storage.Config.HostPath.Path,
}
}
}
// Only merge if key doesn't already exist and has a value
for key, val := range persistence {
if _, exists := values[key]; !exists && val != nil {
values[key] = val
}
}
}