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,44 @@
package dto
// RepositoryListResponse Repository 列表响应
type RepositoryListResponse struct {
RegistryID string `json:"registryId"`
RegistryURL string `json:"registryUrl"`
Repositories []string `json:"repositories"`
Total int `json:"total"`
CatalogSupported bool `json:"catalogSupported"` // Whether _catalog API is supported
Source string `json:"source"` // Data source: "catalog" | "preconfigured" | "unavailable"
Message string `json:"message,omitempty"` // User-friendly message
}
// ArtifactResponse Artifact 响应(简化版本,只包含核心字段)
type ArtifactResponse struct {
RepositoryName string `json:"repositoryName"`
Tag string `json:"tag"`
Digest string `json:"digest"`
Type string `json:"type"` // chart | image | other
Size int64 `json:"size"`
CreatedAt string `json:"createdAt"`
}
// TagResponse Tag 响应(前端期望的扁平化结构)
type TagResponse struct {
RepositoryName string `json:"repositoryName"` // Repository name
Tag string `json:"tag"` // Tag name (e.g. "1.0.0", "latest")
Type string `json:"type"` // Artifact type: chart, image, other
MediaType string `json:"mediaType,omitempty"`
Size int64 `json:"size"` // Artifact size (bytes)
}
// ArtifactListResponse Artifact 列表响应(包装格式,用于详细接口)
type ArtifactListResponse struct {
RepositoryName string `json:"repositoryName"`
Artifacts []*ArtifactResponse `json:"artifacts"`
Total int `json:"total"`
}
// ValuesSchemaResponse Values Schema 响应
type ValuesSchemaResponse struct {
Schema string `json:"schema"`
}

View File

@ -0,0 +1,35 @@
package dto
// RegisterRequest 用户注册请求
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
// LoginRequest 用户登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// RefreshTokenRequest 刷新 Token 请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refreshToken" binding:"required"`
}
// AuthResponse 认证响应
type AuthResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
UserID string `json:"userId"`
Username string `json:"username"`
}
// UserResponse 用户信息响应
type UserResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

View File

@ -0,0 +1,82 @@
package dto
// CreateClusterRequest 创建集群请求
type CreateClusterRequest struct {
Name string `json:"name" binding:"required"`
Host string `json:"host" binding:"required"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
}
// UpdateClusterRequest 更新集群请求
type UpdateClusterRequest struct {
Name string `json:"name"`
Host string `json:"host"`
CAData string `json:"caData"`
CADataAlt string `json:"ca_data"`
CertData string `json:"certData"`
CertDataAlt string `json:"cert_data"`
KeyData string `json:"keyData"`
KeyDataAlt string `json:"key_data"`
Token string `json:"token"`
Description string `json:"description"`
}
// Normalize 将多种命名风格的字段合并到统一字段
func (r *CreateClusterRequest) Normalize() {
if r.CAData == "" {
r.CAData = r.CADataAlt
}
if r.CertData == "" {
r.CertData = r.CertDataAlt
}
if r.KeyData == "" {
r.KeyData = r.KeyDataAlt
}
}
// Normalize 将多种命名风格的字段合并到统一字段
func (r *UpdateClusterRequest) Normalize() {
if r.CAData == "" {
r.CAData = r.CADataAlt
}
if r.CertData == "" {
r.CertData = r.CertDataAlt
}
if r.KeyData == "" {
r.KeyData = r.KeyDataAlt
}
}
// ClusterResponse 集群响应(敏感数据已脱敏)
type ClusterResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Description string `json:"description"`
// 认证配置状态(不返回实际证书数据,仅返回是否已配置)
HasCAData bool `json:"hasCaData"`
HasCertData bool `json:"hasCertData"`
HasKeyData bool `json:"hasKeyData"`
HasToken bool `json:"hasToken"`
// 脱敏数据(仅用于前端显示,实际值为掩码)
CAData string `json:"caData,omitempty"` // 脱敏显示(••••••••)
CertData string `json:"certData,omitempty"` // 脱敏显示(••••••••)
KeyData string `json:"keyData,omitempty"` // 脱敏显示(••••••••)
Token string `json:"token,omitempty"` // 脱敏显示(••••••••)
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// ClusterHealthResponse 集群健康状态响应
type ClusterHealthResponse struct {
Healthy bool `json:"healthy"`
Message string `json:"message,omitempty"`
Version string `json:"version,omitempty"`
}

View File

@ -0,0 +1,63 @@
package dto
import (
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// ToRegistryResponse 转换 Registry 实体为响应 DTO脱敏
func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
response := &RegistryResponse{
ID: registry.ID,
Name: registry.Name,
URL: registry.URL,
Description: registry.Description,
Username: registry.Username,
Insecure: registry.Insecure,
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// 脱敏处理密码
if registry.Password != "" {
response.HasPassword = true
response.Password = crypto.MaskSensitiveData(registry.Password)
}
return response
}
// ToClusterResponse 转换 Cluster 实体为响应 DTO脱敏
func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
response := &ClusterResponse{
ID: cluster.ID,
Name: cluster.Name,
Host: cluster.Host,
Description: cluster.Description,
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
// 设置认证配置状态标志
response.HasCAData = cluster.CAData != ""
response.HasCertData = cluster.CertData != ""
response.HasKeyData = cluster.KeyData != ""
response.HasToken = cluster.Token != ""
// 脱敏处理敏感数据(仅显示掩码)
if cluster.CAData != "" {
response.CAData = crypto.MaskSensitiveData(cluster.CAData)
}
if cluster.CertData != "" {
response.CertData = crypto.MaskSensitiveData(cluster.CertData)
}
if cluster.KeyData != "" {
response.KeyData = crypto.MaskSensitiveData(cluster.KeyData)
}
if cluster.Token != "" {
response.Token = crypto.MaskSensitiveData(cluster.Token)
}
return response
}

View File

@ -0,0 +1,15 @@
package dto
// ErrorResponse 错误响应
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message,omitempty"`
Code int `json:"code,omitempty"`
}
// SuccessResponse 成功响应
type SuccessResponse struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

View File

@ -0,0 +1,133 @@
package dto
// CreateInstanceRequest 创建实例请求
type CreateInstanceRequest struct {
Name string `json:"name" binding:"required"`
Namespace string `json:"namespace" binding:"required"`
RegistryID string `json:"registryId" binding:"required"`
RegistryIDAlt string `json:"registry_id"`
Repository string `json:"repository" binding:"required"`
Tag string `json:"tag" binding:"required"`
Description string `json:"description"`
Values map[string]interface{} `json:"values"`
ValuesYAML string `json:"valuesYaml"`
}
// UpdateInstanceRequest 更新实例请求
type UpdateInstanceRequest struct {
Version string `json:"version"`
Description string `json:"description"`
Values map[string]interface{} `json:"values"`
ValuesYAML string `json:"valuesYaml"`
}
// Normalize 将多种命名风格的字段合并到统一字段
func (r *CreateInstanceRequest) Normalize() {
if r.RegistryID == "" {
r.RegistryID = r.RegistryIDAlt
}
}
// RollbackInstanceRequest 回滚实例请求
type RollbackInstanceRequest struct {
Revision int `json:"revision" binding:"required"`
Wait bool `json:"wait"`
Timeout int `json:"timeout"` // seconds
}
// DeleteInstanceRequest 删除实例请求
type DeleteInstanceRequest struct {
KeepHistory bool `json:"keepHistory"`
Timeout int `json:"timeout"` // seconds
}
// InstanceResponse 实例响应
type InstanceResponse struct {
ID string `json:"id"`
ClusterID string `json:"clusterId"`
Name string `json:"name"`
Namespace string `json:"namespace"`
RegistryID string `json:"registryId"`
Repository string `json:"repository"`
Chart string `json:"chart"`
Version string `json:"version"`
Description string `json:"description"`
Status string `json:"status"`
StatusReason string `json:"statusReason,omitempty"`
LastOperation string `json:"lastOperation,omitempty"`
LastError string `json:"lastError,omitempty"`
Revision int `json:"revision"`
Values map[string]interface{} `json:"values,omitempty"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// InstanceStatusResponse 实例状态响应
type InstanceStatusResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Status string `json:"status"`
Revision int `json:"revision"`
Chart string `json:"chart"`
Version string `json:"version"`
UpdatedAt string `json:"updatedAt"`
}
// ReleaseHistoryResponse Release 历史响应
type ReleaseHistoryResponse struct {
Revision int `json:"revision"`
Updated string `json:"updated"`
Status string `json:"status"`
Chart string `json:"chart"`
AppVersion string `json:"appVersion"`
Description string `json:"description"`
}
// InstanceListResponse 实例列表响应
type InstanceListResponse struct {
Instances []*InstanceResponse `json:"instances"`
Total int `json:"total"`
}
// InstanceEntryPortResponse Service 端口响应
type InstanceEntryPortResponse struct {
Name string `json:"name,omitempty"`
Protocol string `json:"protocol"`
Port int32 `json:"port"`
TargetPort string `json:"targetPort,omitempty"`
NodePort int32 `json:"nodePort,omitempty"`
}
// InstanceEntryPathResponse Ingress path 响应
type InstanceEntryPathResponse struct {
Path string `json:"path"`
ServiceName string `json:"serviceName,omitempty"`
ServicePort string `json:"servicePort,omitempty"`
}
// InstanceEntryHostResponse Ingress host 响应
type InstanceEntryHostResponse struct {
Host string `json:"host"`
Paths []InstanceEntryPathResponse `json:"paths,omitempty"`
}
// InstanceEntryTLSResponse Ingress TLS 响应
type InstanceEntryTLSResponse struct {
Hosts []string `json:"hosts,omitempty"`
SecretName string `json:"secretName,omitempty"`
}
// InstanceEntryResponse 实例入口响应
type InstanceEntryResponse struct {
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Type string `json:"type,omitempty"`
ClusterIP string `json:"clusterIP,omitempty"`
ExternalIPs []string `json:"externalIPs,omitempty"`
LoadBalancerIngress []string `json:"loadBalancerIngress,omitempty"`
Ports []InstanceEntryPortResponse `json:"ports,omitempty"`
Hosts []InstanceEntryHostResponse `json:"hosts,omitempty"`
TLS []InstanceEntryTLSResponse `json:"tls,omitempty"`
}

View File

@ -0,0 +1,143 @@
package dto
import (
"time"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
// ClusterMetricsResponse 集群监控响应
type ClusterMetricsResponse struct {
ClusterID string `json:"clusterId"`
ClusterName string `json:"clusterName"`
Status string `json:"status"`
Uptime string `json:"uptime"`
NodeCount int `json:"nodeCount"`
PodCount int `json:"podCount"`
LastCheck time.Time `json:"lastCheck"`
TotalCPU string `json:"totalCpu"`
TotalMemory string `json:"totalMemory"`
TotalGPU int `json:"totalGpu"`
UsedCPU string `json:"usedCpu"`
UsedMemory string `json:"usedMemory"`
UsedGPU int `json:"usedGpu"`
CPUUsage float64 `json:"cpuUsage"`
MemoryUsage float64 `json:"memoryUsage"`
GPUUsage float64 `json:"gpuUsage"`
MaxNodeCPU string `json:"maxNodeCpu"`
MaxNodeMemory string `json:"maxNodeMemory"`
MaxNodeGPU int `json:"maxNodeGpu"`
MaxNodeCPUUsage float64 `json:"maxNodeCpuUsage"`
MaxNodeMemUsage float64 `json:"maxNodeMemUsage"`
MaxNodeGPUUsage float64 `json:"maxNodeGpuUsage"`
Nodes []NodeMetricsResponse `json:"nodes,omitempty"`
}
// NodeMetricsResponse 节点监控响应
type NodeMetricsResponse struct {
NodeName string `json:"nodeName"`
Status string `json:"status"`
Role string `json:"role"`
Age string `json:"age"`
PodCount int `json:"podCount"`
CPUCapacity string `json:"cpuCapacity"`
CPUAllocatable string `json:"cpuAllocatable"`
CPUUsage string `json:"cpuUsage"`
CPUPercent float64 `json:"cpuPercent"`
MemoryCapacity string `json:"memoryCapacity"`
MemoryAllocatable string `json:"memoryAllocatable"`
MemoryUsage string `json:"memoryUsage"`
MemoryPercent float64 `json:"memoryPercent"`
GPUCapacity int `json:"gpuCapacity"`
GPUUsage int `json:"gpuUsage"`
GPUPercent float64 `json:"gpuPercent"`
GPUType string `json:"gpuType,omitempty"`
OSImage string `json:"osImage,omitempty"`
KernelVersion string `json:"kernelVersion,omitempty"`
ContainerRuntime string `json:"containerRuntime,omitempty"`
KubeletVersion string `json:"kubeletVersion,omitempty"`
}
// MonitoringSummaryResponse 监控汇总响应
type MonitoringSummaryResponse struct {
TotalClusters int `json:"totalClusters"`
HealthyClusters int `json:"healthyClusters"`
WarningClusters int `json:"warningClusters"`
ErrorClusters int `json:"errorClusters"`
TotalNodes int `json:"totalNodes"`
TotalPods int `json:"totalPods"`
LastUpdate time.Time `json:"lastUpdate"`
}
// ToClusterMetricsResponse 转换为响应
func ToClusterMetricsResponse(m *entity.ClusterMetrics) *ClusterMetricsResponse {
resp := &ClusterMetricsResponse{
ClusterID: m.ClusterID,
ClusterName: m.ClusterName,
Status: m.Status,
Uptime: m.Uptime,
NodeCount: m.NodeCount,
PodCount: m.PodCount,
LastCheck: m.LastCheck,
TotalCPU: m.TotalCPU,
TotalMemory: m.TotalMemory,
TotalGPU: m.TotalGPU,
UsedCPU: m.UsedCPU,
UsedMemory: m.UsedMemory,
UsedGPU: m.UsedGPU,
CPUUsage: m.CPUUsage,
MemoryUsage: m.MemoryUsage,
GPUUsage: m.GPUUsage,
MaxNodeCPU: m.MaxNodeCPU,
MaxNodeMemory: m.MaxNodeMemory,
MaxNodeGPU: m.MaxNodeGPU,
MaxNodeCPUUsage: m.MaxNodeCPUUsage,
MaxNodeMemUsage: m.MaxNodeMemUsage,
MaxNodeGPUUsage: m.MaxNodeGPUUsage,
}
if len(m.Nodes) > 0 {
resp.Nodes = make([]NodeMetricsResponse, len(m.Nodes))
for i, node := range m.Nodes {
resp.Nodes[i] = NodeMetricsResponse{
NodeName: node.NodeName,
Status: node.Status,
Role: node.Role,
Age: node.Age,
PodCount: node.PodCount,
CPUCapacity: node.CPUCapacity,
CPUAllocatable: node.CPUAllocatable,
CPUUsage: node.CPUUsage,
CPUPercent: node.CPUPercent,
MemoryCapacity: node.MemoryCapacity,
MemoryAllocatable: node.MemoryAllocatable,
MemoryUsage: node.MemoryUsage,
MemoryPercent: node.MemoryPercent,
GPUCapacity: node.GPUCapacity,
GPUUsage: node.GPUUsage,
GPUPercent: node.GPUPercent,
GPUType: node.GPUType,
OSImage: node.OSImage,
KernelVersion: node.KernelVersion,
ContainerRuntime: node.ContainerRuntime,
KubeletVersion: node.KubeletVersion,
}
}
}
return resp
}
// ToMonitoringSummaryResponse 转换为汇总响应
func ToMonitoringSummaryResponse(s *entity.MonitoringSummary) *MonitoringSummaryResponse {
return &MonitoringSummaryResponse{
TotalClusters: s.TotalClusters,
HealthyClusters: s.HealthyClusters,
WarningClusters: s.WarningClusters,
ErrorClusters: s.ErrorClusters,
TotalNodes: s.TotalNodes,
TotalPods: s.TotalPods,
LastUpdate: s.LastUpdate,
}
}

View File

@ -0,0 +1,42 @@
package dto
// CreateRegistryRequest 创建 Registry 请求
type CreateRegistryRequest struct {
Name string `json:"name" binding:"required"`
URL string `json:"url" binding:"required"`
Username string `json:"username"`
Password string `json:"password"`
Description string `json:"description"`
Insecure bool `json:"insecure"`
}
// UpdateRegistryRequest 更新 Registry 请求
type UpdateRegistryRequest struct {
Name string `json:"name"`
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
Description string `json:"description"`
Insecure bool `json:"insecure"`
}
// RegistryResponse Registry 响应(敏感数据已脱敏)
type RegistryResponse struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Username string `json:"username,omitempty"` // 明文返回用户名(不敏感)
Password string `json:"password,omitempty"` // 脱敏显示(••••••••)
HasPassword bool `json:"hasPassword"` // 是否已设置密码
Insecure bool `json:"insecure"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
// RegistryHealthResponse Registry 健康状态响应
type RegistryHealthResponse struct {
Healthy bool `json:"healthy"`
Message string `json:"message,omitempty"`
}

View File

@ -0,0 +1,193 @@
package rest
import (
"errors"
"net/http"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// ArtifactHandler Artifact Handler
type ArtifactHandler struct {
artifactService *service.ArtifactService
}
// NewArtifactHandler 创建 Artifact Handler
func NewArtifactHandler(artifactService *service.ArtifactService) *ArtifactHandler {
return &ArtifactHandler{
artifactService: artifactService,
}
}
// ListRepositories 列出 Registry 中的所有 repositories
// @Summary 列出 Registry 中的所有 Repositories
// @Description 列出指定 Registry 中的所有 Repository
// @Tags Artifacts
// @Accept json
// @Produce json
// @Param registry_id path string true "Registry ID"
// @Success 200 {object} dto.RepositoryListResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /registries/{registry_id}/repositories [get]
func (h *ArtifactHandler) ListRepositories(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
repositories, err := h.artifactService.ListRepositories(r.Context(), registryID)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list repositories", err.Error())
return
}
// Get registry info for URL
registry, err := h.artifactService.GetRegistry(r.Context(), registryID)
registryURL := ""
if err == nil && registry != nil {
registryURL = registry.URL
}
// Determine source and message based on repository count
source := "catalog"
catalogSupported := true
message := ""
if len(repositories) == 0 {
source = "unavailable"
message = "No repositories found in this registry"
}
response := &dto.RepositoryListResponse{
RegistryID: registryID,
RegistryURL: registryURL,
Repositories: repositories,
Total: len(repositories),
CatalogSupported: catalogSupported,
Source: source,
Message: message,
}
respondJSON(w, http.StatusOK, response)
}
// ListArtifacts 列出 repository 中的所有 artifacts返回扁平化的 Tag 数组)
// @Summary 列出 Repository 中的所有 Artifacts
// @Description 列出指定 Repository 中的所有 Artifact支持按类型过滤
// @Tags Artifacts
// @Accept json
// @Produce json
// @Param registry_id path string true "Registry ID"
// @Param repository_name path string true "Repository Name (URL encoded, e.g. charts%2Fnginx)"
// @Param media_type query string false "过滤 Artifact 类型 (all, chart, image, other)" default(all)
// @Success 200 {array} dto.TagResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts [get]
func (h *ArtifactHandler) ListArtifacts(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
repositoryName := vars["repository_name"]
// 获取 mediaType 过滤参数(默认为 "all"
mediaTypeFilter := r.URL.Query().Get("media_type")
if mediaTypeFilter == "" {
mediaTypeFilter = "all"
}
artifacts, err := h.artifactService.ListArtifacts(r.Context(), registryID, repositoryName, mediaTypeFilter)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list artifacts", err.Error())
return
}
// 转换为前端期望的扁平化 Tag 数组
tagResponses := make([]*dto.TagResponse, 0, len(artifacts))
for _, artifact := range artifacts {
tagResponses = append(tagResponses, &dto.TagResponse{
RepositoryName: artifact.Repository,
Tag: artifact.Tag,
Type: string(artifact.Type),
MediaType: artifact.MediaType,
Size: artifact.Size,
})
}
// 直接返回数组,不包装
respondJSON(w, http.StatusOK, tagResponses)
}
// GetArtifact 获取 artifact 详情
// @Summary 获取 Artifact 详情
// @Description 获取指定 Artifact 的详细信息
// @Tags Artifacts
// @Accept json
// @Produce json
// @Param registry_id path string true "Registry ID"
// @Param repository_name path string true "Repository Name (URL encoded)"
// @Param reference path string true "Artifact Reference (tag or digest)"
// @Success 200 {object} dto.ArtifactResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference} [get]
func (h *ArtifactHandler) GetArtifact(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
repositoryName := vars["repository_name"]
reference := vars["reference"]
artifact, err := h.artifactService.GetArtifact(r.Context(), registryID, repositoryName, reference)
if err != nil {
respondError(w, http.StatusNotFound, "Artifact not found", err.Error())
return
}
response := &dto.ArtifactResponse{
RepositoryName: artifact.Repository,
Tag: artifact.Tag,
Digest: artifact.Digest,
Type: string(artifact.Type),
Size: artifact.Size,
CreatedAt: artifact.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
respondJSON(w, http.StatusOK, response)
}
// GetArtifactValuesSchema 获取 Helm Chart 的 values schema
// @Summary 获取 Helm Chart Values Schema
// @Description 获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型)
// @Tags Artifacts
// @Accept json
// @Produce json
// @Param registry_id path string true "Registry ID"
// @Param repository_name path string true "Repository Name (URL encoded)"
// @Param reference path string true "Artifact Reference (tag or digest)"
// @Success 200 {object} dto.ValuesSchemaResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema [get]
func (h *ArtifactHandler) GetArtifactValuesSchema(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
repositoryName := vars["repository_name"]
reference := vars["reference"]
schema, err := h.artifactService.GetValuesSchema(r.Context(), registryID, repositoryName, reference)
if err != nil {
switch {
case errors.Is(err, entity.ErrRegistryNotFound),
errors.Is(err, entity.ErrRepositoryNotFound),
errors.Is(err, entity.ErrArtifactNotFound),
errors.Is(err, entity.ErrValuesSchemaNotFound):
respondError(w, http.StatusNotFound, "Values schema not found", err.Error())
default:
respondError(w, http.StatusInternalServerError, "Failed to get values schema", err.Error())
}
return
}
response := &dto.ValuesSchemaResponse{
Schema: schema,
}
respondJSON(w, http.StatusOK, response)
}

View File

@ -0,0 +1,127 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// AuthHandler 认证 Handler
type AuthHandler struct {
authService *service.AuthService
}
// NewAuthHandler 创建认证 Handler
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
return &AuthHandler{
authService: authService,
}
}
// Register 用户注册
// @Summary 用户注册
// @Description 创建一个新的后台用户
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "注册信息"
// @Success 201 {object} dto.UserResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /auth/register [post]
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
var req dto.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 调用领域服务
user, err := h.authService.Register(r.Context(), req.Username, req.Password)
if err != nil {
respondError(w, http.StatusBadRequest, "Registration failed", err.Error())
return
}
// 返回响应
response := &dto.UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
respondJSON(w, http.StatusCreated, response)
}
// Login 用户登录
// @Summary 用户登录
// @Description 使用用户名和密码获取访问令牌
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.LoginRequest true "登录信息"
// @Success 200 {object} dto.AuthResponse
// @Failure 401 {object} dto.ErrorResponse
// @Router /auth/login [post]
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req dto.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 调用领域服务
accessToken, refreshToken, err := h.authService.Login(r.Context(), req.Username, req.Password)
if err != nil {
respondError(w, http.StatusUnauthorized, "Login failed", err.Error())
return
}
// 获取用户信息
// TODO: 从 token 解析用户信息或从服务获取
// 返回响应
response := &dto.AuthResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
Username: req.Username,
}
respondJSON(w, http.StatusOK, response)
}
// RefreshToken 刷新 Token
// @Summary 刷新访问令牌
// @Description 使用刷新令牌获取新的访问令牌
// @Tags Auth
// @Accept json
// @Produce json
// @Param request body dto.RefreshTokenRequest true "刷新令牌"
// @Success 200 {object} dto.AuthResponse
// @Failure 401 {object} dto.ErrorResponse
// @Router /auth/refresh [post]
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
var req dto.RefreshTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 调用领域服务
newAccessToken, err := h.authService.RefreshToken(r.Context(), req.RefreshToken)
if err != nil {
respondError(w, http.StatusUnauthorized, "Token refresh failed", err.Error())
return
}
// 返回响应
response := &dto.AuthResponse{
AccessToken: newAccessToken,
RefreshToken: req.RefreshToken,
}
respondJSON(w, http.StatusOK, response)
}

View File

@ -0,0 +1,221 @@
package rest
import (
"encoding/json"
"net/http"
"os"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// ClusterHandler 集群 Handler
type ClusterHandler struct {
clusterService *service.ClusterService
}
// NewClusterHandler 创建集群 Handler
func NewClusterHandler(clusterService *service.ClusterService) *ClusterHandler {
return &ClusterHandler{
clusterService: clusterService,
}
}
// CreateCluster 创建集群
// @Summary 创建集群
// @Description 创建一个新的 Kubernetes 集群配置
// @Tags Clusters
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.CreateClusterRequest true "集群信息"
// @Success 201 {object} dto.ClusterResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /clusters [post]
func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) {
var req dto.CreateClusterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
req.Normalize()
// 创建实体
cluster := entity.NewCluster(req.Name, req.Host)
cluster.Description = req.Description
if req.CertData != "" && req.KeyData != "" {
cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData)
} else if req.Token != "" {
cluster.SetTokenAuth(req.Token)
} else if os.Getenv("ADAPTER_MODE") == "mock" {
// Mock 模式:如果没有提供认证信息,使用默认的 Mock 证书
cluster.SetCertAuth(
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ0EgQ2VydGlmaWNhdGUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==",
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ2xpZW50IENlcnRpZmljYXRlCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=",
"LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNb2NrIFByaXZhdGUgS2V5Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t",
)
}
// 调用领域服务
if err := h.clusterService.CreateCluster(r.Context(), cluster); err != nil {
respondError(w, http.StatusBadRequest, "Failed to create cluster", err.Error())
return
}
// 返回响应
response := h.toClusterResponse(cluster)
respondJSON(w, http.StatusCreated, response)
}
// GetCluster 获取集群详情
// @Summary 获取集群详情
// @Tags Clusters
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 200 {object} dto.ClusterResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id} [get]
func (h *ClusterHandler) GetCluster(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
cluster, err := h.clusterService.GetCluster(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusNotFound, "Cluster not found", err.Error())
return
}
response := h.toClusterResponse(cluster)
respondJSON(w, http.StatusOK, response)
}
// GetAllClusters 获取所有集群
// @Summary 列出所有集群
// @Tags Clusters
// @Produce json
// @Security BearerAuth
// @Success 200 {array} dto.ClusterResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /clusters [get]
func (h *ClusterHandler) GetAllClusters(w http.ResponseWriter, r *http.Request) {
clusters, err := h.clusterService.ListClusters(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list clusters", err.Error())
return
}
responses := make([]*dto.ClusterResponse, 0, len(clusters))
for _, cluster := range clusters {
responses = append(responses, h.toClusterResponse(cluster))
}
respondJSON(w, http.StatusOK, responses)
}
// UpdateCluster 更新集群
// @Summary 更新集群
// @Tags Clusters
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param request body dto.UpdateClusterRequest true "更新内容"
// @Success 200 {object} dto.ClusterResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id} [put]
func (h *ClusterHandler) UpdateCluster(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
var req dto.UpdateClusterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
req.Normalize()
// 获取现有集群
cluster, err := h.clusterService.GetCluster(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusNotFound, "Cluster not found", err.Error())
return
}
// 更新字段
cluster.Update(req.Name, req.Host, req.Description)
if req.CertData != "" && req.KeyData != "" {
cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData)
} else if req.Token != "" {
cluster.SetTokenAuth(req.Token)
}
// 调用领域服务
if err := h.clusterService.UpdateCluster(r.Context(), cluster); err != nil {
respondError(w, http.StatusBadRequest, "Failed to update cluster", err.Error())
return
}
response := h.toClusterResponse(cluster)
respondJSON(w, http.StatusOK, response)
}
// DeleteCluster 删除集群
// @Summary 删除集群
// @Tags Clusters
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 204 {string} string "No Content"
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id} [delete]
func (h *ClusterHandler) DeleteCluster(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
if err := h.clusterService.DeleteCluster(r.Context(), clusterID); err != nil {
respondError(w, http.StatusNotFound, "Failed to delete cluster", err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
// GetClusterHealth 获取集群健康状态
// @Summary 获取集群健康状态
// @Tags Clusters
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 200 {object} dto.ClusterHealthResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/health [get]
func (h *ClusterHandler) GetClusterHealth(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
// 检查集群是否存在
_, err := h.clusterService.GetCluster(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusNotFound, "Cluster not found", err.Error())
return
}
// TODO: 实现真实的健康检查
response := &dto.ClusterHealthResponse{
Healthy: true,
Message: "Cluster is healthy",
Version: "v1.28.0",
}
respondJSON(w, http.StatusOK, response)
}
// toClusterResponse 将 Cluster 实体转换为响应 DTO脱敏
func (h *ClusterHandler) toClusterResponse(cluster *entity.Cluster) *dto.ClusterResponse {
return dto.ToClusterResponse(cluster)
}

View File

@ -0,0 +1,371 @@
package rest
import (
"encoding/json"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// InstanceHandler 实例 Handler
type InstanceHandler struct {
instanceService *service.InstanceService
}
// NewInstanceHandler 创建实例 Handler
func NewInstanceHandler(instanceService *service.InstanceService) *InstanceHandler {
return &InstanceHandler{
instanceService: instanceService,
}
}
// CreateInstance 创建实例
// @Summary 创建实例
// @Description 在指定集群上部署一个 artifact
// @Tags Instances
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param request body dto.CreateInstanceRequest true "实例配置"
// @Success 201 {object} dto.InstanceResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances [post]
func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
var req dto.CreateInstanceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
req.Normalize()
// Extract chart name from repository (e.g., "charts/nginx" -> "nginx")
chart := req.Repository
if lastSlash := strings.LastIndex(req.Repository, "/"); lastSlash != -1 {
chart = req.Repository[lastSlash+1:]
}
// 创建实体
instance := entity.NewInstance(
clusterID,
req.Name,
req.Namespace,
req.RegistryID,
req.Repository,
chart, // Extracted chart name
req.Tag, // Tag mapped to version
)
instance.Description = req.Description
if req.Values != nil {
instance.SetValues(req.Values)
}
if req.ValuesYAML != "" {
instance.SetValuesYAML(req.ValuesYAML)
}
// 调用领域服务
if err := h.instanceService.CreateInstance(r.Context(), instance); err != nil {
respondError(w, http.StatusBadRequest, "Failed to create instance", err.Error())
return
}
// 返回响应
response := &dto.InstanceResponse{
ID: instance.ID,
ClusterID: instance.ClusterID,
Name: instance.Name,
Namespace: instance.Namespace,
RegistryID: instance.RegistryID,
Repository: instance.Repository,
Chart: instance.Chart,
Version: instance.Version,
Description: instance.Description,
Status: string(instance.Status),
StatusReason: instance.StatusReason,
LastOperation: string(instance.LastOperation),
LastError: instance.LastError,
Revision: instance.Revision,
Values: instance.Values,
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
respondJSON(w, http.StatusCreated, response)
}
// GetInstance 获取实例详情
// @Summary 获取实例详情
// @Tags Instances
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param instance_id path string true "实例 ID"
// @Success 200 {object} dto.InstanceResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances/{instance_id} [get]
func (h *InstanceHandler) GetInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceID := vars["instance_id"]
instance, err := h.instanceService.GetInstance(r.Context(), instanceID)
if err != nil {
respondError(w, http.StatusNotFound, "Instance not found", err.Error())
return
}
response := &dto.InstanceResponse{
ID: instance.ID,
ClusterID: instance.ClusterID,
Name: instance.Name,
Namespace: instance.Namespace,
RegistryID: instance.RegistryID,
Repository: instance.Repository,
Chart: instance.Chart,
Version: instance.Version,
Description: instance.Description,
Status: string(instance.Status),
StatusReason: instance.StatusReason,
LastOperation: string(instance.LastOperation),
LastError: instance.LastError,
Revision: instance.Revision,
Values: instance.Values,
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
respondJSON(w, http.StatusOK, response)
}
// ListInstances 列出集群的所有实例
// @Summary 列出实例
// @Tags Instances
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 200 {object} dto.InstanceListResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances [get]
func (h *InstanceHandler) ListInstances(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
instances, err := h.instanceService.ListInstancesByCluster(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list instances", err.Error())
return
}
responses := make([]*dto.InstanceResponse, 0, len(instances))
for _, instance := range instances {
responses = append(responses, &dto.InstanceResponse{
ID: instance.ID,
ClusterID: instance.ClusterID,
Name: instance.Name,
Namespace: instance.Namespace,
RegistryID: instance.RegistryID,
Repository: instance.Repository,
Chart: instance.Chart,
Version: instance.Version,
Description: instance.Description,
Status: string(instance.Status),
StatusReason: instance.StatusReason,
LastOperation: string(instance.LastOperation),
LastError: instance.LastError,
Revision: instance.Revision,
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
response := &dto.InstanceListResponse{
Instances: responses,
Total: len(responses),
}
respondJSON(w, http.StatusOK, response)
}
// UpdateInstance 更新实例
// @Summary 更新实例
// @Tags Instances
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param instance_id path string true "实例 ID"
// @Param request body dto.UpdateInstanceRequest true "更新内容"
// @Success 200 {object} dto.InstanceResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances/{instance_id} [put]
func (h *InstanceHandler) UpdateInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceID := vars["instance_id"]
var req dto.UpdateInstanceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 获取现有实例
instance, err := h.instanceService.GetInstance(r.Context(), instanceID)
if err != nil {
respondError(w, http.StatusNotFound, "Instance not found", err.Error())
return
}
// 更新字段
if req.Version != "" {
instance.Upgrade(req.Version, req.Values)
}
if req.Description != "" {
instance.Description = req.Description
}
if req.ValuesYAML != "" {
instance.SetValuesYAML(req.ValuesYAML)
}
// 调用领域服务
if err := h.instanceService.UpdateInstance(r.Context(), instance); err != nil {
respondError(w, http.StatusBadRequest, "Failed to update instance", err.Error())
return
}
response := &dto.InstanceResponse{
ID: instance.ID,
ClusterID: instance.ClusterID,
Name: instance.Name,
Namespace: instance.Namespace,
RegistryID: instance.RegistryID,
Repository: instance.Repository,
Chart: instance.Chart,
Version: instance.Version,
Description: instance.Description,
Status: string(instance.Status),
StatusReason: instance.StatusReason,
LastOperation: string(instance.LastOperation),
LastError: instance.LastError,
Revision: instance.Revision,
Values: instance.Values,
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
respondJSON(w, http.StatusOK, response)
}
// DeleteInstance 删除实例
// @Summary 删除实例
// @Tags Instances
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param instance_id path string true "实例 ID"
// @Success 204 {string} string "No Content"
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances/{instance_id} [delete]
func (h *InstanceHandler) DeleteInstance(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
instanceID := vars["instance_id"]
if err := h.instanceService.DeleteInstance(r.Context(), instanceID); err != nil {
respondError(w, http.StatusNotFound, "Failed to delete instance", err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
// ListInstanceEntries 获取实例入口
// @Summary 获取实例 Service/Ingress 入口
// @Tags Instances
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Param instance_id path string true "实例 ID"
// @Success 200 {array} dto.InstanceEntryResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /clusters/{cluster_id}/instances/{instance_id}/entries [get]
func (h *InstanceHandler) ListInstanceEntries(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
instanceID := vars["instance_id"]
entries, err := h.instanceService.ListInstanceEntries(r.Context(), clusterID, instanceID)
if err != nil {
status := http.StatusInternalServerError
switch err {
case entity.ErrInstanceNotFound:
status = http.StatusNotFound
case entity.ErrClusterNotFound:
status = http.StatusNotFound
}
respondError(w, status, "Failed to list instance entries", err.Error())
return
}
responses := make([]*dto.InstanceEntryResponse, 0, len(entries))
for _, entry := range entries {
responses = append(responses, convertInstanceEntry(entry))
}
respondJSON(w, http.StatusOK, responses)
}
func convertInstanceEntry(entry *entity.InstanceEntry) *dto.InstanceEntryResponse {
portResponses := make([]dto.InstanceEntryPortResponse, 0, len(entry.Ports))
for _, port := range entry.Ports {
portResponses = append(portResponses, dto.InstanceEntryPortResponse{
Name: port.Name,
Protocol: port.Protocol,
Port: port.Port,
TargetPort: port.TargetPort,
NodePort: port.NodePort,
})
}
hostResponses := make([]dto.InstanceEntryHostResponse, 0, len(entry.Hosts))
for _, host := range entry.Hosts {
pathResponses := make([]dto.InstanceEntryPathResponse, 0, len(host.Paths))
for _, path := range host.Paths {
pathResponses = append(pathResponses, dto.InstanceEntryPathResponse{
Path: path.Path,
ServiceName: path.ServiceName,
ServicePort: path.ServicePort,
})
}
hostResponses = append(hostResponses, dto.InstanceEntryHostResponse{
Host: host.Host,
Paths: pathResponses,
})
}
tlsResponses := make([]dto.InstanceEntryTLSResponse, 0, len(entry.TLS))
for _, tls := range entry.TLS {
tlsResponses = append(tlsResponses, dto.InstanceEntryTLSResponse{
Hosts: tls.Hosts,
SecretName: tls.SecretName,
})
}
return &dto.InstanceEntryResponse{
Kind: entry.Kind,
Name: entry.Name,
Namespace: entry.Namespace,
Type: entry.Type,
ClusterIP: entry.ClusterIP,
ExternalIPs: entry.ExternalIPs,
LoadBalancerIngress: entry.LoadBalancerIngress,
Ports: portResponses,
Hosts: hostResponses,
TLS: tlsResponses,
}
}

View File

@ -0,0 +1,137 @@
package rest
import (
"net/http"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// MonitoringHandler 监控处理器
type MonitoringHandler struct {
monitoringService *service.MonitoringService
}
// NewMonitoringHandler 创建监控处理器
func NewMonitoringHandler(monitoringService *service.MonitoringService) *MonitoringHandler {
return &MonitoringHandler{
monitoringService: monitoringService,
}
}
// GetClusterMonitoring 获取单个集群的监控信息
// @Summary 获取集群监控
// @Tags Monitoring
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 200 {object} dto.ClusterMetricsResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /monitoring/clusters/{cluster_id} [get]
func (h *MonitoringHandler) GetClusterMonitoring(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
metrics, err := h.monitoringService.GetClusterMonitoring(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error())
return
}
response := dto.ToClusterMetricsResponse(metrics)
respondJSON(w, http.StatusOK, response)
}
// ListClusterMonitoring 获取所有集群的监控信息
// @Summary 列出集群监控
// @Tags Monitoring
// @Produce json
// @Security BearerAuth
// @Success 200 {array} dto.ClusterMetricsResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /monitoring/clusters [get]
func (h *MonitoringHandler) ListClusterMonitoring(w http.ResponseWriter, r *http.Request) {
monitoringList, err := h.monitoringService.ListClusterMonitoring(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error())
return
}
// 转换为响应格式
response := make([]*dto.ClusterMetricsResponse, len(monitoringList))
for i, m := range monitoringList {
response[i] = dto.ToClusterMetricsResponse(m)
}
respondJSON(w, http.StatusOK, response)
}
// GetMonitoringSummary 获取监控汇总信息
// @Summary 获取监控汇总
// @Tags Monitoring
// @Produce json
// @Security BearerAuth
// @Success 200 {object} dto.MonitoringSummaryResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /monitoring/summary [get]
func (h *MonitoringHandler) GetMonitoringSummary(w http.ResponseWriter, r *http.Request) {
summary, err := h.monitoringService.GetMonitoringSummary(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error())
return
}
response := dto.ToMonitoringSummaryResponse(summary)
respondJSON(w, http.StatusOK, response)
}
// GetNodeMetrics 获取集群的节点指标
// @Summary 获取节点指标
// @Tags Monitoring
// @Produce json
// @Security BearerAuth
// @Param cluster_id path string true "集群 ID"
// @Success 200 {array} dto.NodeMetricsResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /monitoring/clusters/{cluster_id}/nodes [get]
func (h *MonitoringHandler) GetNodeMetrics(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
clusterID := vars["cluster_id"]
nodes, err := h.monitoringService.GetNodeMetrics(r.Context(), clusterID)
if err != nil {
respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error())
return
}
// 转换为响应格式
response := make([]dto.NodeMetricsResponse, len(nodes))
for i, node := range nodes {
response[i] = dto.NodeMetricsResponse{
NodeName: node.NodeName,
Status: node.Status,
Role: node.Role,
Age: node.Age,
PodCount: node.PodCount,
CPUCapacity: node.CPUCapacity,
CPUAllocatable: node.CPUAllocatable,
CPUUsage: node.CPUUsage,
CPUPercent: node.CPUPercent,
MemoryCapacity: node.MemoryCapacity,
MemoryAllocatable: node.MemoryAllocatable,
MemoryUsage: node.MemoryUsage,
MemoryPercent: node.MemoryPercent,
GPUCapacity: node.GPUCapacity,
GPUUsage: node.GPUUsage,
GPUPercent: node.GPUPercent,
GPUType: node.GPUType,
OSImage: node.OSImage,
KernelVersion: node.KernelVersion,
ContainerRuntime: node.ContainerRuntime,
KubeletVersion: node.KubeletVersion,
}
}
respondJSON(w, http.StatusOK, response)
}

View File

@ -0,0 +1,201 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/service"
)
// RegistryHandler Registry Handler
type RegistryHandler struct {
registryService *service.RegistryService
}
// NewRegistryHandler 创建 Registry Handler
func NewRegistryHandler(registryService *service.RegistryService) *RegistryHandler {
return &RegistryHandler{
registryService: registryService,
}
}
// CreateRegistry 创建 Registry
// @Summary 创建 Registry
// @Description 新增 OCI Registry 配置
// @Tags Registries
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.CreateRegistryRequest true "Registry 信息"
// @Success 201 {object} dto.RegistryResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /registries [post]
func (h *RegistryHandler) CreateRegistry(w http.ResponseWriter, r *http.Request) {
var req dto.CreateRegistryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 创建实体
registry := entity.NewRegistry(req.Name, req.URL)
registry.Description = req.Description
registry.Insecure = req.Insecure
registry.SetCredentials(req.Username, req.Password)
// 调用领域服务
if err := h.registryService.CreateRegistry(r.Context(), registry); err != nil {
respondError(w, http.StatusBadRequest, "Failed to create registry", err.Error())
return
}
// 返回响应(脱敏)
response := dto.ToRegistryResponse(registry)
respondJSON(w, http.StatusCreated, response)
}
// GetRegistry 获取 Registry 详情
// @Summary 获取 Registry
// @Tags Registries
// @Produce json
// @Security BearerAuth
// @Param registry_id path string true "Registry ID"
// @Success 200 {object} dto.RegistryResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /registries/{registry_id} [get]
func (h *RegistryHandler) GetRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
registry, err := h.registryService.GetRegistry(r.Context(), registryID)
if err != nil {
respondError(w, http.StatusNotFound, "Registry not found", err.Error())
return
}
// 返回响应(脱敏)
response := dto.ToRegistryResponse(registry)
respondJSON(w, http.StatusOK, response)
}
// GetAllRegistries 获取所有 Registries
// @Summary 列出所有 Registries
// @Tags Registries
// @Produce json
// @Security BearerAuth
// @Success 200 {array} dto.RegistryResponse
// @Failure 500 {object} dto.ErrorResponse
// @Router /registries [get]
func (h *RegistryHandler) GetAllRegistries(w http.ResponseWriter, r *http.Request) {
registries, err := h.registryService.ListRegistries(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list registries", err.Error())
return
}
// 转换为响应(脱敏)
responses := make([]*dto.RegistryResponse, 0, len(registries))
for _, registry := range registries {
responses = append(responses, dto.ToRegistryResponse(registry))
}
respondJSON(w, http.StatusOK, responses)
}
// UpdateRegistry 更新 Registry
// @Summary 更新 Registry
// @Tags Registries
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param registry_id path string true "Registry ID"
// @Param request body dto.UpdateRegistryRequest true "更新内容"
// @Success 200 {object} dto.RegistryResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /registries/{registry_id} [put]
func (h *RegistryHandler) UpdateRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
var req dto.UpdateRegistryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
return
}
// 获取现有 Registry
registry, err := h.registryService.GetRegistry(r.Context(), registryID)
if err != nil {
respondError(w, http.StatusNotFound, "Registry not found", err.Error())
return
}
// 更新字段
registry.Update(req.Name, req.URL, req.Description)
registry.Insecure = req.Insecure
if req.Username != "" || req.Password != "" {
registry.SetCredentials(req.Username, req.Password)
}
// 调用领域服务
if err := h.registryService.UpdateRegistry(r.Context(), registry); err != nil {
respondError(w, http.StatusBadRequest, "Failed to update registry", err.Error())
return
}
// 返回响应(脱敏)
response := dto.ToRegistryResponse(registry)
respondJSON(w, http.StatusOK, response)
}
// DeleteRegistry 删除 Registry
// @Summary 删除 Registry
// @Tags Registries
// @Produce json
// @Security BearerAuth
// @Param registry_id path string true "Registry ID"
// @Success 204 {string} string "No Content"
// @Failure 404 {object} dto.ErrorResponse
// @Router /registries/{registry_id} [delete]
func (h *RegistryHandler) DeleteRegistry(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
if err := h.registryService.DeleteRegistry(r.Context(), registryID); err != nil {
respondError(w, http.StatusNotFound, "Failed to delete registry", err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
// GetRegistryHealth 获取 Registry 健康状态
// @Summary 检查 Registry 健康
// @Tags Registries
// @Produce json
// @Security BearerAuth
// @Param registry_id path string true "Registry ID"
// @Success 200 {object} dto.RegistryHealthResponse
// @Router /registries/{registry_id}/health [get]
func (h *RegistryHandler) GetRegistryHealth(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
registryID := vars["registry_id"]
// 调用领域服务检查健康状态
err := h.registryService.CheckHealth(r.Context(), registryID)
response := &dto.RegistryHealthResponse{
Healthy: err == nil,
}
if err != nil {
response.Message = err.Error()
} else {
response.Message = "Registry is healthy"
}
respondJSON(w, http.StatusOK, response)
}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OCDP Backend API - Swagger UI</title>
<link rel="stylesheet" href="/api/docs/assets/swagger-ui.css">
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
background: #fafafa;
}
#swagger-ui {
max-width: 1460px;
margin: 0 auto;
}
.topbar {
display: none;
}
.swagger-ui .info .title {
font-size: 36px;
}
.swagger-ui .info {
margin: 50px 0;
}
.swagger-ui .scheme-container {
background: #fff;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.15);
margin: 0 0 20px;
padding: 30px 0;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="/api/docs/assets/swagger-ui-bundle.js"></script>
<script src="/api/docs/assets/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "/api/docs/openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
docExpansion: "list",
filter: true,
showRequestHeaders: true,
tryItOutEnabled: true,
persistAuthorization: true,
supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'],
validatorUrl: null,
onComplete: function() {
console.log("Swagger UI loaded successfully");
}
});
window.ui = ui;
};
</script>
</body>
</html>

View File

@ -0,0 +1,68 @@
package rest
import (
_ "embed"
"net/http"
repoDocs "github.com/ocdp/cluster-service/docs"
)
var (
//go:embed swagger-ui.html
swaggerHTML []byte
//go:embed swaggerui/swagger-ui.css
swaggerCSS []byte
//go:embed swaggerui/swagger-ui-bundle.js
swaggerBundleJS []byte
//go:embed swaggerui/swagger-ui-standalone-preset.js
swaggerStandalonePresetJS []byte
)
// SwaggerHandler Swagger UI Handler
type SwaggerHandler struct{}
// NewSwaggerHandler 创建 Swagger Handler
func NewSwaggerHandler() *SwaggerHandler {
return &SwaggerHandler{}
}
// ServeSwaggerUI 提供 Swagger UI 页面
func (h *SwaggerHandler) ServeSwaggerUI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(swaggerHTML)
}
// ServeSwaggerCSS 提供 Swagger UI 样式
func (h *SwaggerHandler) ServeSwaggerCSS(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.WriteHeader(http.StatusOK)
w.Write(swaggerCSS)
}
// ServeSwaggerBundle 提供 Swagger UI 主脚本
func (h *SwaggerHandler) ServeSwaggerBundle(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.WriteHeader(http.StatusOK)
w.Write(swaggerBundleJS)
}
// ServeSwaggerStandalonePreset 提供 Swagger UI 预设脚本
func (h *SwaggerHandler) ServeSwaggerStandalonePreset(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.WriteHeader(http.StatusOK)
w.Write(swaggerStandalonePresetJS)
}
// ServeOpenAPISpec 提供 OpenAPI 规范文件
func (h *SwaggerHandler) ServeOpenAPISpec(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(repoDocs.OpenAPISpec)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,35 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
)
// respondJSON 返回 JSON 响应
func respondJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
// respondError 返回错误响应
func respondError(w http.ResponseWriter, statusCode int, error string, message string) {
response := &dto.ErrorResponse{
Error: error,
Message: message,
Code: statusCode,
}
respondJSON(w, statusCode, response)
}
// respondSuccess 返回成功响应
func respondSuccess(w http.ResponseWriter, message string, data interface{}) {
response := &dto.SuccessResponse{
Message: message,
Data: data,
}
respondJSON(w, http.StatusOK, response)
}

View File

@ -0,0 +1,220 @@
package output
import (
"fmt"
helmMock "github.com/ocdp/cluster-service/internal/adapter/output/helm/mock"
helmReal "github.com/ocdp/cluster-service/internal/adapter/output/helm/real"
"github.com/ocdp/cluster-service/internal/adapter/output/k8s"
ociMock "github.com/ocdp/cluster-service/internal/adapter/output/oci/mock"
ociReal "github.com/ocdp/cluster-service/internal/adapter/output/oci/real"
"github.com/ocdp/cluster-service/internal/adapter/output/persistence/mock"
"github.com/ocdp/cluster-service/internal/adapter/output/persistence/postgres"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// AdapterMode 适配器模式
type AdapterMode string
const (
ModeMock AdapterMode = "mock" // Mock 模式(内存存储,用于开发调试)
// 默认模式:连接真实 PostgreSQL 和服务(任何非 "mock" 的值都是默认模式)
)
// AdapterFactory 适配器工厂
// 用于创建所有 Output Adapters支持 Mock 和真实实现切换
type AdapterFactory struct {
mode AdapterMode
encryptor crypto.Encryptor // 加密器(用于敏感数据加密)
// 数据库连接字符串(非 Mock 模式需要)
dbConnString string
// 数据库连接(非 Mock 模式)
db *postgres.DB
}
// NewAdapterFactory 创建适配器工厂
func NewAdapterFactory(mode AdapterMode, encryptor crypto.Encryptor, dbConnString string) *AdapterFactory {
return &AdapterFactory{
mode: mode,
encryptor: encryptor,
dbConnString: dbConnString,
}
}
// CreateUserRepository 创建用户仓储
func (f *AdapterFactory) CreateUserRepository() (repository.UserRepository, error) {
if f.mode == ModeMock {
return mock.NewUserRepositoryMock(), nil
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewUserRepository(f.db), nil
}
// CreateClusterRepository 创建集群仓储
func (f *AdapterFactory) CreateClusterRepository() (repository.ClusterRepository, error) {
if f.mode == ModeMock {
return mock.NewClusterRepositoryMock(f.encryptor), nil
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewClusterRepository(f.db, f.encryptor), nil
}
// CreateRegistryRepository 创建 Registry 仓储
func (f *AdapterFactory) CreateRegistryRepository() (repository.RegistryRepository, error) {
if f.mode == ModeMock {
return mock.NewRegistryRepositoryMock(f.encryptor), nil
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewRegistryRepository(f.db, f.encryptor), nil
}
// CreateInstanceRepository 创建实例仓储
func (f *AdapterFactory) CreateInstanceRepository() (repository.InstanceRepository, error) {
if f.mode == ModeMock {
return mock.NewInstanceRepositoryMock(), nil
}
// 默认真实实现PostgreSQL
if err := f.ensureDBConnection(); err != nil {
return nil, err
}
return postgres.NewInstanceRepository(f.db), nil
}
// CreateOCIClient 创建 OCI 客户端
func (f *AdapterFactory) CreateOCIClient() (repository.OCIClient, error) {
if f.mode == ModeMock {
return ociMock.NewOCIClientMock(), nil
}
// 默认真实实现ORAS SDK
return ociReal.NewOCIClient(), nil
}
// CreateHelmClient 创建 Helm 客户端
func (f *AdapterFactory) CreateHelmClient() (repository.HelmClient, error) {
if f.mode == ModeMock {
return helmMock.NewHelmClientMock(), nil
}
// 默认真实实现Helm SDK
return helmReal.NewHelmClient(), nil
}
// CreateMetricsClient 创建 Metrics 客户端
func (f *AdapterFactory) CreateMetricsClient(clusterRepo repository.ClusterRepository) repository.MetricsClient {
// Metrics client 总是使用真实的 Kubernetes API
return k8s.NewMetricsClient(clusterRepo)
}
// CreateEntryClient 创建实例入口查询客户端
func (f *AdapterFactory) CreateEntryClient() repository.InstanceEntryClient {
return k8s.NewEntryClient()
}
// CreateAllRepositories 一次性创建所有 Repositories
func (f *AdapterFactory) CreateAllRepositories() (*Repositories, error) {
userRepo, err := f.CreateUserRepository()
if err != nil {
return nil, fmt.Errorf("failed to create user repository: %w", err)
}
clusterRepo, err := f.CreateClusterRepository()
if err != nil {
return nil, fmt.Errorf("failed to create cluster repository: %w", err)
}
registryRepo, err := f.CreateRegistryRepository()
if err != nil {
return nil, fmt.Errorf("failed to create registry repository: %w", err)
}
instanceRepo, err := f.CreateInstanceRepository()
if err != nil {
return nil, fmt.Errorf("failed to create instance repository: %w", err)
}
ociClient, err := f.CreateOCIClient()
if err != nil {
return nil, fmt.Errorf("failed to create OCI client: %w", err)
}
helmClient, err := f.CreateHelmClient()
if err != nil {
return nil, fmt.Errorf("failed to create Helm client: %w", err)
}
// 创建 Metrics client依赖 clusterRepo
metricsClient := f.CreateMetricsClient(clusterRepo)
entryClient := f.CreateEntryClient()
return &Repositories{
UserRepo: userRepo,
ClusterRepo: clusterRepo,
RegistryRepo: registryRepo,
InstanceRepo: instanceRepo,
OCIClient: ociClient,
HelmClient: helmClient,
MetricsClient: metricsClient,
EntryClient: entryClient,
}, nil
}
// Repositories 所有仓储的集合
type Repositories struct {
UserRepo repository.UserRepository
ClusterRepo repository.ClusterRepository
RegistryRepo repository.RegistryRepository
InstanceRepo repository.InstanceRepository
OCIClient repository.OCIClient
HelmClient repository.HelmClient
MetricsClient repository.MetricsClient
EntryClient repository.InstanceEntryClient
}
// ensureDBConnection 确保数据库连接已建立
func (f *AdapterFactory) ensureDBConnection() error {
if f.db != nil {
return nil
}
if f.dbConnString == "" {
return fmt.Errorf("database connection string is required (set DATABASE_URL environment variable)")
}
db, err := postgres.NewDB(f.dbConnString)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
// 初始化数据库 schema
if err := db.InitSchema(); err != nil {
return fmt.Errorf("failed to initialize database schema: %w", err)
}
f.db = db
return nil
}
// Close 关闭工厂资源
func (f *AdapterFactory) Close() error {
if f.db != nil {
return f.db.Close()
}
return nil
}

View File

@ -0,0 +1,196 @@
package mock
import (
"context"
"fmt"
"time"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// HelmClientMock Helm 客户端 Mock 实现
type HelmClientMock struct {
// Mock 数据存储
releases map[string]map[string]*entity.Instance // clusterID -> releaseName -> instance
history map[string]map[string][]*entity.ReleaseHistory // clusterID -> releaseName -> []history
}
// NewHelmClientMock 创建 Mock 实现
func NewHelmClientMock() repository.HelmClient {
return &HelmClientMock{
releases: make(map[string]map[string]*entity.Instance),
history: make(map[string]map[string][]*entity.ReleaseHistory),
}
}
func (c *HelmClientMock) Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
// 初始化集群数据
if c.releases[cluster.ID] == nil {
c.releases[cluster.ID] = make(map[string]*entity.Instance)
c.history[cluster.ID] = make(map[string][]*entity.ReleaseHistory)
}
// 检查是否已存在
key := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name)
if _, exists := c.releases[cluster.ID][key]; exists {
return entity.ErrInstanceExists
}
// Mock 安装
instance.Status = entity.StatusDeployed
instance.Revision = 1
instance.UpdatedAt = time.Now()
c.releases[cluster.ID][key] = instance
// 添加历史记录
c.history[cluster.ID][key] = []*entity.ReleaseHistory{
{
Revision: 1,
Updated: time.Now(),
Status: entity.StatusDeployed,
Chart: fmt.Sprintf("%s-%s", instance.Chart, instance.Version),
AppVersion: instance.Version,
Description: "Install complete",
},
}
return nil
}
func (c *HelmClientMock) Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
key := fmt.Sprintf("%s/%s", instance.Namespace, instance.Name)
existing, exists := c.releases[cluster.ID][key]
if !exists {
return entity.ErrInstanceNotFound
}
// Mock 升级
instance.Revision = existing.Revision + 1
instance.Status = entity.StatusDeployed
instance.UpdatedAt = time.Now()
c.releases[cluster.ID][key] = instance
// 添加历史记录
history := &entity.ReleaseHistory{
Revision: instance.Revision,
Updated: time.Now(),
Status: entity.StatusDeployed,
Chart: fmt.Sprintf("%s-%s", instance.Chart, instance.Version),
AppVersion: instance.Version,
Description: "Upgrade complete",
}
c.history[cluster.ID][key] = append(c.history[cluster.ID][key], history)
return nil
}
func (c *HelmClientMock) Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error {
key := fmt.Sprintf("%s/%s", namespace, releaseName)
if _, exists := c.releases[cluster.ID][key]; !exists {
return entity.ErrInstanceNotFound
}
// Mock 卸载
delete(c.releases[cluster.ID], key)
return nil
}
func (c *HelmClientMock) Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error {
key := fmt.Sprintf("%s/%s", namespace, releaseName)
instance, exists := c.releases[cluster.ID][key]
if !exists {
return entity.ErrInstanceNotFound
}
// 检查历史记录是否存在
histories := c.history[cluster.ID][key]
if revision > len(histories) || revision < 1 {
return fmt.Errorf("revision %d not found", revision)
}
// Mock 回滚
instance.Revision = len(histories) + 1
instance.Status = entity.StatusDeployed
instance.UpdatedAt = time.Now()
c.releases[cluster.ID][key] = instance
// 添加回滚历史记录
history := &entity.ReleaseHistory{
Revision: instance.Revision,
Updated: time.Now(),
Status: entity.StatusDeployed,
Chart: instance.Chart,
AppVersion: instance.Version,
Description: fmt.Sprintf("Rollback to revision %d", revision),
}
c.history[cluster.ID][key] = append(c.history[cluster.ID][key], history)
return nil
}
func (c *HelmClientMock) GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error) {
key := fmt.Sprintf("%s/%s", namespace, releaseName)
instance, exists := c.releases[cluster.ID][key]
if !exists {
return nil, entity.ErrInstanceNotFound
}
return instance, nil
}
func (c *HelmClientMock) GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error) {
key := fmt.Sprintf("%s/%s", namespace, releaseName)
if _, exists := c.releases[cluster.ID][key]; !exists {
return nil, entity.ErrInstanceNotFound
}
histories := c.history[cluster.ID][key]
if histories == nil {
return []*entity.ReleaseHistory{}, nil
}
return histories, nil
}
func (c *HelmClientMock) List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error) {
clusterReleases := c.releases[cluster.ID]
if clusterReleases == nil {
return []*entity.Instance{}, nil
}
instances := make([]*entity.Instance, 0)
for key, instance := range clusterReleases {
// 如果指定了 namespace只返回该 namespace 的
if namespace != "" && namespace != "all" {
keyNamespace := instance.Namespace
if keyNamespace != namespace {
continue
}
}
instances = append(instances, c.releases[cluster.ID][key])
}
return instances, nil
}
func (c *HelmClientMock) GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error) {
key := fmt.Sprintf("%s/%s", namespace, releaseName)
instance, exists := c.releases[cluster.ID][key]
if !exists {
return nil, entity.ErrInstanceNotFound
}
return instance.Values, nil
}

View File

@ -0,0 +1,313 @@
package real
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
)
// HelmClient 真实的 Helm 客户端实现
type HelmClient struct {
settings *cli.EnvSettings
}
// NewHelmClient 创建真实的 Helm 客户端
func NewHelmClient() repository.HelmClient {
return &HelmClient{
settings: cli.New(),
}
}
// getActionConfig 获取 Helm action configuration
func (h *HelmClient) getActionConfig(cluster *entity.Cluster, namespace string) (*action.Configuration, error) {
actionConfig := new(action.Configuration)
// 创建临时 kubeconfig 文件
kubeconfigContent := cluster.GetKubeConfig()
tmpDir, err := os.MkdirTemp("", "helm-kubeconfig-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp dir: %w", err)
}
kubeconfigPath := filepath.Join(tmpDir, "kubeconfig")
if err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0600); err != nil {
return nil, fmt.Errorf("failed to write kubeconfig: %w", err)
}
// 使用 kubeconfig 初始化 action config
if err := actionConfig.Init(
&kubeconfigGetter{kubeconfigPath: kubeconfigPath},
namespace,
os.Getenv("HELM_DRIVER"), // storage driver: configmap, secret, memory
func(format string, v ...interface{}) {
// Log function
},
); err != nil {
return nil, fmt.Errorf("failed to initialize action config: %w", err)
}
return actionConfig, nil
}
// kubeconfigGetter implements RESTClientGetter
type kubeconfigGetter struct {
kubeconfigPath string
}
func (k *kubeconfigGetter) ToRESTConfig() (*rest.Config, error) {
return clientcmd.BuildConfigFromFlags("", k.kubeconfigPath)
}
func (k *kubeconfigGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := k.ToRESTConfig()
if err != nil {
return nil, err
}
discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config)
// Wrap in a memory cache
return memory.NewMemCacheClient(discoveryClient), nil
}
func (k *kubeconfigGetter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := k.ToDiscoveryClient()
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
return mapper, nil
}
func (k *kubeconfigGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: k.kubeconfigPath},
&clientcmd.ConfigOverrides{},
)
}
// Install 安装 Helm Chart
func (h *HelmClient) Install(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
actionConfig, err := h.getActionConfig(cluster, instance.Namespace)
if err != nil {
return err
}
install := action.NewInstall(actionConfig)
install.ReleaseName = instance.Name
install.Namespace = instance.Namespace
install.CreateNamespace = true
install.Wait = true
install.Timeout = 5 * time.Minute
// 加载 Chart从本地路径或 OCI registry
// 这里简化处理,假设 chart 已经被拉取到本地
chartPath := fmt.Sprintf("/tmp/charts/%s-%s.tgz", instance.Chart, instance.Version)
chart, err := loader.Load(chartPath)
if err != nil {
return fmt.Errorf("failed to load chart: %w", err)
}
// 执行安装
rel, err := install.Run(chart, instance.Values)
if err != nil {
return fmt.Errorf("failed to install release: %w", err)
}
// 更新 revision状态由调用方根据操作结果设置
instance.Revision = rel.Version
// 注意:不在这里设置 Status让调用方通过 MarkSuccess/MarkFailure 来设置
return nil
}
// Upgrade 升级 Helm Release
func (h *HelmClient) Upgrade(ctx context.Context, cluster *entity.Cluster, instance *entity.Instance) error {
actionConfig, err := h.getActionConfig(cluster, instance.Namespace)
if err != nil {
return err
}
upgrade := action.NewUpgrade(actionConfig)
upgrade.Namespace = instance.Namespace
upgrade.Wait = true
upgrade.Timeout = 5 * time.Minute
// 加载 Chart
chartPath := fmt.Sprintf("/tmp/charts/%s-%s.tgz", instance.Chart, instance.Version)
chart, err := loader.Load(chartPath)
if err != nil {
return fmt.Errorf("failed to load chart: %w", err)
}
// 执行升级
rel, err := upgrade.Run(instance.Name, chart, instance.Values)
if err != nil {
return fmt.Errorf("failed to upgrade release: %w", err)
}
// 更新 revision状态由调用方根据操作结果设置
instance.Revision = rel.Version
// 注意:不在这里设置 Status让调用方通过 MarkSuccess/MarkFailure 来设置
return nil
}
// Uninstall 卸载 Helm Release
func (h *HelmClient) Uninstall(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) error {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return err
}
uninstall := action.NewUninstall(actionConfig)
uninstall.Wait = true
uninstall.Timeout = 5 * time.Minute
_, err = uninstall.Run(releaseName)
if err != nil {
if errors.Is(err, driver.ErrReleaseNotFound) {
return entity.ErrInstanceNotFound
}
return fmt.Errorf("failed to uninstall release: %w", err)
}
return nil
}
// Rollback 回滚 Helm Release
func (h *HelmClient) Rollback(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string, revision int) error {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return err
}
rollback := action.NewRollback(actionConfig)
rollback.Version = revision
rollback.Wait = true
rollback.Timeout = 5 * time.Minute
if err := rollback.Run(releaseName); err != nil {
return fmt.Errorf("failed to rollback release: %w", err)
}
return nil
}
// GetStatus 获取 Release 状态
func (h *HelmClient) GetStatus(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (*entity.Instance, error) {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return nil, err
}
status := action.NewStatus(actionConfig)
rel, err := status.Run(releaseName)
if err != nil {
return nil, fmt.Errorf("failed to get release status: %w", err)
}
return h.convertReleaseToInstance(rel), nil
}
// GetHistory 获取 Release 历史
func (h *HelmClient) GetHistory(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) ([]*entity.ReleaseHistory, error) {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return nil, err
}
history := action.NewHistory(actionConfig)
history.Max = 256
releases, err := history.Run(releaseName)
if err != nil {
return nil, fmt.Errorf("failed to get release history: %w", err)
}
result := make([]*entity.ReleaseHistory, 0, len(releases))
for _, rel := range releases {
result = append(result, &entity.ReleaseHistory{
Revision: rel.Version,
Updated: rel.Info.LastDeployed.Time,
Status: entity.InstanceStatus(rel.Info.Status),
Chart: rel.Chart.Metadata.Name,
AppVersion: rel.Chart.Metadata.AppVersion,
Description: rel.Info.Description,
})
}
return result, nil
}
// List 列出集群中的所有 Releases
func (h *HelmClient) List(ctx context.Context, cluster *entity.Cluster, namespace string) ([]*entity.Instance, error) {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return nil, err
}
list := action.NewList(actionConfig)
if namespace == "" {
list.AllNamespaces = true
}
releases, err := list.Run()
if err != nil {
return nil, fmt.Errorf("failed to list releases: %w", err)
}
instances := make([]*entity.Instance, 0, len(releases))
for _, rel := range releases {
instances = append(instances, h.convertReleaseToInstance(rel))
}
return instances, nil
}
// GetValues 获取 Release 的 values
func (h *HelmClient) GetValues(ctx context.Context, cluster *entity.Cluster, releaseName, namespace string) (map[string]interface{}, error) {
actionConfig, err := h.getActionConfig(cluster, namespace)
if err != nil {
return nil, err
}
getValues := action.NewGetValues(actionConfig)
values, err := getValues.Run(releaseName)
if err != nil {
return nil, fmt.Errorf("failed to get values: %w", err)
}
return values, nil
}
// convertReleaseToInstance 转换 Helm Release 为 Instance
func (h *HelmClient) convertReleaseToInstance(rel *release.Release) *entity.Instance {
return &entity.Instance{
Name: rel.Name,
Namespace: rel.Namespace,
Chart: rel.Chart.Metadata.Name,
Version: rel.Chart.Metadata.Version,
Status: entity.InstanceStatus(rel.Info.Status),
Revision: rel.Version,
Values: rel.Config,
UpdatedAt: rel.Info.LastDeployed.Time,
}
}

View File

@ -0,0 +1,321 @@
package k8s
import (
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// EntryClient 使用 Kubernetes API 查询实例相关 Service/Ingress
type EntryClient struct{}
// NewEntryClient 创建 EntryClient
func NewEntryClient() repository.InstanceEntryClient {
return &EntryClient{}
}
// ListEntries 查询实例的 Service/Ingress 入口
func (c *EntryClient) ListEntries(
ctx context.Context,
cluster *entity.Cluster,
instance *entity.Instance,
) ([]*entity.InstanceEntry, error) {
clientset, err := c.createClientset(cluster)
if err != nil {
return nil, err
}
selector := fmt.Sprintf("app.kubernetes.io/instance=%s", instance.Name)
serviceEntries, err := c.collectServiceEntries(ctx, clientset, instance, selector)
if err != nil {
return nil, err
}
ingressEntries, err := c.collectIngressEntries(ctx, clientset, instance, selector)
if err != nil {
return nil, err
}
return append(serviceEntries, ingressEntries...), nil
}
func (c *EntryClient) collectServiceEntries(
ctx context.Context,
clientset *kubernetes.Clientset,
instance *entity.Instance,
selector string,
) ([]*entity.InstanceEntry, error) {
services, err := c.listServices(ctx, clientset, instance.Namespace, selector)
if err != nil {
return nil, err
}
entries := convertServicesToEntries(services, instance, selector == "")
if len(entries) == 0 && selector != "" {
// Fallback: widen the search scope and filter manually.
services, err = c.listServices(ctx, clientset, instance.Namespace, "")
if err != nil {
return nil, err
}
entries = convertServicesToEntries(services, instance, true)
}
return entries, nil
}
func (c *EntryClient) collectIngressEntries(
ctx context.Context,
clientset *kubernetes.Clientset,
instance *entity.Instance,
selector string,
) ([]*entity.InstanceEntry, error) {
ingresses, err := c.listIngresses(ctx, clientset, instance.Namespace, selector)
if err != nil {
return nil, err
}
entries := convertIngressesToEntries(ingresses, instance, selector == "")
if len(entries) == 0 && selector != "" {
ingresses, err = c.listIngresses(ctx, clientset, instance.Namespace, "")
if err != nil {
return nil, err
}
entries = convertIngressesToEntries(ingresses, instance, true)
}
return entries, nil
}
func (c *EntryClient) listServices(
ctx context.Context,
clientset *kubernetes.Clientset,
namespace, selector string,
) ([]corev1.Service, error) {
listOptions := metav1.ListOptions{}
if selector != "" {
listOptions.LabelSelector = selector
}
services, err := clientset.CoreV1().
Services(namespace).
List(ctx, listOptions)
if err != nil {
return nil, fmt.Errorf("failed to list services: %w", err)
}
return services.Items, nil
}
func (c *EntryClient) listIngresses(
ctx context.Context,
clientset *kubernetes.Clientset,
namespace, selector string,
) ([]networkingv1.Ingress, error) {
listOptions := metav1.ListOptions{}
if selector != "" {
listOptions.LabelSelector = selector
}
ingresses, err := clientset.NetworkingV1().
Ingresses(namespace).
List(ctx, listOptions)
if err != nil {
return nil, fmt.Errorf("failed to list ingresses: %w", err)
}
return ingresses.Items, nil
}
func convertServicesToEntries(services []corev1.Service, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry {
entries := make([]*entity.InstanceEntry, 0, len(services))
for _, svc := range services {
if enforceMatch && !resourceMatchesInstance(svc.ObjectMeta, instance) {
continue
}
entries = append(entries, convertServiceToEntry(&svc))
}
return entries
}
func convertIngressesToEntries(ingresses []networkingv1.Ingress, instance *entity.Instance, enforceMatch bool) []*entity.InstanceEntry {
entries := make([]*entity.InstanceEntry, 0, len(ingresses))
for _, ing := range ingresses {
if enforceMatch && !resourceMatchesInstance(ing.ObjectMeta, instance) {
continue
}
entries = append(entries, convertIngressToEntry(&ing))
}
return entries
}
func (c *EntryClient) createClientset(cluster *entity.Cluster) (*kubernetes.Clientset, error) {
config, err := clientcmd.RESTConfigFromKubeConfig([]byte(cluster.GetKubeConfig()))
if err != nil {
config = &rest.Config{
Host: cluster.Host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(cluster.CAData),
CertData: []byte(cluster.CertData),
KeyData: []byte(cluster.KeyData),
},
BearerToken: cluster.Token,
}
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create kubernetes client: %w", err)
}
return clientset, nil
}
func convertServiceToEntry(svc *corev1.Service) *entity.InstanceEntry {
clusterIP := svc.Spec.ClusterIP
if clusterIP == corev1.ClusterIPNone {
clusterIP = ""
}
lbIngress := make([]string, 0, len(svc.Status.LoadBalancer.Ingress))
for _, ing := range svc.Status.LoadBalancer.Ingress {
if ing.IP != "" {
lbIngress = append(lbIngress, ing.IP)
}
if ing.Hostname != "" {
lbIngress = append(lbIngress, ing.Hostname)
}
}
ports := make([]entity.InstanceEntryPort, 0, len(svc.Spec.Ports))
for _, port := range svc.Spec.Ports {
ports = append(ports, entity.InstanceEntryPort{
Name: port.Name,
Protocol: string(port.Protocol),
Port: port.Port,
TargetPort: intOrStringToString(port.TargetPort),
NodePort: port.NodePort,
})
}
return &entity.InstanceEntry{
Kind: "Service",
Name: svc.Name,
Namespace: svc.Namespace,
Type: string(svc.Spec.Type),
ClusterIP: clusterIP,
ExternalIPs: append([]string{}, svc.Spec.ExternalIPs...),
LoadBalancerIngress: lbIngress,
Ports: ports,
}
}
func convertIngressToEntry(ing *networkingv1.Ingress) *entity.InstanceEntry {
lbIngress := make([]string, 0, len(ing.Status.LoadBalancer.Ingress))
for _, addr := range ing.Status.LoadBalancer.Ingress {
if addr.IP != "" {
lbIngress = append(lbIngress, addr.IP)
}
if addr.Hostname != "" {
lbIngress = append(lbIngress, addr.Hostname)
}
}
hosts := make([]entity.InstanceEntryHost, 0, len(ing.Spec.Rules))
for _, rule := range ing.Spec.Rules {
hostEntry := entity.InstanceEntryHost{
Host: rule.Host,
}
if rule.HTTP != nil {
paths := make([]entity.InstanceEntryPath, 0, len(rule.HTTP.Paths))
for _, path := range rule.HTTP.Paths {
name := ""
port := ""
if path.Backend.Service != nil {
name = path.Backend.Service.Name
port = serviceBackendPortString(path.Backend.Service.Port)
}
paths = append(paths, entity.InstanceEntryPath{
Path: path.Path,
ServiceName: name,
ServicePort: port,
})
}
hostEntry.Paths = paths
}
hosts = append(hosts, hostEntry)
}
tlsEntries := make([]entity.InstanceEntryTLS, 0, len(ing.Spec.TLS))
for _, tls := range ing.Spec.TLS {
tlsEntries = append(tlsEntries, entity.InstanceEntryTLS{
Hosts: append([]string{}, tls.Hosts...),
SecretName: tls.SecretName,
})
}
entryType := "Ingress"
if ing.Spec.IngressClassName != nil {
entryType = *ing.Spec.IngressClassName
}
return &entity.InstanceEntry{
Kind: "Ingress",
Name: ing.Name,
Namespace: ing.Namespace,
Type: entryType,
LoadBalancerIngress: lbIngress,
Hosts: hosts,
TLS: tlsEntries,
}
}
func intOrStringToString(v intstr.IntOrString) string {
if v.Type == intstr.String {
return v.StrVal
}
return fmt.Sprintf("%d", v.IntValue())
}
func serviceBackendPortString(port networkingv1.ServiceBackendPort) string {
if port.Name != "" {
return port.Name
}
if port.Number != 0 {
return fmt.Sprintf("%d", port.Number)
}
return ""
}
func resourceMatchesInstance(meta metav1.ObjectMeta, instance *entity.Instance) bool {
if instance == nil {
return false
}
labels := meta.GetLabels()
if labels != nil {
if labels["app.kubernetes.io/instance"] == instance.Name {
return true
}
labelKeys := []string{"app", "app.kubernetes.io/name", "app.kubernetes.io/component", "release"}
for _, key := range labelKeys {
if labels[key] == instance.Name {
return true
}
}
}
annotations := meta.GetAnnotations()
if annotations != nil {
if annotations["meta.helm.sh/release-name"] == instance.Name {
if ns := annotations["meta.helm.sh/release-namespace"]; ns == "" || ns == instance.Namespace {
return true
}
}
}
name := meta.GetName()
if name == instance.Name || strings.HasPrefix(name, instance.Name+"-") {
return true
}
return false
}

View File

@ -0,0 +1,54 @@
package k8s
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/ocdp/cluster-service/internal/domain/entity"
)
func TestResourceMatchesInstance(t *testing.T) {
instance := &entity.Instance{
Name: "demo",
Namespace: "default",
}
testCases := []struct {
name string
meta metav1.ObjectMeta
want bool
}{
{
name: "matches by standard label",
meta: metav1.ObjectMeta{Labels: map[string]string{
"app.kubernetes.io/instance": "demo",
}},
want: true,
},
{
name: "matches by helm annotations",
meta: metav1.ObjectMeta{Annotations: map[string]string{
"meta.helm.sh/release-name": "demo",
"meta.helm.sh/release-namespace": "default",
}},
want: true,
},
{
name: "matches by resource name prefix",
meta: metav1.ObjectMeta{Name: "demo-nginx"},
want: true,
},
{
name: "does not match unrelated resource",
meta: metav1.ObjectMeta{Name: "other"},
want: false,
},
}
for _, tc := range testCases {
if got := resourceMatchesInstance(tc.meta, instance); got != tc.want {
t.Fatalf("%s: expected %v, got %v", tc.name, tc.want, got)
}
}
}

View File

@ -0,0 +1,370 @@
package k8s
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
metricsv "k8s.io/metrics/pkg/client/clientset/versioned"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// MetricsClient 实现从 Kubernetes 集群获取监控指标
type MetricsClient struct {
clusterRepo repository.ClusterRepository
}
// NewMetricsClient 创建 MetricsClient
func NewMetricsClient(clusterRepo repository.ClusterRepository) *MetricsClient {
return &MetricsClient{
clusterRepo: clusterRepo,
}
}
// GetClusterMetrics 获取集群监控指标
func (c *MetricsClient) GetClusterMetrics(ctx context.Context, clusterID string) (*entity.ClusterMetrics, error) {
// 获取集群信息
cluster, err := c.clusterRepo.GetByID(ctx, clusterID)
if err != nil {
return nil, fmt.Errorf("failed to get cluster: %w", err)
}
// 创建 Kubernetes 客户端
clientset, metricsClient, err := c.createK8sClients(cluster)
if err != nil {
return nil, fmt.Errorf("failed to create k8s client: %w", err)
}
// 获取节点列表
nodes, err := clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list nodes: %w", err)
}
// 获取所有 Pods
pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list pods: %w", err)
}
// 获取节点指标CPU/内存使用情况)
nodeMetrics, err := c.getNodeMetricsData(ctx, clientset, metricsClient, nodes.Items)
if err != nil {
// 如果无法获取 metrics记录错误但继续
fmt.Printf("Warning: failed to get node metrics: %v\n", err)
}
// 计算集群级别汇总
metrics := c.aggregateClusterMetrics(cluster, nodes.Items, pods.Items, nodeMetrics)
return metrics, nil
}
// GetNodeMetrics 获取集群节点指标
func (c *MetricsClient) GetNodeMetrics(ctx context.Context, clusterID string) ([]*entity.NodeMetrics, error) {
cluster, err := c.clusterRepo.GetByID(ctx, clusterID)
if err != nil {
return nil, fmt.Errorf("failed to get cluster: %w", err)
}
clientset, metricsClient, err := c.createK8sClients(cluster)
if err != nil {
return nil, fmt.Errorf("failed to create k8s client: %w", err)
}
nodes, err := clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list nodes: %w", err)
}
return c.getNodeMetricsData(ctx, clientset, metricsClient, nodes.Items)
}
// createK8sClients 创建 Kubernetes 客户端
func (c *MetricsClient) createK8sClients(cluster *entity.Cluster) (*kubernetes.Clientset, *metricsv.Clientset, error) {
config, err := clientcmd.RESTConfigFromKubeConfig([]byte(cluster.GetKubeConfig()))
if err != nil {
// 如果无法从 kubeconfig 创建,尝试使用集群配置
config = &rest.Config{
Host: cluster.Host,
TLSClientConfig: rest.TLSClientConfig{
CAData: []byte(cluster.CAData),
CertData: []byte(cluster.CertData),
KeyData: []byte(cluster.KeyData),
},
}
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, nil, fmt.Errorf("failed to create clientset: %w", err)
}
metricsClient, err := metricsv.NewForConfig(config)
if err != nil {
// Metrics API 可能不可用,返回 nil 但不报错
return clientset, nil, nil
}
return clientset, metricsClient, nil
}
// getNodeMetricsData 获取节点详细指标
func (c *MetricsClient) getNodeMetricsData(
ctx context.Context,
clientset *kubernetes.Clientset,
metricsClient *metricsv.Clientset,
nodes []corev1.Node,
) ([]*entity.NodeMetrics, error) {
result := make([]*entity.NodeMetrics, 0, len(nodes))
for _, node := range nodes {
nodeMetric := &entity.NodeMetrics{
NodeName: node.Name,
Status: getNodeStatus(&node),
Role: getNodeRole(&node),
Age: getNodeAge(&node),
OSImage: node.Status.NodeInfo.OSImage,
KernelVersion: node.Status.NodeInfo.KernelVersion,
ContainerRuntime: node.Status.NodeInfo.ContainerRuntimeVersion,
KubeletVersion: node.Status.NodeInfo.KubeletVersion,
}
// CPU
cpuCapacity := node.Status.Capacity.Cpu()
cpuAllocatable := node.Status.Allocatable.Cpu()
nodeMetric.CPUCapacity = fmt.Sprintf("%.2f cores", float64(cpuCapacity.MilliValue())/1000.0)
nodeMetric.CPUAllocatable = fmt.Sprintf("%.2f cores", float64(cpuAllocatable.MilliValue())/1000.0)
// Memory
memCapacity := node.Status.Capacity.Memory()
memAllocatable := node.Status.Allocatable.Memory()
nodeMetric.MemoryCapacity = formatBytes(memCapacity.Value())
nodeMetric.MemoryAllocatable = formatBytes(memAllocatable.Value())
// GPU (从 node allocatable 中查找)
if gpu, ok := node.Status.Allocatable["nvidia.com/gpu"]; ok {
nodeMetric.GPUCapacity = int(gpu.Value())
// 尝试获取 GPU 类型
if gpuType, ok := node.Labels["nvidia.com/gpu.product"]; ok {
nodeMetric.GPUType = gpuType
}
}
// 获取 Pod 数量
pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{
FieldSelector: fmt.Sprintf("spec.nodeName=%s", node.Name),
})
if err == nil {
nodeMetric.PodCount = len(pods.Items)
}
// 如果有 metrics client获取实时使用情况
if metricsClient != nil {
nodeMetricData, err := metricsClient.MetricsV1beta1().NodeMetricses().Get(ctx, node.Name, metav1.GetOptions{})
if err == nil {
// CPU 使用
cpuUsage := nodeMetricData.Usage.Cpu()
nodeMetric.CPUUsage = fmt.Sprintf("%.2f cores", float64(cpuUsage.MilliValue())/1000.0)
if cpuAllocatable.MilliValue() > 0 {
nodeMetric.CPUPercent = float64(cpuUsage.MilliValue()) / float64(cpuAllocatable.MilliValue()) * 100
}
// Memory 使用
memUsage := nodeMetricData.Usage.Memory()
nodeMetric.MemoryUsage = formatBytes(memUsage.Value())
if memAllocatable.Value() > 0 {
nodeMetric.MemoryPercent = float64(memUsage.Value()) / float64(memAllocatable.Value()) * 100
}
}
}
result = append(result, nodeMetric)
}
return result, nil
}
// aggregateClusterMetrics 聚合集群级别指标
func (c *MetricsClient) aggregateClusterMetrics(
cluster *entity.Cluster,
nodes []corev1.Node,
pods []corev1.Pod,
nodeMetrics []*entity.NodeMetrics,
) *entity.ClusterMetrics {
metrics := &entity.ClusterMetrics{
ClusterID: cluster.ID,
ClusterName: cluster.Name,
Status: "healthy",
NodeCount: len(nodes),
PodCount: len(pods),
LastCheck: time.Now(),
Nodes: make([]entity.NodeMetrics, 0),
}
// 汇总资源
var totalCPU, totalMem, usedCPU, usedMem int64
var totalGPU, usedGPU int
healthyNodes := 0
// 单机最大值
var maxNodeCPU, maxNodeMem int64
var maxNodeGPU int
var maxNodeCPUUsage, maxNodeMemUsage, maxNodeGPUUsage float64
for i, node := range nodes {
// CPU
cpuCap := node.Status.Capacity.Cpu()
totalCPU += cpuCap.MilliValue()
if cpuCap.MilliValue() > maxNodeCPU {
maxNodeCPU = cpuCap.MilliValue()
}
// Memory
memCap := node.Status.Capacity.Memory()
totalMem += memCap.Value()
if memCap.Value() > maxNodeMem {
maxNodeMem = memCap.Value()
}
// GPU
if gpu, ok := node.Status.Allocatable["nvidia.com/gpu"]; ok {
gpuCount := int(gpu.Value())
totalGPU += gpuCount
if gpuCount > maxNodeGPU {
maxNodeGPU = gpuCount
}
}
// Node status
if getNodeStatus(&node) == "Ready" {
healthyNodes++
}
// 从 nodeMetrics 获取使用情况
if i < len(nodeMetrics) && nodeMetrics[i] != nil {
metrics.Nodes = append(metrics.Nodes, *nodeMetrics[i])
// 更新单机最大使用率
if nodeMetrics[i].CPUPercent > maxNodeCPUUsage {
maxNodeCPUUsage = nodeMetrics[i].CPUPercent
}
if nodeMetrics[i].MemoryPercent > maxNodeMemUsage {
maxNodeMemUsage = nodeMetrics[i].MemoryPercent
}
if nodeMetrics[i].GPUPercent > maxNodeGPUUsage {
maxNodeGPUUsage = nodeMetrics[i].GPUPercent
}
}
}
// 计算集群 uptime简化使用最老节点的年龄
if len(nodes) > 0 {
metrics.Uptime = getNodeAge(&nodes[0])
}
// 格式化总资源
metrics.TotalCPU = fmt.Sprintf("%.2f cores", float64(totalCPU)/1000.0)
metrics.TotalMemory = formatBytes(totalMem)
metrics.TotalGPU = totalGPU
// 格式化单机最大值
metrics.MaxNodeCPU = fmt.Sprintf("%.2f cores", float64(maxNodeCPU)/1000.0)
metrics.MaxNodeMemory = formatBytes(maxNodeMem)
metrics.MaxNodeGPU = maxNodeGPU
metrics.MaxNodeCPUUsage = maxNodeCPUUsage
metrics.MaxNodeMemUsage = maxNodeMemUsage
metrics.MaxNodeGPUUsage = maxNodeGPUUsage
// 使用情况(简化处理)
if len(nodeMetrics) > 0 {
for _, nm := range nodeMetrics {
// 解析使用的 CPU 和内存
// 这里简化处理,实际应该解析字符串
usedCPU += int64(nm.CPUPercent * float64(totalCPU) / 100.0)
usedMem += int64(nm.MemoryPercent * float64(totalMem) / 100.0)
usedGPU += nm.GPUUsage
}
if totalCPU > 0 {
metrics.CPUUsage = float64(usedCPU) / float64(totalCPU) * 100
}
if totalMem > 0 {
metrics.MemoryUsage = float64(usedMem) / float64(totalMem) * 100
}
if totalGPU > 0 {
metrics.GPUUsage = float64(usedGPU) / float64(totalGPU) * 100
}
metrics.UsedCPU = fmt.Sprintf("%.2f cores", float64(usedCPU)/1000.0)
metrics.UsedMemory = formatBytes(usedMem)
metrics.UsedGPU = usedGPU
}
// 确定集群状态
if healthyNodes == len(nodes) {
metrics.Status = "healthy"
} else if healthyNodes > 0 {
metrics.Status = "warning"
} else {
metrics.Status = "error"
}
return metrics
}
// Helper functions
func getNodeStatus(node *corev1.Node) string {
for _, condition := range node.Status.Conditions {
if condition.Type == corev1.NodeReady {
if condition.Status == corev1.ConditionTrue {
return "Ready"
}
return "NotReady"
}
}
return "Unknown"
}
func getNodeRole(node *corev1.Node) string {
if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok {
return "control-plane"
}
if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok {
return "control-plane"
}
return "worker"
}
func getNodeAge(node *corev1.Node) string {
age := time.Since(node.CreationTimestamp.Time)
days := int(age.Hours() / 24)
hours := int(age.Hours()) % 24
if days > 0 {
return fmt.Sprintf("%dd %dh", days, hours)
}
return fmt.Sprintf("%dh", hours)
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}

View File

@ -0,0 +1,284 @@
package mock
import (
"context"
"fmt"
"strings"
"time"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// OCIClientMock OCI Registry 客户端 Mock 实现
type OCIClientMock struct {
// Mock 数据存储
repositories map[string][]string // registryID -> []repositoryName
artifacts map[string]map[string][]*entity.Artifact // registryID -> repository -> []artifact
}
// NewOCIClientMock 创建 Mock 实现
func NewOCIClientMock() repository.OCIClient {
mock := &OCIClientMock{
repositories: make(map[string][]string),
artifacts: make(map[string]map[string][]*entity.Artifact),
}
// 初始化一些测试数据
mock.initMockData()
return mock
}
func (c *OCIClientMock) initMockData() {
// Note: This method intentionally left empty
// Mock data will be generated dynamically per registry to support any registry ID
}
// initArtifactsForRegistry initializes mock artifacts for a given registry ID
func (c *OCIClientMock) initArtifactsForRegistry(registryID string) {
c.artifacts[registryID] = make(map[string][]*entity.Artifact)
// vllm-serve artifacts (OCI 格式的 Helm Chart)
c.artifacts[registryID]["charts/vllm-serve"] = []*entity.Artifact{
{
RegistryID: registryID,
Repository: "charts/vllm-serve",
Tag: "0.1.0",
Digest: "sha256:abc123def456",
Type: entity.ArtifactTypeChart,
Size: 12345678,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigType: "application/vnd.cncf.helm.config.v1+json", // Helm Chart 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "vllm-serve",
"org.opencontainers.image.version": "0.1.0",
},
CreatedAt: time.Now().Add(-24 * time.Hour),
},
{
RegistryID: registryID,
Repository: "charts/vllm-serve",
Tag: "0.2.0",
Digest: "sha256:xyz789uvw012",
Type: entity.ArtifactTypeChart,
Size: 13456789,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigType: "application/vnd.cncf.helm.config.v1+json", // Helm Chart 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "vllm-serve",
"org.opencontainers.image.version": "0.2.0",
},
CreatedAt: time.Now(),
},
}
// nginx artifacts (OCI 格式的 Helm Chart)
c.artifacts[registryID]["charts/nginx"] = []*entity.Artifact{
{
RegistryID: registryID,
Repository: "charts/nginx",
Tag: "1.0.0",
Digest: "sha256:nginx123456",
Type: entity.ArtifactTypeChart,
Size: 5678901,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigType: "application/vnd.cncf.helm.config.v1+json", // Helm Chart 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "nginx",
},
CreatedAt: time.Now().Add(-48 * time.Hour),
},
}
// redis artifacts (OCI 格式的 Helm Chart)
c.artifacts[registryID]["charts/redis"] = []*entity.Artifact{
{
RegistryID: registryID,
Repository: "charts/redis",
Tag: "6.2.0",
Digest: "sha256:redis789abc",
Type: entity.ArtifactTypeChart,
Size: 8901234,
MediaType: "application/vnd.oci.image.manifest.v1+json",
ConfigType: "application/vnd.cncf.helm.config.v1+json", // Helm Chart 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "redis",
"org.opencontainers.image.version": "6.2.0",
},
CreatedAt: time.Now().Add(-72 * time.Hour),
},
}
// alpine artifacts (Docker Image)
c.artifacts[registryID]["library/alpine"] = []*entity.Artifact{
{
RegistryID: registryID,
Repository: "library/alpine",
Tag: "3.18",
Digest: "sha256:alpine123",
Type: entity.ArtifactTypeImage,
Size: 2345678,
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
ConfigType: "application/vnd.docker.container.image.v1+json", // Docker Image 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "alpine",
"org.opencontainers.image.version": "3.18",
},
CreatedAt: time.Now().Add(-96 * time.Hour),
},
{
RegistryID: registryID,
Repository: "library/alpine",
Tag: "latest",
Digest: "sha256:alpine456",
Type: entity.ArtifactTypeImage,
Size: 2456789,
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
ConfigType: "application/vnd.docker.container.image.v1+json", // Docker Image 的 config type
Annotations: map[string]string{
"org.opencontainers.image.title": "alpine",
},
CreatedAt: time.Now().Add(-24 * time.Hour),
},
}
}
func (c *OCIClientMock) ListRepositories(ctx context.Context, registry *entity.Registry) ([]string, error) {
// Check if we have cached data for this registry
repos, exists := c.repositories[registry.ID]
if !exists {
// Generate mock data dynamically for any registry
repos = []string{
"charts/vllm-serve",
"charts/nginx",
"charts/redis",
"library/alpine",
}
c.repositories[registry.ID] = repos
// Also initialize artifacts for this registry
c.initArtifactsForRegistry(registry.ID)
}
return repos, nil
}
func (c *OCIClientMock) ListArtifacts(ctx context.Context, registry *entity.Registry, repository, mediaTypeFilter string) ([]*entity.Artifact, error) {
regArtifacts, exists := c.artifacts[registry.ID]
if !exists {
// Initialize artifacts for this registry if not exists
c.initArtifactsForRegistry(registry.ID)
regArtifacts = c.artifacts[registry.ID]
}
artifacts, exists := regArtifacts[repository]
if !exists {
return []*entity.Artifact{}, nil
}
// 应用 mediaType 过滤
if mediaTypeFilter == "" || mediaTypeFilter == "all" {
return artifacts, nil
}
filtered := make([]*entity.Artifact, 0)
filter := strings.ToLower(strings.TrimSpace(mediaTypeFilter))
for _, artifact := range artifacts {
switch filter {
case "chart":
if artifact.Type == entity.ArtifactTypeChart {
filtered = append(filtered, artifact)
}
case "image":
if artifact.Type == entity.ArtifactTypeImage {
filtered = append(filtered, artifact)
}
case "other":
if artifact.Type == entity.ArtifactTypeOther {
filtered = append(filtered, artifact)
}
}
}
return filtered, nil
}
func (c *OCIClientMock) GetArtifact(ctx context.Context, registry *entity.Registry, repository, reference string) (*entity.Artifact, error) {
regArtifacts, exists := c.artifacts[registry.ID]
if !exists {
// Initialize artifacts for this registry if not exists
c.initArtifactsForRegistry(registry.ID)
regArtifacts = c.artifacts[registry.ID]
}
artifacts, exists := regArtifacts[repository]
if !exists {
return nil, entity.ErrArtifactNotFound
}
// 根据 tag 或 digest 查找
for _, artifact := range artifacts {
if artifact.Tag == reference || artifact.Digest == reference {
return artifact, nil
}
}
return nil, entity.ErrArtifactNotFound
}
func (c *OCIClientMock) GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
artifact, err := c.GetArtifact(ctx, registry, repository, reference)
if err != nil {
return "", err
}
if !artifact.IsChart() {
return "", fmt.Errorf("not a helm chart")
}
// 返回 Mock values schema
mockSchema := `{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": {
"type": "integer",
"default": 1
},
"image": {
"type": "object",
"properties": {
"repository": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
}`
return mockSchema, nil
}
func (c *OCIClientMock) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
_, err := c.GetArtifact(ctx, registry, repository, reference)
if err != nil {
return err
}
// Mock 实现,不实际下载
return nil
}
func (c *OCIClientMock) PushArtifact(ctx context.Context, registry *entity.Registry, repository, tag, sourcePath string) error {
// Mock 实现,不实际上传
return nil
}
func (c *OCIClientMock) CheckHealth(ctx context.Context, registry *entity.Registry) error {
// Mock 实现,总是返回健康
return nil
}

View File

@ -0,0 +1,468 @@
package real
import (
"archive/tar"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)
// OCIClient 真实的 OCI 客户端实现(使用 ORAS
type OCIClient struct {
httpClient *http.Client
}
// NewOCIClient 创建真实的 OCI 客户端
func NewOCIClient() repository.OCIClient {
return &OCIClient{
httpClient: &http.Client{},
}
}
// getRegistry 创建 ORAS Registry 客户端
func (c *OCIClient) getRegistry(reg *entity.Registry) (*remote.Registry, error) {
// 解析 Registry URL
registryURL := strings.TrimPrefix(reg.URL, "https://")
registryURL = strings.TrimPrefix(registryURL, "http://")
registry, err := remote.NewRegistry(registryURL)
if err != nil {
return nil, fmt.Errorf("failed to create registry client: %w", err)
}
// 设置认证
if reg.Username != "" && reg.Password != "" {
registry.Client = &auth.Client{
Client: c.httpClient,
Credential: auth.StaticCredential(registryURL, auth.Credential{
Username: reg.Username,
Password: reg.Password,
}),
}
}
// 设置 PlainHTTP如果是 insecure
registry.PlainHTTP = reg.Insecure
return registry, nil
}
// ListRepositories 列出 Registry 中的所有 repositories
func (c *OCIClient) ListRepositories(ctx context.Context, registry *entity.Registry) ([]string, error) {
reg, err := c.getRegistry(registry)
if err != nil {
return nil, err
}
repositories := make([]string, 0)
err = reg.Repositories(ctx, "", func(repos []string) error {
repositories = append(repositories, repos...)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list repositories: %w", err)
}
return repositories, nil
}
// ListArtifacts 列出指定 repository 的所有 artifacts
// mediaTypeFilter: "all", "image", "chart", "other" - 使用模糊匹配过滤
func (c *OCIClient) ListArtifacts(ctx context.Context, registry *entity.Registry, repository, mediaTypeFilter string) ([]*entity.Artifact, error) {
reg, err := c.getRegistry(registry)
if err != nil {
return nil, err
}
repo, err := reg.Repository(ctx, repository)
if err != nil {
return nil, fmt.Errorf("failed to get repository: %w", err)
}
artifacts := make([]*entity.Artifact, 0)
err = repo.Tags(ctx, "", func(tags []string) error {
for _, tag := range tags {
// 获取 manifest 以获取更多信息
desc, err := repo.Resolve(ctx, tag)
if err != nil {
// 跳过无法解析的 tag
continue
}
artifact := &entity.Artifact{
Repository: repository,
Tag: tag,
Digest: desc.Digest.String(),
MediaType: desc.MediaType,
Size: desc.Size,
}
// 尝试获取 config.mediaType 以更准确判断类型
if manifestBytes, err := repo.Fetch(ctx, desc); err == nil {
defer manifestBytes.Close()
if manifestData, err := io.ReadAll(manifestBytes); err == nil {
var manifest map[string]interface{}
if err := json.Unmarshal(manifestData, &manifest); err == nil {
// 获取 config.mediaType
if config, ok := manifest["config"].(map[string]interface{}); ok {
if configMediaType, ok := config["mediaType"].(string); ok {
artifact.ConfigType = configMediaType
}
}
}
}
}
// 使用智能类型判断(综合多种信息)
artifact.DetermineType()
// 应用 mediaType 过滤
if c.shouldIncludeArtifact(artifact, mediaTypeFilter) {
artifacts = append(artifacts, artifact)
}
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list artifacts: %w", err)
}
return artifacts, nil
}
// shouldIncludeArtifact 判断是否应该包含该 artifact
func (c *OCIClient) shouldIncludeArtifact(artifact *entity.Artifact, filter string) bool {
// 默认或 "all" 返回所有
if filter == "" || filter == "all" {
return true
}
filter = strings.ToLower(strings.TrimSpace(filter))
switch filter {
case "chart":
// 只返回 Helm Charts
return artifact.Type == entity.ArtifactTypeChart
case "image":
// 返回 Docker 或 OCI images
return artifact.Type == entity.ArtifactTypeImage
case "other":
// 返回其他类型
return artifact.Type == entity.ArtifactTypeOther
default:
// 未知的 filter返回所有
return true
}
}
// GetArtifact 获取指定 artifact 的详细信息
func (c *OCIClient) GetArtifact(ctx context.Context, registry *entity.Registry, repository, reference string) (*entity.Artifact, error) {
reg, err := c.getRegistry(registry)
if err != nil {
return nil, err
}
repo, err := reg.Repository(ctx, repository)
if err != nil {
return nil, fmt.Errorf("failed to get repository: %w", err)
}
// 解析 reference
desc, err := repo.Resolve(ctx, reference)
if err != nil {
return nil, fmt.Errorf("failed to resolve artifact: %w", err)
}
// 获取 manifest
manifestBytes, err := repo.Fetch(ctx, desc)
if err != nil {
return nil, fmt.Errorf("failed to fetch manifest: %w", err)
}
defer manifestBytes.Close()
manifestData, err := io.ReadAll(manifestBytes)
if err != nil {
return nil, fmt.Errorf("failed to read manifest: %w", err)
}
// 解析 manifest 获取配置信息
var manifest map[string]interface{}
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
}
artifact := &entity.Artifact{
Repository: repository,
Tag: reference,
Digest: desc.Digest.String(),
MediaType: desc.MediaType,
Size: desc.Size,
Annotations: make(map[string]string),
}
// 获取 config.mediaType 和 annotations
if config, ok := manifest["config"].(map[string]interface{}); ok {
// 获取 config.mediaType用于准确的类型判断
if configMediaType, ok := config["mediaType"].(string); ok {
artifact.ConfigType = configMediaType
}
// 获取 annotations
if annotations, ok := config["annotations"].(map[string]interface{}); ok {
for k, v := range annotations {
if str, ok := v.(string); ok {
artifact.Annotations[k] = str
}
}
}
}
// 使用智能类型判断(综合 ConfigType, Annotations, Repository 名称等)
artifact.DetermineType()
return artifact, nil
}
// GetValuesSchema 获取 Helm Chart 的 values schema
func (c *OCIClient) GetValuesSchema(ctx context.Context, registry *entity.Registry, repository, reference string) (string, error) {
reg, err := c.getRegistry(registry)
if err != nil {
return "", err
}
repo, err := reg.Repository(ctx, repository)
if err != nil {
return "", fmt.Errorf("failed to get repository: %w", err)
}
// 解析 reference (tag 或 digest)
desc, err := repo.Resolve(ctx, reference)
if err != nil {
return "", fmt.Errorf("failed to resolve artifact: %w", err)
}
manifestReader, err := repo.Fetch(ctx, desc)
if err != nil {
return "", fmt.Errorf("failed to fetch manifest: %w", err)
}
defer manifestReader.Close()
manifestBytes, err := io.ReadAll(manifestReader)
if err != nil {
return "", fmt.Errorf("failed to read manifest: %w", err)
}
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
return "", fmt.Errorf("failed to unmarshal manifest: %w", err)
}
// 优先查找是否存在独立的 values schema layer一些 registry 会将 values.schema.json 作为单独的 layer 存储)
var valuesSchemaLayer *ocispec.Descriptor
for i := range manifest.Layers {
layer := manifest.Layers[i]
mediaType := strings.ToLower(layer.MediaType)
if strings.Contains(mediaType, "helm.values.schema") ||
strings.Contains(mediaType, "values.schema") {
valuesSchemaLayer = &manifest.Layers[i]
break
}
}
// 如果存在独立的 values schema layer直接返回
if valuesSchemaLayer != nil {
reader, err := repo.Fetch(ctx, *valuesSchemaLayer)
if err != nil {
return "", fmt.Errorf("failed to fetch values schema layer: %w", err)
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
return "", fmt.Errorf("failed to read values schema layer: %w", err)
}
if len(data) == 0 {
return "", entity.ErrValuesSchemaNotFound
}
return string(data), nil
}
// 回退:查找 Helm Chart layertar+gzip 包含 chart 内容)并从中读取 values.schema.json
var chartLayer *ocispec.Descriptor
for i := range manifest.Layers {
layer := manifest.Layers[i]
if strings.Contains(layer.MediaType, "cncf.helm.chart") ||
strings.Contains(layer.MediaType, "helm.chart.content") {
chartLayer = &manifest.Layers[i]
break
}
}
if chartLayer == nil {
return "", entity.ErrValuesSchemaNotFound
}
if chartLayer.Digest == "" {
return "", fmt.Errorf("chart layer digest is empty")
}
if _, err := digest.Parse(string(chartLayer.Digest)); err != nil {
return "", fmt.Errorf("invalid chart layer digest: %w", err)
}
layerReader, err := repo.Fetch(ctx, *chartLayer)
if err != nil {
return "", fmt.Errorf("failed to fetch chart layer: %w", err)
}
defer layerReader.Close()
gzipReader, err := gzip.NewReader(layerReader)
if err != nil {
return "", fmt.Errorf("failed to create gzip reader: %w", err)
}
defer gzipReader.Close()
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", fmt.Errorf("failed to read chart archive: %w", err)
}
if header.Typeflag != tar.TypeReg {
continue
}
if strings.HasSuffix(header.Name, "values.schema.json") {
data, err := io.ReadAll(tarReader)
if err != nil {
return "", fmt.Errorf("failed to read values.schema.json: %w", err)
}
if len(data) == 0 {
return "", entity.ErrValuesSchemaNotFound
}
return string(data), nil
}
}
return "", entity.ErrValuesSchemaNotFound
}
// PullArtifact 下载 artifact 到本地
func (c *OCIClient) PullArtifact(ctx context.Context, registry *entity.Registry, repository, reference, destPath string) error {
reg, err := c.getRegistry(registry)
if err != nil {
return err
}
repo, err := reg.Repository(ctx, repository)
if err != nil {
return fmt.Errorf("failed to get repository: %w", err)
}
// 解析 reference
desc, err := repo.Resolve(ctx, reference)
if err != nil {
return fmt.Errorf("failed to resolve artifact: %w", err)
}
// 获取 manifest 内容
manifestReader, err := repo.Fetch(ctx, desc)
if err != nil {
return fmt.Errorf("failed to fetch manifest: %w", err)
}
defer manifestReader.Close()
manifestBytes, err := io.ReadAll(manifestReader)
if err != nil {
return fmt.Errorf("failed to read manifest: %w", err)
}
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
return fmt.Errorf("failed to unmarshal manifest: %w", err)
}
var chartLayer *ocispec.Descriptor
for i := range manifest.Layers {
layer := manifest.Layers[i]
if strings.Contains(layer.MediaType, "cncf.helm.chart") ||
strings.Contains(layer.MediaType, "helm.chart.content") {
chartLayer = &layer
break
}
}
if chartLayer == nil {
return fmt.Errorf("helm chart layer not found in manifest")
}
content, err := repo.Fetch(ctx, *chartLayer)
if err != nil {
return fmt.Errorf("failed to fetch chart layer: %w", err)
}
defer content.Close()
// 确保目标目录存在
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// 写入文件
file, err := os.Create(destPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer file.Close()
if _, err := io.Copy(file, content); err != nil {
return fmt.Errorf("failed to write artifact: %w", err)
}
return nil
}
// PushArtifact 推送 artifact 到 Registry
func (c *OCIClient) PushArtifact(ctx context.Context, registry *entity.Registry, repository, tag, sourcePath string) error {
// 这是一个简化实现
// 实际应该实现完整的 OCI artifact push 流程
return fmt.Errorf("push artifact not fully implemented yet")
}
// CheckHealth 检查 Registry 健康状态
func (c *OCIClient) CheckHealth(ctx context.Context, registry *entity.Registry) error {
reg, err := c.getRegistry(registry)
if err != nil {
return err
}
// 尝试 ping registry
err = reg.Ping(ctx)
if err != nil {
return fmt.Errorf("registry health check failed: %w", err)
}
return nil
}

View File

@ -0,0 +1,174 @@
package mock
import (
"context"
"sync"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// ClusterRepositoryMock 集群仓储 Mock 实现(内存存储,支持加密)
type ClusterRepositoryMock struct {
mu sync.RWMutex
clusters map[string]*entity.Cluster // key: cluster ID
encryptor crypto.Encryptor // 加密器
}
// NewClusterRepositoryMock 创建 Mock 实现
func NewClusterRepositoryMock(encryptor crypto.Encryptor) repository.ClusterRepository {
return &ClusterRepositoryMock{
clusters: make(map[string]*entity.Cluster),
encryptor: encryptor,
}
}
func (r *ClusterRepositoryMock) Create(ctx context.Context, cluster *entity.Cluster) error {
r.mu.Lock()
defer r.mu.Unlock()
// 检查名称是否已存在
for _, c := range r.clusters {
if c.Name == cluster.Name {
return entity.ErrClusterExists
}
}
// Mock 模式:如果没有提供认证信息,自动填充默认的 Mock 证书
if (cluster.CertData == "" || cluster.KeyData == "") && cluster.Token == "" {
cluster.CAData = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ0EgQ2VydGlmaWNhdGUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ=="
cluster.CertData = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ2xpZW50IENlcnRpZmljYXRlCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0="
cluster.KeyData = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNb2NrIFByaXZhdGUgS2V5Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t"
}
// 加密敏感数据后存储
encryptedCluster := r.encryptCluster(cluster)
r.clusters[cluster.ID] = encryptedCluster
return nil
}
func (r *ClusterRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
cluster, exists := r.clusters[id]
if !exists {
return nil, entity.ErrClusterNotFound
}
// 解密敏感数据后返回
return r.decryptCluster(cluster), nil
}
func (r *ClusterRepositoryMock) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, cluster := range r.clusters {
if cluster.Name == name {
// 解密敏感数据后返回
return r.decryptCluster(cluster), nil
}
}
return nil, entity.ErrClusterNotFound
}
func (r *ClusterRepositoryMock) Update(ctx context.Context, cluster *entity.Cluster) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clusters[cluster.ID]; !exists {
return entity.ErrClusterNotFound
}
// 加密敏感数据后存储
encryptedCluster := r.encryptCluster(cluster)
r.clusters[cluster.ID] = encryptedCluster
return nil
}
func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.clusters[id]; !exists {
return entity.ErrClusterNotFound
}
delete(r.clusters, id)
return nil
}
func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) {
r.mu.RLock()
defer r.mu.RUnlock()
clusters := make([]*entity.Cluster, 0, len(r.clusters))
for _, cluster := range r.clusters {
// 解密敏感数据后返回
clusters = append(clusters, r.decryptCluster(cluster))
}
return clusters, nil
}
// encryptCluster 加密 Cluster 的敏感数据
func (r *ClusterRepositoryMock) encryptCluster(cluster *entity.Cluster) *entity.Cluster {
encrypted := *cluster // 复制
// 加密证书数据
if cluster.CAData != "" && !crypto.IsEncrypted(cluster.CAData) {
if encryptedData, err := r.encryptor.Encrypt(cluster.CAData); err == nil {
encrypted.CAData = encryptedData
}
}
if cluster.CertData != "" && !crypto.IsEncrypted(cluster.CertData) {
if encryptedData, err := r.encryptor.Encrypt(cluster.CertData); err == nil {
encrypted.CertData = encryptedData
}
}
if cluster.KeyData != "" && !crypto.IsEncrypted(cluster.KeyData) {
if encryptedData, err := r.encryptor.Encrypt(cluster.KeyData); err == nil {
encrypted.KeyData = encryptedData
}
}
if cluster.Token != "" && !crypto.IsEncrypted(cluster.Token) {
if encryptedData, err := r.encryptor.Encrypt(cluster.Token); err == nil {
encrypted.Token = encryptedData
}
}
return &encrypted
}
// decryptCluster 解密 Cluster 的敏感数据
func (r *ClusterRepositoryMock) decryptCluster(cluster *entity.Cluster) *entity.Cluster {
decrypted := *cluster // 复制
// 解密证书数据
if cluster.CAData != "" && crypto.IsEncrypted(cluster.CAData) {
if decryptedData, err := r.encryptor.Decrypt(cluster.CAData); err == nil {
decrypted.CAData = decryptedData
}
}
if cluster.CertData != "" && crypto.IsEncrypted(cluster.CertData) {
if decryptedData, err := r.encryptor.Decrypt(cluster.CertData); err == nil {
decrypted.CertData = decryptedData
}
}
if cluster.KeyData != "" && crypto.IsEncrypted(cluster.KeyData) {
if decryptedData, err := r.encryptor.Decrypt(cluster.KeyData); err == nil {
decrypted.KeyData = decryptedData
}
}
if cluster.Token != "" && crypto.IsEncrypted(cluster.Token) {
if decryptedData, err := r.encryptor.Decrypt(cluster.Token); err == nil {
decrypted.Token = decryptedData
}
}
return &decrypted
}

View File

@ -0,0 +1,113 @@
package mock
import (
"context"
"sync"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// InstanceRepositoryMock 实例仓储 Mock 实现(内存存储)
type InstanceRepositoryMock struct {
mu sync.RWMutex
instances map[string]*entity.Instance // key: instance ID
}
// NewInstanceRepositoryMock 创建 Mock 实现
func NewInstanceRepositoryMock() repository.InstanceRepository {
return &InstanceRepositoryMock{
instances: make(map[string]*entity.Instance),
}
}
func (r *InstanceRepositoryMock) Create(ctx context.Context, instance *entity.Instance) error {
r.mu.Lock()
defer r.mu.Unlock()
// 检查同一集群中名称是否已存在
for _, inst := range r.instances {
if inst.ClusterID == instance.ClusterID && inst.Name == instance.Name {
return entity.ErrInstanceExists
}
}
r.instances[instance.ID] = instance
return nil
}
func (r *InstanceRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instance, exists := r.instances[id]
if !exists {
return nil, entity.ErrInstanceNotFound
}
return instance, nil
}
func (r *InstanceRepositoryMock) GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, instance := range r.instances {
if instance.ClusterID == clusterID && instance.Name == name {
return instance, nil
}
}
return nil, entity.ErrInstanceNotFound
}
func (r *InstanceRepositoryMock) Update(ctx context.Context, instance *entity.Instance) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.instances[instance.ID]; !exists {
return entity.ErrInstanceNotFound
}
r.instances[instance.ID] = instance
return nil
}
func (r *InstanceRepositoryMock) Delete(ctx context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.instances[id]; !exists {
return entity.ErrInstanceNotFound
}
delete(r.instances, id)
return nil
}
func (r *InstanceRepositoryMock) ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0)
for _, instance := range r.instances {
if instance.ClusterID == clusterID {
instances = append(instances, instance)
}
}
return instances, nil
}
func (r *InstanceRepositoryMock) List(ctx context.Context) ([]*entity.Instance, error) {
r.mu.RLock()
defer r.mu.RUnlock()
instances := make([]*entity.Instance, 0, len(r.instances))
for _, instance := range r.instances {
instances = append(instances, instance)
}
return instances, nil
}

View File

@ -0,0 +1,137 @@
package mock
import (
"context"
"sync"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// RegistryRepositoryMock Registry 仓储 Mock 实现(内存存储,支持加密)
type RegistryRepositoryMock struct {
mu sync.RWMutex
registries map[string]*entity.Registry // key: registry ID
encryptor crypto.Encryptor // 加密器
}
// NewRegistryRepositoryMock 创建 Mock 实现
func NewRegistryRepositoryMock(encryptor crypto.Encryptor) repository.RegistryRepository {
return &RegistryRepositoryMock{
registries: make(map[string]*entity.Registry),
encryptor: encryptor,
}
}
func (r *RegistryRepositoryMock) Create(ctx context.Context, registry *entity.Registry) error {
r.mu.Lock()
defer r.mu.Unlock()
// 检查名称是否已存在
for _, reg := range r.registries {
if reg.Name == registry.Name {
return entity.ErrRegistryExists
}
}
// 加密敏感数据后存储
encryptedRegistry := r.encryptRegistry(registry)
r.registries[registry.ID] = encryptedRegistry
return nil
}
func (r *RegistryRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Registry, error) {
r.mu.RLock()
defer r.mu.RUnlock()
registry, exists := r.registries[id]
if !exists {
return nil, entity.ErrRegistryNotFound
}
// 解密敏感数据后返回
return r.decryptRegistry(registry), nil
}
func (r *RegistryRepositoryMock) GetByName(ctx context.Context, name string) (*entity.Registry, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, registry := range r.registries {
if registry.Name == name {
// 解密敏感数据后返回
return r.decryptRegistry(registry), nil
}
}
return nil, entity.ErrRegistryNotFound
}
func (r *RegistryRepositoryMock) Update(ctx context.Context, registry *entity.Registry) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.registries[registry.ID]; !exists {
return entity.ErrRegistryNotFound
}
// 加密敏感数据后存储
encryptedRegistry := r.encryptRegistry(registry)
r.registries[registry.ID] = encryptedRegistry
return nil
}
func (r *RegistryRepositoryMock) Delete(ctx context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.registries[id]; !exists {
return entity.ErrRegistryNotFound
}
delete(r.registries, id)
return nil
}
func (r *RegistryRepositoryMock) List(ctx context.Context) ([]*entity.Registry, error) {
r.mu.RLock()
defer r.mu.RUnlock()
registries := make([]*entity.Registry, 0, len(r.registries))
for _, registry := range r.registries {
// 解密敏感数据后返回
registries = append(registries, r.decryptRegistry(registry))
}
return registries, nil
}
// encryptRegistry 加密 Registry 的敏感数据
func (r *RegistryRepositoryMock) encryptRegistry(registry *entity.Registry) *entity.Registry {
encrypted := *registry // 复制
// 加密密码
if registry.Password != "" && !crypto.IsEncrypted(registry.Password) {
if encryptedPassword, err := r.encryptor.Encrypt(registry.Password); err == nil {
encrypted.Password = encryptedPassword
}
}
return &encrypted
}
// decryptRegistry 解密 Registry 的敏感数据
func (r *RegistryRepositoryMock) decryptRegistry(registry *entity.Registry) *entity.Registry {
decrypted := *registry // 复制
// 解密密码
if registry.Password != "" && crypto.IsEncrypted(registry.Password) {
if decryptedPassword, err := r.encryptor.Decrypt(registry.Password); err == nil {
decrypted.Password = decryptedPassword
}
}
return &decrypted
}

View File

@ -0,0 +1,99 @@
package mock
import (
"context"
"sync"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// UserRepositoryMock 用户仓储 Mock 实现(内存存储)
type UserRepositoryMock struct {
mu sync.RWMutex
users map[string]*entity.User // key: user ID
}
// NewUserRepositoryMock 创建 Mock 实现
func NewUserRepositoryMock() repository.UserRepository {
return &UserRepositoryMock{
users: make(map[string]*entity.User),
}
}
func (r *UserRepositoryMock) Create(ctx context.Context, user *entity.User) error {
r.mu.Lock()
defer r.mu.Unlock()
// 检查是否已存在
for _, u := range r.users {
if u.Username == user.Username {
return entity.ErrUserExists
}
}
r.users[user.ID] = user
return nil
}
func (r *UserRepositoryMock) GetByID(ctx context.Context, id string) (*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, exists := r.users[id]
if !exists {
return nil, entity.ErrUserNotFound
}
return user, nil
}
func (r *UserRepositoryMock) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, user := range r.users {
if user.Username == username {
return user, nil
}
}
return nil, entity.ErrUserNotFound
}
func (r *UserRepositoryMock) Update(ctx context.Context, user *entity.User) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.users[user.ID]; !exists {
return entity.ErrUserNotFound
}
r.users[user.ID] = user
return nil
}
func (r *UserRepositoryMock) Delete(ctx context.Context, id string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.users[id]; !exists {
return entity.ErrUserNotFound
}
delete(r.users, id)
return nil
}
func (r *UserRepositoryMock) List(ctx context.Context) ([]*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
users := make([]*entity.User, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return users, nil
}

View File

@ -0,0 +1,337 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// ClusterRepository PostgreSQL 集群仓储实现
type ClusterRepository struct {
db *DB
encryptor crypto.Encryptor
}
// NewClusterRepository 创建 PostgreSQL 集群仓储
func NewClusterRepository(db *DB, encryptor crypto.Encryptor) repository.ClusterRepository {
return &ClusterRepository{
db: db,
encryptor: encryptor,
}
}
// Create 创建集群
func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) error {
if cluster.ID == "" {
cluster.ID = uuid.New().String()
}
// 加密敏感数据
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
if err != nil {
return fmt.Errorf("failed to encrypt CA data: %w", err)
}
encryptedCertData, err := r.encryptor.Encrypt(cluster.CertData)
if err != nil {
return fmt.Errorf("failed to encrypt cert data: %w", err)
}
encryptedKeyData, err := r.encryptor.Encrypt(cluster.KeyData)
if err != nil {
return fmt.Errorf("failed to encrypt key data: %w", err)
}
encryptedToken, err := r.encryptor.Encrypt(cluster.Token)
if err != nil {
return fmt.Errorf("failed to encrypt token: %w", err)
}
query := `
INSERT INTO clusters (id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`
_, err = r.db.conn.ExecContext(ctx, query,
cluster.ID,
cluster.Name,
cluster.Host,
encryptedCAData,
encryptedCertData,
encryptedKeyData,
encryptedToken,
cluster.Description,
cluster.CreatedAt,
cluster.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create cluster: %w", err)
}
return nil
}
// GetByID 根据 ID 获取集群
func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
FROM clusters
WHERE id = $1
`
cluster := &entity.Cluster{}
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&cluster.ID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
&encryptedCertData,
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrClusterNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get cluster: %w", err)
}
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
}
return cluster, nil
}
// GetByName 根据名称获取集群
func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
FROM clusters
WHERE name = $1
`
cluster := &entity.Cluster{}
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
&cluster.ID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
&encryptedCertData,
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrClusterNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get cluster: %w", err)
}
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
}
return cluster, nil
}
// Update 更新集群
func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster) error {
cluster.UpdatedAt = time.Now()
// 加密敏感数据
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
if err != nil {
return fmt.Errorf("failed to encrypt CA data: %w", err)
}
encryptedCertData, err := r.encryptor.Encrypt(cluster.CertData)
if err != nil {
return fmt.Errorf("failed to encrypt cert data: %w", err)
}
encryptedKeyData, err := r.encryptor.Encrypt(cluster.KeyData)
if err != nil {
return fmt.Errorf("failed to encrypt key data: %w", err)
}
encryptedToken, err := r.encryptor.Encrypt(cluster.Token)
if err != nil {
return fmt.Errorf("failed to encrypt token: %w", err)
}
query := `
UPDATE clusters
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
token = $6, description = $7, updated_at = $8
WHERE id = $9
`
result, err := r.db.conn.ExecContext(ctx, query,
cluster.Name,
cluster.Host,
encryptedCAData,
encryptedCertData,
encryptedKeyData,
encryptedToken,
cluster.Description,
cluster.UpdatedAt,
cluster.ID,
)
if err != nil {
return fmt.Errorf("failed to update cluster: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrClusterNotFound
}
return nil
}
// Delete 删除集群
func (r *ClusterRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM clusters WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete cluster: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrClusterNotFound
}
return nil
}
// List 列出所有集群
func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) {
query := `
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
FROM clusters
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list clusters: %w", err)
}
defer rows.Close()
clusters := make([]*entity.Cluster, 0)
for rows.Next() {
cluster := &entity.Cluster{}
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
err := rows.Scan(
&cluster.ID,
&cluster.Name,
&cluster.Host,
&encryptedCAData,
&encryptedCertData,
&encryptedKeyData,
&encryptedToken,
&cluster.Description,
&cluster.CreatedAt,
&cluster.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan cluster: %w", err)
}
// 解密敏感数据
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
}
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
}
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
if err != nil {
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
}
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
if err != nil {
return nil, fmt.Errorf("failed to decrypt token: %w", err)
}
clusters = append(clusters, cluster)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return clusters, nil
}

View File

@ -0,0 +1,135 @@
package postgres
import (
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
// DB 数据库连接包装器
type DB struct {
conn *sql.DB
}
// NewDB 创建新的数据库连接
func NewDB(connString string) (*DB, error) {
if connString == "" {
return nil, fmt.Errorf("database connection string cannot be empty")
}
conn, err := sql.Open("postgres", connString)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// 配置连接池
conn.SetMaxOpenConns(25)
conn.SetMaxIdleConns(5)
conn.SetConnMaxLifetime(5 * time.Minute)
// 测试连接
if err := conn.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &DB{conn: conn}, nil
}
// Close 关闭数据库连接
func (db *DB) Close() error {
if db.conn != nil {
return db.conn.Close()
}
return nil
}
// GetConn 获取底层连接(用于事务等高级操作)
func (db *DB) GetConn() *sql.DB {
return db.conn
}
// InitSchema 初始化数据库 schema
func (db *DB) InitSchema() error {
schema := `
-- Users 表
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
-- Clusters 表
CREATE TABLE IF NOT EXISTS clusters (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
host TEXT NOT NULL,
ca_data TEXT,
cert_data TEXT,
key_data TEXT,
token TEXT,
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_clusters_name ON clusters(name);
-- Registries 表
CREATE TABLE IF NOT EXISTS registries (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
url TEXT NOT NULL,
description TEXT,
username VARCHAR(255),
password TEXT,
insecure BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_registries_name ON registries(name);
-- Instances 表
CREATE TABLE IF NOT EXISTS instances (
id VARCHAR(36) PRIMARY KEY,
cluster_id VARCHAR(36) NOT NULL,
name VARCHAR(255) NOT NULL,
namespace VARCHAR(255) NOT NULL,
registry_id VARCHAR(36) NOT NULL,
repository TEXT NOT NULL,
chart VARCHAR(255) NOT NULL,
version VARCHAR(255) NOT NULL,
description TEXT,
values JSONB,
values_yaml TEXT,
status VARCHAR(50) NOT NULL,
status_reason TEXT,
last_operation VARCHAR(50),
last_error TEXT,
revision INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_cluster FOREIGN KEY (cluster_id) REFERENCES clusters(id) ON DELETE CASCADE,
CONSTRAINT fk_registry FOREIGN KEY (registry_id) REFERENCES registries(id) ON DELETE CASCADE,
CONSTRAINT unique_cluster_name UNIQUE (cluster_id, name, namespace)
);
CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id);
CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id);
CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
`
_, err := db.conn.Exec(schema)
if err != nil {
return fmt.Errorf("failed to initialize schema: %w", err)
}
return nil
}

View File

@ -0,0 +1,433 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// InstanceRepository PostgreSQL 实例仓储实现
type InstanceRepository struct {
db *DB
}
// NewInstanceRepository 创建 PostgreSQL 实例仓储
func NewInstanceRepository(db *DB) repository.InstanceRepository {
return &InstanceRepository{db: db}
}
// Create 创建实例
func (r *InstanceRepository) Create(ctx context.Context, instance *entity.Instance) error {
if instance.ID == "" {
instance.ID = uuid.New().String()
}
// 将 Values 转换为 JSON
valuesJSON, err := json.Marshal(instance.Values)
if err != nil {
return fmt.Errorf("failed to marshal values: %w", err)
}
query := `
INSERT INTO instances (id, cluster_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, status, status_reason, last_operation, last_error,
revision, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
`
_, err = r.db.conn.ExecContext(ctx, query,
instance.ID,
instance.ClusterID,
instance.Name,
instance.Namespace,
instance.RegistryID,
instance.Repository,
instance.Chart,
instance.Version,
instance.Description,
valuesJSON,
instance.ValuesYAML,
instance.Status,
instance.StatusReason,
instance.LastOperation,
instance.LastError,
instance.Revision,
instance.CreatedAt,
instance.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create instance: %w", err)
}
return nil
}
// GetByID 根据 ID 获取实例
func (r *InstanceRepository) GetByID(ctx context.Context, id string) (*entity.Instance, error) {
query := `
SELECT id, cluster_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, status, status_reason, last_operation, last_error,
revision, created_at, updated_at
FROM instances
WHERE id = $1
`
instance := &entity.Instance{}
var (
valuesJSON []byte
statusReason sql.NullString
lastOperation sql.NullString
lastError sql.NullString
)
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&instance.ID,
&instance.ClusterID,
&instance.Name,
&instance.Namespace,
&instance.RegistryID,
&instance.Repository,
&instance.Chart,
&instance.Version,
&instance.Description,
&valuesJSON,
&instance.ValuesYAML,
&instance.Status,
&statusReason,
&lastOperation,
&lastError,
&instance.Revision,
&instance.CreatedAt,
&instance.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrInstanceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get instance: %w", err)
}
// 解析 JSON Values
if len(valuesJSON) > 0 {
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
}
}
if statusReason.Valid {
instance.StatusReason = statusReason.String
}
if lastOperation.Valid {
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
}
if lastError.Valid {
instance.LastError = lastError.String
}
return instance, nil
}
// GetByClusterAndName 根据集群 ID 和名称获取实例
func (r *InstanceRepository) GetByClusterAndName(ctx context.Context, clusterID, name string) (*entity.Instance, error) {
query := `
SELECT id, cluster_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, status, status_reason, last_operation, last_error,
revision, created_at, updated_at
FROM instances
WHERE cluster_id = $1 AND name = $2
`
instance := &entity.Instance{}
var (
valuesJSON []byte
statusReason sql.NullString
lastOperation sql.NullString
lastError sql.NullString
)
err := r.db.conn.QueryRowContext(ctx, query, clusterID, name).Scan(
&instance.ID,
&instance.ClusterID,
&instance.Name,
&instance.Namespace,
&instance.RegistryID,
&instance.Repository,
&instance.Chart,
&instance.Version,
&instance.Description,
&valuesJSON,
&instance.ValuesYAML,
&instance.Status,
&statusReason,
&lastOperation,
&lastError,
&instance.Revision,
&instance.CreatedAt,
&instance.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrInstanceNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get instance: %w", err)
}
// 解析 JSON Values
if len(valuesJSON) > 0 {
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
}
}
if statusReason.Valid {
instance.StatusReason = statusReason.String
}
if lastOperation.Valid {
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
}
if lastError.Valid {
instance.LastError = lastError.String
}
return instance, nil
}
// Update 更新实例
func (r *InstanceRepository) Update(ctx context.Context, instance *entity.Instance) error {
instance.UpdatedAt = time.Now()
// 将 Values 转换为 JSON
valuesJSON, err := json.Marshal(instance.Values)
if err != nil {
return fmt.Errorf("failed to marshal values: %w", err)
}
query := `
UPDATE instances
SET cluster_id = $1, name = $2, namespace = $3, registry_id = $4, repository = $5,
chart = $6, version = $7, description = $8, values = $9, values_yaml = $10,
status = $11, status_reason = $12, last_operation = $13, last_error = $14,
revision = $15, updated_at = $16
WHERE id = $17
`
result, err := r.db.conn.ExecContext(ctx, query,
instance.ClusterID,
instance.Name,
instance.Namespace,
instance.RegistryID,
instance.Repository,
instance.Chart,
instance.Version,
instance.Description,
valuesJSON,
instance.ValuesYAML,
instance.Status,
instance.StatusReason,
instance.LastOperation,
instance.LastError,
instance.Revision,
instance.UpdatedAt,
instance.ID,
)
if err != nil {
return fmt.Errorf("failed to update instance: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrInstanceNotFound
}
return nil
}
// Delete 删除实例
func (r *InstanceRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM instances WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete instance: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrInstanceNotFound
}
return nil
}
// ListByCluster 列出指定集群的所有实例
func (r *InstanceRepository) ListByCluster(ctx context.Context, clusterID string) ([]*entity.Instance, error) {
query := `
SELECT id, cluster_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, status, status_reason, last_operation, last_error,
revision, created_at, updated_at
FROM instances
WHERE cluster_id = $1
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query, clusterID)
if err != nil {
return nil, fmt.Errorf("failed to list instances: %w", err)
}
defer rows.Close()
instances := make([]*entity.Instance, 0)
for rows.Next() {
instance := &entity.Instance{}
var (
valuesJSON []byte
statusReason sql.NullString
lastOperation sql.NullString
lastError sql.NullString
)
err := rows.Scan(
&instance.ID,
&instance.ClusterID,
&instance.Name,
&instance.Namespace,
&instance.RegistryID,
&instance.Repository,
&instance.Chart,
&instance.Version,
&instance.Description,
&valuesJSON,
&instance.ValuesYAML,
&instance.Status,
&statusReason,
&lastOperation,
&lastError,
&instance.Revision,
&instance.CreatedAt,
&instance.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan instance: %w", err)
}
// 解析 JSON Values
if len(valuesJSON) > 0 {
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
}
}
if statusReason.Valid {
instance.StatusReason = statusReason.String
}
if lastOperation.Valid {
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
}
if lastError.Valid {
instance.LastError = lastError.String
}
instances = append(instances, instance)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return instances, nil
}
// List 列出所有实例
func (r *InstanceRepository) List(ctx context.Context) ([]*entity.Instance, error) {
query := `
SELECT id, cluster_id, name, namespace, registry_id, repository, chart, version,
description, values, values_yaml, status, status_reason, last_operation, last_error,
revision, created_at, updated_at
FROM instances
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list instances: %w", err)
}
defer rows.Close()
instances := make([]*entity.Instance, 0)
for rows.Next() {
instance := &entity.Instance{}
var (
valuesJSON []byte
statusReason sql.NullString
lastOperation sql.NullString
lastError sql.NullString
)
err := rows.Scan(
&instance.ID,
&instance.ClusterID,
&instance.Name,
&instance.Namespace,
&instance.RegistryID,
&instance.Repository,
&instance.Chart,
&instance.Version,
&instance.Description,
&valuesJSON,
&instance.ValuesYAML,
&instance.Status,
&statusReason,
&lastOperation,
&lastError,
&instance.Revision,
&instance.CreatedAt,
&instance.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan instance: %w", err)
}
// 解析 JSON Values
if len(valuesJSON) > 0 {
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
}
}
if statusReason.Valid {
instance.StatusReason = statusReason.String
}
if lastOperation.Valid {
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
}
if lastError.Valid {
instance.LastError = lastError.String
}
instances = append(instances, instance)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return instances, nil
}

View File

@ -0,0 +1,257 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
)
// RegistryRepository PostgreSQL Registry 仓储实现
type RegistryRepository struct {
db *DB
encryptor crypto.Encryptor
}
// NewRegistryRepository 创建 PostgreSQL Registry 仓储
func NewRegistryRepository(db *DB, encryptor crypto.Encryptor) repository.RegistryRepository {
return &RegistryRepository{
db: db,
encryptor: encryptor,
}
}
// Create 创建 Registry
func (r *RegistryRepository) Create(ctx context.Context, registry *entity.Registry) error {
if registry.ID == "" {
registry.ID = uuid.New().String()
}
// 加密密码
encryptedPassword, err := r.encryptor.Encrypt(registry.Password)
if err != nil {
return fmt.Errorf("failed to encrypt password: %w", err)
}
query := `
INSERT INTO registries (id, name, url, description, username, password, insecure, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
`
_, err = r.db.conn.ExecContext(ctx, query,
registry.ID,
registry.Name,
registry.URL,
registry.Description,
registry.Username,
encryptedPassword,
registry.Insecure,
registry.CreatedAt,
registry.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create registry: %w", err)
}
return nil
}
// GetByID 根据 ID 获取 Registry
func (r *RegistryRepository) GetByID(ctx context.Context, id string) (*entity.Registry, error) {
query := `
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
FROM registries
WHERE id = $1
`
registry := &entity.Registry{}
var encryptedPassword string
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&registry.ID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.CreatedAt,
&registry.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrRegistryNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get registry: %w", err)
}
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
}
return registry, nil
}
// GetByName 根据名称获取 Registry
func (r *RegistryRepository) GetByName(ctx context.Context, name string) (*entity.Registry, error) {
query := `
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
FROM registries
WHERE name = $1
`
registry := &entity.Registry{}
var encryptedPassword string
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
&registry.ID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.CreatedAt,
&registry.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrRegistryNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get registry: %w", err)
}
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
}
return registry, nil
}
// Update 更新 Registry
func (r *RegistryRepository) Update(ctx context.Context, registry *entity.Registry) error {
registry.UpdatedAt = time.Now()
// 加密密码
encryptedPassword, err := r.encryptor.Encrypt(registry.Password)
if err != nil {
return fmt.Errorf("failed to encrypt password: %w", err)
}
query := `
UPDATE registries
SET name = $1, url = $2, description = $3, username = $4, password = $5,
insecure = $6, updated_at = $7
WHERE id = $8
`
result, err := r.db.conn.ExecContext(ctx, query,
registry.Name,
registry.URL,
registry.Description,
registry.Username,
encryptedPassword,
registry.Insecure,
registry.UpdatedAt,
registry.ID,
)
if err != nil {
return fmt.Errorf("failed to update registry: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrRegistryNotFound
}
return nil
}
// Delete 删除 Registry
func (r *RegistryRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM registries WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete registry: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrRegistryNotFound
}
return nil
}
// List 列出所有 Registries
func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, error) {
query := `
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
FROM registries
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list registries: %w", err)
}
defer rows.Close()
registries := make([]*entity.Registry, 0)
for rows.Next() {
registry := &entity.Registry{}
var encryptedPassword string
err := rows.Scan(
&registry.ID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.CreatedAt,
&registry.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan registry: %w", err)
}
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
}
registries = append(registries, registry)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return registries, nil
}

View File

@ -0,0 +1,204 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/domain/repository"
)
// UserRepository PostgreSQL 用户仓储实现
type UserRepository struct {
db *DB
}
// NewUserRepository 创建 PostgreSQL 用户仓储
func NewUserRepository(db *DB) repository.UserRepository {
return &UserRepository{db: db}
}
// Create 创建用户
func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
if user.ID == "" {
user.ID = uuid.New().String()
}
query := `
INSERT INTO users (id, username, password_hash, email, revoked_after, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
`
_, err := r.db.conn.ExecContext(ctx, query,
user.ID,
user.Username,
user.PasswordHash,
user.Email,
user.RevokedAfter,
user.CreatedAt,
user.UpdatedAt,
)
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
return nil
}
// GetByID 根据 ID 获取用户
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
FROM users
WHERE id = $1
`
user := &entity.User{}
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// GetByUsername 根据用户名获取用户
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
FROM users
WHERE username = $1
`
user := &entity.User{}
err := r.db.conn.QueryRowContext(ctx, query, username).Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, entity.ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// Update 更新用户
func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
user.UpdatedAt = time.Now()
query := `
UPDATE users
SET username = $1, password_hash = $2, email = $3, revoked_after = $4, updated_at = $5
WHERE id = $6
`
result, err := r.db.conn.ExecContext(ctx, query,
user.Username,
user.PasswordHash,
user.Email,
user.RevokedAfter,
user.UpdatedAt,
user.ID,
)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrUserNotFound
}
return nil
}
// Delete 删除用户
func (r *UserRepository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM users WHERE id = $1`
result, err := r.db.conn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
return entity.ErrUserNotFound
}
return nil
}
// List 列出所有用户
func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
query := `
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
FROM users
ORDER BY created_at DESC
`
rows, err := r.db.conn.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
defer rows.Close()
users := make([]*entity.User, 0)
for rows.Next() {
user := &entity.User{}
err := rows.Scan(
&user.ID,
&user.Username,
&user.PasswordHash,
&user.Email,
&user.RevokedAfter,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan user: %w", err)
}
users = append(users, user)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return users, nil
}

View File

@ -0,0 +1,137 @@
package bootstrap
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
// BootstrapConfig 预注入配置
type BootstrapConfig struct {
Enabled bool `json:"enabled"`
Users []UserSeed `json:"users"`
Registries []RegistrySeed `json:"registries"`
Clusters []ClusterSeed `json:"clusters"`
}
// UserSeed 用户预注入数据
type UserSeed struct {
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
}
// RegistrySeed Registry 预注入数据
type RegistrySeed struct {
Name string `json:"name"`
URL string `json:"url"`
Description string `json:"description"`
Username string `json:"username"`
Password string `json:"password"`
Insecure bool `json:"insecure"`
}
// ClusterSeed Cluster 预注入数据
type ClusterSeed struct {
Name string `json:"name"`
Host string `json:"host"`
Description string `json:"description"`
CAData string `json:"ca_data"`
CertData string `json:"cert_data"`
KeyData string `json:"key_data"`
Token string `json:"token,omitempty"`
}
// LoadBootstrapConfig 加载预注入配置
// 支持从文件或环境变量加载
//
// 加载优先级:
// 1. 环境变量 BOOTSTRAP_CONFIG_JSON (最高优先级)
// 2. Mock 模式: 配置文件 config/bootstrap.json
// 3. 真实模式: GetDefaultBootstrapConfig() 中的真实数据
func LoadBootstrapConfig() (*BootstrapConfig, error) {
// 1. 优先从环境变量加载
if configJSON := os.Getenv("BOOTSTRAP_CONFIG_JSON"); configJSON != "" {
var config BootstrapConfig
if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
return nil, fmt.Errorf("failed to parse BOOTSTRAP_CONFIG_JSON: %w", err)
}
return &config, nil
}
// 2. 检查适配器模式
adapterMode := os.Getenv("ADAPTER_MODE")
// Mock 模式: 使用配置文件(假数据)
if adapterMode == "mock" {
configPath := os.Getenv("BOOTSTRAP_CONFIG_FILE")
if configPath == "" {
configPath = filepath.Join("config", "bootstrap.json")
}
// 检查文件是否存在
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// 配置文件不存在,使用默认配置
return GetDefaultBootstrapConfig(), nil
}
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read bootstrap config file %s: %w", configPath, err)
}
var config BootstrapConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse bootstrap config: %w", err)
}
return &config, nil
}
// 3. 真实模式 (mode 1, mode 2): 使用代码中的真实预注入数据
return GetDefaultBootstrapConfig(), nil
}
// GetDefaultBootstrapConfig 获取默认的预注入配置(示例)
func GetDefaultBootstrapConfig() *BootstrapConfig {
return &BootstrapConfig{
Enabled: true,
Users: []UserSeed{
{
Username: "admin",
Password: "admin123",
Email: "admin@example.com",
},
},
Registries: []RegistrySeed{
{
Name: "harbor-bwgdi",
URL: "https://harbor.bwgdi.com",
Description: "BWGDI Harbor Registry",
Username: "admin",
Password: "BWGDIP@ssw0rd1401#",
Insecure: false,
},
},
Clusters: []ClusterSeed{
{
Name: "cluster1",
Host: "https://10.6.14.123:6443",
Description: "K3s Cluster 1",
CAData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTlRVME9ETTJOemt3SGhjTk1qVXdPREU0TURJeU1URTVXaGNOTXpVd09ERTJNREl5TVRFNQpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTlRVME9ETTJOemt3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTaVBJUW5LZXR2VjQ3cHUyLytMV1lZaGJjbUY3V3RZQnArOGxDaUVKdkcKaFAyaE5BWVVmZDUrRnN5VVN3bDBTV3NoT3BORmRMc0NzY3pISkhycUpWYUVvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTlCa3lhSGpPVG1RM29LYWlOaXFmCjVwZTF4L293Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnTzR4M3EyNmhhL1Z0NTRCT1Awc1hVNGt5ckVpNDR6TUcKc0d0Z25LY0NLbk1DSVFEcVhsSzBqSGNKSVE2bTRWanRub0VQWGdzQ2JrdW45WmxvVmxhbWtPNXAzZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
CertData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrakNDQVRlZ0F3SUJBZ0lJVjVQT1FRblJoSGd3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOelUxTkRnek5qYzVNQjRYRFRJMU1EZ3hPREF5TWpFeE9Wb1hEVEkyTURneApPREF5TWpFeE9Wb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJMTjcrbjNXRDY0TThTMEEKT1Bpd2hReFZRNWdLTStRTk11REFzSlM1UVZFdTIyajZwaFlQYTNyQWFLU1hnZE1EdVYvbTRUamxTQmxCM2dJQwpnZW5wdTc2alNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVGlxTWRFM0xYbElwVHRiREJnN0ZVcmV1NHVVREFLQmdncWhrak9QUVFEQWdOSkFEQkcKQWlFQXRPQ0s4ZmdzZmxhaTczcXdXMkhQbWM2bDVXNmR2L1BzNGhHNDZFRkV0VlFDSVFDenFkQitkZnFiWkJ5cwpNUm0zbDU1N3pNOFBNcDhRUE5lVFdiM0VoOEdtVGc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlCZGpDQ0FSMmdBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwClpXNTBMV05oUURFM05UVTBPRE0yTnprd0hoY05NalV3T0RFNE1ESXlNVEU1V2hjTk16VXdPREUyTURJeU1URTUKV2pBak1TRXdId1lEVlFRRERCaHJNM010WTJ4cFpXNTBMV05oUURFM05UVTBPRE0yTnprd1dUQVRCZ2NxaGtqTwpQUUlCQmdncWhrak9QUU1CQndOQ0FBU3JxQzd2RUhKYzQzUThIWG5MT0VQeXkyM0tYZzlHOVkycTJUaVFLMGhoCkJvNnh1WUxDMTFSWkhGNC85NGZJZitZa3BCcmRpcFFNTjRSaVVrUGZzM28zbzBJd1FEQU9CZ05WSFE4QkFmOEUKQkFNQ0FxUXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVU0cWpIUk55MTVTS1U3V3d3WU94VgpLM3J1TGxBd0NnWUlLb1pJemowRUF3SURSd0F3UkFJZ041WmJQaEs4YkwxWllmcStGTVNNbkFCdEgzRSsxcnFoClpRUHY4UWM3S09nQ0lCMWhBclM5SXhKU1dYYlV3ZWE4WU0yVUNEMlplYTVxMHJMQnd4SHFqb3RjCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
KeyData: "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUpuM2dPd0lBNzJGMXE2dkhvMHdDRk1RS0VXVmVnejlQYy9NRFhVVDU5c3pvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFczN2NmZkWVByZ3p4TFFBNCtMQ0ZERlZEbUFvejVBMHk0TUN3bExsQlVTN2JhUHFtRmc5cgplc0JvcEplQjB3TzVYK2JoT09WSUdVSGVBZ0tCNmVtN3ZnPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=",
},
{
Name: "cluster2",
Host: "https://10.6.80.12:6443",
Description: "Kubernetes Cluster 2",
CAData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJWCtGQVJITzJWdVl3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRFd016QXdNelEyTlROYUZ3MHpOVEV3TWpnd016VXhOVE5hTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUROdFJSeG5JYVU2MS93UHVWNkpiR0hLaWtaZWVmYXlNOEFzVHRQeXQwaU5BaFgvVWNUT1pSVWYyZmUKTXBKSFNDdy9QQjJ2d1dCZDB2OVBEVWZ6RTYxL0lKcmhWZU54NmRxK0VPdVFqRmI2TlMvbkpiWmpXVFoyRFhBRQpkS1lwaGpXWGV3dWVuK0htTjlyK2tIZGlORVdmc0xDb1hWOFFMSmVRZXF4NHY2eTFkaEE1Ly9sdGxRV0ZsN2ZFCkRzeUpQb05tQmhzSy9SNEpYVDZ4Q0NqYmJmRFF6OE1hTXA0aWZnRW9ac0R6T2RlK3ZDL3diMEcxVmlpL1FjOEEKSCtSb2tJUkI2MTZqM0VjOWhsd1V4UjNyZThqOGFFdDJob1BkbTVhekt1YjQ0LzlKc3VaU1BWR0FYVXVjekQyawpYUU5UOWErOVl4RXZJZ0psdFpuRGVYSjZmeTFqQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSVEo2WWgwQ3lWVDRGNEhJUSszYWVhQzZzMUlUQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ1pZM0xuUDl4Qgp1MjJaMENtazdiNUI2T1RtRS9obWlNRDNXY3kyb3RpcVhvZUE1VENRWnZxUk1PTk1NR3NCZFYza3FRRFhyaVR1CkQ4MDdaL3Q3SlAvOGo1RmRncDBCbkpoOUtlQkhaeVBybWFQNW9veFg4VWhFZHF0bWdsTUtBSk0xVmpKTExZNUwKMUcyRVNWa09NKytTSkV5MGJMbU9LM3M2YUI1L05pK3BVVS82Z1ZFNDFIZnh1SEJVYUtrRXNJR1d0WnNxbEY1cwp1RVAzZnY0ZmJRZVAxTmEvRlNaSmh4NlBybEdjZlE2Vmh6a1haY2Q1RExKMHZHbHZoTGdwREowdUVsUEd6NU5KCldFelVJZ3BGV25UMUd4TlhuNm02Sm9oMmNoWU5oQ25KOGZCS0Q4elozei9LdExCa2JwMDdMRlgwbzhXQUhEQmcKK1A4cjUwTm5IT3FHCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
CertData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJWUlIcnhuOXYvOTR3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TlRFd016QXdNelEyTlROYUZ3MHlOakV3TXpBd016VXhOVE5hTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEd0NGWW0KY1JldG5xWjJBR21FUGJ2L1pRVzdrSzFKNHlBUmI2ODVlNEl5QjQ2OXdKOFVtd1crOXB2OWNsVm5YV3pnQkY3WQpnbkIyNi9DTWtqOVpnRkhOaWFPK3RXcXg3cHJKTkdDaHhiY29VMDZzQUIwR3MvUkVHK3VYMnFZa3RnVHpRNWFrCitGKzZrZElRek5VdnpwWFUzUFlHcDFEcGlzNWxZNFYzMkhnSkRaZkMrRzlpT1ROd1dtTzV3bGF1K1lsQkRGTVIKS2tnVFo1MDY5OXl5NWxnUlRoaTczSG1hUCtLWGdIT0QrNkNmeUZ6Ty80KzdLaExjanZpTGFUVjBjNGkzYkxidQo0K0llU2pwMEpxU2lxQlFtRHhHRitYMndCSkNiRVZObWJrd0hCVlh5eXlxdGJWV2dibEN6SWJ0UDBadHE3RUMwClo0WkNDemc5RFNqRGQwZWZBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkZNbnBpSFFMSlZQZ1hnYwpoRDdkcDVvTHF6VWhNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFzTHJBMEhFOVNGNHAvSzBQejlVdFZLdk9rCjNUaEZ0ODZGTGlWNEJMcTZ5RSt1aHdHazk0b3p1Y3c1T2h1WEduTWFaUlFMYnliS3pJcjQvUUNqQVQ5eFVURWQKSFQ4c1c1UEhHMm5lbGJRckFNdVhRaFpXdlZTRmZ6Tk5GZG0rNStzdnVXajVtMklyNXNYRURlV2dBdmNLd3k2cwpVUjIxSmdtVXZHSFFtTVVZYWpnYW8wS3NjQmtNOEpZekFKdXZWdkJtTytwdzN5T2hVVmMyY0JnV0gybmx3L3RLCjZRR0Y0ZUZPRnJaYzM5UHp2NmlVOHFBYnNrQlVTVlhuaXg3dTNZUzFwTHNuZitSY0U0MmR1RzV4Nll3UFBlb28KRXBwWVluZ1R5TlpKKzVGaHVZdTUwMDJsQm1DV3JrSkxEek5NWlR3ai9DeG52ekVnSWJPWFpndnRpSXhpCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
KeyData: "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBOEFoV0puRVhyWjZtZGdCcGhEMjcvMlVGdTVDdFNlTWdFVyt2T1h1Q01nZU92Y0NmCkZKc0Z2dmFiL1hKVloxMXM0QVJlMklKd2R1dndqSkkvV1lCUnpZbWp2clZxc2U2YXlUUmdvY1czS0ZOT3JBQWQKQnJQMFJCdnJsOXFtSkxZRTgwT1dwUGhmdXBIU0VNelZMODZWMU56MkJxZFE2WXJPWldPRmQ5aDRDUTJYd3ZodgpZamt6Y0ZwanVjSldydm1KUVF4VEVTcElFMmVkT3ZmY3N1WllFVTRZdTl4NW1qL2lsNEJ6Zy91Z244aGN6ditQCnV5b1MzSTc0aTJrMWRIT0l0MnkyN3VQaUhrbzZkQ2Frb3FnVUpnOFJoZmw5c0FTUW14RlRabTVNQndWVjhzc3EKclcxVm9HNVFzeUc3VDlHYmF1eEF0R2VHUWdzNFBRMG93M2RIbndJREFRQUJBb0lCQUFxSWt4OUV2MEZEUVJMVQptY3pQMkx3d2RydndjV3BZcVVPYW54bnFyWi84Yk9zdTFNeFdzVDNjSEtSV3JDREpITW9INXhHaFI4WXdQSEl1CnlORG9ySzVVWi9jcWh2QWdCSExuOVlXajQ1SEZkaUplTHVmb1pjUEhaZU5ZR1FwclluUTZkeFh1UUdVem1RQmIKdk05SVJaTDl6MTRqWVkyZUpjaVZRWG9zNmJlYjUxYjgxNGljMTg1RHNtK2RhekRuNG14M2tNT0lueFR2K01pNQpxSWx5OU8vQURIaWpNd2taNVY5K3grSlpxM3Exc09SeTBKcUUwd1czbFcwQnFxSWRGRFRSelAvMFdiVGZZdDU3CmlRNjJySnhEN1RGNzR3Ni8xc3VqalU3Y2VsK1ltdTRvRFZjb05pOGdoTE1UZXE1OWpPMk1xR1FqMU5HUHRuTHkKb0hFOUs4RUNnWUVBOVRiQ3VEUlBtVDFmN0MwUldYUkJnejlENWhhRExkaS82aitjMGx5amR0TjkyR2JHdFNFMQozVVIvc2dsRit3bVliWmJmNExqUnpibnNZTGFleHRtakpzWXdFK0t4SSt3SEloSElPRFFaSTBaT08vMTJYdm1oCjB4dDdUNmNTVTZZSHZEbkp4WkpFaGt3TjBwL1ZoSHZMZFZMWmd3ZnNtQWlVekNTTVBmaUkySmtDZ1lFQStwYzcKTUJ0ZFNBZnd5cElMaUR6dis2WjFBQnVrWUphWnFQTk9IRGdLeElRNVJEQVZ5K3hSQXJWQ1V3RE5WdDJtTGJHUQpHZysvWXl4ZllEd2dSYTIxMUJDL0pUU3E4S1dHYVdXM0h2Z0VmMk54cVVIckNkT3VGZGhqdWkrMlRBdEdBb0w1CjluSGx3TXBZVVpydjF6dENCRmx4L1ZYd3NxUGZ6K2l5ZG1CVUxQY0NnWUVBcFM5Q2RMd29jdDQ1WSt2b0tBNTgKbzJGVzZBUjZVY1FWWkVOOTdPZWk1a1VLSFdEK3NyMndmMkhKYzdGemh1eXIxZ2N3d1QwL2VBcXJCV3VBQWd4UwpMNmlLY3ByZklZZTZObVVzTDFCSkxzNEpuYmZjcVpZWVFSSGVPNFljZm1UMkNRSVV2aGNPT2ptNWhnMU4xSFZnClZhUitDaHFvY3JJMUtsL2thVXFuUk9FQ2dZRUF5ZWx0RVhnYkUxMENrZFpYWUhEcFZUVnNkS2ZSTE5wcitZd0IKMWc3NTdobzBJbE0wWE5tTzlNV2tLVWt1S3QzeGRrUHFQbldOMnBUNFRJeGwzSDc1VVdRbEFBK041TlVhbG5ZVQp0T2xXaG1aVVFQTVNOUnJRM0YwOURkby80c242b1M5enhUVkUwTEM1dFJkSVJYNUQxVWxVNWJHSGZnazQzMGM1CjlOUHRQMFVDZ1lFQXk1L05hZXJlZDlQSDcyVzNDNW1UQy9jbEQxdUdmZXdPVkFkdko1eldlMDh4Q01CcEpya1QKU3dKM3NZOXYyaEdwSUxYZnU5YnppL0RWaW1sZk5MNkZBV2VaR3BCYm1qTHBEcUxWRzdhcUNHQVcvRG9iNmVlWApweEFiQTBLaUhoaE9sdUdONHdkbFdQRzNWdTlZNXZIb3RBNW1iZlRpaHhUYTlEZWRkZXlkNC9RPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=",
},
},
}
}

View File

@ -0,0 +1,181 @@
package bootstrap
import (
"context"
"fmt"
"log"
"time"
"github.com/google/uuid"
"github.com/ocdp/cluster-service/internal/adapter/output"
"github.com/ocdp/cluster-service/internal/domain/entity"
"github.com/ocdp/cluster-service/internal/pkg/password"
)
// Seeder 预注入管理器
type Seeder struct {
repos *output.Repositories
passwordHasher *password.Hasher
config *BootstrapConfig
}
// NewSeeder 创建预注入管理器
func NewSeeder(repos *output.Repositories, passwordHasher *password.Hasher, config *BootstrapConfig) *Seeder {
return &Seeder{
repos: repos,
passwordHasher: passwordHasher,
config: config,
}
}
// SeedAll 执行所有预注入
func (s *Seeder) SeedAll(ctx context.Context) error {
if !s.config.Enabled {
log.Println(" Bootstrap seeding is disabled")
return nil
}
log.Println(" 🌱 Starting bootstrap seeding...")
// 1. 注入用户
if err := s.seedUsers(ctx); err != nil {
return fmt.Errorf("failed to seed users: %w", err)
}
// 2. 注入 Registries
if err := s.seedRegistries(ctx); err != nil {
return fmt.Errorf("failed to seed registries: %w", err)
}
// 3. 注入 Clusters
if err := s.seedClusters(ctx); err != nil {
return fmt.Errorf("failed to seed clusters: %w", err)
}
log.Println(" ✅ Bootstrap seeding completed")
return nil
}
// seedUsers 注入用户
func (s *Seeder) seedUsers(ctx context.Context) error {
if len(s.config.Users) == 0 {
log.Println(" ↳ No users to seed")
return nil
}
log.Printf(" ↳ Seeding %d user(s)...", len(s.config.Users))
for _, userSeed := range s.config.Users {
// 检查用户是否已存在
existingUser, _ := s.repos.UserRepo.GetByUsername(ctx, userSeed.Username)
if existingUser != nil {
log.Printf(" ⊙ User '%s' already exists, skipping", userSeed.Username)
continue
}
// 哈希密码
passwordHash, err := s.passwordHasher.Hash(userSeed.Password)
if err != nil {
log.Printf(" ✗ Failed to hash password for user '%s': %v", userSeed.Username, err)
continue
}
// 创建用户
user := entity.NewUser(userSeed.Username, passwordHash, userSeed.Email)
user.ID = uuid.New().String()
if err := s.repos.UserRepo.Create(ctx, user); err != nil {
log.Printf(" ✗ Failed to create user '%s': %v", userSeed.Username, err)
continue
}
log.Printf(" ✓ User '%s' created", userSeed.Username)
}
return nil
}
// seedRegistries 注入 Registries
func (s *Seeder) seedRegistries(ctx context.Context) error {
if len(s.config.Registries) == 0 {
log.Println(" ↳ No registries to seed")
return nil
}
log.Printf(" ↳ Seeding %d registry(ies)...", len(s.config.Registries))
for _, registrySeed := range s.config.Registries {
// 检查 Registry 是否已存在
existingRegistry, _ := s.repos.RegistryRepo.GetByName(ctx, registrySeed.Name)
if existingRegistry != nil {
log.Printf(" ⊙ Registry '%s' already exists, skipping", registrySeed.Name)
continue
}
// 创建 Registry
registry := &entity.Registry{
ID: uuid.New().String(),
Name: registrySeed.Name,
URL: registrySeed.URL,
Description: registrySeed.Description,
Username: registrySeed.Username,
Password: registrySeed.Password,
Insecure: registrySeed.Insecure,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repos.RegistryRepo.Create(ctx, registry); err != nil {
log.Printf(" ✗ Failed to create registry '%s': %v", registrySeed.Name, err)
continue
}
log.Printf(" ✓ Registry '%s' created (credentials encrypted)", registrySeed.Name)
}
return nil
}
// seedClusters 注入 Clusters
func (s *Seeder) seedClusters(ctx context.Context) error {
if len(s.config.Clusters) == 0 {
log.Println(" ↳ No clusters to seed")
return nil
}
log.Printf(" ↳ Seeding %d cluster(s)...", len(s.config.Clusters))
for _, clusterSeed := range s.config.Clusters {
// 检查 Cluster 是否已存在
existingCluster, _ := s.repos.ClusterRepo.GetByName(ctx, clusterSeed.Name)
if existingCluster != nil {
log.Printf(" ⊙ Cluster '%s' already exists, skipping", clusterSeed.Name)
continue
}
// 创建 Cluster
cluster := &entity.Cluster{
ID: uuid.New().String(),
Name: clusterSeed.Name,
Host: clusterSeed.Host,
Description: clusterSeed.Description,
CAData: clusterSeed.CAData,
CertData: clusterSeed.CertData,
KeyData: clusterSeed.KeyData,
Token: clusterSeed.Token,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.repos.ClusterRepo.Create(ctx, cluster); err != nil {
log.Printf(" ✗ Failed to create cluster '%s': %v", clusterSeed.Name, err)
continue
}
log.Printf(" ✓ Cluster '%s' created (credentials encrypted)", clusterSeed.Name)
}
return nil
}

View 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
}

View 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
}

View 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")
)

View 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
}

View 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
}

View 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"`
}

View 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
}

View 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
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

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)
}

View File

@ -0,0 +1,128 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
)
// Encryptor 加密器接口
type Encryptor interface {
Encrypt(plaintext string) (string, error)
Decrypt(ciphertext string) (string, error)
}
// AESEncryptor AES 加密器
type AESEncryptor struct {
key []byte
}
// NewAESEncryptor 创建 AES 加密器
// key: 加密密钥会自动派生为32字节密钥
func NewAESEncryptor(key string) *AESEncryptor {
// 使用 SHA256 派生固定长度的密钥
hash := sha256.Sum256([]byte(key))
return &AESEncryptor{
key: hash[:],
}
}
// Encrypt 加密字符串
// 返回 Base64 编码的密文
func (e *AESEncryptor) Encrypt(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
// 创建 GCM 模式
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// 生成随机 nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密nonce 会自动附加到密文前面)
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
// Base64 编码
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密字符串
// 输入为 Base64 编码的密文
func (e *AESEncryptor) Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
// Base64 解码
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
block, err := aes.NewCipher(e.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return "", errors.New("ciphertext too short")
}
// 提取 nonce 和实际密文
nonce, cipherBytes := data[:nonceSize], data[nonceSize:]
// 解密
plaintext, err := gcm.Open(nil, nonce, cipherBytes, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// MaskSensitiveData 脱敏显示敏感数据
// 如果数据为空或已加密标记,返回掩码
func MaskSensitiveData(data string) string {
if data == "" {
return ""
}
return "••••••••" // 统一返回8个点不泄露长度信息
}
// IsEncrypted 检查字符串是否已加密
// 简单检查:加密后的数据是 Base64 格式且长度较长
func IsEncrypted(data string) bool {
if data == "" {
return false
}
// 加密后的数据至少有 nonce(12) + tag(16) + 内容Base64后会更长
if len(data) < 40 {
return false
}
// 尝试 Base64 解码
_, err := base64.StdEncoding.DecodeString(data)
return err == nil
}

View File

@ -0,0 +1,124 @@
package crypto
import (
"testing"
)
func TestAESEncryptor(t *testing.T) {
encryptor := NewAESEncryptor("test-secret-key")
tests := []struct {
name string
plaintext string
}{
{"simple password", "password123"},
{"harbor password", "BWGDIP@ssw0rd1401#"},
{"empty string", ""},
{"long certificate", "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pP"},
{"unicode", "密码123!@#"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试加密
encrypted, err := encryptor.Encrypt(tt.plaintext)
if err != nil {
t.Fatalf("Encrypt failed: %v", err)
}
// 空字符串应该返回空
if tt.plaintext == "" {
if encrypted != "" {
t.Errorf("Expected empty encrypted string, got %s", encrypted)
}
return
}
// 加密后应该不同
if encrypted == tt.plaintext {
t.Errorf("Encrypted text should differ from plaintext")
}
// 测试解密
decrypted, err := encryptor.Decrypt(encrypted)
if err != nil {
t.Fatalf("Decrypt failed: %v", err)
}
// 解密后应该相同
if decrypted != tt.plaintext {
t.Errorf("Decrypted text mismatch: got %s, want %s", decrypted, tt.plaintext)
}
})
}
}
func TestMaskSensitiveData(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"normal password", "password123", "••••••••"},
{"empty string", "", ""},
{"long string", "very-long-password-with-many-characters", "••••••••"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := MaskSensitiveData(tt.input)
if result != tt.expected {
t.Errorf("MaskSensitiveData(%s) = %s, want %s", tt.input, result, tt.expected)
}
})
}
}
func TestIsEncrypted(t *testing.T) {
encryptor := NewAESEncryptor("test-key")
plaintext := "password123"
encrypted, _ := encryptor.Encrypt(plaintext)
tests := []struct {
name string
input string
expected bool
}{
{"encrypted data", encrypted, true},
{"plaintext", "password123", false},
{"empty string", "", false},
{"short string", "abc", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsEncrypted(tt.input)
if result != tt.expected {
t.Errorf("IsEncrypted(%s) = %v, want %v", tt.input, result, tt.expected)
}
})
}
}
func TestEncryptionConsistency(t *testing.T) {
encryptor := NewAESEncryptor("consistent-key")
plaintext := "test-password"
// 多次加密同一内容,结果应该不同(因为使用随机 nonce
encrypted1, _ := encryptor.Encrypt(plaintext)
encrypted2, _ := encryptor.Encrypt(plaintext)
if encrypted1 == encrypted2 {
t.Error("Multiple encryptions of same plaintext should produce different ciphertexts")
}
// 但解密结果应该相同
decrypted1, _ := encryptor.Decrypt(encrypted1)
decrypted2, _ := encryptor.Decrypt(encrypted2)
if decrypted1 != plaintext || decrypted2 != plaintext {
t.Error("Decryption should produce original plaintext")
}
}

View File

@ -0,0 +1,123 @@
package jwt
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
const (
AccessTokenDuration = 24 * time.Hour // Access Token 有效期
RefreshTokenDuration = 7 * 24 * time.Hour // Refresh Token 有效期
)
// JWTManager JWT 管理器
type JWTManager struct {
secretKey string
}
// NewJWTManager 创建 JWT 管理器
func NewJWTManager(secretKey string) *JWTManager {
return &JWTManager{
secretKey: secretKey,
}
}
// Claims JWT Claims
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// Generate 生成 Access Token 和 Refresh Token
func (m *JWTManager) Generate(userID, username string) (accessToken, refreshToken string, err error) {
// 生成 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err = accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign access token: %w", err)
}
// 生成 Refresh Token
refreshClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(RefreshTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshToken, err = refreshTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", "", fmt.Errorf("failed to sign refresh token: %w", err)
}
return accessToken, refreshToken, nil
}
// Verify 验证 Token
func (m *JWTManager) Verify(tokenString string) (userID, username string, err error) {
userID, username, _, err = m.VerifyWithIssuedAt(tokenString)
return userID, username, err
}
// VerifyWithIssuedAt 验证 Token 并返回签发时间
func (m *JWTManager) VerifyWithIssuedAt(tokenString string) (userID, username string, issuedAt int64, err error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(m.secretKey), nil
})
if err != nil {
return "", "", 0, fmt.Errorf("failed to parse token: %w", err)
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims.UserID, claims.Username, claims.IssuedAt.Unix(), nil
}
return "", "", 0, fmt.Errorf("invalid token")
}
// Refresh 刷新 Token
func (m *JWTManager) Refresh(refreshToken string) (string, error) {
// 验证 Refresh Token
userID, username, err := m.Verify(refreshToken)
if err != nil {
return "", fmt.Errorf("invalid refresh token: %w", err)
}
// 生成新的 Access Token
accessClaims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(AccessTokenDuration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
newAccessToken, err := accessTokenObj.SignedString([]byte(m.secretKey))
if err != nil {
return "", fmt.Errorf("failed to sign new access token: %w", err)
}
return newAccessToken, nil
}

View File

@ -0,0 +1,97 @@
package password
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
const (
// Argon2id 参数
memory = 64 * 1024 // 64 MB
iterations = 3
parallelism = 2
saltLength = 16
keyLength = 32
)
// Hasher 密码哈希器
type Hasher struct{}
// NewHasher 创建密码哈希器
func NewHasher() *Hasher {
return &Hasher{}
}
// Hash 哈希密码(使用 Argon2id
func (h *Hasher) Hash(password string) (string, error) {
// 生成随机 salt
salt := make([]byte, saltLength)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("failed to generate salt: %w", err)
}
// 使用 Argon2id 哈希密码
hash := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLength)
// 编码为字符串格式: $argon2id$v=19$m=65536,t=3,p=2$salt$hash
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, memory, iterations, parallelism, b64Salt, b64Hash)
return encodedHash, nil
}
// Verify 验证密码
func (h *Hasher) Verify(password, encodedHash string) error {
// 解析编码的哈希
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return fmt.Errorf("invalid hash format")
}
if parts[1] != "argon2id" {
return fmt.Errorf("unsupported algorithm: %s", parts[1])
}
// 解析参数
var version int
var m, t, p uint32
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return fmt.Errorf("failed to parse version: %w", err)
}
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
if err != nil {
return fmt.Errorf("failed to parse parameters: %w", err)
}
// 解码 salt 和 hash
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return fmt.Errorf("failed to decode salt: %w", err)
}
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return fmt.Errorf("failed to decode hash: %w", err)
}
// 使用相同参数哈希输入的密码
computedHash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(hash)))
// 使用常量时间比较防止时序攻击
if subtle.ConstantTimeCompare(hash, computedHash) == 1 {
return nil
}
return fmt.Errorf("password does not match")
}