Files
ocdp-go/backend/cmd/api/main.go
Ivan087 4441f58299 fix: direct K8s scaling, replicas from K8s API, button labels, modify fetch
- Add ScaleClient using K8s API (like kubectl scale deploy --replicas=N)
  - ScaleDeployment: patch Deployment.Spec.Replicas directly
  - GetDeploymentReplicas: query actual K8s deployment replicas
  - Search by labels then fallback to deployment name match
- Wire ScaleClient to InstanceService via SetScaleClient in main.go
- ModifyModal: fetch full instance detail on open (list excludes values)
- InstanceCard: add text labels to action buttons (Entries/Diag/Modify/Delete)
  - Text visible on sm+ screens, icon-only on xs
2026-05-13 14:54:24 +08:00

381 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/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,
)
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())
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)
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}/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)
})
}