Files
ocdp-go/backend/cmd/api/main.go
Ivan087 7d9545f827 feat: fix YAML field conversion, admin namespace, streaming logs, and vllm-serve deploy
- Fix Axios keysToSnake converting user values map keys (gpuMem -> gpu_mem)
  - Add skipRecurseKeys to keysToSnake for values/valuesYaml fields
  - Add values_yaml alt json tag and Normalize() in DTOs
  - Check both camelCase/snake_case in enforceNamespaceValues
  - Read both tailLines/tail_lines query param for diagnostics
- Admin users can freely choose namespace in LaunchModal (free-text input)
  - Block only kube-system/kube-public/kube-node-lease for admin
  - Regular users keep existing namespace restrictions
- Add SSE streaming pod logs endpoint (backend + frontend)
  - New PodLogStreamer interface and K8s Follow:true implementation
  - SSE handler with text/event-stream output
  - Frontend DiagnosticsModal: Stream button, auto-scroll, live indicator
- Remove per-card Refresh button from InstanceCard (redundant with page refresh)
- Deploy bge-m3 on vllm-serve 0.6.0 (gpuMem=10000, status=deployed)
2026-05-12 16:50:25 +08:00

377 lines
15 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/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,
)
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)
monitoringService := service.NewMonitoringService(
repos.ClusterRepo,
repos.MetricsClient,
)
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)
api.HandleFunc("/auth/refresh", authHandler.RefreshToken)
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)
// ===== 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("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts", artifactHandler.ListArtifacts).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)
// ===== 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}/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) {
// 设置 CORS 头
origin := r.Header.Get("Origin")
if origin == "" {
origin = "*"
}
w.Header().Set("Access-Control-Allow-Origin", origin)
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-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
// 处理 OPTIONS 预检请求
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}