GET /api/v1/containers behind bearer token auth, JSON errors for 401/404/405/500, one audit row per authenticated request, graceful shutdown; unit and e2e tests for auth, routing, auditing, request logging and server lifecycle.
107 lines
3.1 KiB
Go
107 lines
3.1 KiB
Go
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
// Copyright (C) 2026 Bryan Joshua Pedini
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// HTTP server timeouts
|
|
const (
|
|
readHeaderTimeout = 10 * time.Second
|
|
readTimeout = 30 * time.Second
|
|
writeTimeout = 30 * time.Second
|
|
idleTimeout = 120 * time.Second
|
|
)
|
|
|
|
// defaultShutdownTimeout bounds the graceful shutdown
|
|
const defaultShutdownTimeout = 15 * time.Second
|
|
|
|
// WebServer holds the HTTP server and its dependencies
|
|
type WebServer struct {
|
|
HTTPServer *http.Server
|
|
log *slog.Logger
|
|
tokens TokenLookup
|
|
audit AuditRecorder
|
|
docker ContainerLister
|
|
encode func(v any) ([]byte, error)
|
|
}
|
|
|
|
// NewWebServer builds the server, its router and its middleware
|
|
func NewWebServer(log *slog.Logger, tokens TokenLookup, audit AuditRecorder, docker ContainerLister) *WebServer {
|
|
s := &WebServer{
|
|
log: log,
|
|
tokens: tokens,
|
|
audit: audit,
|
|
docker: docker,
|
|
encode: json.Marshal,
|
|
}
|
|
s.HTTPServer = &http.Server{
|
|
Handler: s.Handler(),
|
|
ReadHeaderTimeout: readHeaderTimeout,
|
|
ReadTimeout: readTimeout,
|
|
WriteTimeout: writeTimeout,
|
|
IdleTimeout: idleTimeout,
|
|
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn),
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Handler returns the complete request handler: logging middleware around the router
|
|
func (s *WebServer) Handler() http.Handler {
|
|
defer traceFunc(context.Background(), s.log, "WebServer.Handler")()
|
|
|
|
// Create the router; paths are matched exactly (no cleaning redirects, no strict-slash redirects)
|
|
r := mux.NewRouter()
|
|
r.SkipClean(true)
|
|
r.NotFoundHandler = http.HandlerFunc(s.handleNotFound)
|
|
r.MethodNotAllowedHandler = http.HandlerFunc(s.handleMethodNotAllowed)
|
|
|
|
// Associate the various handlers (routes)
|
|
s.Routes(r)
|
|
s.log.Debug("routes registered", "route", http.MethodGet+" "+containersPath)
|
|
|
|
return s.logRequests(r)
|
|
}
|
|
|
|
// Serve serves HTTP on ln until ctx is cancelled, then shuts down gracefully within shutdownTimeout
|
|
// It returns an error if the server stops unexpectedly or the graceful shutdown fails
|
|
func (s *WebServer) Serve(ctx context.Context, ln net.Listener, shutdownTimeout time.Duration) error {
|
|
defer traceFunc(ctx, s.log, "WebServer.Serve")()
|
|
|
|
// Start serving in the background
|
|
s.log.InfoContext(ctx, "server listening", "addr", ln.Addr().String(), "version", APP_VERSION, "commit", COMMIT_ID)
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
errCh <- s.HTTPServer.Serve(ln)
|
|
}()
|
|
|
|
// Wait for either a server failure or the shutdown request
|
|
select {
|
|
case err := <-errCh:
|
|
return fmt.Errorf("http server: %w", err)
|
|
case <-ctx.Done():
|
|
}
|
|
|
|
// Graceful shutdown
|
|
s.log.DebugContext(ctx, "shutdown requested, draining http server", "timeout", shutdownTimeout)
|
|
shCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), shutdownTimeout)
|
|
defer cancel()
|
|
if err := s.HTTPServer.Shutdown(shCtx); err != nil {
|
|
s.HTTPServer.Close()
|
|
return fmt.Errorf("http server shutdown: %w", err)
|
|
}
|
|
<-errCh
|
|
s.log.DebugContext(ctx, "http server shut down")
|
|
return nil
|
|
}
|