This commit is contained in:
mangomqy
2025-11-13 02:54:06 +00:00
commit c5e51ed069
254 changed files with 54901 additions and 0 deletions

View File

@ -0,0 +1,80 @@
package service
import (
"context"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// ArtifactService Artifact 浏览领域服务
type ArtifactService struct {
registryRepo repository.RegistryRepository
ociClient repository.OCIClient
}
// NewArtifactService 创建 Artifact 服务
func NewArtifactService(
registryRepo repository.RegistryRepository,
ociClient repository.OCIClient,
) *ArtifactService {
return &ArtifactService{
registryRepo: registryRepo,
ociClient: ociClient,
}
}
// GetRegistry 获取 Registry 信息
func (s *ArtifactService) GetRegistry(ctx context.Context, registryID string) (*entity.Registry, error) {
return s.registryRepo.GetByID(ctx, registryID)
}
// ListRepositories 列出 Registry 中的所有 repositories
func (s *ArtifactService) ListRepositories(ctx context.Context, registryID string) ([]string, error) {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return nil, entity.ErrRegistryNotFound
}
return s.ociClient.ListRepositories(ctx, registry)
}
// ListArtifacts 列出 repository 中的所有 artifacts
func (s *ArtifactService) ListArtifacts(ctx context.Context, registryID, repository, mediaTypeFilter string) ([]*entity.Artifact, error) {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return nil, entity.ErrRegistryNotFound
}
return s.ociClient.ListArtifacts(ctx, registry, repository, mediaTypeFilter)
}
// GetArtifact 获取 artifact 详情
func (s *ArtifactService) GetArtifact(ctx context.Context, registryID, repository, reference string) (*entity.Artifact, error) {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return nil, entity.ErrRegistryNotFound
}
return s.ociClient.GetArtifact(ctx, registry, repository, reference)
}
// GetValuesSchema 获取 Helm Chart 的 values schema
func (s *ArtifactService) GetValuesSchema(ctx context.Context, registryID, repository, reference string) (string, error) {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return "", entity.ErrRegistryNotFound
}
return s.ociClient.GetValuesSchema(ctx, registry, repository, reference)
}
// PullArtifact 下载 artifact
func (s *ArtifactService) PullArtifact(ctx context.Context, registryID, repository, reference, destPath string) error {
registry, err := s.registryRepo.GetByID(ctx, registryID)
if err != nil {
return entity.ErrRegistryNotFound
}
return s.ociClient.PullArtifact(ctx, registry, repository, reference, destPath)
}

View File

@ -0,0 +1,165 @@
package service
import (
"context"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// AuthService 认证领域服务
type AuthService struct {
userRepo repository.UserRepository
passwordHasher PasswordHasher
tokenGenerator TokenGenerator
}
// PasswordHasher 密码哈希接口
type PasswordHasher interface {
Hash(password string) (string, error)
Verify(password, hash string) error
}
// TokenGenerator Token 生成器接口
type TokenGenerator interface {
Generate(userID, username string) (accessToken, refreshToken string, err error)
Verify(token string) (userID, username string, err error)
VerifyWithIssuedAt(token string) (userID, username string, issuedAt int64, err error)
Refresh(refreshToken string) (newAccessToken string, err error)
}
// NewAuthService 创建认证服务
func NewAuthService(
userRepo repository.UserRepository,
passwordHasher PasswordHasher,
tokenGenerator TokenGenerator,
) *AuthService {
return &AuthService{
userRepo: userRepo,
passwordHasher: passwordHasher,
tokenGenerator: tokenGenerator,
}
}
// Register 注册新用户(仅需用户名和密码,邮箱将自动补全)
func (s *AuthService) Register(ctx context.Context, username, password string) (*entity.User, error) {
// 检查用户是否已存在
existingUser, _ := s.userRepo.GetByUsername(ctx, username)
if existingUser != nil {
return nil, entity.ErrUserExists
}
// 哈希密码
passwordHash, err := s.passwordHasher.Hash(password)
if err != nil {
return nil, err
}
// 默认生成占位邮箱,避免数据库约束失败
email := username + "@local.ocdp"
// 创建用户
user := entity.NewUser(username, passwordHash, email)
user.ID = uuid.New().String()
if err := user.Validate(); err != nil {
return nil, err
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
// Login 用户登录
func (s *AuthService) Login(ctx context.Context, username, password string) (accessToken, refreshToken string, err error) {
// 查找用户
user, err := s.userRepo.GetByUsername(ctx, username)
if err != nil {
return "", "", entity.ErrUserNotFound
}
// 验证密码
if err := s.passwordHasher.Verify(password, user.PasswordHash); err != nil {
return "", "", entity.ErrInvalidPassword
}
// 生成 Token
accessToken, refreshToken, err = s.tokenGenerator.Generate(user.ID, user.Username)
if err != nil {
return "", "", err
}
return accessToken, refreshToken, nil
}
// RefreshToken 刷新 Token
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (string, error) {
return s.tokenGenerator.Refresh(refreshToken)
}
// GetUserByID 根据 ID 获取用户
func (s *AuthService) GetUserByID(ctx context.Context, id string) (*entity.User, error) {
return s.userRepo.GetByID(ctx, id)
}
// VerifyAccessToken 验证 Access Token包括 revoked_after 检查)
func (s *AuthService) VerifyAccessToken(ctx context.Context, token string) (userID, username string, err error) {
// 1. JWT 自验证
userID, username, issuedAt, err := s.tokenGenerator.VerifyWithIssuedAt(token)
if err != nil {
return "", "", err
}
// 2. 检查用户级别的撤销时间
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return "", "", entity.ErrUserNotFound
}
// 3. 如果 Token 签发时间早于 revoked_after则失效
if issuedAt < user.RevokedAfter.Unix() {
return "", "", entity.ErrTokenRevoked
}
return userID, username, nil
}
// ChangePassword 修改密码(会触发全局登出)
func (s *AuthService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error {
// 1. 获取用户
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return entity.ErrUserNotFound
}
// 2. 验证旧密码
if err := s.passwordHasher.Verify(oldPassword, user.PasswordHash); err != nil {
return entity.ErrInvalidPassword
}
// 3. 哈希新密码
newPasswordHash, err := s.passwordHasher.Hash(newPassword)
if err != nil {
return err
}
// 4. 更新密码(会自动触发 revoked_after 更新)
user.UpdatePassword(newPasswordHash)
// 5. 保存到数据库
return s.userRepo.Update(ctx, user)
}
// ForceLogoutAll 强制全局登出(管理员操作)
func (s *AuthService) ForceLogoutAll(ctx context.Context, userID string) error {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return entity.ErrUserNotFound
}
user.RevokeAllTokens()
return s.userRepo.Update(ctx, user)
}

View File

@ -0,0 +1,77 @@
package service
import (
"context"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// ClusterService 集群管理领域服务
type ClusterService struct {
clusterRepo repository.ClusterRepository
}
// NewClusterService 创建集群服务
func NewClusterService(clusterRepo repository.ClusterRepository) *ClusterService {
return &ClusterService{
clusterRepo: clusterRepo,
}
}
// CreateCluster 创建新集群
func (s *ClusterService) CreateCluster(ctx context.Context, cluster *entity.Cluster) error {
// 生成 ID
cluster.ID = uuid.New().String()
// 验证
if err := cluster.Validate(); err != nil {
return err
}
// 检查是否已存在
existingCluster, _ := s.clusterRepo.GetByName(ctx, cluster.Name)
if existingCluster != nil {
return entity.ErrClusterExists
}
return s.clusterRepo.Create(ctx, cluster)
}
// GetCluster 获取集群
func (s *ClusterService) GetCluster(ctx context.Context, id string) (*entity.Cluster, error) {
return s.clusterRepo.GetByID(ctx, id)
}
// UpdateCluster 更新集群
func (s *ClusterService) UpdateCluster(ctx context.Context, cluster *entity.Cluster) error {
// 检查是否存在
_, err := s.clusterRepo.GetByID(ctx, cluster.ID)
if err != nil {
return entity.ErrClusterNotFound
}
// 验证
if err := cluster.Validate(); err != nil {
return err
}
return s.clusterRepo.Update(ctx, cluster)
}
// DeleteCluster 删除集群
func (s *ClusterService) DeleteCluster(ctx context.Context, id string) error {
// 检查是否存在
_, err := s.clusterRepo.GetByID(ctx, id)
if err != nil {
return entity.ErrClusterNotFound
}
return s.clusterRepo.Delete(ctx, id)
}
// ListClusters 列出所有集群
func (s *ClusterService) ListClusters(ctx context.Context) ([]*entity.Cluster, error) {
return s.clusterRepo.List(ctx)
}

View File

@ -0,0 +1,456 @@
package service
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// InstanceService Helm 实例管理领域服务
type InstanceService struct {
instanceRepo repository.InstanceRepository
clusterRepo repository.ClusterRepository
registryRepo repository.RegistryRepository
helmClient repository.HelmClient
ociClient repository.OCIClient
entryClient repository.InstanceEntryClient
}
// NewInstanceService 创建实例服务
func NewInstanceService(
instanceRepo repository.InstanceRepository,
clusterRepo repository.ClusterRepository,
registryRepo repository.RegistryRepository,
helmClient repository.HelmClient,
ociClient repository.OCIClient,
entryClient repository.InstanceEntryClient,
) *InstanceService {
return &InstanceService{
instanceRepo: instanceRepo,
clusterRepo: clusterRepo,
registryRepo: registryRepo,
helmClient: helmClient,
ociClient: ociClient,
entryClient: entryClient,
}
}
const chartCacheDir = "/tmp/charts"
func (s *InstanceService) chartArchivePath(instance *entity.Instance) string {
filename := fmt.Sprintf("%s-%s.tgz", instance.Chart, instance.Version)
return filepath.Join(chartCacheDir, filename)
}
func (s *InstanceService) downloadChart(ctx context.Context, registry *entity.Registry, instance *entity.Instance) error {
if err := os.MkdirAll(chartCacheDir, 0755); err != nil {
return fmt.Errorf("failed to ensure chart cache dir: %w", err)
}
chartPath := s.chartArchivePath(instance)
if err := s.ociClient.PullArtifact(ctx, registry, instance.Repository, instance.Version, chartPath); err != nil {
return fmt.Errorf("failed to download chart artifact: %w", err)
}
return nil
}
// CreateInstance 创建(安装)新实例
func (s *InstanceService) CreateInstance(ctx context.Context, instance *entity.Instance) error {
// 生成 ID
instance.ID = uuid.New().String()
// 验证
if err := instance.Validate(); err != nil {
return err
}
// 检查集群是否存在
cluster, err := s.clusterRepo.GetByID(ctx, instance.ClusterID)
if err != nil {
return entity.ErrClusterNotFound
}
// 检查 Registry 是否存在
registry, err := s.registryRepo.GetByID(ctx, instance.RegistryID)
if err != nil {
return entity.ErrRegistryNotFound
}
// 检查实例是否已存在
existingInstance, _ := s.instanceRepo.GetByClusterAndName(ctx, instance.ClusterID, instance.Name)
if existingInstance != nil {
return entity.ErrInstanceExists
}
instance.BeginOperation(entity.OperationInstall, "Preparing installation")
// 先写入数据库,记录 pending 状态
if err := s.instanceRepo.Create(ctx, instance); err != nil {
return err
}
// 下载 chart artifact 供 Helm 使用
if err := s.downloadChart(ctx, registry, instance); err != nil {
instance.MarkFailure("Failed to download chart", err)
_ = s.instanceRepo.Update(ctx, instance)
return err
}
// 异步执行 Helm 安装并监控状态
go s.executeAndSyncInstall(context.Background(), instance.ID, cluster, registry, instance)
// 立即返回,状态同步由后台任务处理
return nil
}
// GetInstance 获取实例
func (s *InstanceService) GetInstance(ctx context.Context, id string) (*entity.Instance, error) {
return s.instanceRepo.GetByID(ctx, id)
}
// GetInstanceStatus 获取实例实时状态
func (s *InstanceService) GetInstanceStatus(ctx context.Context, id string) (*entity.Instance, error) {
// 从数据库获取基本信息
instance, err := s.instanceRepo.GetByID(ctx, id)
if err != nil {
return nil, entity.ErrInstanceNotFound
}
// 获取集群信息
cluster, err := s.clusterRepo.GetByID(ctx, instance.ClusterID)
if err != nil {
return nil, entity.ErrClusterNotFound
}
// 从 Helm 获取实时状态
liveStatus, err := s.helmClient.GetStatus(ctx, cluster, instance.Name, instance.Namespace)
if err != nil {
return instance, err // 返回数据库中的信息,但标记错误
}
// 合并实时状态
instance.Status = liveStatus.Status
instance.Revision = liveStatus.Revision
return instance, nil
}
// UpdateInstance 更新(升级)实例
func (s *InstanceService) UpdateInstance(ctx context.Context, instance *entity.Instance) error {
// 检查实例是否存在
existingInstance, err := s.instanceRepo.GetByID(ctx, instance.ID)
if err != nil {
return entity.ErrInstanceNotFound
}
// 获取集群信息
cluster, err := s.clusterRepo.GetByID(ctx, existingInstance.ClusterID)
if err != nil {
return entity.ErrClusterNotFound
}
// 获取 Registry 信息
registry, err := s.registryRepo.GetByID(ctx, instance.RegistryID)
if err != nil {
return entity.ErrRegistryNotFound
}
instance.BeginOperation(entity.OperationUpgrade, "Pending upgrade")
if err := s.instanceRepo.Update(ctx, instance); err != nil {
return err
}
// 下载所需 Chart
if err := s.downloadChart(ctx, registry, instance); err != nil {
instance.MarkFailure("Failed to download chart", err)
_ = s.instanceRepo.Update(ctx, instance)
return err
}
// 异步执行 Helm 升级并监控状态
go s.executeAndSyncUpgrade(context.Background(), instance.ID, cluster, registry, instance)
// 立即返回,状态同步由后台任务处理
return nil
}
// DeleteInstance 删除(卸载)实例
func (s *InstanceService) DeleteInstance(ctx context.Context, id string) error {
// 检查实例是否存在
instance, err := s.instanceRepo.GetByID(ctx, id)
if err != nil {
return entity.ErrInstanceNotFound
}
// 获取集群信息
cluster, err := s.clusterRepo.GetByID(ctx, instance.ClusterID)
if err != nil {
return entity.ErrClusterNotFound
}
instance.BeginOperation(entity.OperationDelete, "Pending uninstall")
if err := s.instanceRepo.Update(ctx, instance); err != nil {
return err
}
// 异步执行 Helm 卸载并监控状态
go s.executeAndSyncUninstall(context.Background(), instance.ID, cluster, instance.Name, instance.Namespace)
// 立即返回,状态同步由后台任务处理
return nil
}
// RollbackInstance 回滚实例
func (s *InstanceService) RollbackInstance(ctx context.Context, id string, revision int) error {
// 检查实例是否存在
instance, err := s.instanceRepo.GetByID(ctx, id)
if err != nil {
return entity.ErrInstanceNotFound
}
// 获取集群信息
cluster, err := s.clusterRepo.GetByID(ctx, instance.ClusterID)
if err != nil {
return entity.ErrClusterNotFound
}
instance.BeginOperation(entity.OperationRollback, fmt.Sprintf("Rolling back to revision %d", revision))
if err := s.instanceRepo.Update(ctx, instance); err != nil {
return err
}
// 异步执行 Helm 回滚并监控状态
go s.executeAndSyncRollback(context.Background(), instance.ID, cluster, instance.Name, instance.Namespace, revision)
// 立即返回,状态同步由后台任务处理
return nil
}
// GetInstanceHistory 获取实例历史
func (s *InstanceService) GetInstanceHistory(ctx context.Context, id string) ([]*entity.ReleaseHistory, error) {
// 检查实例是否存在
instance, err := s.instanceRepo.GetByID(ctx, id)
if err != nil {
return nil, entity.ErrInstanceNotFound
}
// 获取集群信息
cluster, err := s.clusterRepo.GetByID(ctx, instance.ClusterID)
if err != nil {
return nil, entity.ErrClusterNotFound
}
// 从 Helm 获取历史
return s.helmClient.GetHistory(ctx, cluster, instance.Name, instance.Namespace)
}
// ListInstancesByCluster 列出集群的所有实例
func (s *InstanceService) ListInstancesByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error) {
// 检查集群是否存在
_, err := s.clusterRepo.GetByID(ctx, clusterID)
if err != nil {
return nil, entity.ErrClusterNotFound
}
return s.instanceRepo.ListByCluster(ctx, clusterID)
}
// ListInstanceEntries 列出实例关联的入口信息Service / Ingress
func (s *InstanceService) ListInstanceEntries(ctx context.Context, clusterID, instanceID string) ([]*entity.InstanceEntry, error) {
instance, err := s.instanceRepo.GetByID(ctx, instanceID)
if err != nil {
return nil, entity.ErrInstanceNotFound
}
if instance.ClusterID != clusterID {
return nil, entity.ErrInstanceNotFound
}
cluster, err := s.clusterRepo.GetByID(ctx, clusterID)
if err != nil {
return nil, entity.ErrClusterNotFound
}
if s.entryClient == nil {
return nil, fmt.Errorf("instance entry client is not configured")
}
return s.entryClient.ListEntries(ctx, cluster, instance)
}
// executeAndSyncInstall 异步执行安装并监控状态
func (s *InstanceService) executeAndSyncInstall(ctx context.Context, instanceID string, cluster *entity.Cluster, registry *entity.Registry, instance *entity.Instance) {
// 执行 Helm 安装
if err := s.helmClient.Install(ctx, cluster, instance); err != nil {
// 更新实例状态为失败
instance, updateErr := s.instanceRepo.GetByID(ctx, instanceID)
if updateErr == nil && instance != nil {
instance.MarkFailure("Helm install failed", err)
_ = s.instanceRepo.Update(ctx, instance)
}
return
}
// 安装成功后,同步状态
s.syncInstanceStatus(ctx, instanceID, cluster, instance.Name, instance.Namespace, entity.OperationInstall)
}
// executeAndSyncUpgrade 异步执行升级并监控状态
func (s *InstanceService) executeAndSyncUpgrade(ctx context.Context, instanceID string, cluster *entity.Cluster, registry *entity.Registry, instance *entity.Instance) {
// 执行 Helm 升级
if err := s.helmClient.Upgrade(ctx, cluster, instance); err != nil {
// 更新实例状态为失败
instance, updateErr := s.instanceRepo.GetByID(ctx, instanceID)
if updateErr == nil && instance != nil {
instance.MarkFailure("Helm upgrade failed", err)
_ = s.instanceRepo.Update(ctx, instance)
}
return
}
// 升级成功后,同步状态
s.syncInstanceStatus(ctx, instanceID, cluster, instance.Name, instance.Namespace, entity.OperationUpgrade)
}
// executeAndSyncRollback 异步执行回滚并监控状态
func (s *InstanceService) executeAndSyncRollback(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string, revision int) {
// 执行 Helm 回滚
if err := s.helmClient.Rollback(ctx, cluster, releaseName, namespace, revision); err != nil {
// 更新实例状态为失败
instance, updateErr := s.instanceRepo.GetByID(ctx, instanceID)
if updateErr == nil && instance != nil {
instance.MarkFailure("Helm rollback failed", err)
_ = s.instanceRepo.Update(ctx, instance)
}
return
}
// 回滚成功后,同步状态
s.syncInstanceStatus(ctx, instanceID, cluster, releaseName, namespace, entity.OperationRollback)
}
// executeAndSyncUninstall 异步执行卸载并监控状态
func (s *InstanceService) executeAndSyncUninstall(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string) {
// 执行 Helm 卸载
err := s.helmClient.Uninstall(ctx, cluster, releaseName, namespace)
// 获取实例
instance, getErr := s.instanceRepo.GetByID(ctx, instanceID)
if getErr != nil {
return
}
if err != nil {
// 如果错误不是"未找到",则标记为失败
if !errors.Is(err, entity.ErrInstanceNotFound) {
instance.MarkFailure("Helm uninstall failed", err)
_ = s.instanceRepo.Update(ctx, instance)
} else {
// 如果未找到,说明已经卸载,直接删除数据库记录
_ = s.instanceRepo.Delete(ctx, instanceID)
}
return
}
// 卸载成功,标记为已卸载
instance.MarkSuccess(entity.StatusUninstalled, instance.Revision, "Instance uninstalled successfully")
_ = s.instanceRepo.Update(ctx, instance)
// 验证卸载是否完成:尝试获取状态,如果获取不到说明已卸载
time.Sleep(3 * time.Second)
_, statusErr := s.helmClient.GetStatus(ctx, cluster, releaseName, namespace)
if statusErr != nil {
// 无法获取状态,说明已卸载,删除数据库记录
_ = s.instanceRepo.Delete(ctx, instanceID)
} else {
// 仍然可以获取状态,可能还在卸载中,继续等待
// 设置状态为 uninstalled但不删除记录让用户手动删除或等待自动清理
instance.MarkSuccess(entity.StatusUninstalled, instance.Revision, "Uninstall in progress")
_ = s.instanceRepo.Update(ctx, instance)
}
}
// syncInstanceStatus 同步实例状态(定期检查 Helm 状态并更新数据库)
func (s *InstanceService) syncInstanceStatus(ctx context.Context, instanceID string, cluster *entity.Cluster, releaseName, namespace string, operation entity.InstanceOperation) {
maxAttempts := 30 // 最多尝试30次约5分钟
interval := 10 * time.Second // 每10秒检查一次
for i := 0; i < maxAttempts; i++ {
time.Sleep(interval)
// 获取数据库中的实例
instance, err := s.instanceRepo.GetByID(ctx, instanceID)
if err != nil {
// 实例不存在,停止同步
return
}
// 从 Helm 获取实时状态
liveStatus, err := s.helmClient.GetStatus(ctx, cluster, releaseName, namespace)
if err != nil {
// 如果获取状态失败,可能是还在部署中,继续等待
if i < maxAttempts-1 {
continue
}
// 最后一次尝试失败,标记为失败
instance.MarkFailure("Failed to get status from Helm", err)
_ = s.instanceRepo.Update(ctx, instance)
return
}
// 根据操作类型和 Helm 状态更新实例状态
shouldUpdate := false
switch operation {
case entity.OperationInstall:
// 安装操作:如果 Helm 状态是 deployed则更新为 deployed
if liveStatus.Status == entity.StatusDeployed {
instance.MarkSuccess(entity.StatusDeployed, liveStatus.Revision, "Instance deployed successfully")
shouldUpdate = true
} else if liveStatus.Status == entity.StatusFailed {
instance.MarkFailure("Installation failed", fmt.Errorf("Helm status: %s", liveStatus.Status))
shouldUpdate = true
}
case entity.OperationUpgrade:
// 升级操作:如果 Helm 状态是 deployed则更新为 deployed
if liveStatus.Status == entity.StatusDeployed {
instance.MarkSuccess(entity.StatusDeployed, liveStatus.Revision, "Instance upgraded successfully")
shouldUpdate = true
} else if liveStatus.Status == entity.StatusFailed {
instance.MarkFailure("Upgrade failed", fmt.Errorf("Helm status: %s", liveStatus.Status))
shouldUpdate = true
}
case entity.OperationRollback:
// 回滚操作:如果 Helm 状态是 deployed则更新为 deployed
if liveStatus.Status == entity.StatusDeployed {
instance.MarkSuccess(entity.StatusDeployed, liveStatus.Revision, "Instance rolled back successfully")
shouldUpdate = true
} else if liveStatus.Status == entity.StatusFailed {
instance.MarkFailure("Rollback failed", fmt.Errorf("Helm status: %s", liveStatus.Status))
shouldUpdate = true
}
}
// 如果状态已更新为最终状态,停止同步
if shouldUpdate {
_ = s.instanceRepo.Update(ctx, instance)
return
}
// 如果状态已经是最终状态deployed 或 failed停止同步
if instance.Status == entity.StatusDeployed || instance.Status == entity.StatusFailed {
return
}
}
// 超时,标记为失败
instance, err := s.instanceRepo.GetByID(ctx, instanceID)
if err == nil && instance != nil {
instance.MarkFailure("Operation timeout", fmt.Errorf("Status sync timeout after %d attempts", maxAttempts))
_ = s.instanceRepo.Update(ctx, instance)
}
}

View File

@ -0,0 +1,111 @@
package service
import (
"context"
"errors"
"testing"
persistencemock "github.com/ocdp/cluster-service/internal/adapter/output/persistence/mock"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
func TestDeleteInstanceIgnoresMissingRelease(t *testing.T) {
ctx := context.Background()
instanceRepo := persistencemock.NewInstanceRepositoryMock()
instance := &entity.Instance{
ID: "inst-1",
ClusterID: "cluster-1",
Name: "demo",
Namespace: "default",
}
if err := instanceRepo.Create(ctx, instance); err != nil {
t.Fatalf("failed to seed instance: %v", err)
}
cluster := &entity.Cluster{ID: "cluster-1", Name: "cluster", Host: "https://example.com"}
clusterRepo := &stubClusterRepo{cluster: cluster}
svc := NewInstanceService(
instanceRepo,
clusterRepo,
nil,
&stubHelmClient{uninstallErr: entity.ErrInstanceNotFound},
nil,
nil,
)
if err := svc.DeleteInstance(ctx, instance.ID); err != nil {
t.Fatalf("DeleteInstance returned error: %v", err)
}
if _, err := instanceRepo.GetByID(ctx, instance.ID); !errors.Is(err, entity.ErrInstanceNotFound) {
t.Fatalf("expected instance removed, got err=%v", err)
}
}
type stubClusterRepo struct {
cluster *entity.Cluster
}
func (s *stubClusterRepo) Create(ctx context.Context, cluster *entity.Cluster) error {
s.cluster = cluster
return nil
}
func (s *stubClusterRepo) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
if s.cluster != nil && s.cluster.ID == id {
return s.cluster, nil
}
return nil, entity.ErrClusterNotFound
}
func (*stubClusterRepo) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
return nil, entity.ErrClusterNotFound
}
func (*stubClusterRepo) Update(ctx context.Context, cluster *entity.Cluster) error { return nil }
func (*stubClusterRepo) Delete(ctx context.Context, id string) error { return nil }
func (*stubClusterRepo) List(ctx context.Context) ([]*entity.Cluster, error) { return nil, nil }
type stubHelmClient struct {
uninstallErr error
}
func (*stubHelmClient) Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
return nil
}
func (*stubHelmClient) Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
return nil
}
func (s *stubHelmClient) Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error {
return s.uninstallErr
}
func (*stubHelmClient) Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error {
return nil
}
func (*stubHelmClient) GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error) {
return nil, nil
}
func (*stubHelmClient) GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error) {
return nil, nil
}
func (*stubHelmClient) List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error) {
return nil, nil
}
func (*stubHelmClient) GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error) {
return nil, nil
}
var _ repository.ClusterRepository = (*stubClusterRepo)(nil)
var _ repository.HelmClient = (*stubHelmClient)(nil)

View File

@ -0,0 +1,102 @@
package service
import (
"context"
"fmt"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// MonitoringService 监控服务
type MonitoringService struct {
clusterRepo repository.ClusterRepository
metricsClient repository.MetricsClient
}
// NewMonitoringService 创建监控服务
func NewMonitoringService(
clusterRepo repository.ClusterRepository,
metricsClient repository.MetricsClient,
) *MonitoringService {
return &MonitoringService{
clusterRepo: clusterRepo,
metricsClient: metricsClient,
}
}
// GetClusterMonitoring 获取单个集群的监控信息
func (s *MonitoringService) GetClusterMonitoring(ctx context.Context, clusterID string) (*entity.ClusterMetrics, error) {
metrics, err := s.metricsClient.GetClusterMetrics(ctx, clusterID)
if err != nil {
return nil, fmt.Errorf("failed to get cluster metrics: %w", err)
}
return metrics, nil
}
// ListClusterMonitoring 获取所有集群的监控信息
func (s *MonitoringService) ListClusterMonitoring(ctx context.Context) ([]*entity.ClusterMetrics, error) {
// 获取所有集群
clusters, err := s.clusterRepo.List(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list clusters: %w", err)
}
// 获取每个集群的监控数据
result := make([]*entity.ClusterMetrics, 0, len(clusters))
for _, cluster := range clusters {
metrics, err := s.metricsClient.GetClusterMetrics(ctx, cluster.ID)
if err != nil {
// 如果某个集群获取失败,记录错误但继续
fmt.Printf("Warning: failed to get metrics for cluster %s: %v\n", cluster.ID, err)
// 返回基本信息
metrics = &entity.ClusterMetrics{
ClusterID: cluster.ID,
ClusterName: cluster.Name,
Status: "unknown",
}
}
result = append(result, metrics)
}
return result, nil
}
// GetMonitoringSummary 获取监控汇总信息
func (s *MonitoringService) GetMonitoringSummary(ctx context.Context) (*entity.MonitoringSummary, error) {
// 获取所有集群监控数据
monitoringList, err := s.ListClusterMonitoring(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list monitoring: %w", err)
}
// 统计汇总
summary := &entity.MonitoringSummary{
TotalClusters: len(monitoringList),
}
for _, m := range monitoringList {
switch m.Status {
case "healthy":
summary.HealthyClusters++
case "warning":
summary.WarningClusters++
case "error":
summary.ErrorClusters++
}
summary.TotalNodes += m.NodeCount
summary.TotalPods += m.PodCount
}
return summary, nil
}
// GetNodeMetrics 获取集群的节点指标
func (s *MonitoringService) GetNodeMetrics(ctx context.Context, clusterID string) ([]*entity.NodeMetrics, error) {
nodes, err := s.metricsClient.GetNodeMetrics(ctx, clusterID)
if err != nil {
return nil, fmt.Errorf("failed to get node metrics: %w", err)
}
return nodes, nil
}

View File

@ -0,0 +1,92 @@
package service
import (
"context"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// RegistryService Registry 管理领域服务
type RegistryService struct {
registryRepo repository.RegistryRepository
ociClient repository.OCIClient
}
// NewRegistryService 创建 Registry 服务
func NewRegistryService(
registryRepo repository.RegistryRepository,
ociClient repository.OCIClient,
) *RegistryService {
return &RegistryService{
registryRepo: registryRepo,
ociClient: ociClient,
}
}
// CreateRegistry 创建新 Registry
func (s *RegistryService) CreateRegistry(ctx context.Context, registry *entity.Registry) error {
// 生成 ID
registry.ID = uuid.New().String()
// 验证
if err := registry.Validate(); err != nil {
return err
}
// 检查是否已存在
existingRegistry, _ := s.registryRepo.GetByName(ctx, registry.Name)
if existingRegistry != nil {
return entity.ErrRegistryExists
}
return s.registryRepo.Create(ctx, registry)
}
// GetRegistry 获取 Registry
func (s *RegistryService) GetRegistry(ctx context.Context, id string) (*entity.Registry, error) {
return s.registryRepo.GetByID(ctx, id)
}
// UpdateRegistry 更新 Registry
func (s *RegistryService) UpdateRegistry(ctx context.Context, registry *entity.Registry) error {
// 检查是否存在
_, err := s.registryRepo.GetByID(ctx, registry.ID)
if err != nil {
return entity.ErrRegistryNotFound
}
// 验证
if err := registry.Validate(); err != nil {
return err
}
return s.registryRepo.Update(ctx, registry)
}
// DeleteRegistry 删除 Registry
func (s *RegistryService) DeleteRegistry(ctx context.Context, id string) error {
// 检查是否存在
_, err := s.registryRepo.GetByID(ctx, id)
if err != nil {
return entity.ErrRegistryNotFound
}
return s.registryRepo.Delete(ctx, id)
}
// ListRegistries 列出所有 Registries
func (s *RegistryService) ListRegistries(ctx context.Context) ([]*entity.Registry, error) {
return s.registryRepo.List(ctx)
}
// CheckHealth 检查 Registry 健康状态
func (s *RegistryService) CheckHealth(ctx context.Context, id string) error {
registry, err := s.registryRepo.GetByID(ctx, id)
if err != nil {
return entity.ErrRegistryNotFound
}
return s.ociClient.CheckHealth(ctx, registry)
}