mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Implement handlers for creating, listing, and updating model bindings. Register new routes in the admin server group and add DTO definitions. Update provider handlers to trigger binding synchronization on changes to ensure upstream mappings remain current.
214 lines
6.4 KiB
Go
214 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
_ "github.com/ez-api/ez-api/docs"
|
|
"github.com/ez-api/ez-api/internal/api"
|
|
"github.com/ez-api/ez-api/internal/config"
|
|
"github.com/ez-api/ez-api/internal/middleware"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/ez-api/foundation/logging"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/redis/go-redis/v9"
|
|
swaggerFiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// @title EZ-API Control Plane
|
|
// @version 0.0.1
|
|
// @description Management API for EZ-API Gateway system.
|
|
// @termsOfService http://swagger.io/terms/
|
|
|
|
// @contact.name API Support
|
|
// @contact.url http://www.swagger.io/support
|
|
// @contact.email support@swagger.io
|
|
|
|
// @license.name Apache 2.0
|
|
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
|
|
|
// @host localhost:8080
|
|
// @BasePath /
|
|
|
|
// @securityDefinitions.apikey AdminAuth
|
|
// @in header
|
|
// @name Authorization
|
|
|
|
// @securityDefinitions.apikey MasterAuth
|
|
// @in header
|
|
// @name Authorization
|
|
|
|
func fatal(logger *slog.Logger, msg string, args ...any) {
|
|
logger.Error(msg, args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func main() {
|
|
logger, _ := logging.New(logging.Options{Service: "ez-api"})
|
|
|
|
// 1. Load Configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
fatal(logger, "failed to load config", "err", err)
|
|
}
|
|
|
|
// 2. Initialize Redis Client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: cfg.Redis.Addr,
|
|
Password: cfg.Redis.Password,
|
|
DB: cfg.Redis.DB,
|
|
})
|
|
|
|
// Verify Redis connection
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
fatal(logger, "failed to connect to redis", "err", err)
|
|
}
|
|
logger.Info("connected to redis successfully")
|
|
|
|
// 3. Initialize GORM (PostgreSQL)
|
|
db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{})
|
|
if err != nil {
|
|
fatal(logger, "failed to connect to postgresql", "err", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
fatal(logger, "failed to get generic database object", "err", err)
|
|
}
|
|
// Verify DB connection
|
|
if err := sqlDB.Ping(); err != nil {
|
|
fatal(logger, "failed to ping postgresql", "err", err)
|
|
}
|
|
logger.Info("connected to postgresql successfully")
|
|
|
|
// Auto Migrate
|
|
if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.Provider{}, &model.Model{}, &model.Binding{}, &model.LogRecord{}); err != nil {
|
|
fatal(logger, "failed to auto migrate", "err", err)
|
|
}
|
|
|
|
// 4. Setup Services and Handlers
|
|
syncService := service.NewSyncService(rdb)
|
|
logWriter := service.NewLogWriter(db, cfg.Log.QueueCapacity, cfg.Log.BatchSize, cfg.Log.FlushInterval)
|
|
logCtx, cancelLogs := context.WithCancel(context.Background())
|
|
defer cancelLogs()
|
|
logWriter.Start(logCtx)
|
|
|
|
adminService, err := service.NewAdminService()
|
|
if err != nil {
|
|
fatal(logger, "failed to create admin service", "err", err)
|
|
}
|
|
masterService := service.NewMasterService(db)
|
|
healthService := service.NewHealthCheckService(db, rdb)
|
|
|
|
handler := api.NewHandler(db, syncService, logWriter)
|
|
adminHandler := api.NewAdminHandler(masterService, syncService)
|
|
masterHandler := api.NewMasterHandler(masterService, syncService)
|
|
featureHandler := api.NewFeatureHandler(rdb)
|
|
|
|
// 4.1 Prime Redis snapshots so DP can start with data
|
|
if err := syncService.SyncAll(db); err != nil {
|
|
logger.Warn("initial sync warning", "err", err)
|
|
}
|
|
|
|
// 5. Setup Gin Router
|
|
r := gin.Default()
|
|
r.Use(middleware.RequestID())
|
|
|
|
// CORS Middleware
|
|
r.Use(func(c *gin.Context) {
|
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict this in production
|
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(204)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
})
|
|
|
|
// Health Check Endpoint
|
|
r.GET("/health", func(c *gin.Context) {
|
|
status := healthService.Check(c.Request.Context())
|
|
httpStatus := http.StatusOK
|
|
if status.Status == "down" {
|
|
httpStatus = http.StatusServiceUnavailable
|
|
}
|
|
c.JSON(httpStatus, status)
|
|
})
|
|
|
|
// Swagger Documentation
|
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
|
|
|
// API Routes
|
|
// Admin Routes
|
|
adminGroup := r.Group("/admin")
|
|
adminGroup.Use(middleware.AdminAuthMiddleware(adminService))
|
|
{
|
|
adminGroup.POST("/masters", adminHandler.CreateMaster)
|
|
adminGroup.POST("/masters/:id/keys", adminHandler.IssueChildKeyForMaster)
|
|
adminGroup.GET("/features", featureHandler.ListFeatures)
|
|
adminGroup.PUT("/features", featureHandler.UpdateFeatures)
|
|
// Other admin routes for managing providers, models, etc.
|
|
adminGroup.POST("/providers", handler.CreateProvider)
|
|
adminGroup.PUT("/providers/:id", handler.UpdateProvider)
|
|
adminGroup.POST("/models", handler.CreateModel)
|
|
adminGroup.GET("/models", handler.ListModels)
|
|
adminGroup.PUT("/models/:id", handler.UpdateModel)
|
|
adminGroup.POST("/bindings", handler.CreateBinding)
|
|
adminGroup.GET("/bindings", handler.ListBindings)
|
|
adminGroup.PUT("/bindings/:id", handler.UpdateBinding)
|
|
adminGroup.POST("/sync/snapshot", handler.SyncSnapshot)
|
|
}
|
|
|
|
// Master Routes
|
|
masterGroup := r.Group("/v1")
|
|
masterGroup.Use(middleware.MasterAuthMiddleware(masterService))
|
|
{
|
|
masterGroup.POST("/tokens", masterHandler.IssueChildKey)
|
|
}
|
|
|
|
// Public/General Routes (if any)
|
|
r.POST("/logs", handler.IngestLog)
|
|
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Server.Port,
|
|
Handler: r,
|
|
}
|
|
|
|
// 6. Start Server with Graceful Shutdown
|
|
go func() {
|
|
logger.Info("starting ez-api", "port", cfg.Server.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
fatal(logger, "server failed", "err", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
logger.Info("shutting down server...")
|
|
|
|
// Shutdown with timeout
|
|
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
fatal(logger, "server forced to shutdown", "err", err)
|
|
}
|
|
|
|
logger.Info("server exited properly")
|
|
}
|