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:
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user