ocdp v1
This commit is contained in:
171
backend/internal/domain/entity/artifact.go
Normal file
171
backend/internal/domain/entity/artifact.go
Normal file
@ -0,0 +1,171 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ArtifactType Artifact 类型
|
||||
type ArtifactType string
|
||||
|
||||
const (
|
||||
ArtifactTypeChart ArtifactType = "chart" // Helm Chart
|
||||
ArtifactTypeImage ArtifactType = "image" // Docker/OCI Image
|
||||
ArtifactTypeOther ArtifactType = "other" // Other types
|
||||
)
|
||||
|
||||
// Artifact OCI Artifact 领域实体
|
||||
type Artifact struct {
|
||||
RegistryID string
|
||||
Repository string
|
||||
Tag string
|
||||
Digest string
|
||||
Type ArtifactType
|
||||
Size int64
|
||||
MediaType string
|
||||
ConfigType string // Config layer 的 mediaType (用于更准确的类型判断)
|
||||
Annotations map[string]string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// Repository 仓库信息
|
||||
type Repository struct {
|
||||
RegistryID string
|
||||
Name string
|
||||
TagCount int
|
||||
}
|
||||
|
||||
// NewArtifact 创建新 Artifact
|
||||
func NewArtifact(registryID, repository, tag, digest string) *Artifact {
|
||||
return &Artifact{
|
||||
RegistryID: registryID,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
Annotations: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetType 设置 Artifact 类型(根据 mediaType 识别为 chart | image | other)
|
||||
// 已废弃:请使用 DetermineType() 方法,它提供更准确的类型判断
|
||||
func (a *Artifact) SetType(mediaType string) {
|
||||
lowerMediaType := strings.ToLower(strings.TrimSpace(mediaType))
|
||||
|
||||
containsAny := func(target string, keywords ...string) bool {
|
||||
for _, keyword := range keywords {
|
||||
if keyword != "" && strings.Contains(target, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case lowerMediaType == "":
|
||||
a.Type = ArtifactTypeOther
|
||||
case containsAny(lowerMediaType,
|
||||
"helm", "cncf.helm", "helm.chart", "helm+", "chart+json", "chart.v1", "helm-package", "helm.config",
|
||||
):
|
||||
a.Type = ArtifactTypeChart
|
||||
case containsAny(lowerMediaType,
|
||||
"docker", "vnd.docker", "docker.distribution", "docker.container.image",
|
||||
"vnd.oci", "oci.image", "opencontainers", "container.image",
|
||||
):
|
||||
a.Type = ArtifactTypeImage
|
||||
case strings.Contains(lowerMediaType, "image") || strings.Contains(lowerMediaType, "manifest") || strings.Contains(lowerMediaType, "container"):
|
||||
a.Type = ArtifactTypeImage
|
||||
default:
|
||||
a.Type = ArtifactTypeOther
|
||||
}
|
||||
}
|
||||
|
||||
// DetermineType 智能判断 Artifact 类型(综合多种信息)
|
||||
// 优先级:
|
||||
// 1. ConfigType (config.mediaType) - 最准确
|
||||
// 2. Annotations - 可能包含类型标注
|
||||
// 3. Repository 名称 - charts/ 前缀暗示
|
||||
// 4. MediaType - 兜底判断
|
||||
func (a *Artifact) DetermineType() {
|
||||
containsAny := func(target string, keywords ...string) bool {
|
||||
for _, keyword := range keywords {
|
||||
if keyword != "" && strings.Contains(target, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 1. 优先检查 ConfigType(最准确的判断方式)
|
||||
if a.ConfigType != "" {
|
||||
lowerConfigType := strings.ToLower(strings.TrimSpace(a.ConfigType))
|
||||
|
||||
// Helm Chart 的 config.mediaType
|
||||
if containsAny(lowerConfigType,
|
||||
"helm.config", "cncf.helm", "helm.chart", "chart.content",
|
||||
) {
|
||||
a.Type = ArtifactTypeChart
|
||||
return
|
||||
}
|
||||
|
||||
// Docker/OCI Image 的 config.mediaType
|
||||
if containsAny(lowerConfigType,
|
||||
"docker.container.image", "oci.image.config",
|
||||
) {
|
||||
a.Type = ArtifactTypeImage
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查 Annotations
|
||||
for key, value := range a.Annotations {
|
||||
lowerKey := strings.ToLower(key)
|
||||
lowerValue := strings.ToLower(value)
|
||||
|
||||
if containsAny(lowerKey, "helm", "chart") ||
|
||||
containsAny(lowerValue, "helm", "chart") {
|
||||
a.Type = ArtifactTypeChart
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查 Repository 名称(辅助判断)
|
||||
if strings.HasPrefix(strings.ToLower(a.Repository), "charts/") {
|
||||
// charts/ 开头的仓库很可能是 Helm Chart
|
||||
// 但需要结合 MediaType 进一步确认
|
||||
lowerMediaType := strings.ToLower(strings.TrimSpace(a.MediaType))
|
||||
|
||||
// 如果是 OCI manifest 格式,很可能是以 OCI 格式存储的 Helm Chart
|
||||
if strings.Contains(lowerMediaType, "oci.image.manifest") ||
|
||||
strings.Contains(lowerMediaType, "vnd.oci") {
|
||||
a.Type = ArtifactTypeChart
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 回退到基于 MediaType 的判断(兜底逻辑)
|
||||
lowerMediaType := strings.ToLower(strings.TrimSpace(a.MediaType))
|
||||
|
||||
switch {
|
||||
case lowerMediaType == "":
|
||||
a.Type = ArtifactTypeOther
|
||||
case containsAny(lowerMediaType,
|
||||
"helm", "cncf.helm", "helm.chart", "helm+", "chart+json", "chart.v1", "helm-package", "helm.config",
|
||||
):
|
||||
a.Type = ArtifactTypeChart
|
||||
case containsAny(lowerMediaType,
|
||||
"docker", "vnd.docker", "docker.distribution", "docker.container.image",
|
||||
):
|
||||
a.Type = ArtifactTypeImage
|
||||
case strings.Contains(lowerMediaType, "image") || strings.Contains(lowerMediaType, "manifest"):
|
||||
a.Type = ArtifactTypeImage
|
||||
default:
|
||||
a.Type = ArtifactTypeOther
|
||||
}
|
||||
}
|
||||
|
||||
// IsChart 判断是否为 Helm Chart
|
||||
func (a *Artifact) IsChart() bool {
|
||||
return a.Type == ArtifactTypeChart
|
||||
}
|
||||
|
||||
103
backend/internal/domain/entity/cluster.go
Normal file
103
backend/internal/domain/entity/cluster.go
Normal file
@ -0,0 +1,103 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cluster Kubernetes 集群领域实体
|
||||
type Cluster struct {
|
||||
ID string
|
||||
Name string
|
||||
Host string // Kubernetes API Server URL
|
||||
CAData string // Base64 encoded CA certificate
|
||||
CertData string // Base64 encoded client certificate
|
||||
KeyData string // Base64 encoded client key
|
||||
Token string // Bearer token (alternative to cert auth)
|
||||
Description string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewCluster 创建新集群
|
||||
func NewCluster(name, host string) *Cluster {
|
||||
now := time.Now()
|
||||
return &Cluster{
|
||||
Name: name,
|
||||
Host: host,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update 更新集群信息
|
||||
func (c *Cluster) Update(name, host, description string) {
|
||||
if name != "" {
|
||||
c.Name = name
|
||||
}
|
||||
if host != "" {
|
||||
c.Host = host
|
||||
}
|
||||
c.Description = description
|
||||
c.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SetCertAuth 设置证书认证
|
||||
func (c *Cluster) SetCertAuth(caData, certData, keyData string) {
|
||||
c.CAData = caData
|
||||
c.CertData = certData
|
||||
c.KeyData = keyData
|
||||
c.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SetTokenAuth 设置 Token 认证
|
||||
func (c *Cluster) SetTokenAuth(token string) {
|
||||
c.Token = token
|
||||
c.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Validate 验证集群配置
|
||||
func (c *Cluster) Validate() error {
|
||||
if c.Name == "" {
|
||||
return ErrInvalidClusterName
|
||||
}
|
||||
if c.Host == "" {
|
||||
return ErrInvalidClusterHost
|
||||
}
|
||||
// 必须有认证方式:证书或 Token
|
||||
if (c.CertData == "" || c.KeyData == "") && c.Token == "" {
|
||||
return ErrInvalidClusterAuth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKubeConfig 生成 kubeconfig 内容
|
||||
func (c *Cluster) GetKubeConfig() string {
|
||||
// 如果 CAData 已经包含完整的 kubeconfig,直接返回
|
||||
if len(c.CAData) > 100 && (c.CAData[:11] == "apiVersion:" || c.CAData[:5] == "kind:") {
|
||||
return c.CAData
|
||||
}
|
||||
|
||||
// 否则从证书数据生成 kubeconfig
|
||||
kubeconfig := `apiVersion: v1
|
||||
kind: Config
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: ` + c.CAData + `
|
||||
server: ` + c.Host + `
|
||||
name: ` + c.Name + `
|
||||
contexts:
|
||||
- context:
|
||||
cluster: ` + c.Name + `
|
||||
user: ` + c.Name + `
|
||||
name: ` + c.Name + `
|
||||
current-context: ` + c.Name + `
|
||||
users:
|
||||
- name: ` + c.Name + `
|
||||
user:
|
||||
client-certificate-data: ` + c.CertData + `
|
||||
client-key-data: ` + c.KeyData + `
|
||||
`
|
||||
|
||||
return kubeconfig
|
||||
}
|
||||
|
||||
40
backend/internal/domain/entity/errors.go
Normal file
40
backend/internal/domain/entity/errors.go
Normal file
@ -0,0 +1,40 @@
|
||||
package entity
|
||||
|
||||
import "errors"
|
||||
|
||||
// 领域错误定义
|
||||
var (
|
||||
// User errors
|
||||
ErrInvalidUsername = errors.New("invalid username")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
ErrTokenRevoked = errors.New("token has been revoked")
|
||||
|
||||
// Cluster errors
|
||||
ErrInvalidClusterName = errors.New("invalid cluster name")
|
||||
ErrInvalidClusterHost = errors.New("invalid cluster host")
|
||||
ErrInvalidClusterAuth = errors.New("invalid cluster authentication config")
|
||||
ErrClusterNotFound = errors.New("cluster not found")
|
||||
ErrClusterExists = errors.New("cluster already exists")
|
||||
|
||||
// Registry errors
|
||||
ErrInvalidRegistryName = errors.New("invalid registry name")
|
||||
ErrInvalidRegistryURL = errors.New("invalid registry URL")
|
||||
ErrRegistryNotFound = errors.New("registry not found")
|
||||
ErrRegistryExists = errors.New("registry already exists")
|
||||
|
||||
// Instance errors
|
||||
ErrInvalidClusterID = errors.New("invalid cluster ID")
|
||||
ErrInvalidInstanceName = errors.New("invalid instance name")
|
||||
ErrInvalidNamespace = errors.New("invalid namespace")
|
||||
ErrInvalidChart = errors.New("invalid chart name")
|
||||
ErrInvalidVersion = errors.New("invalid version")
|
||||
ErrInstanceNotFound = errors.New("instance not found")
|
||||
ErrInstanceExists = errors.New("instance already exists")
|
||||
|
||||
// Artifact errors
|
||||
ErrArtifactNotFound = errors.New("artifact not found")
|
||||
ErrRepositoryNotFound = errors.New("repository not found")
|
||||
ErrValuesSchemaNotFound = errors.New("values schema not found")
|
||||
)
|
||||
185
backend/internal/domain/entity/instance.go
Normal file
185
backend/internal/domain/entity/instance.go
Normal file
@ -0,0 +1,185 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// InstanceStatus 实例状态
|
||||
type InstanceStatus string
|
||||
|
||||
const (
|
||||
StatusDeployed InstanceStatus = "deployed"
|
||||
StatusUninstalled InstanceStatus = "uninstalled"
|
||||
StatusSuperseded InstanceStatus = "superseded"
|
||||
StatusFailed InstanceStatus = "failed"
|
||||
StatusPending InstanceStatus = "pending-install"
|
||||
StatusUpgrading InstanceStatus = "pending-upgrade"
|
||||
StatusRollingBack InstanceStatus = "pending-rollback"
|
||||
StatusTerminating InstanceStatus = "pending-delete"
|
||||
StatusUnknown InstanceStatus = "unknown"
|
||||
)
|
||||
|
||||
// InstanceOperation 实例操作类型
|
||||
type InstanceOperation string
|
||||
|
||||
const (
|
||||
OperationNone InstanceOperation = ""
|
||||
OperationInstall InstanceOperation = "install"
|
||||
OperationUpgrade InstanceOperation = "upgrade"
|
||||
OperationRollback InstanceOperation = "rollback"
|
||||
OperationDelete InstanceOperation = "delete"
|
||||
OperationSync InstanceOperation = "sync"
|
||||
)
|
||||
|
||||
// Instance Helm 应用实例领域实体
|
||||
type Instance struct {
|
||||
ID string
|
||||
ClusterID string
|
||||
Name string // Helm Release Name
|
||||
Namespace string
|
||||
RegistryID string
|
||||
Repository string // OCI Repository (e.g., charts/app)
|
||||
Chart string // Chart Name
|
||||
Version string // Chart Version
|
||||
Description string
|
||||
Values map[string]interface{} // Helm Values (JSON)
|
||||
ValuesYAML string // Helm Values (YAML format)
|
||||
Status InstanceStatus
|
||||
StatusReason string
|
||||
LastOperation InstanceOperation
|
||||
LastError string
|
||||
Revision int // Helm Release Revision
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewInstance 创建新实例
|
||||
func NewInstance(clusterID, name, namespace, registryID, repository, chart, version string) *Instance {
|
||||
now := time.Now()
|
||||
return &Instance{
|
||||
ClusterID: clusterID,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
RegistryID: registryID,
|
||||
Repository: repository,
|
||||
Chart: chart,
|
||||
Version: version,
|
||||
Status: StatusPending,
|
||||
StatusReason: "Pending install",
|
||||
LastOperation: OperationInstall,
|
||||
Revision: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// SetValues 设置 Helm Values
|
||||
func (i *Instance) SetValues(values map[string]interface{}) {
|
||||
i.Values = values
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SetValuesYAML 设置 YAML 格式的 Values
|
||||
func (i *Instance) SetValuesYAML(yaml string) {
|
||||
i.ValuesYAML = yaml
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// UpdateStatus 更新实例状态
|
||||
func (i *Instance) UpdateStatus(status InstanceStatus, revision int) {
|
||||
i.Status = status
|
||||
i.Revision = revision
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// BeginOperation 标记开始执行某个操作
|
||||
func (i *Instance) BeginOperation(op InstanceOperation, reason string) {
|
||||
i.LastOperation = op
|
||||
if pendingStatus := pendingStatusForOperation(op); pendingStatus != "" {
|
||||
i.Status = pendingStatus
|
||||
}
|
||||
if reason != "" {
|
||||
i.StatusReason = reason
|
||||
}
|
||||
i.LastError = ""
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// MarkSuccess 标记操作成功
|
||||
func (i *Instance) MarkSuccess(status InstanceStatus, revision int, reason string) {
|
||||
if status != "" {
|
||||
i.Status = status
|
||||
}
|
||||
if revision > 0 {
|
||||
i.Revision = revision
|
||||
}
|
||||
i.StatusReason = reason
|
||||
i.LastError = ""
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// MarkFailure 标记操作失败
|
||||
func (i *Instance) MarkFailure(reason string, err error) {
|
||||
i.Status = StatusFailed
|
||||
if reason != "" {
|
||||
i.StatusReason = reason
|
||||
}
|
||||
if err != nil {
|
||||
i.LastError = err.Error()
|
||||
}
|
||||
i.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
func pendingStatusForOperation(op InstanceOperation) InstanceStatus {
|
||||
switch op {
|
||||
case OperationInstall:
|
||||
return StatusPending
|
||||
case OperationUpgrade:
|
||||
return StatusUpgrading
|
||||
case OperationRollback:
|
||||
return StatusRollingBack
|
||||
case OperationDelete:
|
||||
return StatusTerminating
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade 升级实例
|
||||
func (i *Instance) Upgrade(version string, values map[string]interface{}) {
|
||||
i.Version = version
|
||||
if values != nil {
|
||||
i.Values = values
|
||||
}
|
||||
i.BeginOperation(OperationUpgrade, "Pending upgrade")
|
||||
}
|
||||
|
||||
// Validate 验证实例配置
|
||||
func (i *Instance) Validate() error {
|
||||
if i.ClusterID == "" {
|
||||
return ErrInvalidClusterID
|
||||
}
|
||||
if i.Name == "" {
|
||||
return ErrInvalidInstanceName
|
||||
}
|
||||
if i.Namespace == "" {
|
||||
return ErrInvalidNamespace
|
||||
}
|
||||
if i.Chart == "" {
|
||||
return ErrInvalidChart
|
||||
}
|
||||
if i.Version == "" {
|
||||
return ErrInvalidVersion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseHistory Helm Release 历史记录
|
||||
type ReleaseHistory struct {
|
||||
Revision int
|
||||
Updated time.Time
|
||||
Status InstanceStatus
|
||||
Chart string
|
||||
AppVersion string
|
||||
Description string
|
||||
}
|
||||
43
backend/internal/domain/entity/instance_entry.go
Normal file
43
backend/internal/domain/entity/instance_entry.go
Normal file
@ -0,0 +1,43 @@
|
||||
package entity
|
||||
|
||||
// InstanceEntry 描述实例关联的访问入口(Service、Ingress 等)
|
||||
type InstanceEntry struct {
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string
|
||||
Type string
|
||||
ClusterIP string
|
||||
ExternalIPs []string
|
||||
LoadBalancerIngress []string
|
||||
Ports []InstanceEntryPort
|
||||
Hosts []InstanceEntryHost
|
||||
TLS []InstanceEntryTLS
|
||||
}
|
||||
|
||||
// InstanceEntryPort Service 端口信息
|
||||
type InstanceEntryPort struct {
|
||||
Name string
|
||||
Protocol string
|
||||
Port int32
|
||||
TargetPort string
|
||||
NodePort int32
|
||||
}
|
||||
|
||||
// InstanceEntryHost Ingress Host 配置
|
||||
type InstanceEntryHost struct {
|
||||
Host string
|
||||
Paths []InstanceEntryPath
|
||||
}
|
||||
|
||||
// InstanceEntryPath Ingress path 详情
|
||||
type InstanceEntryPath struct {
|
||||
Path string
|
||||
ServiceName string
|
||||
ServicePort string
|
||||
}
|
||||
|
||||
// InstanceEntryTLS Ingress TLS 配置
|
||||
type InstanceEntryTLS struct {
|
||||
Hosts []string
|
||||
SecretName string
|
||||
}
|
||||
83
backend/internal/domain/entity/metrics.go
Normal file
83
backend/internal/domain/entity/metrics.go
Normal file
@ -0,0 +1,83 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// ClusterMetrics 集群监控指标
|
||||
type ClusterMetrics struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"` // healthy, warning, error, unknown
|
||||
Uptime string `json:"uptime"`
|
||||
NodeCount int `json:"node_count"`
|
||||
PodCount int `json:"pod_count"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
|
||||
// 集群级别资源汇总
|
||||
TotalCPU string `json:"total_cpu"` // 如 "8 cores"
|
||||
TotalMemory string `json:"total_memory"` // 如 "32 GB"
|
||||
TotalGPU int `json:"total_gpu"` // GPU 总数
|
||||
|
||||
UsedCPU string `json:"used_cpu"` // 如 "4.5 cores"
|
||||
UsedMemory string `json:"used_memory"` // 如 "16 GB"
|
||||
UsedGPU int `json:"used_gpu"` // 使用的 GPU 数
|
||||
|
||||
CPUUsage float64 `json:"cpu_usage"` // 百分比
|
||||
MemoryUsage float64 `json:"memory_usage"` // 百分比
|
||||
GPUUsage float64 `json:"gpu_usage"` // 百分比
|
||||
|
||||
// 单机资源最大值
|
||||
MaxNodeCPU string `json:"max_node_cpu"` // 单机最大CPU容量,如 "8 cores"
|
||||
MaxNodeMemory string `json:"max_node_memory"` // 单机最大内存容量,如 "32 GB"
|
||||
MaxNodeGPU int `json:"max_node_gpu"` // 单机最大GPU数量
|
||||
MaxNodeCPUUsage float64 `json:"max_node_cpu_usage"` // 单机最高CPU使用率
|
||||
MaxNodeMemUsage float64 `json:"max_node_mem_usage"` // 单机最高内存使用率
|
||||
MaxNodeGPUUsage float64 `json:"max_node_gpu_usage"` // 单机最高GPU使用率
|
||||
|
||||
// 节点列表(简化信息)
|
||||
Nodes []NodeMetrics `json:"nodes,omitempty"`
|
||||
}
|
||||
|
||||
// NodeMetrics 节点监控指标
|
||||
type NodeMetrics struct {
|
||||
NodeName string `json:"node_name"`
|
||||
Status string `json:"status"` // Ready, NotReady
|
||||
Role string `json:"role"` // control-plane, worker
|
||||
Age string `json:"age"`
|
||||
PodCount int `json:"pod_count"`
|
||||
|
||||
// CPU 资源
|
||||
CPUCapacity string `json:"cpu_capacity"` // 如 "4 cores"
|
||||
CPUAllocatable string `json:"cpu_allocatable"`
|
||||
CPUUsage string `json:"cpu_usage"`
|
||||
CPUPercent float64 `json:"cpu_percent"`
|
||||
|
||||
// 内存资源
|
||||
MemoryCapacity string `json:"memory_capacity"` // 如 "16 GB"
|
||||
MemoryAllocatable string `json:"memory_allocatable"`
|
||||
MemoryUsage string `json:"memory_usage"`
|
||||
MemoryPercent float64 `json:"memory_percent"`
|
||||
|
||||
// GPU 资源(如果有)
|
||||
GPUCapacity int `json:"gpu_capacity"` // GPU 总数
|
||||
GPUUsage int `json:"gpu_usage"` // 已使用的 GPU
|
||||
GPUPercent float64 `json:"gpu_percent"`
|
||||
GPUType string `json:"gpu_type,omitempty"` // GPU 型号,如 "NVIDIA-Tesla-T4"
|
||||
|
||||
// 其他信息
|
||||
OSImage string `json:"os_image,omitempty"`
|
||||
KernelVersion string `json:"kernel_version,omitempty"`
|
||||
ContainerRuntime string `json:"container_runtime,omitempty"`
|
||||
KubeletVersion string `json:"kubelet_version,omitempty"`
|
||||
}
|
||||
|
||||
// MonitoringSummary 监控汇总
|
||||
type MonitoringSummary struct {
|
||||
TotalClusters int `json:"total_clusters"`
|
||||
HealthyClusters int `json:"healthy_clusters"`
|
||||
WarningClusters int `json:"warning_clusters"`
|
||||
ErrorClusters int `json:"error_clusters"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
TotalPods int `json:"total_pods"`
|
||||
LastUpdate time.Time `json:"last_update"`
|
||||
}
|
||||
|
||||
60
backend/internal/domain/entity/registry.go
Normal file
60
backend/internal/domain/entity/registry.go
Normal file
@ -0,0 +1,60 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Registry OCI Registry 领域实体
|
||||
type Registry struct {
|
||||
ID string
|
||||
Name string
|
||||
URL string
|
||||
Description string
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool // 是否跳过 TLS 验证
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewRegistry 创建新 Registry
|
||||
func NewRegistry(name, url string) *Registry {
|
||||
now := time.Now()
|
||||
return &Registry{
|
||||
Name: name,
|
||||
URL: url,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Update 更新 Registry 信息
|
||||
func (r *Registry) Update(name, url, description string) {
|
||||
if name != "" {
|
||||
r.Name = name
|
||||
}
|
||||
if url != "" {
|
||||
r.URL = url
|
||||
}
|
||||
r.Description = description
|
||||
r.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// SetCredentials 设置认证凭据
|
||||
func (r *Registry) SetCredentials(username, password string) {
|
||||
r.Username = username
|
||||
r.Password = password
|
||||
r.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Validate 验证 Registry 配置
|
||||
func (r *Registry) Validate() error {
|
||||
if r.Name == "" {
|
||||
return ErrInvalidRegistryName
|
||||
}
|
||||
if r.URL == "" {
|
||||
return ErrInvalidRegistryURL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
54
backend/internal/domain/entity/user.go
Normal file
54
backend/internal/domain/entity/user.go
Normal file
@ -0,0 +1,54 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// User 用户领域实体
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
PasswordHash string
|
||||
Email string
|
||||
RevokedAfter time.Time // 全局 Token 撤销时间
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewUser 创建新用户
|
||||
func NewUser(username, passwordHash, email string) *User {
|
||||
now := time.Now()
|
||||
return &User{
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Email: email,
|
||||
RevokedAfter: time.Unix(0, 0), // 初始值:1970-01-01
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码(会触发全局登出)
|
||||
func (u *User) UpdatePassword(newPasswordHash string) {
|
||||
u.PasswordHash = newPasswordHash
|
||||
u.RevokedAfter = time.Now() // 撤销所有旧 Token
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// RevokeAllTokens 撤销所有 Token(强制全局登出)
|
||||
func (u *User) RevokeAllTokens() {
|
||||
u.RevokedAfter = time.Now()
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Validate 验证用户数据
|
||||
func (u *User) Validate() error {
|
||||
if u.Username == "" {
|
||||
return ErrInvalidUsername
|
||||
}
|
||||
if u.PasswordHash == "" {
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
28
backend/internal/domain/repository/cluster_repository.go
Normal file
28
backend/internal/domain/repository/cluster_repository.go
Normal file
@ -0,0 +1,28 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// ClusterRepository 集群仓储接口(Output Port)
|
||||
type ClusterRepository interface {
|
||||
// Create 创建集群
|
||||
Create(ctx context.Context, cluster *entity.Cluster) error
|
||||
|
||||
// GetByID 根据 ID 获取集群
|
||||
GetByID(ctx context.Context, id string) (*entity.Cluster, error)
|
||||
|
||||
// GetByName 根据名称获取集群
|
||||
GetByName(ctx context.Context, name string) (*entity.Cluster, error)
|
||||
|
||||
// Update 更新集群
|
||||
Update(ctx context.Context, cluster *entity.Cluster) error
|
||||
|
||||
// Delete 删除集群
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// List 列出所有集群
|
||||
List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
}
|
||||
|
||||
34
backend/internal/domain/repository/helm_client.go
Normal file
34
backend/internal/domain/repository/helm_client.go
Normal file
@ -0,0 +1,34 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// HelmClient Helm 客户端接口(Output Port)
|
||||
type HelmClient interface {
|
||||
// Install 安装 Helm Chart
|
||||
Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error
|
||||
|
||||
// Upgrade 升级 Helm Release
|
||||
Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error
|
||||
|
||||
// Uninstall 卸载 Helm Release
|
||||
Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error
|
||||
|
||||
// Rollback 回滚 Helm Release
|
||||
Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error
|
||||
|
||||
// GetStatus 获取 Release 状态
|
||||
GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error)
|
||||
|
||||
// GetHistory 获取 Release 历史
|
||||
GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error)
|
||||
|
||||
// List 列出集群中的所有 Releases
|
||||
List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error)
|
||||
|
||||
// GetValues 获取 Release 的 values
|
||||
GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
13
backend/internal/domain/repository/instance_entry_client.go
Normal file
13
backend/internal/domain/repository/instance_entry_client.go
Normal file
@ -0,0 +1,13 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// InstanceEntryClient 从 Kubernetes 集群中查询实例访问入口的接口
|
||||
type InstanceEntryClient interface {
|
||||
// ListEntries 返回指定实例(Helm Release)相关的 Service/Ingress 等入口信息
|
||||
ListEntries(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) ([]*entity.InstanceEntry, error)
|
||||
}
|
||||
31
backend/internal/domain/repository/instance_repository.go
Normal file
31
backend/internal/domain/repository/instance_repository.go
Normal file
@ -0,0 +1,31 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// InstanceRepository 实例仓储接口(Output Port)
|
||||
type InstanceRepository interface {
|
||||
// Create 创建实例
|
||||
Create(ctx context.Context, instance *entity.Instance) error
|
||||
|
||||
// GetByID 根据 ID 获取实例
|
||||
GetByID(ctx context.Context, id string) (*entity.Instance, error)
|
||||
|
||||
// GetByClusterAndName 根据集群 ID 和名称获取实例
|
||||
GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error)
|
||||
|
||||
// Update 更新实例
|
||||
Update(ctx context.Context, instance *entity.Instance) error
|
||||
|
||||
// Delete 删除实例
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// ListByCluster 列出指定集群的所有实例
|
||||
ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error)
|
||||
|
||||
// List 列出所有实例
|
||||
List(ctx context.Context) ([]*entity.Instance, error)
|
||||
}
|
||||
|
||||
17
backend/internal/domain/repository/metrics_client.go
Normal file
17
backend/internal/domain/repository/metrics_client.go
Normal file
@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// MetricsClient 定义获取集群监控指标的接口
|
||||
type MetricsClient interface {
|
||||
// GetClusterMetrics 获取集群的监控指标
|
||||
GetClusterMetrics(ctx context.Context, clusterID string) (*entity.ClusterMetrics, error)
|
||||
|
||||
// GetNodeMetrics 获取集群的节点指标
|
||||
GetNodeMetrics(ctx context.Context, clusterID string) ([]*entity.NodeMetrics, error)
|
||||
}
|
||||
|
||||
32
backend/internal/domain/repository/oci_client.go
Normal file
32
backend/internal/domain/repository/oci_client.go
Normal file
@ -0,0 +1,32 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// OCIClient OCI Registry 客户端接口(Output Port)
|
||||
type OCIClient interface {
|
||||
// ListRepositories 列出 Registry 中的所有 repositories
|
||||
ListRepositories(ctx context.Context, registry *entity.Registry) ([]string, error)
|
||||
|
||||
// ListArtifacts 列出指定 repository 的所有 artifacts
|
||||
// mediaTypeFilter: "all", "image", "chart", "other" - 使用模糊匹配过滤
|
||||
ListArtifacts(ctx context.Context, registry *entity.Registry, repository, mediaTypeFilter string) ([]*entity.Artifact, error)
|
||||
|
||||
// GetArtifact 获取指定 artifact 的详细信息
|
||||
GetArtifact(ctx context.Context, registry *entity.Registry, repository, reference string) (*entity.Artifact, error)
|
||||
|
||||
// GetValuesSchema 获取 Helm Chart 的 values schema
|
||||
GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error)
|
||||
|
||||
// PullArtifact 下载 artifact 到本地
|
||||
PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error
|
||||
|
||||
// PushArtifact 推送 artifact 到 Registry
|
||||
PushArtifact(ctx context.Context, registry *entity.Registry, repository, tag, sourcePath string) error
|
||||
|
||||
// CheckHealth 检查 Registry 健康状态
|
||||
CheckHealth(ctx context.Context, registry *entity.Registry) error
|
||||
}
|
||||
|
||||
28
backend/internal/domain/repository/registry_repository.go
Normal file
28
backend/internal/domain/repository/registry_repository.go
Normal file
@ -0,0 +1,28 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// RegistryRepository Registry 仓储接口(Output Port)
|
||||
type RegistryRepository interface {
|
||||
// Create 创建 Registry
|
||||
Create(ctx context.Context, registry *entity.Registry) error
|
||||
|
||||
// GetByID 根据 ID 获取 Registry
|
||||
GetByID(ctx context.Context, id string) (*entity.Registry, error)
|
||||
|
||||
// GetByName 根据名称获取 Registry
|
||||
GetByName(ctx context.Context, name string) (*entity.Registry, error)
|
||||
|
||||
// Update 更新 Registry
|
||||
Update(ctx context.Context, registry *entity.Registry) error
|
||||
|
||||
// Delete 删除 Registry
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// List 列出所有 Registries
|
||||
List(ctx context.Context) ([]*entity.Registry, error)
|
||||
}
|
||||
|
||||
28
backend/internal/domain/repository/user_repository.go
Normal file
28
backend/internal/domain/repository/user_repository.go
Normal file
@ -0,0 +1,28 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// UserRepository 用户仓储接口(Output Port)
|
||||
type UserRepository interface {
|
||||
// Create 创建用户
|
||||
Create(ctx context.Context, user *entity.User) error
|
||||
|
||||
// GetByID 根据 ID 获取用户
|
||||
GetByID(ctx context.Context, id string) (*entity.User, error)
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
GetByUsername(ctx context.Context, username string) (*entity.User, error)
|
||||
|
||||
// Update 更新用户
|
||||
Update(ctx context.Context, user *entity.User) error
|
||||
|
||||
// Delete 删除用户
|
||||
Delete(ctx context.Context, id string) error
|
||||
|
||||
// List 列出所有用户
|
||||
List(ctx context.Context) ([]*entity.User, error)
|
||||
}
|
||||
|
||||
80
backend/internal/domain/service/artifact_service.go
Normal file
80
backend/internal/domain/service/artifact_service.go
Normal 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)
|
||||
}
|
||||
|
||||
165
backend/internal/domain/service/auth_service.go
Normal file
165
backend/internal/domain/service/auth_service.go
Normal 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)
|
||||
}
|
||||
77
backend/internal/domain/service/cluster_service.go
Normal file
77
backend/internal/domain/service/cluster_service.go
Normal 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)
|
||||
}
|
||||
|
||||
456
backend/internal/domain/service/instance_service.go
Normal file
456
backend/internal/domain/service/instance_service.go
Normal 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)
|
||||
}
|
||||
}
|
||||
111
backend/internal/domain/service/instance_service_test.go
Normal file
111
backend/internal/domain/service/instance_service_test.go
Normal 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)
|
||||
102
backend/internal/domain/service/monitoring_service.go
Normal file
102
backend/internal/domain/service/monitoring_service.go
Normal 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
|
||||
}
|
||||
|
||||
92
backend/internal/domain/service/registry_service.go
Normal file
92
backend/internal/domain/service/registry_service.go
Normal 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user