ocdp v1
This commit is contained in:
193
backend/internal/adapter/input/http/rest/artifact_handler.go
Normal file
193
backend/internal/adapter/input/http/rest/artifact_handler.go
Normal 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)
|
||||
}
|
||||
127
backend/internal/adapter/input/http/rest/auth_handler.go
Normal file
127
backend/internal/adapter/input/http/rest/auth_handler.go
Normal 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)
|
||||
}
|
||||
221
backend/internal/adapter/input/http/rest/cluster_handler.go
Normal file
221
backend/internal/adapter/input/http/rest/cluster_handler.go
Normal 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)
|
||||
}
|
||||
371
backend/internal/adapter/input/http/rest/instance_handler.go
Normal file
371
backend/internal/adapter/input/http/rest/instance_handler.go
Normal 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,
|
||||
}
|
||||
}
|
||||
137
backend/internal/adapter/input/http/rest/monitoring_handler.go
Normal file
137
backend/internal/adapter/input/http/rest/monitoring_handler.go
Normal 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)
|
||||
}
|
||||
201
backend/internal/adapter/input/http/rest/registry_handler.go
Normal file
201
backend/internal/adapter/input/http/rest/registry_handler.go
Normal 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)
|
||||
}
|
||||
89
backend/internal/adapter/input/http/rest/swagger-ui.html
Normal file
89
backend/internal/adapter/input/http/rest/swagger-ui.html
Normal 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>
|
||||
68
backend/internal/adapter/input/http/rest/swagger_handler.go
Normal file
68
backend/internal/adapter/input/http/rest/swagger_handler.go
Normal 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
35
backend/internal/adapter/input/http/rest/utils.go
Normal file
35
backend/internal/adapter/input/http/rest/utils.go
Normal 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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user