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.
This commit is contained in:
1 parent
9a85e9acc8
commit
b792ad256e
11 files changed
+2028
No files matched your search
@@ -0,0 +1,106 @@
|
||||
// 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
|
||||
}
|
||||
Reference in new issue
Block a user