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