Files
ocdp-go/backend/cmd/api/main.go
Ivan087 33ddaf97db fix: scale replicas in response, K8s metrics client, quota precheck, auth tests
- Add GetMetrics method to MetricsClient interface and implement cluster metrics API
- Add QuotaPrecheck service for validating resource quotas before deployment
- Add auth DTO with role/permission models and auth handler tests
- Add instance diagnostics: mounted NFS volumes, labels, annotations in pod diagnostics
- Update workspace handler with GetWorkspace endpoint and shared-user list
- Fix monitoring handler to use correct service method name
- Add tail_lines fallback in instance handler for snake_case query params
- Update nginx config for SSE log streaming support (no buffering)
- Add comprehensive test coverage: auth_service_test, auth_handler_test,
  auth_dto_test, metrics_client_test, quota_precheck_test
- Update error messages for quota validation and instance operations
- ModifyModal: fix YAML lineWidth:0, modified keys summary, delta-only submit
- InstanceCard: correctly disable scale-minus when replicas <= 0
- SidebarLayout: add hover transition for sidebar items
- Update todo.md and lessons.md with latest fixes
2026-05-20 16:56:29 +08:00

440 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @title OCDP Backend API
// @version 1.0
// @description OCDP (Open Cloud Development Platform) Backend API
// @description
// @description RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments.
//
// @contact.name API Support
// @contact.email support@ocdp.io
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @host localhost:8080
// @BasePath /api/v1
//
// @schemes http https
//
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/ocdp/cluster-service/internal/adapter/input/http/rest"
"github.com/ocdp/cluster-service/internal/adapter/output"
"github.com/ocdp/cluster-service/internal/adapter/output/k8s"
"github.com/ocdp/cluster-service/internal/bootstrap"
"github.com/ocdp/cluster-service/internal/domain/service"
"github.com/ocdp/cluster-service/internal/pkg/authz"
"github.com/ocdp/cluster-service/internal/pkg/crypto"
"github.com/ocdp/cluster-service/internal/pkg/jwt"
"github.com/ocdp/cluster-service/internal/pkg/password"
)
func main() {
log.Println("🚀 Starting OCDP Backend (Hexagonal Architecture)")
// ===== 1. 读取配置 =====
config := loadConfig()
log.Printf("📝 Configuration: mode=%s, port=%s", config.AdapterMode, config.Port)
// ===== 2. 创建加密器(用于敏感数据加密存储) =====
encryptor := crypto.NewAESEncryptor(config.EncryptionKey)
log.Println("✅ Encryption enabled for sensitive data")
// ===== 3. 创建 Output Adapters通过工厂 =====
factory := output.NewAdapterFactory(
output.AdapterMode(config.AdapterMode),
encryptor,
config.DatabaseURL,
)
repos, err := factory.CreateAllRepositories()
if err != nil {
log.Fatalf("❌ Failed to create adapters: %v", err)
}
log.Printf("✅ Output Adapters initialized (mode: %s)", config.AdapterMode)
// ===== 4. 创建工具包实例 =====
passwordHasher := password.NewHasher()
tokenGenerator := jwt.NewJWTManager(config.JWTSecret)
log.Println("✅ Utilities initialized")
// ===== 5. 创建 Domain Services =====
authService := service.NewAuthService(
repos.UserRepo,
repos.WorkspaceRepo,
passwordHasher,
tokenGenerator,
)
authService.SetUserLifecycleCleanup(
repos.InstanceRepo,
repos.ClusterRepo,
repos.BindingRepo,
repos.TenantKubeClient,
)
clusterService := service.NewClusterService(
repos.ClusterRepo,
)
registryService := service.NewRegistryService(
repos.RegistryRepo,
repos.OCIClient,
)
artifactService := service.NewArtifactService(
repos.RegistryRepo,
repos.OCIClient,
)
instanceService := service.NewInstanceService(
repos.InstanceRepo,
repos.ClusterRepo,
repos.RegistryRepo,
repos.HelmClient,
repos.OCIClient,
repos.EntryClient,
repos.BindingRepo,
)
instanceService.SetDiagnosticsClient(repos.DiagnosticsClient)
instanceService.SetTenantProvisioning(repos.WorkspaceRepo, repos.TenantKubeClient)
instanceService.SetScaleClient(k8s.NewScaleClient())
instanceService.SetUserRepository(repos.UserRepo)
monitoringService := service.NewMonitoringService(
repos.ClusterRepo,
repos.MetricsClient,
repos.InstanceRepo,
repos.UserRepo,
)
workspaceService := service.NewWorkspaceService(
repos.WorkspaceRepo,
repos.BindingRepo,
repos.ClusterRepo,
repos.TenantKubeClient,
repos.AuditRepo,
)
log.Println("✅ Domain Services initialized")
// ===== 6. 加载并执行 Bootstrap 预注入 =====
bootstrapConfig, err := bootstrap.LoadBootstrapConfig()
if err != nil {
log.Printf("⚠️ Warning: Failed to load bootstrap config: %v", err)
// 使用安全的空配置,避免在配置错误时写入任何预置账号或集群凭据。
bootstrapConfig = bootstrap.GetDefaultBootstrapConfig()
}
seeder := bootstrap.NewSeeder(repos, passwordHasher, bootstrapConfig)
if err := seeder.SeedAll(context.Background()); err != nil {
log.Printf("⚠️ Warning: Failed to seed data: %v", err)
}
// ===== 7. 创建 Input Adapters (REST Handlers) =====
authHandler := rest.NewAuthHandler(authService)
clusterHandler := rest.NewClusterHandler(clusterService)
registryHandler := rest.NewRegistryHandler(registryService)
artifactHandler := rest.NewArtifactHandler(artifactService)
instanceHandler := rest.NewInstanceHandler(instanceService)
monitoringHandler := rest.NewMonitoringHandler(monitoringService)
workspaceHandler := rest.NewWorkspaceHandler(workspaceService)
swaggerHandler := rest.NewSwaggerHandler()
log.Println("✅ Input Adapters (REST handlers) initialized")
// ===== 8. 设置路由 =====
router := setupRouter(
authHandler,
authService,
clusterHandler,
registryHandler,
artifactHandler,
instanceHandler,
monitoringHandler,
workspaceHandler,
swaggerHandler,
)
// ===== 9. 启动服务器 =====
addr := fmt.Sprintf(":%s", config.Port)
log.Printf("🌐 Server starting on %s", addr)
log.Println("")
log.Println("📍 Available endpoints:")
log.Printf(" - REST API: http://localhost:%s/api/v1", config.Port)
log.Printf(" - Swagger UI: http://localhost:%s/api/docs", config.Port)
log.Printf(" - OpenAPI Spec: http://localhost:%s/api/docs/openapi.yaml", config.Port)
log.Printf(" - Health: http://localhost:%s/health", config.Port)
log.Println("")
log.Println("✨ Press Ctrl+C to stop")
log.Println("")
if err := http.ListenAndServe(addr, router); err != nil {
log.Fatalf("❌ Server failed: %v", err)
}
}
// Config 应用配置
type Config struct {
AdapterMode string
Port string
JWTSecret string
EncryptionKey string
DatabaseURL string
}
// loadConfig 加载配置
func loadConfig() *Config {
return &Config{
AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式)
Port: getEnv("PORT", "8080"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"),
DatabaseURL: getEnv("DATABASE_URL", ""),
}
}
// getEnv 获取环境变量,如果不存在则返回默认值
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
// setupRouter 设置路由
func setupRouter(
authHandler *rest.AuthHandler,
authService *service.AuthService,
clusterHandler *rest.ClusterHandler,
registryHandler *rest.RegistryHandler,
artifactHandler *rest.ArtifactHandler,
instanceHandler *rest.InstanceHandler,
monitoringHandler *rest.MonitoringHandler,
workspaceHandler *rest.WorkspaceHandler,
swaggerHandler *rest.SwaggerHandler,
) *mux.Router {
router := mux.NewRouter().StrictSlash(true)
// 全局中间件
router.Use(loggingMiddleware)
router.Use(corsMiddleware)
// 健康检查
router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"healthy"}`))
})
// ===== Swagger UI =====
router.HandleFunc("/api/docs", swaggerHandler.ServeSwaggerUI).Methods(http.MethodGet)
router.HandleFunc("/api/docs/assets/swagger-ui.css", swaggerHandler.ServeSwaggerCSS).Methods(http.MethodGet)
router.HandleFunc("/api/docs/assets/swagger-ui-bundle.js", swaggerHandler.ServeSwaggerBundle).Methods(http.MethodGet)
router.HandleFunc("/api/docs/assets/swagger-ui-standalone-preset.js", swaggerHandler.ServeSwaggerStandalonePreset).Methods(http.MethodGet)
router.HandleFunc("/api/docs/openapi.yaml", swaggerHandler.ServeOpenAPISpec).Methods(http.MethodGet)
// API v1
api := router.PathPrefix("/api/v1").Subrouter()
// ===== 认证路由 =====
api.HandleFunc("/auth/login", authHandler.Login).Methods(http.MethodPost)
api.HandleFunc("/auth/refresh", authHandler.RefreshToken).Methods(http.MethodPost)
protected := api.PathPrefix("").Subrouter()
protected.Use(authMiddleware(authService))
protected.HandleFunc("/auth/me", authHandler.Me).Methods(http.MethodGet)
protected.HandleFunc("/auth/register", authHandler.Register).Methods(http.MethodPost)
protected.HandleFunc("/users", authHandler.ListUsers).Methods(http.MethodGet)
protected.HandleFunc("/users", authHandler.Register).Methods(http.MethodPost)
protected.HandleFunc("/users/{user_id}", authHandler.UpdateUser).Methods(http.MethodPut)
protected.HandleFunc("/users/{user_id}", authHandler.DeleteUser).Methods(http.MethodDelete)
// ===== 集群路由 =====
protected.HandleFunc("/clusters", clusterHandler.CreateCluster).Methods(http.MethodPost)
protected.HandleFunc("/clusters", clusterHandler.GetAllClusters).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}", clusterHandler.GetCluster).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}", clusterHandler.UpdateCluster).Methods(http.MethodPut)
protected.HandleFunc("/clusters/{cluster_id}", clusterHandler.DeleteCluster).Methods(http.MethodDelete)
protected.HandleFunc("/clusters/{cluster_id}/health", clusterHandler.GetClusterHealth).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/stats", monitoringHandler.GetClusterStats).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/kubeconfig", workspaceHandler.IssueClusterKubeconfig).Methods(http.MethodGet)
// ===== Registry 路由 =====
protected.HandleFunc("/registries", registryHandler.CreateRegistry).Methods(http.MethodPost)
protected.HandleFunc("/registries", registryHandler.GetAllRegistries).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}", registryHandler.GetRegistry).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}", registryHandler.UpdateRegistry).Methods(http.MethodPut)
protected.HandleFunc("/registries/{registry_id}", registryHandler.DeleteRegistry).Methods(http.MethodDelete)
protected.HandleFunc("/registries/{registry_id}/health", registryHandler.GetRegistryHealth).Methods(http.MethodGet)
// ===== Artifact 路由 =====
protected.HandleFunc("/registries/{registry_id}/repositories", artifactHandler.ListRepositories).Methods(http.MethodGet)
protected.HandleFunc("/repositories/{repository_name:.+}/tags", artifactHandler.ListRepositoryTags).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts", artifactHandler.ListArtifacts).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/tags", artifactHandler.ListRepositoryTags).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}", artifactHandler.GetArtifact).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values-schema", artifactHandler.GetArtifactValuesSchema).Methods(http.MethodGet)
protected.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values-yaml", artifactHandler.GetArtifactValuesYAML).Methods(http.MethodGet)
// ===== Instance 路由 =====
protected.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.CreateInstance).Methods(http.MethodPost)
protected.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.ListInstances).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.GetInstance).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.UpdateInstance).Methods(http.MethodPut)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.DeleteInstance).Methods(http.MethodDelete)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/entries", instanceHandler.ListInstanceEntries).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/diagnostics", instanceHandler.GetInstanceDiagnostics).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/logs/stream", instanceHandler.StreamInstanceLogs).Methods(http.MethodGet)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/scale", instanceHandler.ScaleInstance).Methods(http.MethodPost)
protected.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/values-diff", instanceHandler.GetInstanceValuesDiff).Methods(http.MethodGet)
// ===== Monitoring 路由 =====
protected.HandleFunc("/monitoring/clusters", monitoringHandler.ListClusterMonitoring).Methods(http.MethodGet)
protected.HandleFunc("/monitoring/clusters/{cluster_id}", monitoringHandler.GetClusterMonitoring).Methods(http.MethodGet)
protected.HandleFunc("/monitoring/clusters/{cluster_id}/metrics", monitoringHandler.GetClusterMonitoring).Methods(http.MethodGet)
protected.HandleFunc("/monitoring/clusters/{cluster_id}/nodes", monitoringHandler.GetNodeMetrics).Methods(http.MethodGet)
protected.HandleFunc("/monitoring/summary", monitoringHandler.GetMonitoringSummary).Methods(http.MethodGet)
// ===== Workspace 路由 =====
protected.HandleFunc("/workspaces", workspaceHandler.ListWorkspaces).Methods(http.MethodGet)
protected.HandleFunc("/workspaces", workspaceHandler.CreateWorkspace).Methods(http.MethodPost)
protected.HandleFunc("/workspaces/credentials/kubeconfig", workspaceHandler.IssueCurrentKubeconfig).Methods(http.MethodGet)
protected.HandleFunc("/workspaces/{workspace_id}/clusters", workspaceHandler.InitClusterBinding).Methods(http.MethodPost)
protected.HandleFunc("/workspaces/{workspace_id}/kubeconfig", workspaceHandler.IssueKubeconfig).Methods(http.MethodPost)
protected.HandleFunc("/workspaces/{workspace_id}/suspend", workspaceHandler.SuspendWorkspace).Methods(http.MethodPost)
// 处理 MethodNotAllowed 错误OPTIONS 请求会触发)
router.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions {
// CORS 预检已在中间件处理,这里直接返回
w.WriteHeader(http.StatusNoContent)
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
})
return router
}
func authMiddleware(authService *service.AuthService) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get("Authorization")
if !strings.HasPrefix(header, "Bearer ") {
writeJSONError(w, http.StatusUnauthorized, "Unauthorized", "missing bearer token")
return
}
token := strings.TrimSpace(strings.TrimPrefix(header, "Bearer "))
if token == "" {
writeJSONError(w, http.StatusUnauthorized, "Unauthorized", "missing bearer token")
return
}
principal, err := authService.VerifyAccessToken(r.Context(), token)
if err != nil {
writeJSONError(w, http.StatusUnauthorized, "Unauthorized", err.Error())
return
}
next.ServeHTTP(w, r.WithContext(authz.WithPrincipal(r.Context(), principal)))
})
}
}
func writeJSONError(w http.ResponseWriter, status int, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":%q,"message":%q}`, code, message)))
}
// loggingMiddleware 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s %v", r.Method, r.RequestURI, r.RemoteAddr, time.Since(start))
})
}
// corsMiddleware CORS 中间件
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
w.Header().Add("Vary", "Origin")
if corsOriginAllowed(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Max-Age", "86400")
// 处理 OPTIONS 预检请求
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func corsOriginAllowed(origin string) bool {
origin = strings.TrimSpace(origin)
if origin == "" {
return false
}
for _, allowed := range corsAllowedOrigins() {
if origin == allowed {
return true
}
}
return false
}
func corsAllowedOrigins() []string {
configured := strings.TrimSpace(os.Getenv("CORS_ALLOWED_ORIGINS"))
if configured == "" {
configured = strings.TrimSpace(os.Getenv("ALLOWED_ORIGINS"))
}
if configured == "" {
return []string{
"http://localhost:3000",
"http://localhost:5173",
"http://localhost:8080",
"http://localhost:18080",
"http://localhost:18081",
"http://127.0.0.1:3000",
"http://127.0.0.1:5173",
"http://127.0.0.1:8080",
"http://127.0.0.1:18080",
"http://127.0.0.1:18081",
"http://10.6.80.114:18080",
}
}
origins := make([]string, 0)
for _, origin := range strings.Split(configured, ",") {
origin = strings.TrimSpace(origin)
if origin == "" || origin == "*" {
continue
}
origins = append(origins, origin)
}
return origins
}