Initial commit with server startup infrastructure
Core infrastructure: - Uber fx dependency injection - Chi router with middleware stack - SQLite database with embedded migrations - Embedded templates and static assets - Structured logging with slog Features implemented: - Authentication (login, logout, session management, argon2id hashing) - App management (create, edit, delete, list) - Deployment pipeline (clone, build, deploy, health check) - Webhook processing for Gitea - Notifications (ntfy, Slack) - Environment variables, labels, volumes per app - SSH key generation for deploy keys Server startup: - Server.Run() starts HTTP server on configured port - Server.Shutdown() for graceful shutdown - SetupRoutes() wires all handlers with chi router
This commit is contained in:
88
internal/server/routes.go
Normal file
88
internal/server/routes.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
chimw "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"git.eeqj.de/sneak/upaas/static"
|
||||
)
|
||||
|
||||
// requestTimeout is the maximum duration for handling a request.
|
||||
const requestTimeout = 60 * time.Second
|
||||
|
||||
// SetupRoutes configures all HTTP routes.
|
||||
func (s *Server) SetupRoutes() {
|
||||
s.router = chi.NewRouter()
|
||||
|
||||
// Global middleware
|
||||
s.router.Use(chimw.Recoverer)
|
||||
s.router.Use(chimw.RequestID)
|
||||
s.router.Use(s.mw.Logging())
|
||||
s.router.Use(s.mw.CORS())
|
||||
s.router.Use(chimw.Timeout(requestTimeout))
|
||||
s.router.Use(s.mw.SetupRequired())
|
||||
|
||||
// Health check (no auth required)
|
||||
s.router.Get("/health", s.handlers.HandleHealthCheck())
|
||||
|
||||
// Static files
|
||||
s.router.Handle("/static/*", http.StripPrefix(
|
||||
"/static/",
|
||||
http.FileServer(http.FS(static.Static)),
|
||||
))
|
||||
|
||||
// Public routes
|
||||
s.router.Get("/login", s.handlers.HandleLoginGET())
|
||||
s.router.Post("/login", s.handlers.HandleLoginPOST())
|
||||
s.router.Get("/setup", s.handlers.HandleSetupGET())
|
||||
s.router.Post("/setup", s.handlers.HandleSetupPOST())
|
||||
|
||||
// Webhook endpoint (uses secret for auth, not session)
|
||||
s.router.Post("/webhook/{secret}", s.handlers.HandleWebhook())
|
||||
|
||||
// Protected routes (require session auth)
|
||||
s.router.Group(func(r chi.Router) {
|
||||
r.Use(s.mw.SessionAuth())
|
||||
|
||||
// Dashboard
|
||||
r.Get("/", s.handlers.HandleDashboard())
|
||||
|
||||
// Logout
|
||||
r.Get("/logout", s.handlers.HandleLogout())
|
||||
|
||||
// App routes
|
||||
r.Get("/apps/new", s.handlers.HandleAppNew())
|
||||
r.Post("/apps", s.handlers.HandleAppCreate())
|
||||
r.Get("/apps/{id}", s.handlers.HandleAppDetail())
|
||||
r.Get("/apps/{id}/edit", s.handlers.HandleAppEdit())
|
||||
r.Post("/apps/{id}", s.handlers.HandleAppUpdate())
|
||||
r.Post("/apps/{id}/delete", s.handlers.HandleAppDelete())
|
||||
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
|
||||
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
|
||||
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
|
||||
|
||||
// Environment variables
|
||||
r.Post("/apps/{id}/env-vars", s.handlers.HandleEnvVarAdd())
|
||||
r.Post("/apps/{id}/env-vars/{varID}/delete", s.handlers.HandleEnvVarDelete())
|
||||
|
||||
// Labels
|
||||
r.Post("/apps/{id}/labels", s.handlers.HandleLabelAdd())
|
||||
r.Post("/apps/{id}/labels/{labelID}/delete", s.handlers.HandleLabelDelete())
|
||||
|
||||
// Volumes
|
||||
r.Post("/apps/{id}/volumes", s.handlers.HandleVolumeAdd())
|
||||
r.Post("/apps/{id}/volumes/{volumeID}/delete", s.handlers.HandleVolumeDelete())
|
||||
})
|
||||
|
||||
// Metrics endpoint (optional, with basic auth)
|
||||
if s.params.Config.MetricsUsername != "" {
|
||||
s.router.Group(func(r chi.Router) {
|
||||
r.Use(s.mw.MetricsAuth())
|
||||
r.Get("/metrics", promhttp.Handler().ServeHTTP)
|
||||
})
|
||||
}
|
||||
}
|
||||
121
internal/server/server.go
Normal file
121
internal/server/server.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Package server provides the HTTP server.
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"git.eeqj.de/sneak/upaas/internal/config"
|
||||
"git.eeqj.de/sneak/upaas/internal/globals"
|
||||
"git.eeqj.de/sneak/upaas/internal/handlers"
|
||||
"git.eeqj.de/sneak/upaas/internal/logger"
|
||||
"git.eeqj.de/sneak/upaas/internal/middleware"
|
||||
)
|
||||
|
||||
// Params contains dependencies for Server.
|
||||
type Params struct {
|
||||
fx.In
|
||||
|
||||
Logger *logger.Logger
|
||||
Globals *globals.Globals
|
||||
Config *config.Config
|
||||
Middleware *middleware.Middleware
|
||||
Handlers *handlers.Handlers
|
||||
}
|
||||
|
||||
// shutdownTimeout is how long to wait for graceful shutdown.
|
||||
const shutdownTimeout = 30 * time.Second
|
||||
|
||||
// readHeaderTimeout is the maximum duration for reading request headers.
|
||||
const readHeaderTimeout = 10 * time.Second
|
||||
|
||||
// Server is the HTTP server.
|
||||
type Server struct {
|
||||
startupTime time.Time
|
||||
port int
|
||||
log *slog.Logger
|
||||
router *chi.Mux
|
||||
httpServer *http.Server
|
||||
params Params
|
||||
mw *middleware.Middleware
|
||||
handlers *handlers.Handlers
|
||||
}
|
||||
|
||||
// New creates a new Server instance.
|
||||
func New(lifecycle fx.Lifecycle, params Params) (*Server, error) {
|
||||
srv := &Server{
|
||||
port: params.Config.Port,
|
||||
log: params.Logger.Get(),
|
||||
params: params,
|
||||
mw: params.Middleware,
|
||||
handlers: params.Handlers,
|
||||
}
|
||||
|
||||
lifecycle.Append(fx.Hook{
|
||||
OnStart: func(_ context.Context) error {
|
||||
srv.startupTime = time.Now()
|
||||
go srv.Run()
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return srv.Shutdown(ctx)
|
||||
},
|
||||
})
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Run starts the HTTP server.
|
||||
func (s *Server) Run() {
|
||||
s.SetupRoutes()
|
||||
|
||||
listenAddr := fmt.Sprintf(":%d", s.port)
|
||||
s.httpServer = &http.Server{
|
||||
Addr: listenAddr,
|
||||
Handler: s,
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
}
|
||||
|
||||
s.log.Info("http server starting", "addr", listenAddr)
|
||||
|
||||
err := s.httpServer.ListenAndServe()
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.log.Error("http server error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the server.
|
||||
func (s *Server) Shutdown(ctx context.Context) error {
|
||||
if s.httpServer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.log.Info("shutting down http server")
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, shutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
err := s.httpServer.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
s.log.Error("http server shutdown error", "error", err)
|
||||
|
||||
return fmt.Errorf("shutting down http server: %w", err)
|
||||
}
|
||||
|
||||
s.log.Info("http server stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
s.router.ServeHTTP(writer, request)
|
||||
}
|
||||
Reference in New Issue
Block a user