-
Notifications
You must be signed in to change notification settings - Fork 69
CNV-87533: router: add GET /health endpoint and tests #1173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect how the e2e target URL and HTTP client are configured.
ast-grep outline test/e2e/framework --items all
rg -n -C 5 'PluginURL|BearerToken|HTTPClient|CheckRedirect|func New' test/e2e/framework
rg -n -C 3 'PluginURL\s*[:=]|plugin.*url|PLUGIN.*URL' --glob '*.go' .Repository: openshift/monitoring-plugin Length of output: 8171 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- health test ---'
cat -n test/e2e/health_test.go | sed -n '1,70p'
printf '%s\n' '--- framework client and URL setup ---'
cat -n test/e2e/framework/framework.go | sed -n '43,120p'
printf '%s\n' '--- framework request helper ---'
cat -n test/e2e/framework/framework.go | sed -n '145,160p'
printf '%s\n' '--- PLUGIN_URL documentation/configuration ---'
rg -n -C 4 'PLUGIN_URL|PluginURL' --glob '!test/e2e/framework/framework.go' --glob '*.go' --glob '*.md' --glob '*.yaml' --glob '*.yml' .Repository: openshift/monitoring-plugin Length of output: 18159 🏁 Script executed: #!/bin/bash
set -euo pipefail
cat -n test/e2e/health_test.go | sed -n '1,70p'
cat -n test/e2e/framework/framework.go | sed -n '43,120p'
cat -n test/e2e/framework/framework.go | sed -n '145,160p'
rg -n -C 4 'PLUGIN_URL|PluginURL' --glob '!test/e2e/framework/framework.go' --glob '*.go' --glob '*.md' --glob '*.yaml' --glob '*.yml' .Repository: openshift/monitoring-plugin Length of output: 18020 Sensitive Data Exposure Reachability: Internal Enforce HTTPS for
🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
|
|
||
| resp, err := f.HTTPClient().Do(req) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Confirm whether the framework client rejects HTTPS-to-HTTP redirects.
rg -n -C 8 'func .*HTTPClient|http\.Client|CheckRedirect|Do\(' test/e2e/frameworkRepository: openshift/monitoring-plugin Length of output: 3367 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- health test ---'
cat -n test/e2e/health_test.go | sed -n '1,80p'
printf '%s\n' '--- framework URL and client setup ---'
cat -n test/e2e/framework/framework.go | sed -n '43,115p'
printf '%s\n' '--- relevant URL/client configuration references ---'
rg -n -C 4 'PLUGIN_URL|PluginURL|BearerToken|Authorization|CheckRedirect|httpClient' test/e2e --glob '*.go'Repository: openshift/monitoring-plugin Length of output: 37412 🏁 Script executed: #!/bin/bash
set -euo pipefail
go_version="$(go version 2>/dev/null || true)"
printf '%s\n' "--- toolchain: ${go_version:-unavailable} ---"
printf '%s\n' '--- local net/http redirect implementation/docs ---'
go_root="$(go env GOROOT 2>/dev/null || true)"
if [ -n "$go_root" ] && [ -f "$go_root/src/net/http/client.go" ]; then
rg -n -C 12 'func redirectBehavior|Authorization|shouldCopyHeaderOnRedirect|CheckRedirect|ErrSchemeMismatch' "$go_root/src/net/http/client.go"
else
printf '%s\n' 'Go source unavailable'
fiRepository: openshift/monitoring-plugin Length of output: 16402 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- repository conventions and learnings for e2e HTTP tests ---'Repository: openshift/monitoring-plugin Length of output: 229 🤖 get_repo_knowledge executed:
Length of output: 6362 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- exact Go redirect behavior from available source ---'
go_root="$(go env GOROOT 2>/dev/null || true)"
if [ -n "$go_root" ] && [ -f "$go_root/src/net/http/client.go" ]; then
sed -n '430,575p' "$go_root/src/net/http/client.go"
else
printf '%s\n' 'Go source unavailable'
fiRepository: openshift/monitoring-plugin Length of output: 5384 🏁 Script executed: #!/bin/bash
set -euo pipefail
cat -n test/e2e/health_test.go | sed -n '1,70p'
cat -n test/e2e/framework/framework.go | sed -n '43,114p'
printf '%s\n' '--- Go redirect source ---'
go_root="$(go env GOROOT 2>/dev/null || true)"
if [ -f "$go_root/src/net/http/client.go" ]; then
rg -n -C 10 'shouldCopyHeaderOnRedirect|Authorization|CheckRedirect' "$go_root/src/net/http/client.go"
fiRepository: openshift/monitoring-plugin Length of output: 17488 🌐 Web query:
💡 Result: In Go's net/http package, the http.Client automatically strips sensitive headers—including Authorization, WWW-Authenticate, Cookie, and others—when following redirects to a domain that is not a subdomain match or an exact match of the initial domain [1][2][3]. This security mechanism is designed to prevent the accidental leakage of authentication credentials to untrusted third-party servers [4][5]. Official Documentation and Behavior The official documentation explicitly states that when following redirects, the Client forwards all headers set on the initial request with specific exceptions for sensitive headers [1][2]. These sensitive headers are ignored when redirecting to a domain that does not meet the specified security criteria [1][2]. A redirect from foo.com to either foo.com or sub.foo.com is considered a trusted target and will preserve these headers, whereas a redirect to a different domain (e.g., bar.com) will cause the headers to be dropped [1][2][3]. Technical Context This behavior was formally reinforced and clarified in response to vulnerabilities, including CVE-2023-45289, which addressed instances where sensitive headers were being inappropriately forwarded [4][5]. Consequently, the Go standard library maintains a strict policy to ensure that credentials are not leaked during cross-origin redirects [5][3]. Handling Custom Requirements If your application requires the propagation of Authorization headers across cross-origin redirects, you must manually manage this behavior by implementing a custom CheckRedirect function on your http.Client [6]. Within this function, you can inspect the request and the sequence of redirects to manually re-add the necessary headers to the new request [6]. Example pattern for custom logic: client:= &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { if len(via) > 0 { // Manually copy sensitive headers from the initial request originalReq:= via if auth:= originalReq.Header.Get("Authorization"); auth!= "" { req.Header.Set("Authorization", auth) } } return nil }, } Please be aware that manually overriding this behavior bypasses the built-in security protections, and you should ensure that the destination domains are trusted before forwarding credentials [6]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
go_root="$(go env GOROOT)"
sed -n '1010,1065p' "$go_root/src/net/http/client.go"Repository: openshift/monitoring-plugin Length of output: 2003 🏁 Script executed: #!/bin/bash
set -euo pipefail
go_root="$(go env GOROOT)"
sed -n '660,715p' "$go_root/src/net/http/client.go"Repository: openshift/monitoring-plugin Length of output: 1954 Sensitive Data Exposure Reachability: Internal Reject non-HTTPS plugin URLs and redirects. Go’s default redirect policy preserves 🤖 Prompt for AI AgentsSource: Path instructions |
||
| 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") | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Set a deadline on the e2e request context.
context.Background()at Line 21 has no deadline or cancellation. The request can continue until the HTTP client returns. Usecontext.WithTimeoutand defercancelbeforeHTTPClient().Do.As per path instructions,
**/*.gorequirescontext.Contextfor cancellation and timeouts.🤖 Prompt for AI Agents
Source: Path instructions