From 835a3de877d5e9e0232459baee238e67c26723b6 Mon Sep 17 00:00:00 2001 From: Igor Karpovich Date: Sat, 21 Mar 2026 15:31:47 +0000 Subject: [PATCH] feat: add health and readiness endpoints --- README.md | 8 +++++ internal/httpapi/handler.go | 14 +++++++++ internal/httpapi/handler_test.go | 53 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/README.md b/README.md index d4afc60..b2c1170 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ When trusted proxies are configured, forwarded headers are honored only for trus Response: - JSON object: `status`, `message`, optional `client` +### `GET /healthz` + +Unauthenticated liveness endpoint for orchestration probes. + +### `GET /readyz` + +Unauthenticated readiness endpoint for orchestration probes. + ## Config Use one YAML file. diff --git a/internal/httpapi/handler.go b/internal/httpapi/handler.go index 2607520..dfce8bb 100644 --- a/internal/httpapi/handler.go +++ b/internal/httpapi/handler.go @@ -34,9 +34,23 @@ func New(cfg *appconfig.Config, resolver *ipresolver.Resolver, updater *provider } func (h *Handler) Register(mux *http.ServeMux) { + mux.HandleFunc("GET /healthz", h.Healthz) + mux.HandleFunc("GET /readyz", h.Readyz) mux.HandleFunc("POST /update", h.Update) } +func (h *Handler) Healthz(w http.ResponseWriter, _ *http.Request) { + writeJSON(w, http.StatusOK, response{Status: "ok", Message: "alive"}) +} + +func (h *Handler) Readyz(w http.ResponseWriter, _ *http.Request) { + if h.cfg == nil || h.resolver == nil || h.updater == nil { + writeJSON(w, http.StatusServiceUnavailable, response{Status: "error", Message: "not ready"}) + return + } + writeJSON(w, http.StatusOK, response{Status: "ok", Message: "ready"}) +} + func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { token, err := bearerToken(r.Header.Get("Authorization")) if err != nil { diff --git a/internal/httpapi/handler_test.go b/internal/httpapi/handler_test.go index 6184d6c..4f75011 100644 --- a/internal/httpapi/handler_test.go +++ b/internal/httpapi/handler_test.go @@ -64,3 +64,56 @@ func TestWriteJSON(t *testing.T) { t.Fatalf("unmarshal: %v", err) } } + +func TestHealthzRoute(t *testing.T) { + cfg := testConfig() + resolver, _ := ipresolver.New(nil) + h := New(cfg, resolver, provider.NewUpdater()) + mux := http.NewServeMux() + h.Register(mux) + + r := httptest.NewRequest(http.MethodGet, "/healthz", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, r) + + if w.Code != http.StatusOK { + t.Fatalf("unexpected code: %d", w.Code) + } + var body response + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if body.Status != "ok" || body.Message != "alive" { + t.Fatalf("unexpected body: %+v", body) + } +} + +func TestReadyzRoute(t *testing.T) { + cfg := testConfig() + resolver, _ := ipresolver.New(nil) + h := New(cfg, resolver, provider.NewUpdater()) + mux := http.NewServeMux() + h.Register(mux) + + r := httptest.NewRequest(http.MethodGet, "/readyz", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, r) + + if w.Code != http.StatusOK { + t.Fatalf("unexpected code: %d", w.Code) + } +} + +func TestReadyzNotReady(t *testing.T) { + h := New(nil, nil, nil) + mux := http.NewServeMux() + h.Register(mux) + + r := httptest.NewRequest(http.MethodGet, "/readyz", nil) + w := httptest.NewRecorder() + mux.ServeHTTP(w, r) + + if w.Code != http.StatusServiceUnavailable { + t.Fatalf("unexpected code: %d", w.Code) + } +}