refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
@ -6,9 +6,9 @@ type RepositoryListResponse struct {
|
||||
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
|
||||
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 响应(简化版本,只包含核心字段)
|
||||
@ -23,11 +23,11 @@ type ArtifactResponse struct {
|
||||
|
||||
// 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
|
||||
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)
|
||||
Size int64 `json:"size"` // Artifact size (bytes)
|
||||
}
|
||||
|
||||
// ArtifactListResponse Artifact 列表响应(包装格式,用于详细接口)
|
||||
@ -42,3 +42,7 @@ type ValuesSchemaResponse struct {
|
||||
Schema string `json:"schema"`
|
||||
}
|
||||
|
||||
// ValuesYAMLResponse Helm Chart 默认 values.yaml 响应
|
||||
type ValuesYAMLResponse struct {
|
||||
ValuesYAML string `json:"valuesYaml"`
|
||||
}
|
||||
|
||||
@ -2,8 +2,18 @@ package dto
|
||||
|
||||
// RegisterRequest 用户注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Role string `json:"role,omitempty"`
|
||||
WorkspaceID string `json:"workspaceId,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
DefaultClusterID string `json:"defaultClusterId,omitempty"`
|
||||
QuotaCPU string `json:"quotaCpu,omitempty"`
|
||||
QuotaMemory string `json:"quotaMemory,omitempty"`
|
||||
QuotaGPU string `json:"quotaGpu,omitempty"`
|
||||
QuotaGPUMem string `json:"quotaGpuMemory,omitempty"`
|
||||
IsActive *bool `json:"isActive,omitempty"`
|
||||
MustChangePassword *bool `json:"mustChangePassword,omitempty"`
|
||||
}
|
||||
|
||||
// LoginRequest 用户登录请求
|
||||
@ -19,17 +29,53 @@ type RefreshTokenRequest struct {
|
||||
|
||||
// AuthResponse 认证响应
|
||||
type AuthResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
UserID string `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
WorkspaceName string `json:"workspaceName,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
DefaultClusterID string `json:"defaultClusterId,omitempty"`
|
||||
QuotaCPU string `json:"quotaCpu,omitempty"`
|
||||
QuotaMemory string `json:"quotaMemory,omitempty"`
|
||||
QuotaGPU string `json:"quotaGpu,omitempty"`
|
||||
QuotaGPUMem string `json:"quotaGpuMemory,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
PermissionVersion int `json:"permissionVersion"`
|
||||
}
|
||||
|
||||
// UserResponse 用户信息响应
|
||||
type UserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
WorkspaceName string `json:"workspaceName,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
DefaultClusterID string `json:"defaultClusterId,omitempty"`
|
||||
QuotaCPU string `json:"quotaCpu,omitempty"`
|
||||
QuotaMemory string `json:"quotaMemory,omitempty"`
|
||||
QuotaGPU string `json:"quotaGpu,omitempty"`
|
||||
QuotaGPUMem string `json:"quotaGpuMemory,omitempty"`
|
||||
IsActive bool `json:"isActive"`
|
||||
MustChangePassword bool `json:"mustChangePassword"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 管理员更新用户状态/角色请求
|
||||
type UpdateUserRequest struct {
|
||||
Role string `json:"role,omitempty"`
|
||||
WorkspaceID string `json:"workspaceId,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
DefaultClusterID string `json:"defaultClusterId,omitempty"`
|
||||
QuotaCPU string `json:"quotaCpu,omitempty"`
|
||||
QuotaMemory string `json:"quotaMemory,omitempty"`
|
||||
QuotaGPU string `json:"quotaGpu,omitempty"`
|
||||
QuotaGPUMem string `json:"quotaGpuMemory,omitempty"`
|
||||
IsActive *bool `json:"isActive,omitempty"`
|
||||
MustChangePassword *bool `json:"mustChangePassword,omitempty"`
|
||||
}
|
||||
|
||||
@ -2,30 +2,38 @@ 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"`
|
||||
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"`
|
||||
Visibility string `json:"visibility"`
|
||||
GlobalShared bool `json:"globalShared"`
|
||||
GlobalSharedAlt bool `json:"global_shared"`
|
||||
DefaultNamespace string `json:"defaultNamespace"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
Visibility string `json:"visibility"`
|
||||
GlobalShared bool `json:"globalShared"`
|
||||
GlobalSharedAlt bool `json:"global_shared"`
|
||||
DefaultNamespace string `json:"defaultNamespace"`
|
||||
}
|
||||
|
||||
// Normalize 将多种命名风格的字段合并到统一字段
|
||||
@ -56,10 +64,15 @@ func (r *UpdateClusterRequest) Normalize() {
|
||||
|
||||
// ClusterResponse 集群响应(敏感数据已脱敏)
|
||||
type ClusterResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Description string `json:"description"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Description string `json:"description"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
OwnerID string `json:"ownerId"`
|
||||
Visibility string `json:"visibility"`
|
||||
DefaultNamespace string `json:"defaultNamespace,omitempty"`
|
||||
AllowedActions []string `json:"allowedActions,omitempty"`
|
||||
// 认证配置状态(不返回实际证书数据,仅返回是否已配置)
|
||||
HasCAData bool `json:"hasCaData"`
|
||||
HasCertData bool `json:"hasCertData"`
|
||||
|
||||
@ -9,6 +9,9 @@ import (
|
||||
func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
|
||||
response := &RegistryResponse{
|
||||
ID: registry.ID,
|
||||
WorkspaceID: registry.WorkspaceID,
|
||||
OwnerID: registry.OwnerID,
|
||||
Visibility: registry.Visibility,
|
||||
Name: registry.Name,
|
||||
URL: registry.URL,
|
||||
Description: registry.Description,
|
||||
@ -17,33 +20,37 @@ func ToRegistryResponse(registry *entity.Registry) *RegistryResponse {
|
||||
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"),
|
||||
ID: cluster.ID,
|
||||
WorkspaceID: cluster.WorkspaceID,
|
||||
OwnerID: cluster.OwnerID,
|
||||
Visibility: cluster.Visibility,
|
||||
Name: cluster.Name,
|
||||
Host: cluster.Host,
|
||||
Description: cluster.Description,
|
||||
DefaultNamespace: cluster.DefaultNamespace,
|
||||
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)
|
||||
@ -57,7 +64,6 @@ func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse {
|
||||
if cluster.Token != "" {
|
||||
response.Token = crypto.MaskSensitiveData(cluster.Token)
|
||||
}
|
||||
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@ -12,4 +12,3 @@ type SuccessResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@ -43,23 +43,26 @@ type DeleteInstanceRequest struct {
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
OwnerID string `json:"ownerId"`
|
||||
AllowedActions []string `json:"allowedActions,omitempty"`
|
||||
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 实例状态响应
|
||||
@ -131,3 +134,70 @@ type InstanceEntryResponse struct {
|
||||
Hosts []InstanceEntryHostResponse `json:"hosts,omitempty"`
|
||||
TLS []InstanceEntryTLSResponse `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
type InstanceDiagnosticsResponse struct {
|
||||
InstanceName string `json:"instanceName"`
|
||||
Namespace string `json:"namespace"`
|
||||
Pods []InstancePodDiagnostics `json:"pods"`
|
||||
Services []InstanceServiceDiagnostics `json:"services"`
|
||||
Events []InstanceEventDiagnostics `json:"events"`
|
||||
Logs []InstancePodLogResponse `json:"logs"`
|
||||
CollectedAt string `json:"collectedAt"`
|
||||
}
|
||||
|
||||
type InstancePodDiagnostics struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Phase string `json:"phase"`
|
||||
NodeName string `json:"nodeName,omitempty"`
|
||||
PodIP string `json:"podIp,omitempty"`
|
||||
HostIP string `json:"hostIp,omitempty"`
|
||||
RestartCount int32 `json:"restartCount"`
|
||||
Containers []InstanceContainerDiagnostics `json:"containers"`
|
||||
Conditions []InstanceConditionDiagnostics `json:"conditions"`
|
||||
CreationTimestamp string `json:"creationTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
type InstanceContainerDiagnostics struct {
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
Ready bool `json:"ready"`
|
||||
RestartCount int32 `json:"restartCount"`
|
||||
State string `json:"state"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type InstanceConditionDiagnostics struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type InstanceServiceDiagnostics struct {
|
||||
Name string `json:"name"`
|
||||
Namespace string `json:"namespace"`
|
||||
Type string `json:"type"`
|
||||
ClusterIP string `json:"clusterIP,omitempty"`
|
||||
Ports []InstanceEntryPortResponse `json:"ports,omitempty"`
|
||||
}
|
||||
|
||||
type InstanceEventDiagnostics struct {
|
||||
Type string `json:"type"`
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
InvolvedKind string `json:"involvedKind"`
|
||||
InvolvedName string `json:"involvedName"`
|
||||
Count int32 `json:"count"`
|
||||
FirstTimestamp string `json:"firstTimestamp,omitempty"`
|
||||
LastTimestamp string `json:"lastTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
type InstancePodLogResponse struct {
|
||||
Pod string `json:"pod"`
|
||||
Container string `json:"container"`
|
||||
TailLines int64 `json:"tailLines"`
|
||||
Log string `json:"log,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@ -8,29 +8,29 @@ import (
|
||||
|
||||
// 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"`
|
||||
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 节点监控响应
|
||||
@ -72,28 +72,28 @@ type MonitoringSummaryResponse struct {
|
||||
// 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,
|
||||
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 {
|
||||
@ -140,4 +140,3 @@ func ToMonitoringSummaryResponse(s *entity.MonitoringSummary) *MonitoringSummary
|
||||
LastUpdate: s.LastUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,36 +2,46 @@ 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"`
|
||||
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"`
|
||||
Visibility string `json:"visibility"`
|
||||
GlobalShared bool `json:"globalShared"`
|
||||
GlobalSharedAlt bool `json:"global_shared"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Description string `json:"description"`
|
||||
Insecure bool `json:"insecure"`
|
||||
Visibility string `json:"visibility"`
|
||||
GlobalShared bool `json:"globalShared"`
|
||||
GlobalSharedAlt bool `json:"global_shared"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Description string `json:"description"`
|
||||
WorkspaceID string `json:"workspaceId"`
|
||||
OwnerID string `json:"ownerId"`
|
||||
Visibility string `json:"visibility"`
|
||||
AllowedActions []string `json:"allowedActions,omitempty"`
|
||||
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 健康状态响应
|
||||
@ -39,4 +49,3 @@ type RegistryHealthResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@ -29,14 +29,19 @@ func NewArtifactHandler(artifactService *service.ArtifactService) *ArtifactHandl
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param registry_id path string true "Registry ID"
|
||||
// @Param artifact_type query string false "Artifact type filter (chart, all)" default(chart)
|
||||
// @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"]
|
||||
artifactType := r.URL.Query().Get("artifact_type")
|
||||
if artifactType == "" {
|
||||
artifactType = "chart"
|
||||
}
|
||||
|
||||
repositories, err := h.artifactService.ListRepositories(r.Context(), registryID)
|
||||
repositories, err := h.artifactService.ListRepositories(r.Context(), registryID, artifactType)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list repositories", err.Error())
|
||||
return
|
||||
@ -50,13 +55,17 @@ func (h *ArtifactHandler) ListRepositories(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// Determine source and message based on repository count
|
||||
source := "catalog"
|
||||
source := "harbor-api"
|
||||
catalogSupported := true
|
||||
message := ""
|
||||
|
||||
if len(repositories) == 0 {
|
||||
source = "unavailable"
|
||||
message = "No repositories found in this registry"
|
||||
if artifactType == "chart" {
|
||||
message = "No chart repositories found in this registry"
|
||||
} else {
|
||||
message = "No repositories found in this registry"
|
||||
}
|
||||
}
|
||||
|
||||
response := &dto.RepositoryListResponse{
|
||||
@ -191,3 +200,37 @@ func (h *ArtifactHandler) GetArtifactValuesSchema(w http.ResponseWriter, r *http
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetArtifactValuesYAML 获取 Helm Chart 的默认 values.yaml
|
||||
// @Summary 获取 Helm Chart 默认 Values YAML
|
||||
// @Description 获取 Helm Chart 包内原始 values.yaml,用于高级覆盖编辑
|
||||
// @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.ValuesYAMLResponse
|
||||
// @Failure 500 {object} dto.ErrorResponse
|
||||
// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-yaml [get]
|
||||
func (h *ArtifactHandler) GetArtifactValuesYAML(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
registryID := vars["registry_id"]
|
||||
repositoryName := vars["repository_name"]
|
||||
reference := vars["reference"]
|
||||
|
||||
valuesYAML, err := h.artifactService.GetValuesYAML(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):
|
||||
respondError(w, http.StatusNotFound, "Values YAML not found", err.Error())
|
||||
default:
|
||||
respondError(w, http.StatusInternalServerError, "Failed to get values YAML", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, &dto.ValuesYAMLResponse{ValuesYAML: valuesYAML})
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"github.com/ocdp/cluster-service/internal/pkg/authz"
|
||||
)
|
||||
|
||||
// AuthHandler 认证 Handler
|
||||
@ -20,9 +25,9 @@ func NewAuthHandler(authService *service.AuthService) *AuthHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
// @Summary 用户注册
|
||||
// @Description 创建一个新的后台用户
|
||||
// Register 管理员创建用户
|
||||
// @Summary 管理员创建用户
|
||||
// @Description 创建一个新的后台用户。公开自注册已禁用,只允许 admin 调用。
|
||||
// @Tags Auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -38,22 +43,64 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
user, err := h.authService.Register(r.Context(), req.Username, req.Password)
|
||||
user, err := h.authService.Register(r.Context(), req.Username, req.Password, req.Role, req.WorkspaceID, service.UserWorkspaceOptions{
|
||||
Namespace: req.Namespace,
|
||||
DefaultClusterID: req.DefaultClusterID,
|
||||
QuotaCPU: req.QuotaCPU,
|
||||
QuotaMemory: req.QuotaMemory,
|
||||
QuotaGPU: req.QuotaGPU,
|
||||
QuotaGPUMem: req.QuotaGPUMem,
|
||||
}, req.IsActive, req.MustChangePassword)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Registration failed", err.Error())
|
||||
respondServiceError(w, err, "Registration failed")
|
||||
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, h.convertUserResponse(r.Context(), user))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusCreated, response)
|
||||
func (h *AuthHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := h.authService.ListUsers(r.Context())
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to list users")
|
||||
return
|
||||
}
|
||||
responses := make([]*dto.UserResponse, 0, len(users))
|
||||
for _, user := range users {
|
||||
responses = append(responses, h.convertUserResponse(r.Context(), user))
|
||||
}
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID := mux.Vars(r)["user_id"]
|
||||
var req dto.UpdateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
user, err := h.authService.UpdateUser(r.Context(), userID, req.Role, req.WorkspaceID, service.UserWorkspaceOptions{
|
||||
Namespace: req.Namespace,
|
||||
DefaultClusterID: req.DefaultClusterID,
|
||||
QuotaCPU: req.QuotaCPU,
|
||||
QuotaMemory: req.QuotaMemory,
|
||||
QuotaGPU: req.QuotaGPU,
|
||||
QuotaGPUMem: req.QuotaGPUMem,
|
||||
}, req.IsActive, req.MustChangePassword)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to update user")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, h.convertUserResponse(r.Context(), user))
|
||||
}
|
||||
|
||||
func (h *AuthHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
userID := mux.Vars(r)["user_id"]
|
||||
if err := h.authService.DeleteUser(r.Context(), userID); err != nil {
|
||||
respondServiceError(w, err, "Failed to delete user")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
@ -74,25 +121,58 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
accessToken, refreshToken, err := h.authService.Login(r.Context(), req.Username, req.Password)
|
||||
accessToken, refreshToken, user, err := h.authService.Login(r.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "Login failed", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
// TODO: 从 token 解析用户信息或从服务获取
|
||||
workspace, _ := h.authService.GetWorkspaceByID(r.Context(), user.WorkspaceID)
|
||||
|
||||
// 返回响应
|
||||
response := &dto.AuthResponse{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
Username: req.Username,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
WorkspaceID: user.WorkspaceID,
|
||||
WorkspaceName: workspaceName(workspace),
|
||||
Namespace: workspaceNamespace(workspace),
|
||||
DefaultClusterID: workspaceDefaultClusterID(workspace),
|
||||
QuotaCPU: workspaceQuotaCPU(workspace),
|
||||
QuotaMemory: workspaceQuotaMemory(workspace),
|
||||
QuotaGPU: workspaceQuotaGPU(workspace),
|
||||
QuotaGPUMem: workspaceQuotaGPUMem(workspace),
|
||||
Permissions: authz.PermissionsForRole(user.Role),
|
||||
PermissionVersion: 1,
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) convertUserResponse(ctx context.Context, user *entity.User) *dto.UserResponse {
|
||||
workspace, _ := h.authService.GetWorkspaceByID(ctx, user.WorkspaceID)
|
||||
return &dto.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
WorkspaceID: user.WorkspaceID,
|
||||
WorkspaceName: workspaceName(workspace),
|
||||
Namespace: workspaceNamespace(workspace),
|
||||
DefaultClusterID: workspaceDefaultClusterID(workspace),
|
||||
QuotaCPU: workspaceQuotaCPU(workspace),
|
||||
QuotaMemory: workspaceQuotaMemory(workspace),
|
||||
QuotaGPU: workspaceQuotaGPU(workspace),
|
||||
QuotaGPUMem: workspaceQuotaGPUMem(workspace),
|
||||
IsActive: user.IsActive,
|
||||
MustChangePassword: user.MustChangePassword,
|
||||
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken 刷新 Token
|
||||
// @Summary 刷新访问令牌
|
||||
// @Description 使用刷新令牌获取新的访问令牌
|
||||
@ -111,17 +191,109 @@ func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
newAccessToken, err := h.authService.RefreshToken(r.Context(), req.RefreshToken)
|
||||
newAccessToken, user, err := h.authService.RefreshToken(r.Context(), req.RefreshToken)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "Token refresh failed", err.Error())
|
||||
return
|
||||
}
|
||||
workspace, _ := h.authService.GetWorkspaceByID(r.Context(), user.WorkspaceID)
|
||||
|
||||
// 返回响应
|
||||
response := &dto.AuthResponse{
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: req.RefreshToken,
|
||||
AccessToken: newAccessToken,
|
||||
RefreshToken: req.RefreshToken,
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
WorkspaceID: user.WorkspaceID,
|
||||
WorkspaceName: workspaceName(workspace),
|
||||
Namespace: workspaceNamespace(workspace),
|
||||
DefaultClusterID: workspaceDefaultClusterID(workspace),
|
||||
QuotaCPU: workspaceQuotaCPU(workspace),
|
||||
QuotaMemory: workspaceQuotaMemory(workspace),
|
||||
QuotaGPU: workspaceQuotaGPU(workspace),
|
||||
QuotaGPUMem: workspaceQuotaGPUMem(workspace),
|
||||
Permissions: authz.PermissionsForRole(user.Role),
|
||||
PermissionVersion: 1,
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
header := r.Header.Get("Authorization")
|
||||
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
|
||||
if token == "" || token == header {
|
||||
respondError(w, http.StatusUnauthorized, "Unauthorized", "missing bearer token")
|
||||
return
|
||||
}
|
||||
principal, err := h.authService.VerifyAccessToken(r.Context(), token)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusUnauthorized, "Unauthorized", err.Error())
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, &dto.AuthResponse{
|
||||
UserID: principal.UserID,
|
||||
Username: principal.Username,
|
||||
Role: principal.Role,
|
||||
WorkspaceID: principal.WorkspaceID,
|
||||
WorkspaceName: principal.WorkspaceName,
|
||||
Namespace: principal.Namespace,
|
||||
DefaultClusterID: principal.DefaultClusterID,
|
||||
QuotaCPU: principal.QuotaCPU,
|
||||
QuotaMemory: principal.QuotaMemory,
|
||||
QuotaGPU: principal.QuotaGPU,
|
||||
QuotaGPUMem: principal.QuotaGPUMem,
|
||||
Permissions: principal.Permissions,
|
||||
PermissionVersion: principal.PermissionVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func workspaceName(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.Name
|
||||
}
|
||||
|
||||
func workspaceNamespace(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.K8sNamespace
|
||||
}
|
||||
|
||||
func workspaceDefaultClusterID(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.DefaultClusterID
|
||||
}
|
||||
|
||||
func workspaceQuotaCPU(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.QuotaCPU
|
||||
}
|
||||
|
||||
func workspaceQuotaMemory(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.QuotaMemory
|
||||
}
|
||||
|
||||
func workspaceQuotaGPU(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.QuotaGPU
|
||||
}
|
||||
|
||||
func workspaceQuotaGPUMem(workspace *entity.Workspace) string {
|
||||
if workspace == nil {
|
||||
return ""
|
||||
}
|
||||
return workspace.QuotaGPUMem
|
||||
}
|
||||
|
||||
@ -45,6 +45,11 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) {
|
||||
// 创建实体
|
||||
cluster := entity.NewCluster(req.Name, req.Host)
|
||||
cluster.Description = req.Description
|
||||
cluster.Visibility = req.Visibility
|
||||
if req.GlobalShared || req.GlobalSharedAlt {
|
||||
cluster.Visibility = "global_shared"
|
||||
}
|
||||
cluster.DefaultNamespace = req.DefaultNamespace
|
||||
|
||||
if req.CertData != "" && req.KeyData != "" {
|
||||
cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData)
|
||||
@ -147,6 +152,15 @@ func (h *ClusterHandler) UpdateCluster(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 更新字段
|
||||
cluster.Update(req.Name, req.Host, req.Description)
|
||||
if req.Visibility != "" {
|
||||
cluster.Visibility = req.Visibility
|
||||
}
|
||||
if req.GlobalShared || req.GlobalSharedAlt {
|
||||
cluster.Visibility = "global_shared"
|
||||
}
|
||||
if req.DefaultNamespace != "" {
|
||||
cluster.DefaultNamespace = req.DefaultNamespace
|
||||
}
|
||||
|
||||
if req.CertData != "" && req.KeyData != "" {
|
||||
cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData)
|
||||
|
||||
@ -2,13 +2,17 @@ package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// InstanceHandler 实例 Handler
|
||||
@ -69,6 +73,14 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
if req.ValuesYAML != "" {
|
||||
instance.SetValuesYAML(req.ValuesYAML)
|
||||
if req.Values == nil {
|
||||
values, err := parseValuesYAML(req.ValuesYAML)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid values YAML", err.Error())
|
||||
return
|
||||
}
|
||||
instance.SetValues(values)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
@ -77,28 +89,7 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request)
|
||||
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)
|
||||
respondJSON(w, http.StatusCreated, convertInstanceResponse(instance, true))
|
||||
}
|
||||
|
||||
// GetInstance 获取实例详情
|
||||
@ -113,6 +104,7 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request)
|
||||
// @Router /clusters/{cluster_id}/instances/{instance_id} [get]
|
||||
func (h *InstanceHandler) GetInstance(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
clusterID := vars["cluster_id"]
|
||||
instanceID := vars["instance_id"]
|
||||
|
||||
instance, err := h.instanceService.GetInstance(r.Context(), instanceID)
|
||||
@ -120,28 +112,12 @@ func (h *InstanceHandler) GetInstance(w http.ResponseWriter, r *http.Request) {
|
||||
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"),
|
||||
if instance.ClusterID != clusterID {
|
||||
respondError(w, http.StatusNotFound, "Instance not found", "resource does not belong to cluster")
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
respondJSON(w, http.StatusOK, convertInstanceResponse(instance, true))
|
||||
}
|
||||
|
||||
// ListInstances 列出集群的所有实例
|
||||
@ -159,30 +135,13 @@ func (h *InstanceHandler) ListInstances(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
instances, err := h.instanceService.ListInstancesByCluster(r.Context(), clusterID)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "Failed to list instances", err.Error())
|
||||
respondServiceError(w, err, "Failed to list instances")
|
||||
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"),
|
||||
})
|
||||
responses = append(responses, convertInstanceResponse(instance, false))
|
||||
}
|
||||
|
||||
response := &dto.InstanceListResponse{
|
||||
@ -225,12 +184,22 @@ func (h *InstanceHandler) UpdateInstance(w http.ResponseWriter, r *http.Request)
|
||||
// 更新字段
|
||||
if req.Version != "" {
|
||||
instance.Upgrade(req.Version, req.Values)
|
||||
} else if req.Values != nil {
|
||||
instance.SetValues(req.Values)
|
||||
}
|
||||
if req.Description != "" {
|
||||
instance.Description = req.Description
|
||||
}
|
||||
if req.ValuesYAML != "" {
|
||||
instance.SetValuesYAML(req.ValuesYAML)
|
||||
if req.Values == nil {
|
||||
values, err := parseValuesYAML(req.ValuesYAML)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid values YAML", err.Error())
|
||||
return
|
||||
}
|
||||
instance.SetValues(values)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用领域服务
|
||||
@ -239,27 +208,7 @@ func (h *InstanceHandler) UpdateInstance(w http.ResponseWriter, r *http.Request)
|
||||
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)
|
||||
respondJSON(w, http.StatusOK, convertInstanceResponse(instance, true))
|
||||
}
|
||||
|
||||
// DeleteInstance 删除实例
|
||||
@ -320,6 +269,35 @@ func (h *InstanceHandler) ListInstanceEntries(w http.ResponseWriter, r *http.Req
|
||||
respondJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
func (h *InstanceHandler) GetInstanceDiagnostics(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
clusterID := vars["cluster_id"]
|
||||
instanceID := vars["instance_id"]
|
||||
tailLines := int64(200)
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("tailLines")); raw != "" {
|
||||
parsed, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || parsed < 0 {
|
||||
respondError(w, http.StatusBadRequest, "Invalid tailLines", "tailLines must be a positive integer")
|
||||
return
|
||||
}
|
||||
tailLines = parsed
|
||||
}
|
||||
|
||||
diagnostics, err := h.instanceService.GetInstanceDiagnostics(r.Context(), clusterID, instanceID, tailLines)
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
switch err {
|
||||
case entity.ErrInstanceNotFound, entity.ErrClusterNotFound:
|
||||
status = http.StatusNotFound
|
||||
case entity.ErrForbidden:
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
respondError(w, status, "Failed to collect instance diagnostics", err.Error())
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, convertInstanceDiagnostics(diagnostics))
|
||||
}
|
||||
|
||||
func convertInstanceEntry(entry *entity.InstanceEntry) *dto.InstanceEntryResponse {
|
||||
portResponses := make([]dto.InstanceEntryPortResponse, 0, len(entry.Ports))
|
||||
for _, port := range entry.Ports {
|
||||
@ -369,3 +347,195 @@ func convertInstanceEntry(entry *entity.InstanceEntry) *dto.InstanceEntryRespons
|
||||
TLS: tlsResponses,
|
||||
}
|
||||
}
|
||||
|
||||
func convertInstanceDiagnostics(diagnostics *entity.InstanceDiagnostics) *dto.InstanceDiagnosticsResponse {
|
||||
if diagnostics == nil {
|
||||
return &dto.InstanceDiagnosticsResponse{}
|
||||
}
|
||||
pods := make([]dto.InstancePodDiagnostics, 0, len(diagnostics.Pods))
|
||||
for _, pod := range diagnostics.Pods {
|
||||
containers := make([]dto.InstanceContainerDiagnostics, 0, len(pod.Containers))
|
||||
for _, container := range pod.Containers {
|
||||
containers = append(containers, dto.InstanceContainerDiagnostics{
|
||||
Name: container.Name,
|
||||
Image: container.Image,
|
||||
Ready: container.Ready,
|
||||
RestartCount: container.RestartCount,
|
||||
State: container.State,
|
||||
Reason: container.Reason,
|
||||
Message: container.Message,
|
||||
})
|
||||
}
|
||||
conditions := make([]dto.InstanceConditionDiagnostics, 0, len(pod.Conditions))
|
||||
for _, condition := range pod.Conditions {
|
||||
conditions = append(conditions, dto.InstanceConditionDiagnostics{
|
||||
Type: condition.Type,
|
||||
Status: condition.Status,
|
||||
Reason: condition.Reason,
|
||||
Message: condition.Message,
|
||||
})
|
||||
}
|
||||
pods = append(pods, dto.InstancePodDiagnostics{
|
||||
Name: pod.Name,
|
||||
Namespace: pod.Namespace,
|
||||
Phase: pod.Phase,
|
||||
NodeName: pod.NodeName,
|
||||
PodIP: pod.PodIP,
|
||||
HostIP: pod.HostIP,
|
||||
RestartCount: pod.RestartCount,
|
||||
Containers: containers,
|
||||
Conditions: conditions,
|
||||
CreationTimestamp: formatTime(pod.CreationTimestamp),
|
||||
})
|
||||
}
|
||||
services := make([]dto.InstanceServiceDiagnostics, 0, len(diagnostics.Services))
|
||||
for _, svc := range diagnostics.Services {
|
||||
ports := make([]dto.InstanceEntryPortResponse, 0, len(svc.Ports))
|
||||
for _, port := range svc.Ports {
|
||||
ports = append(ports, dto.InstanceEntryPortResponse{
|
||||
Name: port.Name,
|
||||
Protocol: port.Protocol,
|
||||
Port: port.Port,
|
||||
TargetPort: port.TargetPort,
|
||||
NodePort: port.NodePort,
|
||||
})
|
||||
}
|
||||
services = append(services, dto.InstanceServiceDiagnostics{
|
||||
Name: svc.Name,
|
||||
Namespace: svc.Namespace,
|
||||
Type: svc.Type,
|
||||
ClusterIP: svc.ClusterIP,
|
||||
Ports: ports,
|
||||
})
|
||||
}
|
||||
events := make([]dto.InstanceEventDiagnostics, 0, len(diagnostics.Events))
|
||||
for _, event := range diagnostics.Events {
|
||||
events = append(events, dto.InstanceEventDiagnostics{
|
||||
Type: event.Type,
|
||||
Reason: event.Reason,
|
||||
Message: event.Message,
|
||||
InvolvedKind: event.InvolvedKind,
|
||||
InvolvedName: event.InvolvedName,
|
||||
Count: event.Count,
|
||||
FirstTimestamp: formatTime(event.FirstTimestamp),
|
||||
LastTimestamp: formatTime(event.LastTimestamp),
|
||||
})
|
||||
}
|
||||
logs := make([]dto.InstancePodLogResponse, 0, len(diagnostics.Logs))
|
||||
for _, logEntry := range diagnostics.Logs {
|
||||
logs = append(logs, dto.InstancePodLogResponse{
|
||||
Pod: logEntry.Pod,
|
||||
Container: logEntry.Container,
|
||||
TailLines: logEntry.TailLines,
|
||||
Log: logEntry.Log,
|
||||
Error: logEntry.Error,
|
||||
})
|
||||
}
|
||||
return &dto.InstanceDiagnosticsResponse{
|
||||
InstanceName: diagnostics.InstanceName,
|
||||
Namespace: diagnostics.Namespace,
|
||||
Pods: pods,
|
||||
Services: services,
|
||||
Events: events,
|
||||
Logs: logs,
|
||||
CollectedAt: formatTime(diagnostics.CollectedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return value.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func convertInstanceResponse(instance *entity.Instance, includeValues bool) *dto.InstanceResponse {
|
||||
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),
|
||||
WorkspaceID: instance.WorkspaceID,
|
||||
OwnerID: instance.OwnerID,
|
||||
StatusReason: instance.StatusReason,
|
||||
LastOperation: string(instance.LastOperation),
|
||||
LastError: instance.LastError,
|
||||
Revision: instance.Revision,
|
||||
AllowedActions: []string{"view", "update", "delete"},
|
||||
CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
if includeValues {
|
||||
response.Values = instance.Values
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func parseValuesYAML(valuesYAML string) (map[string]interface{}, error) {
|
||||
valuesYAML = strings.TrimSpace(valuesYAML)
|
||||
if valuesYAML == "" {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
var decoded interface{}
|
||||
if err := yaml.Unmarshal([]byte(valuesYAML), &decoded); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
normalized, err := normalizeYAMLValue(decoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values, ok := normalized.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("values YAML must be a mapping at the top level")
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func normalizeYAMLValue(value interface{}) (interface{}, error) {
|
||||
switch typed := value.(type) {
|
||||
case map[string]interface{}:
|
||||
normalized := make(map[string]interface{}, len(typed))
|
||||
for key, child := range typed {
|
||||
normalizedChild, err := normalizeYAMLValue(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized[key] = normalizedChild
|
||||
}
|
||||
return normalized, nil
|
||||
case map[interface{}]interface{}:
|
||||
normalized := make(map[string]interface{}, len(typed))
|
||||
for key, child := range typed {
|
||||
keyString, ok := key.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("values YAML contains non-string key %v", key)
|
||||
}
|
||||
normalizedChild, err := normalizeYAMLValue(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized[keyString] = normalizedChild
|
||||
}
|
||||
return normalized, nil
|
||||
case []interface{}:
|
||||
normalized := make([]interface{}, 0, len(typed))
|
||||
for _, child := range typed {
|
||||
normalizedChild, err := normalizeYAMLValue(child)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized = append(normalized, normalizedChild)
|
||||
}
|
||||
return normalized, nil
|
||||
default:
|
||||
return typed, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,10 @@ func (h *RegistryHandler) CreateRegistry(w http.ResponseWriter, r *http.Request)
|
||||
registry := entity.NewRegistry(req.Name, req.URL)
|
||||
registry.Description = req.Description
|
||||
registry.Insecure = req.Insecure
|
||||
registry.Visibility = req.Visibility
|
||||
if req.GlobalShared || req.GlobalSharedAlt {
|
||||
registry.Visibility = "global_shared"
|
||||
}
|
||||
registry.SetCredentials(req.Username, req.Password)
|
||||
|
||||
// 调用领域服务
|
||||
@ -136,6 +140,12 @@ func (h *RegistryHandler) UpdateRegistry(w http.ResponseWriter, r *http.Request)
|
||||
// 更新字段
|
||||
registry.Update(req.Name, req.URL, req.Description)
|
||||
registry.Insecure = req.Insecure
|
||||
if req.Visibility != "" {
|
||||
registry.Visibility = req.Visibility
|
||||
}
|
||||
if req.GlobalShared || req.GlobalSharedAlt {
|
||||
registry.Visibility = "global_shared"
|
||||
}
|
||||
if req.Username != "" || req.Password != "" {
|
||||
registry.SetCredentials(req.Username, req.Password)
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package rest
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
|
||||
"github.com/ocdp/cluster-service/internal/adapter/input/http/dto"
|
||||
)
|
||||
|
||||
@ -32,4 +32,3 @@ func respondSuccess(w http.ResponseWriter, message string, data interface{}) {
|
||||
}
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
165
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
165
backend/internal/adapter/input/http/rest/workspace_handler.go
Normal file
@ -0,0 +1,165 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/service"
|
||||
"github.com/ocdp/cluster-service/internal/pkg/authz"
|
||||
)
|
||||
|
||||
type WorkspaceHandler struct {
|
||||
workspaceService *service.WorkspaceService
|
||||
}
|
||||
|
||||
func NewWorkspaceHandler(workspaceService *service.WorkspaceService) *WorkspaceHandler {
|
||||
return &WorkspaceHandler{workspaceService: workspaceService}
|
||||
}
|
||||
|
||||
type createWorkspaceRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type workspaceResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
K8sNamespace string `json:"k8sNamespace"`
|
||||
K8sSAName string `json:"k8sSaName"`
|
||||
DefaultClusterID string `json:"defaultClusterId,omitempty"`
|
||||
QuotaCPU string `json:"quotaCpu,omitempty"`
|
||||
QuotaMemory string `json:"quotaMemory,omitempty"`
|
||||
QuotaGPU string `json:"quotaGpu,omitempty"`
|
||||
QuotaGPUMem string `json:"quotaGpuMemory,omitempty"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type bindClusterRequest struct {
|
||||
ClusterID string `json:"clusterId"`
|
||||
}
|
||||
|
||||
type kubeconfigRequest struct {
|
||||
ClusterID string `json:"clusterId"`
|
||||
TTLSeconds int64 `json:"ttlSeconds"`
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) ListWorkspaces(w http.ResponseWriter, r *http.Request) {
|
||||
workspaces, err := h.workspaceService.ListWorkspaces(r.Context())
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to list workspaces")
|
||||
return
|
||||
}
|
||||
response := make([]workspaceResponse, 0, len(workspaces))
|
||||
for _, workspace := range workspaces {
|
||||
response = append(response, toWorkspaceResponse(workspace))
|
||||
}
|
||||
respondJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) CreateWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
var req createWorkspaceRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
workspace, err := h.workspaceService.CreateWorkspace(r.Context(), req.Name)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to create workspace")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusCreated, toWorkspaceResponse(workspace))
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) InitClusterBinding(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := mux.Vars(r)["workspace_id"]
|
||||
var req bindClusterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
binding, err := h.workspaceService.EnsureClusterBinding(r.Context(), workspaceID, req.ClusterID)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to initialize workspace cluster binding")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, binding)
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) IssueKubeconfig(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := mux.Vars(r)["workspace_id"]
|
||||
var req kubeconfigRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "Invalid request body", err.Error())
|
||||
return
|
||||
}
|
||||
kubeconfig, err := h.workspaceService.IssueKubeconfig(r.Context(), workspaceID, req.ClusterID, time.Duration(req.TTLSeconds)*time.Second)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to issue kubeconfig")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"kubeconfig": kubeconfig.Kubeconfig,
|
||||
"expiresAt": kubeconfig.ExpiresAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) IssueCurrentKubeconfig(w http.ResponseWriter, r *http.Request) {
|
||||
clusterID := r.URL.Query().Get("clusterId")
|
||||
if clusterID == "" {
|
||||
clusterID = r.URL.Query().Get("cluster_id")
|
||||
}
|
||||
kubeconfig, err := h.workspaceService.IssueCurrentKubeconfig(r.Context(), clusterID, 2*time.Hour)
|
||||
if err != nil {
|
||||
respondServiceError(w, err, "Failed to issue kubeconfig")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-yaml")
|
||||
w.Header().Set("X-OCDP-Kubeconfig-Expires-At", kubeconfig.ExpiresAt.Format(time.RFC3339))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(kubeconfig.Kubeconfig))
|
||||
}
|
||||
|
||||
func (h *WorkspaceHandler) SuspendWorkspace(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := mux.Vars(r)["workspace_id"]
|
||||
if err := h.workspaceService.SuspendWorkspace(r.Context(), workspaceID); err != nil {
|
||||
respondServiceError(w, err, "Failed to suspend workspace")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func toWorkspaceResponse(workspace *entity.Workspace) workspaceResponse {
|
||||
return workspaceResponse{
|
||||
ID: workspace.ID,
|
||||
Name: workspace.Name,
|
||||
Status: string(workspace.Status),
|
||||
K8sNamespace: workspace.K8sNamespace,
|
||||
K8sSAName: workspace.K8sSAName,
|
||||
DefaultClusterID: workspace.DefaultClusterID,
|
||||
QuotaCPU: workspace.QuotaCPU,
|
||||
QuotaMemory: workspace.QuotaMemory,
|
||||
QuotaGPU: workspace.QuotaGPU,
|
||||
QuotaGPUMem: workspace.QuotaGPUMem,
|
||||
CreatedBy: workspace.CreatedBy,
|
||||
CreatedAt: workspace.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: workspace.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func respondServiceError(w http.ResponseWriter, err error, fallback string) {
|
||||
switch err {
|
||||
case entity.ErrUnauthorized, authz.ErrUnauthenticated:
|
||||
respondError(w, http.StatusUnauthorized, "Unauthorized", err.Error())
|
||||
case entity.ErrForbidden, authz.ErrForbidden, entity.ErrUserInactive, entity.ErrWorkspaceSuspended:
|
||||
respondError(w, http.StatusForbidden, "Forbidden", err.Error())
|
||||
case entity.ErrClusterNotFound, entity.ErrRegistryNotFound, entity.ErrInstanceNotFound, entity.ErrWorkspaceNotFound:
|
||||
respondError(w, http.StatusNotFound, fallback, err.Error())
|
||||
default:
|
||||
respondError(w, http.StatusBadRequest, fallback, err.Error())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user