Skip to content

Commit 022444d

Browse files
sradcoAI Assistantcursoragent
committed
management: add update alert rule APIs
Add PATCH /api/v1/alerting/rules for bulk update of platform and user-defined alert rules with drop/restore, label overrides, and per-rule update support. Refactor: introduce UpdateAlertRuleLabels unified method that routes internally to platform (ARC) or user-defined (PR mutation) paths, replacing the error-sniffing fallback pattern in the HTTP handler. Extend drop/restore to user-defined rules via ARC (when ENABLE_USER_WORKLOAD_ARCS is enabled). Rename DropPlatformAlertRule and RestorePlatformAlertRule to DropAlertRule and RestoreAlertRule. Add validateDropRestorePreconditions that checks the PrometheusRule and AlertingRule CR for GitOps management while still allowing drops on operator-managed rules (their whole purpose). Reject requests that combine alertingRuleEnabled with labels or classification — these are mutually exclusive operations to prevent partial-apply inconsistencies. Fix user label updates to read source labels from the PrometheusRule directly instead of the relabeled cache, preventing ARC overlay values from being baked into the PR source. Fixes: - Propagate errors from cleanupARCForDeletedRule instead of swallowing them (nilerr lint) - Preserve rule Drops when clearing the last label override (prevent silent restore) - Validate ARC ownership on fallback restore path (block GitOps-managed ARC modification) - Return errors from findARCByAlertRuleID instead of silently continuing - Use t.Fatalf in get_rule_by_id_test to prevent nil deref on type assertion failure - Use httptest.NewRequestWithContext (noctx) - Add ErrSkip handling in e2e tests matching repo convention - Block user-rule edits when parent PrometheusRule is GitOps-managed - Allow ARC fallback for operator-managed rules (the ARC is a separate resource not reconciled by the operator) Signed-off-by: Shirly Radco <sradco@redhat.com> Signed-off-by: João Vilaça <jvilaca@redhat.com> Signed-off-by: Aviv Litman <alitman@redhat.com> Co-authored-by: AI Assistant <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e1ee423 commit 022444d

19 files changed

Lines changed: 3848 additions & 27 deletions

api/openapi.yaml

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,52 @@ servers:
1212

1313
paths:
1414
/rules:
15+
patch:
16+
operationId: BulkUpdateAlertRules
17+
summary: Bulk update alert rules
18+
description: >
19+
Updates one or more alert rules by their stable IDs. Each rule is
20+
updated independently; per-rule status is returned in the response
21+
so partial success is visible to the caller.
22+
Supports label overrides, drop/restore toggles (platform rules only),
23+
and classification label updates.
24+
requestBody:
25+
required: true
26+
content:
27+
application/json:
28+
schema:
29+
$ref: "#/components/schemas/BulkUpdateAlertRulesRequest"
30+
responses:
31+
"200":
32+
description: Update results (may include per-rule errors)
33+
content:
34+
application/json:
35+
schema:
36+
$ref: "#/components/schemas/BulkUpdateAlertRulesResponse"
37+
"400":
38+
description: Invalid request body
39+
content:
40+
application/json:
41+
schema:
42+
$ref: "#/components/schemas/ErrorResponse"
43+
"401":
44+
description: Missing or invalid authorization token
45+
content:
46+
application/json:
47+
schema:
48+
$ref: "#/components/schemas/ErrorResponse"
49+
"413":
50+
description: Request body exceeds the 1 MB limit
51+
content:
52+
application/json:
53+
schema:
54+
$ref: "#/components/schemas/ErrorResponse"
55+
"500":
56+
description: Unexpected server error
57+
content:
58+
application/json:
59+
schema:
60+
$ref: "#/components/schemas/ErrorResponse"
1561
delete:
1662
operationId: BulkDeleteUserDefinedAlertRules
1763
summary: Bulk delete user-defined alert rules
@@ -203,6 +249,9 @@ components:
203249
description: The stable alert rule ID that was processed.
204250
statusCode:
205251
type: integer
252+
format: int32
253+
minimum: 100
254+
maximum: 599
206255
description: HTTP status code for this rule's deletion result.
207256
message:
208257
type: string
@@ -219,6 +268,96 @@ components:
219268
$ref: "#/components/schemas/DeleteAlertRuleResult"
220269
description: Per-rule deletion results.
221270

271+
AlertRuleClassificationUpdate:
272+
type: object
273+
description: >
274+
Partial update for alert rule classification labels.
275+
Each field supports three states: omitted (leave unchanged),
276+
null (clear the override), or a string value (set the override).
277+
The three-state semantics require a custom JSON decoder; the Go
278+
type AlertRuleClassificationPatch is used at runtime instead of
279+
the generated struct.
280+
x-go-type: AlertRuleClassificationPatch
281+
properties:
282+
openshift_io_alert_rule_component:
283+
type: string
284+
nullable: true
285+
description: Component classification label override.
286+
openshift_io_alert_rule_layer:
287+
type: string
288+
nullable: true
289+
description: Layer classification label override.
290+
openshift_io_alert_rule_component_from:
291+
type: string
292+
nullable: true
293+
description: Dynamic component source label key.
294+
openshift_io_alert_rule_layer_from:
295+
type: string
296+
nullable: true
297+
description: Dynamic layer source label key.
298+
299+
BulkUpdateAlertRulesRequest:
300+
type: object
301+
required:
302+
- ruleIds
303+
properties:
304+
ruleIds:
305+
type: array
306+
minItems: 1
307+
maxItems: 100
308+
items:
309+
type: string
310+
description: List of stable alert rule IDs to update (at most 100 per request).
311+
labels:
312+
type: object
313+
additionalProperties:
314+
type: string
315+
nullable: true
316+
description: >
317+
Label key/value pairs to set. A null or empty-string value removes
318+
the label. Omitting this field leaves existing labels unchanged.
319+
alertingRuleEnabled:
320+
type: boolean
321+
nullable: true
322+
description: >
323+
When false, drops the alert rule via an AlertRelabelConfig Drop
324+
action — the rule no longer appears in Prometheus query results.
325+
When true, restores a previously dropped rule.
326+
Works for both platform and user-defined rules (user-defined
327+
requires ENABLE_USER_WORKLOAD_ARCS).
328+
classification:
329+
$ref: "#/components/schemas/AlertRuleClassificationUpdate"
330+
331+
UpdateAlertRuleResult:
332+
type: object
333+
required:
334+
- id
335+
- statusCode
336+
properties:
337+
id:
338+
type: string
339+
description: The stable alert rule ID that was processed.
340+
statusCode:
341+
type: integer
342+
format: int32
343+
minimum: 100
344+
maximum: 599
345+
description: HTTP status code for this rule's update result.
346+
message:
347+
type: string
348+
description: Error message if update failed; omitted on success.
349+
350+
BulkUpdateAlertRulesResponse:
351+
type: object
352+
required:
353+
- rules
354+
properties:
355+
rules:
356+
type: array
357+
items:
358+
$ref: "#/components/schemas/UpdateAlertRuleResult"
359+
description: Per-rule update results.
360+
222361
ErrorResponse:
223362
type: object
224363
required:
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package managementrouter
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"strings"
8+
9+
"github.com/openshift/monitoring-plugin/pkg/management"
10+
)
11+
12+
func (hr *httpRouter) BulkUpdateAlertRules(w http.ResponseWriter, req *http.Request) {
13+
req.Body = http.MaxBytesReader(w, req.Body, maxRequestBodyBytes)
14+
15+
body, err := io.ReadAll(req.Body)
16+
if err != nil {
17+
writeError(w, http.StatusRequestEntityTooLarge, "request body too large")
18+
return
19+
}
20+
21+
// BulkUpdateAlertRulesRequest.Classification is typed as
22+
// *AlertRuleClassificationPatch (via x-go-type in the spec), so the
23+
// three-state omitted/null/string semantics are preserved on decode.
24+
var payload BulkUpdateAlertRulesRequest
25+
if err := json.Unmarshal(body, &payload); err != nil {
26+
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
27+
return
28+
}
29+
30+
if len(payload.RuleIds) == 0 {
31+
writeError(w, http.StatusBadRequest, "ruleIds is required")
32+
return
33+
}
34+
if len(payload.RuleIds) > maxBulkUpdateRuleIds {
35+
writeError(w, http.StatusBadRequest, "ruleIds exceeds maximum of 100")
36+
return
37+
}
38+
39+
if payload.AlertingRuleEnabled == nil && payload.Labels == nil && payload.Classification == nil {
40+
writeError(w, http.StatusBadRequest, "one of alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required")
41+
return
42+
}
43+
if payload.AlertingRuleEnabled != nil && (payload.Labels != nil || payload.Classification != nil) {
44+
writeError(w, http.StatusBadRequest, "alertingRuleEnabled cannot be combined with labels or classification in the same request")
45+
return
46+
}
47+
48+
var haveToggle bool
49+
var enabled bool
50+
if payload.AlertingRuleEnabled != nil {
51+
enabled = *payload.AlertingRuleEnabled
52+
haveToggle = true
53+
}
54+
55+
results := make([]UpdateAlertRuleResult, 0, len(payload.RuleIds))
56+
57+
for _, rawId := range payload.RuleIds {
58+
id := strings.TrimSpace(rawId)
59+
if id == "" {
60+
msg := "ruleId is empty or whitespace-only"
61+
results = append(results, UpdateAlertRuleResult{
62+
Id: rawId,
63+
StatusCode: int32(http.StatusBadRequest),
64+
Message: &msg,
65+
})
66+
continue
67+
}
68+
69+
if haveToggle {
70+
var err error
71+
if !enabled {
72+
err = hr.managementClient.DropAlertRule(req.Context(), id)
73+
} else {
74+
err = hr.managementClient.RestoreAlertRule(req.Context(), id)
75+
}
76+
if err != nil {
77+
status, message := parseError(err)
78+
results = append(results, UpdateAlertRuleResult{
79+
Id: id,
80+
StatusCode: int32(status),
81+
Message: &message,
82+
})
83+
continue
84+
}
85+
results = append(results, UpdateAlertRuleResult{
86+
Id: id,
87+
StatusCode: int32(http.StatusNoContent),
88+
})
89+
continue
90+
}
91+
92+
if payload.Classification != nil {
93+
cl := payload.Classification
94+
update := management.UpdateRuleClassificationRequest{RuleId: id}
95+
if cl.ComponentSet {
96+
update.Component = cl.Component
97+
update.ComponentSet = true
98+
}
99+
if cl.LayerSet {
100+
update.Layer = cl.Layer
101+
update.LayerSet = true
102+
}
103+
if cl.ComponentFromSet {
104+
update.ComponentFrom = cl.ComponentFrom
105+
update.ComponentFromSet = true
106+
}
107+
if cl.LayerFromSet {
108+
update.LayerFrom = cl.LayerFrom
109+
update.LayerFromSet = true
110+
}
111+
112+
if update.ComponentSet || update.LayerSet || update.ComponentFromSet || update.LayerFromSet {
113+
if err := hr.managementClient.UpdateAlertRuleClassification(req.Context(), update); err != nil {
114+
status, message := parseError(err)
115+
results = append(results, UpdateAlertRuleResult{
116+
Id: id,
117+
StatusCode: int32(status),
118+
Message: &message,
119+
})
120+
continue
121+
}
122+
}
123+
}
124+
125+
if payload.Labels != nil {
126+
newRuleId, err := hr.managementClient.UpdateAlertRuleLabels(req.Context(), id, *payload.Labels)
127+
if err != nil {
128+
status, message := parseError(err)
129+
results = append(results, UpdateAlertRuleResult{
130+
Id: id,
131+
StatusCode: int32(status),
132+
Message: &message,
133+
})
134+
continue
135+
}
136+
results = append(results, UpdateAlertRuleResult{
137+
Id: newRuleId,
138+
StatusCode: int32(http.StatusNoContent),
139+
})
140+
continue
141+
}
142+
143+
results = append(results, UpdateAlertRuleResult{
144+
Id: id,
145+
StatusCode: int32(http.StatusNoContent),
146+
})
147+
}
148+
149+
w.Header().Set("Content-Type", "application/json")
150+
w.WriteHeader(http.StatusOK)
151+
if err := json.NewEncoder(w).Encode(BulkUpdateAlertRulesResponse{Rules: results}); err != nil {
152+
log.WithError(err).Warn("failed to encode bulk update response")
153+
}
154+
}

0 commit comments

Comments
 (0)