From b8e96983717910e3e1e71e0f0d4fcd078f269e82 Mon Sep 17 00:00:00 2001 From: Shirly Radco Date: Wed, 25 Feb 2026 22:39:25 +0200 Subject: [PATCH] router: add single alert rule update and delete-by-ID endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Shirly Radco Signed-off-by: João Vilaça Signed-off-by: Aviv Litman Co-authored-by: AI Assistant --- .../managementrouter/alert_rule_update.go | 168 ++++++++ .../alert_rule_update_test.go | 367 ++++++++++++++++++ internal/managementrouter/router.go | 2 + .../user_defined_alert_rule_delete_by_id.go | 19 + ...er_defined_alert_rule_delete_by_id_test.go | 171 ++++++++ 5 files changed, 727 insertions(+) create mode 100644 internal/managementrouter/alert_rule_update_test.go create mode 100644 internal/managementrouter/user_defined_alert_rule_delete_by_id_test.go diff --git a/internal/managementrouter/alert_rule_update.go b/internal/managementrouter/alert_rule_update.go index 4c84fb7ff..979e973ec 100644 --- a/internal/managementrouter/alert_rule_update.go +++ b/internal/managementrouter/alert_rule_update.go @@ -1,7 +1,175 @@ package managementrouter +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + + "github.com/openshift/monitoring-plugin/pkg/management" +) + +type UpdateAlertRuleRequest struct { + AlertingRule *monitoringv1.Rule `json:"alertingRule,omitempty"` + AlertingRuleEnabled *bool `json:"AlertingRuleEnabled,omitempty"` + Classification *AlertRuleClassificationPatch `json:"classification,omitempty"` +} + type UpdateAlertRuleResponse struct { Id string `json:"id"` StatusCode int `json:"status_code"` Message string `json:"message,omitempty"` } + +func (hr *httpRouter) UpdateAlertRule(w http.ResponseWriter, req *http.Request) { + ruleId, err := getParam(req, "ruleId") + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + + var payload UpdateAlertRuleRequest + if err := json.NewDecoder(req.Body).Decode(&payload); err != nil { + writeError(w, http.StatusBadRequest, "invalid request body") + return + } + + if payload.AlertingRule == nil && payload.AlertingRuleEnabled == nil && payload.Classification == nil { + writeError(w, http.StatusBadRequest, "either alertingRule, AlertingRuleEnabled, or classification is required") + return + } + + // Handle drop/restore for platform alerts + if payload.AlertingRuleEnabled != nil { + var derr error + if !*payload.AlertingRuleEnabled { + derr = hr.managementClient.DropPlatformAlertRule(req.Context(), ruleId) + } else { + derr = hr.managementClient.RestorePlatformAlertRule(req.Context(), ruleId) + } + if derr != nil { + status, message := parseError(derr) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: status, + Message: message, + }) + return + } + if payload.AlertingRule == nil && payload.Classification == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: http.StatusNoContent, + }) + return + } + } + + if payload.Classification != nil { + update := management.UpdateRuleClassificationRequest{RuleId: ruleId} + if payload.Classification.ComponentSet { + update.Component = payload.Classification.Component + update.ComponentSet = true + } + if payload.Classification.LayerSet { + update.Layer = payload.Classification.Layer + update.LayerSet = true + } + if payload.Classification.ComponentFromSet { + update.ComponentFrom = payload.Classification.ComponentFrom + update.ComponentFromSet = true + } + if payload.Classification.LayerFromSet { + update.LayerFrom = payload.Classification.LayerFrom + update.LayerFromSet = true + } + if err := hr.managementClient.UpdateAlertRuleClassification(req.Context(), update); err != nil { + status, message := parseError(err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: status, + Message: message, + }) + return + } + + // If this is a classification-only patch, return success now. + if payload.AlertingRule == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: http.StatusNoContent, + }) + return + } + } + + alertRule := *payload.AlertingRule + + err = hr.managementClient.UpdatePlatformAlertRule(req.Context(), ruleId, alertRule) + if err != nil { + var ve *management.ValidationError + var nf *management.NotFoundError + if errors.As(err, &ve) || errors.As(err, &nf) { + status, message := parseError(err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: status, + Message: message, + }) + return + } + + var na *management.NotAllowedError + if errors.As(err, &na) && strings.Contains(na.Error(), "cannot update non-platform alert rule") { + // Not a platform rule, try user-defined update + newRuleId, err := hr.managementClient.UpdateUserDefinedAlertRule(req.Context(), ruleId, alertRule) + if err != nil { + status, message := parseError(err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: status, + Message: message, + }) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: newRuleId, + StatusCode: http.StatusNoContent, + }) + return + } + + status, message := parseError(err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: status, + Message: message, + }) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(UpdateAlertRuleResponse{ + Id: ruleId, + StatusCode: http.StatusNoContent, + }) +} diff --git a/internal/managementrouter/alert_rule_update_test.go b/internal/managementrouter/alert_rule_update_test.go new file mode 100644 index 000000000..e6d208e4b --- /dev/null +++ b/internal/managementrouter/alert_rule_update_test.go @@ -0,0 +1,367 @@ +package managementrouter_test + +import ( + "bytes" + "context" + "encoding/json" + "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" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/openshift/monitoring-plugin/internal/managementrouter" + alertrule "github.com/openshift/monitoring-plugin/pkg/alert_rule" + "github.com/openshift/monitoring-plugin/pkg/k8s" + "github.com/openshift/monitoring-plugin/pkg/management" + "github.com/openshift/monitoring-plugin/pkg/management/testutils" +) + +var _ = Describe("UpdateAlertRule", func() { + var ( + router http.Handler + mockK8sRules *testutils.MockPrometheusRuleInterface + mockK8s *testutils.MockClient + mockRelabeledRules *testutils.MockRelabeledRulesInterface + ) + + var ( + originalUserRule = monitoringv1.Rule{ + Alert: "user-alert", + Expr: intstr.FromString("up == 0"), + Labels: map[string]string{ + "severity": "warning", + }, + } + userRuleId = alertrule.GetAlertingRuleId(&originalUserRule) + + platformRule = monitoringv1.Rule{Alert: "platform-alert", Expr: intstr.FromString("cpu > 80"), Labels: map[string]string{"severity": "critical"}} + platformRuleId = alertrule.GetAlertingRuleId(&platformRule) + ) + + BeforeEach(func() { + mockK8sRules = &testutils.MockPrometheusRuleInterface{} + + userPR := monitoringv1.PrometheusRule{} + userPR.Name = "user-pr" + userPR.Namespace = "default" + userPR.Spec.Groups = []monitoringv1.RuleGroup{ + { + Name: "g1", + Rules: []monitoringv1.Rule{ + { + Alert: originalUserRule.Alert, + Expr: originalUserRule.Expr, + Labels: map[string]string{"severity": "warning", k8s.AlertRuleLabelId: userRuleId}, + }, + }, + }, + } + + platformPR := monitoringv1.PrometheusRule{} + platformPR.Name = "platform-pr" + platformPR.Namespace = "platform-namespace-1" + platformPR.Spec.Groups = []monitoringv1.RuleGroup{ + { + Name: "pg1", + Rules: []monitoringv1.Rule{ + { + Alert: "platform-alert", + Expr: intstr.FromString("cpu > 80"), + Labels: map[string]string{"severity": "critical"}, + }, + }, + }, + } + + mockK8sRules.SetPrometheusRules(map[string]*monitoringv1.PrometheusRule{ + "default/user-pr": &userPR, + "platform-namespace-1/platform-pr": &platformPR, + }) + + mockNamespace := &testutils.MockNamespaceInterface{ + IsClusterMonitoringNamespaceFunc: func(name string) bool { + return name == "platform-namespace-1" || name == "platform-namespace-2" + }, + } + + mockRelabeledRules = &testutils.MockRelabeledRulesInterface{ + GetFunc: func(ctx context.Context, id string) (monitoringv1.Rule, bool) { + if id == userRuleId { + return monitoringv1.Rule{ + Alert: "user-alert", + Expr: intstr.FromString("up == 0"), + Labels: map[string]string{ + "severity": "warning", + k8s.AlertRuleLabelId: userRuleId, + k8s.PrometheusRuleLabelNamespace: "default", + k8s.PrometheusRuleLabelName: "user-pr", + }, + }, true + } + if id == platformRuleId { + return monitoringv1.Rule{ + Alert: "platform-alert", + Expr: intstr.FromString("cpu > 80"), + Labels: map[string]string{ + "severity": "critical", + k8s.AlertRuleLabelId: platformRuleId, + k8s.PrometheusRuleLabelNamespace: "platform-namespace-1", + k8s.PrometheusRuleLabelName: "platform-pr", + }, + }, true + } + return monitoringv1.Rule{}, false + }, + } + + mockK8s = &testutils.MockClient{ + PrometheusRulesFunc: func() k8s.PrometheusRuleInterface { + return mockK8sRules + }, + NamespaceFunc: func() k8s.NamespaceInterface { + return mockNamespace + }, + RelabeledRulesFunc: func() k8s.RelabeledRulesInterface { + return mockRelabeledRules + }, + } + + mgmt := management.New(context.Background(), mockK8s) + router = managementrouter.New(mgmt) + }) + + Context("when updating a user-defined alert rule", func() { + It("should successfully update the rule and return new ID", func() { + expectedNewId := alertrule.GetAlertingRuleId(&monitoringv1.Rule{ + Alert: "user-alert", + Expr: intstr.FromString("up == 1"), + Labels: map[string]string{ + "severity": "critical", + "team": "sre", + }, + }) + body := map[string]interface{}{ + "alertingRule": map[string]interface{}{ + "alert": "user-alert", + "expr": "up == 1", + "labels": map[string]string{ + "severity": "critical", + "team": "sre", + }, + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+userRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + + Expect(resp.Id).To(Equal(expectedNewId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + Expect(resp.Message).To(BeEmpty()) + }) + + It("should replace all labels without merging", func() { + expectedNewId := alertrule.GetAlertingRuleId(&monitoringv1.Rule{ + Alert: "user-alert", + Expr: intstr.FromString("up == 0"), + Labels: map[string]string{ + "team": "sre", + }, + }) + body := map[string]interface{}{ + "alertingRule": map[string]interface{}{ + "alert": "user-alert", + "expr": "up == 0", + "labels": map[string]string{ + "team": "sre", + }, + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+userRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + + Expect(resp.Id).To(Equal(expectedNewId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + }) + }) + + Context("when updating rule classification via PATCH /rules/{ruleId}", func() { + It("should update classification overrides with nested classification payload", func() { + body := map[string]any{ + "classification": map[string]any{ + "openshift_io_alert_rule_component": "team-x", + "openshift_io_alert_rule_layer": "namespace", + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+userRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + Expect(resp.Id).To(Equal(userRuleId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + }) + }) + + Context("when updating a platform alert rule", func() { + It("should successfully update labels via AlertRelabelConfig", func() { + mockARC := &testutils.MockAlertRelabelConfigInterface{} + mockK8s.AlertRelabelConfigsFunc = func() k8s.AlertRelabelConfigInterface { + return mockARC + } + + body := map[string]interface{}{ + "alertingRule": map[string]interface{}{ + "alert": "platform-alert", + "expr": "cpu > 80", + "labels": map[string]string{ + "severity": "warning", + }, + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+platformRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + Expect(resp.Id).To(Equal(platformRuleId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + Expect(resp.Message).To(BeEmpty()) + }) + }) + + Context("when ruleId is missing", func() { + It("should return 400", func() { + body := map[string]interface{}{ + "alertingRule": map[string]interface{}{ + "alert": "test-alert", + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/%20", bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(w.Body.String()).To(ContainSubstring("missing ruleId")) + }) + }) + + Context("when request body is invalid", func() { + It("should return 400", func() { + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/user-alert", bytes.NewBufferString("{")) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(w.Body.String()).To(ContainSubstring("invalid request body")) + }) + }) + + Context("enabled toggle for platform alerts", func() { + It("should drop (AlertingRuleEnabled=false) and return 204 envelope", func() { + mockARC := &testutils.MockAlertRelabelConfigInterface{} + mockK8s.AlertRelabelConfigsFunc = func() k8s.AlertRelabelConfigInterface { return mockARC } + + body := map[string]interface{}{"AlertingRuleEnabled": false} + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+platformRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + Expect(resp.Id).To(Equal(platformRuleId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + Expect(resp.Message).To(BeEmpty()) + }) + + It("should restore (AlertingRuleEnabled=true) and return 204 envelope", func() { + mockARC := &testutils.MockAlertRelabelConfigInterface{} + mockK8s.AlertRelabelConfigsFunc = func() k8s.AlertRelabelConfigInterface { return mockARC } + + body := map[string]interface{}{"AlertingRuleEnabled": true} + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+platformRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + Expect(resp.Id).To(Equal(platformRuleId)) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + Expect(resp.Message).To(BeEmpty()) + }) + }) + + Context("when alertingRule, AlertingRuleEnabled, and classification are missing", func() { + It("should return 400", func() { + body := map[string]interface{}{} + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/"+userRuleId, bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(w.Body.String()).To(ContainSubstring("either alertingRule, AlertingRuleEnabled, or classification is required")) + }) + }) + + Context("when rule is not found", func() { + It("should return JSON response with 404 status code", func() { + mockRelabeledRules.GetFunc = func(ctx context.Context, id string) (monitoringv1.Rule, bool) { + return monitoringv1.Rule{}, false + } + + mgmt := management.New(context.Background(), mockK8s) + router = managementrouter.New(mgmt) + + body := map[string]interface{}{ + "alertingRule": map[string]interface{}{ + "alert": "missing-alert", + }, + } + buf, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/api/v1/alerting/rules/rid_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", bytes.NewReader(buf)) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusOK)) + var resp managementrouter.UpdateAlertRuleResponse + Expect(json.NewDecoder(w.Body).Decode(&resp)).To(Succeed()) + Expect(resp.Id).To(Equal("rid_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) + Expect(resp.Message).To(ContainSubstring("not found")) + }) + }) +}) diff --git a/internal/managementrouter/router.go b/internal/managementrouter/router.go index d7581a980..f0def407b 100644 --- a/internal/managementrouter/router.go +++ b/internal/managementrouter/router.go @@ -31,6 +31,8 @@ func New(managementClient management.Client) *mux.Router { r.HandleFunc("/api/v1/alerting/rules", httpRouter.CreateAlertRule).Methods(http.MethodPost) r.HandleFunc("/api/v1/alerting/rules", httpRouter.BulkDeleteUserDefinedAlertRules).Methods(http.MethodDelete) r.HandleFunc("/api/v1/alerting/rules", httpRouter.BulkUpdateAlertRules).Methods(http.MethodPatch) + r.HandleFunc("/api/v1/alerting/rules/{ruleId}", httpRouter.DeleteUserDefinedAlertRuleById).Methods(http.MethodDelete) + r.HandleFunc("/api/v1/alerting/rules/{ruleId}", httpRouter.UpdateAlertRule).Methods(http.MethodPatch) return r } diff --git a/internal/managementrouter/user_defined_alert_rule_delete_by_id.go b/internal/managementrouter/user_defined_alert_rule_delete_by_id.go index 3f0930647..778f7f474 100644 --- a/internal/managementrouter/user_defined_alert_rule_delete_by_id.go +++ b/internal/managementrouter/user_defined_alert_rule_delete_by_id.go @@ -1,7 +1,26 @@ package managementrouter +import ( + "net/http" +) + type DeleteUserDefinedAlertRulesResponse struct { Id string `json:"id"` StatusCode int `json:"status_code"` Message string `json:"message,omitempty"` } + +func (hr *httpRouter) DeleteUserDefinedAlertRuleById(w http.ResponseWriter, req *http.Request) { + ruleId, err := getParam(req, "ruleId") + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + + if err := hr.managementClient.DeleteUserDefinedAlertRuleById(req.Context(), ruleId); err != nil { + handleError(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/managementrouter/user_defined_alert_rule_delete_by_id_test.go b/internal/managementrouter/user_defined_alert_rule_delete_by_id_test.go new file mode 100644 index 000000000..69f668581 --- /dev/null +++ b/internal/managementrouter/user_defined_alert_rule_delete_by_id_test.go @@ -0,0 +1,171 @@ +package managementrouter_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + osmv1 "github.com/openshift/api/monitoring/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openshift/monitoring-plugin/internal/managementrouter" + alertrule "github.com/openshift/monitoring-plugin/pkg/alert_rule" + "github.com/openshift/monitoring-plugin/pkg/k8s" + "github.com/openshift/monitoring-plugin/pkg/management" + "github.com/openshift/monitoring-plugin/pkg/management/testutils" +) + +var _ = Describe("DeleteUserDefinedAlertRuleById", func() { + var ( + router http.Handler + mockK8s *testutils.MockClient + ) + + var ( + userRule1Name = "u1" + userRule1 = monitoringv1.Rule{Alert: userRule1Name, Labels: map[string]string{k8s.PrometheusRuleLabelNamespace: "default", k8s.PrometheusRuleLabelName: "user-pr"}} + userRule1Id = alertrule.GetAlertingRuleId(&userRule1) + + userRule2Name = "u2" + userRule2 = monitoringv1.Rule{Alert: userRule2Name, Labels: map[string]string{k8s.PrometheusRuleLabelNamespace: "default", k8s.PrometheusRuleLabelName: "user-pr"}} + userRule2Id = alertrule.GetAlertingRuleId(&userRule2) + + platformRuleName = "p1" + platformRule = monitoringv1.Rule{Alert: platformRuleName, Labels: map[string]string{k8s.PrometheusRuleLabelNamespace: "platform-namespace-1", k8s.PrometheusRuleLabelName: "platform-pr"}} + platformRuleId = alertrule.GetAlertingRuleId(&platformRule) + ) + + BeforeEach(func() { + mockK8s = &testutils.MockClient{} + mgmt := management.New(context.Background(), mockK8s) + router = managementrouter.New(mgmt) + + mockK8s.PrometheusRulesFunc = func() k8s.PrometheusRuleInterface { + return &testutils.MockPrometheusRuleInterface{ + GetFunc: func(ctx context.Context, namespace string, name string) (*monitoringv1.PrometheusRule, bool, error) { + return &monitoringv1.PrometheusRule{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: monitoringv1.PrometheusRuleSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Rules: []monitoringv1.Rule{userRule1, userRule2, platformRule}, + }, + }, + }, + }, true, nil + }, + DeleteFunc: func(ctx context.Context, namespace string, name string) error { + return nil + }, + UpdateFunc: func(ctx context.Context, pr monitoringv1.PrometheusRule) error { + return nil + }, + } + } + + mockK8s.RelabeledRulesFunc = func() k8s.RelabeledRulesInterface { + return &testutils.MockRelabeledRulesInterface{ + GetFunc: func(ctx context.Context, id string) (monitoringv1.Rule, bool) { + switch id { + case userRule1Id: + return userRule1, true + case userRule2Id: + return userRule2, true + case platformRuleId: + return platformRule, true + default: + return monitoringv1.Rule{}, false + } + }, + } + } + + // Provide owning AlertingRule so platform (user-via-platform) deletion can succeed + mockK8s.AlertingRulesFunc = func() k8s.AlertingRuleInterface { + return &testutils.MockAlertingRuleInterface{ + GetFunc: func(ctx context.Context, name string) (*osmv1.AlertingRule, bool, error) { + if name == "platform-alert-rules" { + return &osmv1.AlertingRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: "platform-alert-rules", + Namespace: k8s.ClusterMonitoringNamespace, + }, + Spec: osmv1.AlertingRuleSpec{ + Groups: []osmv1.RuleGroup{ + { + Name: "test-group", + Rules: []osmv1.Rule{ + {Alert: platformRuleName}, + }, + }, + }, + }, + }, true, nil + } + return nil, false, nil + }, + UpdateFunc: func(ctx context.Context, ar osmv1.AlertingRule) error { + return nil + }, + } + } + + mockK8s.NamespaceFunc = func() k8s.NamespaceInterface { + return &testutils.MockNamespaceInterface{ + IsClusterMonitoringNamespaceFunc: func(name string) bool { + return strings.HasPrefix(name, "platform-namespace-") + }, + } + } + }) + + Context("when ruleId is missing or blank", func() { + It("returns 400 with missing ruleId message", func() { + req := httptest.NewRequest(http.MethodDelete, "/api/v1/alerting/rules/%20", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusBadRequest)) + Expect(w.Body.String()).To(ContainSubstring("missing ruleId")) + }) + }) + + Context("when rule is not found", func() { + It("returns 404 with expected message", func() { + req := httptest.NewRequest(http.MethodDelete, "/api/v1/alerting/rules/missing", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusNotFound)) + Expect(w.Body.String()).To(ContainSubstring("AlertRule with id missing not found")) + }) + }) + + Context("when deleting a user-defined rule", func() { + It("returns 204", func() { + req := httptest.NewRequest(http.MethodDelete, "/api/v1/alerting/rules/"+userRule1Id, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusNoContent)) + }) + }) + + Context("when deleting a platform rule", func() { + It("returns 204 for user-via-platform (not operator-managed)", func() { + req := httptest.NewRequest(http.MethodDelete, "/api/v1/alerting/rules/"+platformRuleId, nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + Expect(w.Code).To(Equal(http.StatusNoContent)) + Expect(w.Body.String()).To(BeEmpty()) + }) + }) +})