Files
bryanpedini b792ad256e feat(api): add authenticated containers http api
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.
2026-10-06 10:52:25 +02:00

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
}