ocdp v1
This commit is contained in:
44
backend/internal/adapter/input/http/dto/artifact_dto.go
Normal file
44
backend/internal/adapter/input/http/dto/artifact_dto.go
Normal file
@ -0,0 +1,44 @@
|
||||
package dto
|
||||
|
||||
// RepositoryListResponse Repository 列表响应
|
||||
type RepositoryListResponse struct {
|
||||
RegistryID string `json:"registryId"`
|
||||
RegistryURL string `json:"registryUrl"`
|
||||
Repositories []string `json:"repositories"`
|
||||
Total int `json:"total"`
|
||||
CatalogSupported bool `json:"catalogSupported"` // Whether _catalog API is supported
|
||||
Source string `json:"source"` // Data source: "catalog" | "preconfigured" | "unavailable"
|
||||
Message string `json:"message,omitempty"` // User-friendly message
|
||||
}
|
||||
|
||||
// ArtifactResponse Artifact 响应(简化版本,只包含核心字段)
|
||||
type ArtifactResponse struct {
|
||||
RepositoryName string `json:"repositoryName"`
|
||||
Tag string `json:"tag"`
|
||||
Digest string `json:"digest"`
|
||||
Type string `json:"type"` // chart | image | other
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// TagResponse Tag 响应(前端期望的扁平化结构)
|
||||
type TagResponse struct {
|
||||
RepositoryName string `json:"repositoryName"` // Repository name
|
||||
Tag string `json:"tag"` // Tag name (e.g. "1.0.0", "latest")
|
||||
Type string `json:"type"` // Artifact type: chart, image, other
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
Size int64 `json:"size"` // Artifact size (bytes)
|
||||
}
|
||||
|
||||
// ArtifactListResponse Artifact 列表响应(包装格式,用于详细接口)
|
||||
type ArtifactListResponse struct {
|
||||
RepositoryName string `json:"repositoryName"`
|
||||
Artifacts []*ArtifactResponse `json:"artifacts"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ValuesSchemaResponse Values Schema 响应
|
||||
type ValuesSchemaResponse struct {
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
|
||||
35
backend/internal/adapter/input/http/dto/auth_dto.go
Normal file
35
backend/internal/adapter/input/http/dto/auth_dto.go
Normal file
@ -0,0 +1,35 @@
|
||||
package dto
|
||||
|
||||
// RegisterRequest 用户注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// LoginRequest 用户登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// RefreshTokenRequest 刷新 Token 请求
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refreshToken" binding:"required"`
|
||||
}
|
||||
|
||||
// AuthResponse 认证响应
|
||||
type AuthResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// UserResponse 用户信息响应
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
82
backend/internal/adapter/input/http/dto/cluster_dto.go
Normal file
82
backend/internal/adapter/input/http/dto/cluster_dto.go
Normal file
@ -0,0 +1,82 @@
|
||||
package dto
|
||||
|
||||
// CreateClusterRequest 创建集群请求
|
||||
type CreateClusterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Host string `json:"host" binding:"required"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// UpdateClusterRequest 更新集群请求
|
||||
type UpdateClusterRequest struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
CAData string `json:"caData"`
|
||||
CADataAlt string `json:"ca_data"`
|
||||
CertData string `json:"certData"`
|
||||
CertDataAlt string `json:"cert_data"`
|
||||
KeyData string `json:"keyData"`
|
||||
KeyDataAlt string `json:"key_data"`
|
||||
Token string `json:"token"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Normalize 将多种命名风格的字段合并到统一字段
|
||||
func (r *CreateClusterRequest) Normalize() {
|
||||
if r.CAData == "" {
|
||||
r.CAData = r.CADataAlt
|
||||
}
|
||||
if r.CertData == "" {
|
||||
r.CertData = r.CertDataAlt
|
||||
}
|
||||
if r.KeyData == "" {
|
||||
r.KeyData = r.KeyDataAlt
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize 将多种命名风格的字段合并到统一字段
|
||||
func (r *UpdateClusterRequest) Normalize() {
|
||||
if r.CAData == "" {
|
||||
r.CAData = r.CADataAlt
|
||||
}
|
||||
if r.CertData == "" {
|
||||
r.CertData = r.CertDataAlt
|
||||
}
|
||||
if r.KeyData == "" {
|
||||
r.KeyData = r.KeyDataAlt
|
||||
}
|
||||
}
|
||||
|
||||
// ClusterResponse 集群响应(敏感数据已脱敏)
|
||||
type ClusterResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Description string `json:"description"`
|
||||
// 认证配置状态(不返回实际证书数据,仅返回是否已配置)
|
||||
HasCAData bool `json:"hasCaData"`
|
||||
HasCertData bool `json:"hasCertData"`
|
||||
HasKeyData bool `json:"hasKeyData"`
|
||||
HasToken bool `json:"hasToken"`
|
||||
// 脱敏数据(仅用于前端显示,实际值为掩码)
|
||||
CAData string `json:"caData,omitempty"` // 脱敏显示(••••••••)
|
||||
CertData string `json:"certData,omitempty"` // 脱敏显示(••••••••)
|
||||
KeyData string `json:"keyData,omitempty"` // 脱敏显示(••••••••)
|
||||
Token string `json:"token,omitempty"` // 脱敏显示(••••••••)
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ClusterHealthResponse 集群健康状态响应
|
||||
type ClusterHealthResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
63
backend/internal/adapter/input/http/dto/converter.go
Normal file
63
backend/internal/adapter/input/http/dto/converter.go
Normal file
@ -0,0 +1,63 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/pkg/crypto"
|
||||
)
|
||||
|
||||
// ToRegistryResponse 转换 Registry 实体为响应 DTO(脱敏)
|
||||
func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
|
||||
response := &RegistryResponse{
|
||||
ID: registry.ID,
|
||||
Name: registry.Name,
|
||||
URL: registry.URL,
|
||||
Description: registry.Description,
|
||||
Username: registry.Username,
|
||||
Insecure: registry.Insecure,
|
||||
CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// 脱敏处理密码
|
||||
if registry.Password != "" {
|
||||
response.HasPassword = true
|
||||
response.Password = crypto.MaskSensitiveData(registry.Password)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ToClusterResponse 转换 Cluster 实体为响应 DTO(脱敏)
|
||||
func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
|
||||
response := &ClusterResponse{
|
||||
ID: cluster.ID,
|
||||
Name: cluster.Name,
|
||||
Host: cluster.Host,
|
||||
Description: cluster.Description,
|
||||
CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
|
||||
// 设置认证配置状态标志
|
||||
response.HasCAData = cluster.CAData != ""
|
||||
response.HasCertData = cluster.CertData != ""
|
||||
response.HasKeyData = cluster.KeyData != ""
|
||||
response.HasToken = cluster.Token != ""
|
||||
|
||||
// 脱敏处理敏感数据(仅显示掩码)
|
||||
if cluster.CAData != "" {
|
||||
response.CAData = crypto.MaskSensitiveData(cluster.CAData)
|
||||
}
|
||||
if cluster.CertData != "" {
|
||||
response.CertData = crypto.MaskSensitiveData(cluster.CertData)
|
||||
}
|
||||
if cluster.KeyData != "" {
|
||||
response.KeyData = crypto.MaskSensitiveData(cluster.KeyData)
|
||||
}
|
||||
if cluster.Token != "" {
|
||||
response.Token = crypto.MaskSensitiveData(cluster.Token)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
15
backend/internal/adapter/input/http/dto/error_dto.go
Normal file
15
backend/internal/adapter/input/http/dto/error_dto.go
Normal file
@ -0,0 +1,15 @@
|
||||
package dto
|
||||
|
||||
// ErrorResponse 错误响应
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// SuccessResponse 成功响应
|
||||
type SuccessResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
133
backend/internal/adapter/input/http/dto/instance_dto.go
Normal file
133
backend/internal/adapter/input/http/dto/instance_dto.go
Normal file
@ -0,0 +1,133 @@
|
||||
package dto
|
||||
|
||||
// CreateInstanceRequest 创建实例请求
|
||||
type CreateInstanceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Namespace string `json:"namespace" binding:"required"`
|
||||
RegistryID string `json:"registryId" binding:"required"`
|
||||
RegistryIDAlt string `json:"registry_id"`
|
||||
Repository string `json:"repository" binding:"required"`
|
||||
Tag string `json:"tag" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
ValuesYAML string `json:"valuesYaml"`
|
||||
}
|
||||
|
||||
// UpdateInstanceRequest 更新实例请求
|
||||
type UpdateInstanceRequest struct {
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
ValuesYAML string `json:"valuesYaml"`
|
||||
}
|
||||
|
||||
// Normalize 将多种命名风格的字段合并到统一字段
|
||||
func (r *CreateInstanceRequest) Normalize() {
|
||||
if r.RegistryID == "" {
|
||||
r.RegistryID = r.RegistryIDAlt
|
||||
}
|
||||
}
|
||||
|
||||
// RollbackInstanceRequest 回滚实例请求
|
||||
type RollbackInstanceRequest struct {
|
||||
Revision int `json:"revision" binding:"required"`
|
||||
Wait bool `json:"wait"`
|
||||
Timeout int `json:"timeout"` // seconds
|
||||
}
|
||||
|
||||
// DeleteInstanceRequest 删除实例请求
|
||||
type DeleteInstanceRequest struct {
|
||||
KeepHistory bool `json:"keepHistory"`
|
||||
Timeout int `json:"timeout"` // seconds
|
||||
}
|
||||
|
||||
// InstanceResponse 实例响应
|
||||
type InstanceResponse struct {
|
||||
ID string `json:"id"`
|
||||
ClusterID string `json:"clusterId"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
RegistryID string `json:"registryId"`
|
||||
Repository string `json:"repository"`
|
||||
Chart string `json:"chart"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status"`
|
||||
StatusReason string `json:"statusReason,omitempty"`
|
||||
LastOperation string `json:"lastOperation,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Revision int `json:"revision"`
|
||||
Values map[string]interface{} `json:"values,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// InstanceStatusResponse 实例状态响应
|
||||
type InstanceStatusResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Status string `json:"status"`
|
||||
Revision int `json:"revision"`
|
||||
Chart string `json:"chart"`
|
||||
Version string `json:"version"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ReleaseHistoryResponse Release 历史响应
|
||||
type ReleaseHistoryResponse struct {
|
||||
Revision int `json:"revision"`
|
||||
Updated string `json:"updated"`
|
||||
Status string `json:"status"`
|
||||
Chart string `json:"chart"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// InstanceListResponse 实例列表响应
|
||||
type InstanceListResponse struct {
|
||||
Instances []*InstanceResponse `json:"instances"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// InstanceEntryPortResponse Service 端口响应
|
||||
type InstanceEntryPortResponse struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port int32 `json:"port"`
|
||||
TargetPort string `json:"targetPort,omitempty"`
|
||||
NodePort int32 `json:"nodePort,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceEntryPathResponse Ingress path 响应
|
||||
type InstanceEntryPathResponse struct {
|
||||
Path string `json:"path"`
|
||||
ServiceName string `json:"serviceName,omitempty"`
|
||||
ServicePort string `json:"servicePort,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceEntryHostResponse Ingress host 响应
|
||||
type InstanceEntryHostResponse struct {
|
||||
Host string `json:"host"`
|
||||
Paths []InstanceEntryPathResponse `json:"paths,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceEntryTLSResponse Ingress TLS 响应
|
||||
type InstanceEntryTLSResponse struct {
|
||||
Hosts []string `json:"hosts,omitempty"`
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
}
|
||||
|
||||
// InstanceEntryResponse 实例入口响应
|
||||
type InstanceEntryResponse struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type,omitempty"`
|
||||
ClusterIP string `json:"clusterIP,omitempty"`
|
||||
ExternalIPs []string `json:"externalIPs,omitempty"`
|
||||
LoadBalancerIngress []string `json:"loadBalancerIngress,omitempty"`
|
||||
Ports []InstanceEntryPortResponse `json:"ports,omitempty"`
|
||||
Hosts []InstanceEntryHostResponse `json:"hosts,omitempty"`
|
||||
TLS []InstanceEntryTLSResponse `json:"tls,omitempty"`
|
||||
}
|
||||
143
backend/internal/adapter/input/http/dto/monitoring_dto.go
Normal file
143
backend/internal/adapter/input/http/dto/monitoring_dto.go
Normal file
@ -0,0 +1,143 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
)
|
||||
|
||||
// ClusterMetricsResponse 集群监控响应
|
||||
type ClusterMetricsResponse struct {
|
||||
ClusterID string `json:"clusterId"`
|
||||
ClusterName string `json:"clusterName"`
|
||||
Status string `json:"status"`
|
||||
Uptime string `json:"uptime"`
|
||||
NodeCount int `json:"nodeCount"`
|
||||
PodCount int `json:"podCount"`
|
||||
LastCheck time.Time `json:"lastCheck"`
|
||||
TotalCPU string `json:"totalCpu"`
|
||||
TotalMemory string `json:"totalMemory"`
|
||||
TotalGPU int `json:"totalGpu"`
|
||||
UsedCPU string `json:"usedCpu"`
|
||||
UsedMemory string `json:"usedMemory"`
|
||||
UsedGPU int `json:"usedGpu"`
|
||||
CPUUsage float64 `json:"cpuUsage"`
|
||||
MemoryUsage float64 `json:"memoryUsage"`
|
||||
GPUUsage float64 `json:"gpuUsage"`
|
||||
MaxNodeCPU string `json:"maxNodeCpu"`
|
||||
MaxNodeMemory string `json:"maxNodeMemory"`
|
||||
MaxNodeGPU int `json:"maxNodeGpu"`
|
||||
MaxNodeCPUUsage float64 `json:"maxNodeCpuUsage"`
|
||||
MaxNodeMemUsage float64 `json:"maxNodeMemUsage"`
|
||||
MaxNodeGPUUsage float64 `json:"maxNodeGpuUsage"`
|
||||
Nodes []NodeMetricsResponse `json:"nodes,omitempty"`
|
||||
}
|
||||
|
||||
// NodeMetricsResponse 节点监控响应
|
||||
type NodeMetricsResponse struct {
|
||||
NodeName string `json:"nodeName"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
Age string `json:"age"`
|
||||
PodCount int `json:"podCount"`
|
||||
CPUCapacity string `json:"cpuCapacity"`
|
||||
CPUAllocatable string `json:"cpuAllocatable"`
|
||||
CPUUsage string `json:"cpuUsage"`
|
||||
CPUPercent float64 `json:"cpuPercent"`
|
||||
MemoryCapacity string `json:"memoryCapacity"`
|
||||
MemoryAllocatable string `json:"memoryAllocatable"`
|
||||
MemoryUsage string `json:"memoryUsage"`
|
||||
MemoryPercent float64 `json:"memoryPercent"`
|
||||
GPUCapacity int `json:"gpuCapacity"`
|
||||
GPUUsage int `json:"gpuUsage"`
|
||||
GPUPercent float64 `json:"gpuPercent"`
|
||||
GPUType string `json:"gpuType,omitempty"`
|
||||
OSImage string `json:"osImage,omitempty"`
|
||||
KernelVersion string `json:"kernelVersion,omitempty"`
|
||||
ContainerRuntime string `json:"containerRuntime,omitempty"`
|
||||
KubeletVersion string `json:"kubeletVersion,omitempty"`
|
||||
}
|
||||
|
||||
// MonitoringSummaryResponse 监控汇总响应
|
||||
type MonitoringSummaryResponse struct {
|
||||
TotalClusters int `json:"totalClusters"`
|
||||
HealthyClusters int `json:"healthyClusters"`
|
||||
WarningClusters int `json:"warningClusters"`
|
||||
ErrorClusters int `json:"errorClusters"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
TotalPods int `json:"totalPods"`
|
||||
LastUpdate time.Time `json:"lastUpdate"`
|
||||
}
|
||||
|
||||
// ToClusterMetricsResponse 转换为响应
|
||||
func ToClusterMetricsResponse(m *entity.ClusterMetrics) *ClusterMetricsResponse {
|
||||
resp := &ClusterMetricsResponse{
|
||||
ClusterID: m.ClusterID,
|
||||
ClusterName: m.ClusterName,
|
||||
Status: m.Status,
|
||||
Uptime: m.Uptime,
|
||||
NodeCount: m.NodeCount,
|
||||
PodCount: m.PodCount,
|
||||
LastCheck: m.LastCheck,
|
||||
TotalCPU: m.TotalCPU,
|
||||
TotalMemory: m.TotalMemory,
|
||||
TotalGPU: m.TotalGPU,
|
||||
UsedCPU: m.UsedCPU,
|
||||
UsedMemory: m.UsedMemory,
|
||||
UsedGPU: m.UsedGPU,
|
||||
CPUUsage: m.CPUUsage,
|
||||
MemoryUsage: m.MemoryUsage,
|
||||
GPUUsage: m.GPUUsage,
|
||||
MaxNodeCPU: m.MaxNodeCPU,
|
||||
MaxNodeMemory: m.MaxNodeMemory,
|
||||
MaxNodeGPU: m.MaxNodeGPU,
|
||||
MaxNodeCPUUsage: m.MaxNodeCPUUsage,
|
||||
MaxNodeMemUsage: m.MaxNodeMemUsage,
|
||||
MaxNodeGPUUsage: m.MaxNodeGPUUsage,
|
||||
}
|
||||
|
||||
if len(m.Nodes) > 0 {
|
||||
resp.Nodes = make([]NodeMetricsResponse, len(m.Nodes))
|
||||
for i, node := range m.Nodes {
|
||||
resp.Nodes[i] = NodeMetricsResponse{
|
||||
NodeName: node.NodeName,
|
||||
Status: node.Status,
|
||||
Role: node.Role,
|
||||
Age: node.Age,
|
||||
PodCount: node.PodCount,
|
||||
CPUCapacity: node.CPUCapacity,
|
||||
CPUAllocatable: node.CPUAllocatable,
|
||||
CPUUsage: node.CPUUsage,
|
||||
CPUPercent: node.CPUPercent,
|
||||
MemoryCapacity: node.MemoryCapacity,
|
||||
MemoryAllocatable: node.MemoryAllocatable,
|
||||
MemoryUsage: node.MemoryUsage,
|
||||
MemoryPercent: node.MemoryPercent,
|
||||
GPUCapacity: node.GPUCapacity,
|
||||
GPUUsage: node.GPUUsage,
|
||||
GPUPercent: node.GPUPercent,
|
||||
GPUType: node.GPUType,
|
||||
OSImage: node.OSImage,
|
||||
KernelVersion: node.KernelVersion,
|
||||
ContainerRuntime: node.ContainerRuntime,
|
||||
KubeletVersion: node.KubeletVersion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// ToMonitoringSummaryResponse 转换为汇总响应
|
||||
func ToMonitoringSummaryResponse(s *entity.MonitoringSummary) *MonitoringSummaryResponse {
|
||||
return &MonitoringSummaryResponse{
|
||||
TotalClusters: s.TotalClusters,
|
||||
HealthyClusters: s.HealthyClusters,
|
||||
WarningClusters: s.WarningClusters,
|
||||
ErrorClusters: s.ErrorClusters,
|
||||
TotalNodes: s.TotalNodes,
|
||||
TotalPods: s.TotalPods,
|
||||
LastUpdate: s.LastUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
42
backend/internal/adapter/input/http/dto/registry_dto.go
Normal file
42
backend/internal/adapter/input/http/dto/registry_dto.go
Normal file
@ -0,0 +1,42 @@
|
||||
package dto
|
||||
|
||||
// CreateRegistryRequest 创建 Registry 请求
|
||||
type CreateRegistryRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Description string `json:"description"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}
|
||||
|
||||
// UpdateRegistryRequest 更新 Registry 请求
|
||||
type UpdateRegistryRequest struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Description string `json:"description"`
|
||||
Insecure bool `json:"insecure"`
|
||||
}
|
||||
|
||||
// RegistryResponse Registry 响应(敏感数据已脱敏)
|
||||
type RegistryResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
Username string `json:"username,omitempty"` // 明文返回用户名(不敏感)
|
||||
Password string `json:"password,omitempty"` // 脱敏显示(••••••••)
|
||||
HasPassword bool `json:"hasPassword"` // 是否已设置密码
|
||||
Insecure bool `json:"insecure"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// RegistryHealthResponse Registry 健康状态响应
|
||||
type RegistryHealthResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
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