Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/managementrouter/health_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"

"github.com/openshift/monitoring-plugin/internal/managementrouter"
"github.com/openshift/monitoring-plugin/pkg/k8s"
"github.com/openshift/monitoring-plugin/pkg/management"
)

var _ = Describe("GetHealth", func() {
Expand Down Expand Up @@ -110,10 +112,18 @@ type healthStubManagementClient struct {
alertingHealth func(ctx context.Context) (k8s.AlertingHealth, error)
}

func (s *healthStubManagementClient) ListRules(ctx context.Context, prOptions management.PrometheusRuleOptions, arOptions management.AlertRuleOptions) ([]monitoringv1.Rule, error) {
return nil, nil
}

func (s *healthStubManagementClient) GetAlerts(ctx context.Context, req k8s.GetAlertsRequest) ([]k8s.PrometheusAlert, error) {
return nil, nil
}

func (s *healthStubManagementClient) GetRules(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error) {
return []k8s.PrometheusRuleGroup{}, nil
}

func (s *healthStubManagementClient) GetAlertingHealth(ctx context.Context) (k8s.AlertingHealth, error) {
if s.alertingHealth != nil {
return s.alertingHealth(ctx)
Expand Down
1 change: 1 addition & 0 deletions internal/managementrouter/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func New(managementClient management.Client) *mux.Router {

r.HandleFunc("/api/v1/alerting/health", httpRouter.GetHealth).Methods(http.MethodGet)
r.HandleFunc("/api/v1/alerting/alerts", httpRouter.GetAlerts).Methods(http.MethodGet)
r.HandleFunc("/api/v1/alerting/rules", httpRouter.GetRules).Methods(http.MethodGet)

return r
}
Expand Down
48 changes: 48 additions & 0 deletions internal/managementrouter/rules_get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package managementrouter

import (
"encoding/json"
"log"
"net/http"

"github.com/openshift/monitoring-plugin/pkg/k8s"
)

type GetRulesResponse struct {
Data GetRulesResponseData `json:"data"`
Warnings []string `json:"warnings,omitempty"`
}

type GetRulesResponseData struct {
Groups []k8s.PrometheusRuleGroup `json:"groups"`
}

func (hr *httpRouter) GetRules(w http.ResponseWriter, req *http.Request) {
state, labels, err := parseStateAndLabels(req.URL.Query())
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
ctx := k8s.WithBearerToken(req.Context(), bearerTokenFromRequest(req))

groups, err := hr.managementClient.GetRules(ctx, k8s.GetRulesRequest{
Labels: labels,
State: state,
})
if err != nil {
handleError(w, err)
return
}

w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(GetRulesResponse{
Data: GetRulesResponseData{
Groups: groups,
},
Warnings: hr.rulesWarnings(ctx),
}); err != nil {
log.Printf("failed to encode rules response: %v", err)
}
}
164 changes: 164 additions & 0 deletions internal/managementrouter/rules_get_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package managementrouter_test

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"

"github.com/openshift/monitoring-plugin/internal/managementrouter"
"github.com/openshift/monitoring-plugin/pkg/k8s"
"github.com/openshift/monitoring-plugin/pkg/management"
)

var _ = Describe("GetRules", func() {
var (
mockManagement *stubManagementClient
router http.Handler
)

BeforeEach(func() {
mockManagement = &stubManagementClient{}
router = managementrouter.New(mockManagement)
})

Context("flat label parsing", func() {
It("parses flat query params into Labels map and state", func() {
var captured k8s.GetRulesRequest
mockManagement.getRules = func(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error) {
captured = req
return []k8s.PrometheusRuleGroup{}, nil
}

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/rules?namespace=ns1&severity=critical&state=firing&team=sre", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, req)

Expect(w.Code).To(Equal(http.StatusOK))
Expect(captured.State).To(Equal("firing"))
Expect(captured.Labels["namespace"]).To(Equal("ns1"))
Expect(captured.Labels["severity"]).To(Equal("critical"))
Expect(captured.Labels["team"]).To(Equal("sre"))
})
})

Context("when getting rules without filters", func() {
It("returns groups in response", func() {
mockManagement.getRules = func(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error) {
return []k8s.PrometheusRuleGroup{
{
Name: "group-a",
},
}, nil
}

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/rules", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, req)

Expect(w.Code).To(Equal(http.StatusOK))
Expect(w.Header().Get("Content-Type")).To(Equal("application/json"))

var response managementrouter.GetRulesResponse
err := json.NewDecoder(w.Body).Decode(&response)
Expect(err).NotTo(HaveOccurred())
Expect(response.Data.Groups).To(HaveLen(1))
Expect(response.Data.Groups[0].Name).To(Equal("group-a"))
})

It("returns warnings when user workload Prometheus route is missing", func() {
mockManagement.alertingHealth = func(ctx context.Context) (k8s.AlertingHealth, error) {
return k8s.AlertingHealth{
UserWorkloadEnabled: true,
UserWorkload: &k8s.AlertingStackHealth{
Prometheus: k8s.AlertingRouteHealth{Status: k8s.RouteNotFound},
},
}, nil
}

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/rules", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, req)

var response managementrouter.GetRulesResponse
err := json.NewDecoder(w.Body).Decode(&response)
Expect(err).NotTo(HaveOccurred())
Expect(response.Warnings).To(ContainElement("user workload Prometheus route is missing"))
})

It("suppresses warnings when fallback is healthy", func() {
mockManagement.alertingHealth = func(ctx context.Context) (k8s.AlertingHealth, error) {
return k8s.AlertingHealth{
UserWorkloadEnabled: true,
UserWorkload: &k8s.AlertingStackHealth{
Prometheus: k8s.AlertingRouteHealth{
Status: k8s.RouteUnreachable,
FallbackReachable: true,
},
},
}, nil
}

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/rules", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, req)

var response managementrouter.GetRulesResponse
err := json.NewDecoder(w.Body).Decode(&response)
Expect(err).NotTo(HaveOccurred())
Expect(response.Warnings).To(BeEmpty())
})
})

Context("when handling errors", func() {
It("returns 500 when GetRules fails", func() {
mockManagement.getRules = func(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error) {
return nil, fmt.Errorf("connection error")
}

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerting/rules", nil)
w := httptest.NewRecorder()

router.ServeHTTP(w, req)

Expect(w.Code).To(Equal(http.StatusInternalServerError))
Expect(w.Body.String()).To(ContainSubstring("An unexpected error occurred"))
})
})
})

type stubManagementClient struct {
getRules func(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error)
alertingHealth func(ctx context.Context) (k8s.AlertingHealth, error)
}

func (s *stubManagementClient) ListRules(ctx context.Context, prOptions management.PrometheusRuleOptions, arOptions management.AlertRuleOptions) ([]monitoringv1.Rule, error) {
return nil, nil
}

func (s *stubManagementClient) GetAlerts(ctx context.Context, req k8s.GetAlertsRequest) ([]k8s.PrometheusAlert, error) {
return nil, nil
}

func (s *stubManagementClient) GetRules(ctx context.Context, req k8s.GetRulesRequest) ([]k8s.PrometheusRuleGroup, error) {
if s.getRules != nil {
return s.getRules(ctx, req)
}
return []k8s.PrometheusRuleGroup{}, nil
}

func (s *stubManagementClient) GetAlertingHealth(ctx context.Context) (k8s.AlertingHealth, error) {
if s.alertingHealth != nil {
return s.alertingHealth(ctx)
}
return k8s.AlertingHealth{}, nil
}
5 changes: 5 additions & 0 deletions pkg/k8s/prometheus_rules_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import (
"time"
)

const (
RuleTypeAlerting = "alerting"
RuleTypeRecording = "recording"
)

// GetRulesRequest holds parameters for filtering rules alerts.
type GetRulesRequest struct {
// Labels filters rules by exact label equality. The special key "namespace"
Expand Down
Loading