chore: initial commit of Go API gateway
CI / Test (push) Canceled after 0s
CI / Vet (push) Canceled after 0s
CI / Build (push) Canceled after 0s
CI / Security (govulncheck) (push) Canceled after 0s

Baseline for the Kong replacement on api.riotpiao.com. Brings the working
tree under version control for the first time: gateway source, the task
board that drives the agent runs, test fixtures, and K8s manifests.

Anchor the gateway ignore rule to the repo root. Unanchored, "gateway"
also matched the cmd/gateway/ source directory, so the program entrypoint
was excluded from every commit.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Story Crater Bot
2026-08-19 20:54:34 -07:00
co-authored by Claude Opus 5
commit 058f11cf2b
109 changed files with 8992 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
package server
import (
"net/http"
"sync"
)
// HealthChecker provides health and readiness check information.
type HealthChecker struct {
mu sync.RWMutex
configValid bool
jwksHasFetched bool
authEnabled bool
}
// NewHealthChecker creates a new health checker instance.
func NewHealthChecker(configValid bool, authEnabled bool) *HealthChecker {
return &HealthChecker{
configValid: configValid,
jwksHasFetched: false,
authEnabled: authEnabled,
}
}
// MarkJWKSFetched marks that JWKS has been fetched successfully.
func (hc *HealthChecker) MarkJWKSFetched() {
hc.mu.Lock()
defer hc.mu.Unlock()
hc.jwksHasFetched = true
}
// IsReady checks if the server is ready to serve traffic.
// It returns true if:
// - Configuration is valid
// - If auth is enabled, JWKS has been fetched at least once
func (hc *HealthChecker) IsReady() bool {
hc.mu.RLock()
defer hc.mu.RUnlock()
if !hc.configValid {
return false
}
// If auth is enabled, we must have fetched JWKS at least once
if hc.authEnabled && !hc.jwksHasFetched {
return false
}
return true
}
// IsAlive returns true if the process is running.
// This is always true since if it weren't, we wouldn't be running this code.
func (hc *HealthChecker) IsAlive() bool {
return true
}
// LivenessHandler returns a handler for the /healthz endpoint.
// It returns 200 whenever the process is alive.
// It performs no network I/O.
func LivenessHandler(hc *HealthChecker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !hc.IsAlive() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"alive"}`))
}
}
// ReadinessHandler returns a handler for the /readyz endpoint.
// It returns 200 only when configuration is valid and, if auth is enabled,
// JWKS has been fetched at least once.
// It returns non-2xx status while configuration is invalid or JWKS has never
// been fetched.
func ReadinessHandler(hc *HealthChecker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !hc.IsReady() {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(`{"status":"not_ready"}`))
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ready"}`))
}
}
+173
View File
@@ -0,0 +1,173 @@
package server_test
import (
"context"
"fmt"
"io"
"net/http"
"testing"
"time"
"github.com/Riotpiaole/homelab-frontend/internal/server"
)
// TestHealthEndpoints verifies health endpoint behavior.
// - /healthz returns 200 even with unreachable upstreams
// - /readyz returns non-2xx before JWKS fetch and 200 after
// - Neither endpoint requires authentication
func TestHealthEndpoints(t *testing.T) {
tests := []struct {
name string
configValid bool
authEnabled bool
jwksFetched bool
endpoint string
expectedCode int
description string
}{
{
name: "healthz_always_200",
configValid: true,
authEnabled: false,
jwksFetched: false,
endpoint: "/healthz",
expectedCode: http.StatusOK,
description: "liveness probe returns 200 even before JWKS fetch",
},
{
name: "healthz_200_when_config_invalid",
configValid: false,
authEnabled: false,
jwksFetched: false,
endpoint: "/healthz",
expectedCode: http.StatusOK,
description: "liveness probe returns 200 even when config is invalid",
},
{
name: "readyz_200_no_auth",
configValid: true,
authEnabled: false,
jwksFetched: false,
endpoint: "/readyz",
expectedCode: http.StatusOK,
description: "readiness returns 200 when config valid and auth disabled",
},
{
name: "readyz_503_invalid_config",
configValid: false,
authEnabled: false,
jwksFetched: false,
endpoint: "/readyz",
expectedCode: http.StatusServiceUnavailable,
description: "readiness returns 503 when config invalid",
},
{
name: "readyz_503_auth_enabled_no_jwks",
configValid: true,
authEnabled: true,
jwksFetched: false,
endpoint: "/readyz",
expectedCode: http.StatusServiceUnavailable,
description: "readiness returns 503 when auth enabled but JWKS not fetched",
},
{
name: "readyz_200_auth_enabled_with_jwks",
configValid: true,
authEnabled: true,
jwksFetched: true,
endpoint: "/readyz",
expectedCode: http.StatusOK,
description: "readiness returns 200 when auth enabled and JWKS fetched",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create health checker
hc := server.NewHealthChecker(tt.configValid, tt.authEnabled)
if tt.jwksFetched {
hc.MarkJWKSFetched()
}
// Create handler based on endpoint
var handler http.HandlerFunc
switch tt.endpoint {
case "/healthz":
handler = server.LivenessHandler(hc)
case "/readyz":
handler = server.ReadinessHandler(hc)
default:
t.Fatalf("unknown endpoint: %s", tt.endpoint)
}
// Create server wrapper
gatewayServer := server.New("127.0.0.1:0", 5*time.Second, handler)
// Start server in goroutine
go func() {
if err := gatewayServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Logf("server error: %v", err)
}
}()
// Give server time to start
time.Sleep(100 * time.Millisecond)
// Make request
url := fmt.Sprintf("http://%s%s", gatewayServer.Addr(), tt.endpoint)
resp, err := http.Get(url)
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != tt.expectedCode {
body, _ := io.ReadAll(resp.Body)
t.Errorf("expected status %d, got %d: %s", tt.expectedCode, resp.StatusCode, string(body))
}
// Verify no Authorization header is required
// (we already made the request without one, so this is implicit)
// Cleanup
gatewayServer.Shutdown(context.Background())
})
}
}
// TestHealthEndpointsNoProxy verifies that health endpoints are not proxied.
// This is verified indirectly by the test above - if they were proxied,
// they would return 404 or fail when trying to reach a non-existent upstream.
func TestHealthEndpointsCannotBeShadowed(t *testing.T) {
// Create health checker and handler
hc := server.NewHealthChecker(true, false)
handler := server.LivenessHandler(hc)
// Create server
srv := server.New("127.0.0.1:0", 5*time.Second, handler)
// Start server
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
t.Logf("server error: %v", err)
}
}()
// Give server time to start
time.Sleep(100 * time.Millisecond)
// Request /healthz and verify it's not proxied
resp, err := http.Get(fmt.Sprintf("http://%s/healthz", srv.Addr()))
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
// Cleanup
srv.Shutdown(context.Background())
}
+36
View File
@@ -0,0 +1,36 @@
package server
import (
"net/http"
)
// Router implements an HTTP handler that routes health endpoints
// and passes other requests to an upstream handler.
type Router struct {
healthChecker *HealthChecker
upstreamHandler http.Handler
}
// NewRouter creates a new router with health endpoints.
// Health endpoints (/healthz and /readyz) are handled locally.
// All other paths are passed to the upstream handler.
func NewRouter(healthChecker *HealthChecker, upstreamHandler http.Handler) *Router {
return &Router{
healthChecker: healthChecker,
upstreamHandler: upstreamHandler,
}
}
// ServeHTTP implements http.Handler.
// It routes /healthz and /readyz to health handlers,
// and passes all other paths to the upstream handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/healthz":
LivenessHandler(r.healthChecker)(w, req)
case "/readyz":
ReadinessHandler(r.healthChecker)(w, req)
default:
r.upstreamHandler.ServeHTTP(w, req)
}
}
+83
View File
@@ -0,0 +1,83 @@
package server
import (
"context"
"net"
"net/http"
"sync"
"time"
)
// Server wraps an HTTP server with graceful shutdown support.
type Server struct {
httpServer *http.Server
shutdownTimeout time.Duration
listener net.Listener
listenerMu sync.RWMutex
healthChecker *HealthChecker
}
// New creates a new Server with the given configuration.
func New(listenAddr string, shutdownTimeout time.Duration, handler http.Handler) *Server {
return &Server{
httpServer: &http.Server{
Addr: listenAddr,
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
},
shutdownTimeout: shutdownTimeout,
healthChecker: NewHealthChecker(false, false),
}
}
// ListenAndServe starts the HTTP server and blocks until it exits.
// It returns the error from the server (if any), which will be
// http.ErrServerClosed if Shutdown was called.
func (s *Server) ListenAndServe() error {
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return err
}
s.listenerMu.Lock()
s.listener = listener
s.listenerMu.Unlock()
return s.httpServer.Serve(listener)
}
// Shutdown gracefully shuts down the server. It stops accepting new
// connections and waits for in-flight requests to complete, with a
// bounded deadline. If the deadline is exceeded, it returns an error.
func (s *Server) Shutdown(ctx context.Context) error {
// Create a new context with the shutdown timeout
shutdownCtx, cancel := context.WithTimeout(ctx, s.shutdownTimeout)
defer cancel()
return s.httpServer.Shutdown(shutdownCtx)
}
// Addr returns the network address the server is listening on.
func (s *Server) Addr() string {
s.listenerMu.RLock()
defer s.listenerMu.RUnlock()
if s.listener != nil {
return s.listener.Addr().String()
}
return s.httpServer.Addr
}
// HealthChecker returns the server's health checker.
func (s *Server) HealthChecker() *HealthChecker {
return s.healthChecker
}
// SetHealthChecker sets the server's health checker.
func (s *Server) SetHealthChecker(hc *HealthChecker) {
s.healthChecker = hc
}
// SetHandler sets the server's HTTP handler.
func (s *Server) SetHandler(handler http.Handler) {
s.httpServer.Handler = handler
}
+104
View File
@@ -0,0 +1,104 @@
package server_test
import (
"context"
"fmt"
"io"
"net"
"net/http"
"sync"
"testing"
"time"
"github.com/Riotpiaole/homelab-frontend/internal/server"
)
// TestGracefulShutdown verifies that:
// - A request in-flight when shutdown starts receives its full, uncorrupted response body
// - A request arriving after shutdown starts is refused on a new connection
// - The shutdown completes with exit code 0 (no timeout)
func TestGracefulShutdown(t *testing.T) {
// Create a handler that responds slowly
const responseBody = "slow response body content"
const sleepDuration = 500 * time.Millisecond
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate a slow LLM response
time.Sleep(sleepDuration)
fmt.Fprint(w, responseBody)
})
// Create server with a shutdown timeout longer than the sleep
srv := server.New("127.0.0.1:0", 5*time.Second, handler)
// Start server in a goroutine
var listenErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
listenErr = srv.ListenAndServe()
// http.ErrServerClosed is expected after shutdown
if listenErr != nil && listenErr != http.ErrServerClosed {
t.Logf("unexpected listen error: %v", listenErr)
}
}()
// Give server time to start listening
time.Sleep(100 * time.Millisecond)
// Issue a slow request in a goroutine
var responseBody_got string
var requestErr error
var requestWg sync.WaitGroup
requestWg.Add(1)
go func() {
defer requestWg.Done()
resp, err := http.Get(fmt.Sprintf("http://%s/", srv.Addr()))
if err != nil {
requestErr = err
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
requestErr = err
return
}
responseBody_got = string(body)
}()
// Give the request time to reach the handler
time.Sleep(100 * time.Millisecond)
// Now initiate shutdown while request is in-flight
shutdownErr := srv.Shutdown(context.Background())
// Wait for the in-flight request to complete
requestWg.Wait()
// Verify the in-flight request completed successfully
if requestErr != nil {
t.Fatalf("in-flight request failed: %v", requestErr)
}
if responseBody_got != responseBody {
t.Fatalf("in-flight request got wrong body: %q (expected %q)", responseBody_got, responseBody)
}
// Verify shutdown succeeded (no timeout)
if shutdownErr != nil {
t.Fatalf("shutdown failed: %v", shutdownErr)
}
// Verify in-flight requests were allowed to complete
wg.Wait()
// Now verify that a new request is refused after shutdown
_, err := net.Dial("tcp", srv.Addr())
if err == nil {
// Connection succeeded when it should have failed
t.Fatalf("new connection accepted after shutdown (should have been refused)")
}
// If we get here, the connection was properly refused, which is what we want
}