From 410e56831003889b924c9dcdd40a3411446589c4 Mon Sep 17 00:00:00 2001 From: Shirly Radco Date: Thu, 12 Mar 2026 20:34:26 +0200 Subject: [PATCH] router: add GET /health endpoint and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add GET /api/v1/alerting/health endpoint with handler tests. Signed-off-by: Shirly Radco Signed-off-by: João Vilaça Signed-off-by: Aviv Litman Co-authored-by: AI Assistant --- internal/managementrouter/health_get.go | 33 +++++++ internal/managementrouter/health_get_test.go | 93 ++++++++++++++++++++ internal/managementrouter/router.go | 6 +- pkg/management/get_alerting_health_test.go | 53 +++++++++++ test/e2e/health_test.go | 63 +++++++++++++ 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 internal/managementrouter/health_get.go create mode 100644 internal/managementrouter/health_get_test.go create mode 100644 pkg/management/get_alerting_health_test.go create mode 100644 test/e2e/health_test.go diff --git a/internal/managementrouter/health_get.go b/internal/managementrouter/health_get.go new file mode 100644 index 000000000..5eb698e0b --- /dev/null +++ b/internal/managementrouter/health_get.go @@ -0,0 +1,33 @@ +package managementrouter + +import ( + "encoding/json" + "net/http" + + "github.com/openshift/monitoring-plugin/pkg/k8s" +) + +type GetHealthResponse struct { + Alerting *k8s.AlertingHealth `json:"alerting,omitempty"` +} + +// GetHealth serves GET /api/v1/alerting/health. +func (hr *httpRouter) GetHealth(w http.ResponseWriter, req *http.Request) { + resp := GetHealthResponse{} + + if hr.managementClient != nil { + health, err := hr.managementClient.GetAlertingHealth(req.Context()) + if err != nil { + handleError(w, err) + return + } + resp.Alerting = &health + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(resp); err != nil { + log.WithError(err).Warn("failed to encode health response") + } +} diff --git a/internal/managementrouter/health_get_test.go b/internal/managementrouter/health_get_test.go new file mode 100644 index 000000000..8a0d8d265 --- /dev/null +++ b/internal/managementrouter/health_get_test.go @@ -0,0 +1,93 @@ +package managementrouter_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/openshift/monitoring-plugin/internal/managementrouter" + "github.com/openshift/monitoring-plugin/pkg/k8s" +) + +func sampleAlertingHealth() k8s.AlertingHealth { + return k8s.AlertingHealth{ + Platform: &k8s.AlertingStackHealth{ + Prometheus: k8s.AlertingRouteHealth{Name: "prometheus-k8s", Namespace: "openshift-monitoring", Status: k8s.RouteReachable}, + Alertmanager: k8s.AlertingRouteHealth{Name: "alertmanager-main", Namespace: "openshift-monitoring", Status: k8s.RouteReachable}, + }, + UserWorkloadEnabled: true, + UserWorkload: &k8s.AlertingStackHealth{ + Prometheus: k8s.AlertingRouteHealth{Name: "prometheus-user-workload", Namespace: "openshift-user-workload-monitoring", Status: k8s.RouteReachable}, + Alertmanager: k8s.AlertingRouteHealth{Name: "alertmanager-user-workload", Namespace: "openshift-user-workload-monitoring", Status: k8s.RouteReachable}, + }, + } +} + +func TestGetHealth_Returns200(t *testing.T) { + f := newAGFixture(t) + f.mockK8s.AlertingHealthFunc = func(_ context.Context) (k8s.AlertingHealth, error) { + return sampleAlertingHealth(), nil + } + + w := f.get(t, "/api/v1/alerting/health") + if w.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", w.Code, w.Body) + } + if ct := w.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %q", ct) + } +} + +func TestGetHealth_ReturnsAlertingStructure(t *testing.T) { + f := newAGFixture(t) + f.mockK8s.AlertingHealthFunc = func(_ context.Context) (k8s.AlertingHealth, error) { + return sampleAlertingHealth(), nil + } + + w := f.get(t, "/api/v1/alerting/health") + var response managementrouter.GetHealthResponse + if err := json.NewDecoder(w.Body).Decode(&response); err != nil { + t.Fatalf("decode error: %v", err) + } + if response.Alerting == nil { + t.Fatal("expected non-nil Alerting in response") + } + if response.Alerting.Platform == nil || response.Alerting.Platform.Prometheus.Name != "prometheus-k8s" { + t.Errorf("expected platform prometheus-k8s, got %+v", response.Alerting.Platform) + } + if !response.Alerting.UserWorkloadEnabled { + t.Error("expected UserWorkloadEnabled=true") + } + if response.Alerting.UserWorkload == nil || response.Alerting.UserWorkload.Prometheus.Name != "prometheus-user-workload" { + t.Errorf("expected user workload prometheus-user-workload, got %+v", response.Alerting.UserWorkload) + } +} + +func TestGetHealth_Returns500OnError(t *testing.T) { + f := newAGFixture(t) + f.mockK8s.AlertingHealthFunc = func(_ context.Context) (k8s.AlertingHealth, error) { + return k8s.AlertingHealth{}, fmt.Errorf("connection refused") + } + + w := f.get(t, "/api/v1/alerting/health") + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected 500, got %d: %s", w.Code, w.Body) + } + if body := w.Body.String(); !strings.Contains(body, "An unexpected error occurred") { + t.Errorf("expected error message, got: %s", body) + } +} + +func TestGetHealth_MissingAuthHeaderReturns401(t *testing.T) { + f := newAGFixture(t) + req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/health", nil) + w := httptest.NewRecorder() + f.router.ServeHTTP(w, req) + if w.Code != http.StatusUnauthorized { + t.Fatalf("expected 401, got %d: %s", w.Code, w.Body) + } +} diff --git a/internal/managementrouter/router.go b/internal/managementrouter/router.go index f0ac5cfb8..915b62f8c 100644 --- a/internal/managementrouter/router.go +++ b/internal/managementrouter/router.go @@ -43,10 +43,12 @@ func New(managementClient management.Client) *mux.Router { BaseURL: "/api/v1/alerting", BaseRouter: r, }) - // GET /alerts and GET /rules are not yet in the OpenAPI spec; registered - // manually until their respective branches add the spec entries. + // GET /alerts, GET /rules, and GET /health are not yet in the OpenAPI + // spec; registered manually until their respective branches add the spec + // entries. r.HandleFunc("/api/v1/alerting/alerts", hr.GetAlerts).Methods(http.MethodGet) r.HandleFunc("/api/v1/alerting/rules", hr.GetRules).Methods(http.MethodGet) + r.HandleFunc("/api/v1/alerting/health", hr.GetHealth).Methods(http.MethodGet) return r } diff --git a/pkg/management/get_alerting_health_test.go b/pkg/management/get_alerting_health_test.go new file mode 100644 index 000000000..ec104abbc --- /dev/null +++ b/pkg/management/get_alerting_health_test.go @@ -0,0 +1,53 @@ +package management_test + +import ( + "context" + "testing" + "time" + + "github.com/openshift/monitoring-plugin/pkg/k8s" + "github.com/openshift/monitoring-plugin/pkg/management" + "github.com/openshift/monitoring-plugin/pkg/management/testutils" +) + +func TestGetAlertingHealth_SetsDeadlineWhenCallerHasNone(t *testing.T) { + var hasDeadline bool + mockK8s := &testutils.MockClient{ + AlertingHealthFunc: func(ctx context.Context) (k8s.AlertingHealth, error) { + _, hasDeadline = ctx.Deadline() + return k8s.AlertingHealth{}, nil + }, + } + client := management.New(context.Background(), mockK8s) + if _, err := client.GetAlertingHealth(context.Background()); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !hasDeadline { + t.Fatal("expected GetAlertingHealth to set a deadline when the caller did not") + } +} + +func TestGetAlertingHealth_PreservesCallerDeadline(t *testing.T) { + callerDeadline := time.Now().Add(2 * time.Second) + ctx, cancel := context.WithDeadline(context.Background(), callerDeadline) + defer cancel() + + var gotDeadline time.Time + var sawDeadline bool + mockK8s := &testutils.MockClient{ + AlertingHealthFunc: func(ctx context.Context) (k8s.AlertingHealth, error) { + gotDeadline, sawDeadline = ctx.Deadline() + return k8s.AlertingHealth{}, nil + }, + } + client := management.New(context.Background(), mockK8s) + if _, err := client.GetAlertingHealth(ctx); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !sawDeadline { + t.Fatal("expected the caller's deadline to be forwarded") + } + if !gotDeadline.Equal(callerDeadline) { + t.Errorf("expected caller deadline %v, got %v", callerDeadline, gotDeadline) + } +} diff --git a/test/e2e/health_test.go b/test/e2e/health_test.go new file mode 100644 index 000000000..955b49ec1 --- /dev/null +++ b/test/e2e/health_test.go @@ -0,0 +1,63 @@ +//go:build e2e + +package e2e + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/openshift/monitoring-plugin/pkg/k8s" + "github.com/openshift/monitoring-plugin/test/e2e/framework" +) + +func TestGetHealth(t *testing.T) { + f, err := framework.New() + if err != nil { + t.Fatalf("Failed to create framework: %v", err) + } + + ctx := context.Background() + + healthURL := f.PluginURL + "/api/v1/alerting/health" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthURL, nil) + if err != nil { + t.Fatalf("Failed to create HTTP request: %v", err) + } + if f.BearerToken != "" { + req.Header.Set("Authorization", "Bearer "+f.BearerToken) + } + + resp, err := f.HTTPClient().Do(req) + if err != nil { + t.Fatalf("Failed to make health request: %v", err) + } + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + t.Logf("closing response body: %v", closeErr) + } + }() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("Expected status 200, got %d", resp.StatusCode) + } + + var healthResp struct { + Alerting *k8s.AlertingHealth `json:"alerting"` + } + if err := json.NewDecoder(resp.Body).Decode(&healthResp); err != nil { + t.Fatalf("Failed to decode health response: %v", err) + } + + if healthResp.Alerting == nil { + t.Fatal("Expected 'alerting' field in health response") + } + + if healthResp.Alerting.Platform == nil { + t.Error("Expected 'platform' field in alerting health") + } + + t.Logf("Health response: userWorkloadEnabled=%v", healthResp.Alerting.UserWorkloadEnabled) + t.Log("GET /health e2e test passed successfully") +}