185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
package server
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"ci/internal/handler"
|
|
"ci/internal/service"
|
|
"ci/internal/store"
|
|
"ci/internal/workspace"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// Config holds server configuration.
|
|
type Config struct {
|
|
Port string
|
|
DataDir string
|
|
}
|
|
|
|
// DefaultConfig returns the default server configuration.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
Port: "8080",
|
|
DataDir: "data",
|
|
}
|
|
}
|
|
|
|
// Server wraps the Gin engine and related services.
|
|
type Server struct {
|
|
engine *gin.Engine
|
|
config Config
|
|
svc *service.ProjectService
|
|
}
|
|
|
|
// New creates a new Server with the given config and embedded frontend.
|
|
// Pass nil for frontendFS when using local file serving (dev mode).
|
|
func New(config Config, frontendFS *embed.FS) (*Server, error) {
|
|
// Resolve absolute data directory
|
|
absDataDir, err := filepath.Abs(config.DataDir)
|
|
if err != nil {
|
|
absDataDir = config.DataDir
|
|
}
|
|
|
|
// Initialize store
|
|
st, err := store.New(absDataDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize workspace
|
|
workspaceDir := filepath.Join(absDataDir, "workspaces")
|
|
wm, err := workspace.New(workspaceDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize service
|
|
svc := service.NewProjectService(st, wm)
|
|
|
|
// Initialize handlers
|
|
projectH := handler.NewProjectHandler(svc)
|
|
buildH := handler.NewBuildHandler(svc)
|
|
uploadH := handler.NewUploadHandler(svc)
|
|
deployH := handler.NewDeployHandler(svc)
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
r := gin.Default()
|
|
|
|
// CORS middleware for development
|
|
r.Use(func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
c.Next()
|
|
})
|
|
|
|
// Health check
|
|
r.GET("/api/health", func(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
})
|
|
|
|
// API routes
|
|
api := r.Group("/api")
|
|
{
|
|
// Projects CRUD
|
|
api.GET("/projects", projectH.ListProjects)
|
|
api.POST("/projects", projectH.CreateProject)
|
|
api.GET("/projects/:id", projectH.GetProject)
|
|
api.PUT("/projects/:id", projectH.UpdateProject)
|
|
api.DELETE("/projects/:id", projectH.DeleteProject)
|
|
|
|
// Build & Run
|
|
api.POST("/projects/:id/build", buildH.TriggerBuild)
|
|
api.POST("/projects/:id/start", buildH.StartProject)
|
|
api.POST("/projects/:id/stop", buildH.StopProject)
|
|
api.POST("/projects/:id/restart", buildH.RestartProject)
|
|
api.GET("/projects/:id/builds", buildH.GetBuilds)
|
|
api.GET("/projects/:id/logs/:buildId", buildH.GetBuildLog)
|
|
api.GET("/projects/:id/logs/stream", buildH.StreamLogs)
|
|
|
|
// Deploy config
|
|
api.GET("/projects/:id/deploy-config", deployH.GetDeployConfig)
|
|
api.PUT("/projects/:id/deploy-config", deployH.UpdateDeployConfig)
|
|
api.DELETE("/projects/:id/deploy-config", deployH.DeleteDeployConfig)
|
|
|
|
// Deploy execution
|
|
api.POST("/projects/:id/deploy", deployH.TriggerDeploy)
|
|
api.GET("/projects/:id/deploy-records", deployH.GetDeployRecords)
|
|
api.GET("/projects/:id/deploy-records/:did/log", deployH.GetDeployLog)
|
|
|
|
// Upload
|
|
api.POST("/projects/:id/upload", uploadH.UploadFiles)
|
|
|
|
// Webhook
|
|
api.POST("/webhook/:id", buildH.Webhook)
|
|
}
|
|
|
|
// Serve frontend
|
|
if frontendFS != nil {
|
|
// Embedded mode: serve from embedded dist files
|
|
distFS, err := fs.Sub(frontendFS, "web/dist")
|
|
if err != nil {
|
|
// Try without the prefix
|
|
distFS = frontendFS
|
|
}
|
|
|
|
staticFS, err := fs.Sub(distFS, ".")
|
|
_ = staticFS
|
|
_ = err
|
|
|
|
// Use gin's static file serving with embedded FS
|
|
r.NoRoute(gin.WrapH(http.FileServer(http.FS(distFS))))
|
|
} else {
|
|
// Dev mode: try serving from web/dist directory
|
|
localDist := "web/dist"
|
|
if _, err := os.Stat(localDist); err == nil {
|
|
r.NoRoute(func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
if path == "/" {
|
|
c.File(filepath.Join(localDist, "index.html"))
|
|
return
|
|
}
|
|
// Try the specific file first
|
|
targetPath := filepath.Join(localDist, path)
|
|
if _, err := os.Stat(targetPath); err == nil {
|
|
c.File(targetPath)
|
|
return
|
|
}
|
|
// SPA fallback
|
|
c.File(filepath.Join(localDist, "index.html"))
|
|
})
|
|
} else {
|
|
r.NoRoute(func(c *gin.Context) {
|
|
c.String(http.StatusOK, "CI/CD Server is running. Frontend not yet built — run `cd web && npm run build`.")
|
|
})
|
|
}
|
|
}
|
|
|
|
srv := &Server{
|
|
engine: r,
|
|
config: config,
|
|
svc: svc,
|
|
}
|
|
|
|
return srv, nil
|
|
}
|
|
|
|
// Run starts the HTTP server.
|
|
func (s *Server) Run() error {
|
|
return s.engine.Run(":" + s.config.Port)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the server.
|
|
func (s *Server) Shutdown() {
|
|
s.svc.Shutdown()
|
|
}
|