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