Skip to content

Commit 9997bc6

Browse files
sradcoAI Assistant
andcommitted
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. 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>
1 parent 826838d commit 9997bc6

17 files changed

Lines changed: 3659 additions & 11 deletions

api/openapi.yaml

Lines changed: 140 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,97 @@ 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 (silences) the platform alert rule.
324+
When true, restores a previously dropped rule.
325+
Not applicable to user-defined rules; if set on a user-defined
326+
rule alongside other update fields (labels, classification) that
327+
succeed, the toggle rejection is silently absorbed and the
328+
overall per-rule result is still 204.
329+
classification:
330+
$ref: "#/components/schemas/AlertRuleClassificationUpdate"
331+
332+
UpdateAlertRuleResult:
333+
type: object
334+
required:
335+
- id
336+
- statusCode
337+
properties:
338+
id:
339+
type: string
340+
description: The stable alert rule ID that was processed.
341+
statusCode:
342+
type: integer
343+
format: int32
344+
minimum: 100
345+
maximum: 599
346+
description: HTTP status code for this rule's update result.
347+
message:
348+
type: string
349+
description: Error message if update failed; omitted on success.
350+
351+
BulkUpdateAlertRulesResponse:
352+
type: object
353+
required:
354+
- rules
355+
properties:
356+
rules:
357+
type: array
358+
items:
359+
$ref: "#/components/schemas/UpdateAlertRuleResult"
360+
description: Per-rule update results.
361+
222362
ErrorResponse:
223363
type: object
224364
required:
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package managementrouter
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"io"
7+
"net/http"
8+
9+
monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
10+
11+
"github.com/openshift/monitoring-plugin/pkg/management"
12+
)
13+
14+
func (hr *httpRouter) BulkUpdateAlertRules(w http.ResponseWriter, req *http.Request) {
15+
req.Body = http.MaxBytesReader(w, req.Body, maxRequestBodyBytes)
16+
17+
body, err := io.ReadAll(req.Body)
18+
if err != nil {
19+
writeError(w, http.StatusRequestEntityTooLarge, "request body too large")
20+
return
21+
}
22+
23+
// BulkUpdateAlertRulesRequest.Classification is typed as
24+
// *AlertRuleClassificationPatch (via x-go-type in the spec), so the
25+
// three-state omitted/null/string semantics are preserved on decode.
26+
var payload BulkUpdateAlertRulesRequest
27+
if err := json.Unmarshal(body, &payload); err != nil {
28+
writeError(w, http.StatusBadRequest, "invalid request body")
29+
return
30+
}
31+
32+
if len(payload.RuleIds) == 0 {
33+
writeError(w, http.StatusBadRequest, "ruleIds is required")
34+
return
35+
}
36+
if len(payload.RuleIds) > maxBulkUpdateRuleIds {
37+
writeError(w, http.StatusBadRequest, "ruleIds exceeds maximum of 100")
38+
return
39+
}
40+
41+
if payload.AlertingRuleEnabled == nil && payload.Labels == nil && payload.Classification == nil {
42+
writeError(w, http.StatusBadRequest, "alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required")
43+
return
44+
}
45+
46+
var haveToggle bool
47+
var enabled bool
48+
if payload.AlertingRuleEnabled != nil {
49+
enabled = *payload.AlertingRuleEnabled
50+
haveToggle = true
51+
}
52+
53+
results := make([]UpdateAlertRuleResult, 0, len(payload.RuleIds))
54+
55+
for _, rawId := range payload.RuleIds {
56+
id, err := parseParam(rawId, "ruleId")
57+
if err != nil {
58+
msg := err.Error()
59+
results = append(results, UpdateAlertRuleResult{
60+
Id: rawId,
61+
StatusCode: int32(http.StatusBadRequest),
62+
Message: &msg,
63+
})
64+
continue
65+
}
66+
67+
notAllowedEnabled := false
68+
if haveToggle {
69+
var derr error
70+
if !enabled {
71+
derr = hr.managementClient.DropPlatformAlertRule(req.Context(), id)
72+
} else {
73+
derr = hr.managementClient.RestorePlatformAlertRule(req.Context(), id)
74+
}
75+
if derr != nil {
76+
var na *management.NotAllowedError
77+
if errors.As(derr, &na) {
78+
notAllowedEnabled = true
79+
} else {
80+
status, message := parseError(derr)
81+
results = append(results, UpdateAlertRuleResult{
82+
Id: id,
83+
StatusCode: int32(status),
84+
Message: &message,
85+
})
86+
continue
87+
}
88+
}
89+
}
90+
91+
if payload.Classification != nil {
92+
cl := payload.Classification
93+
update := management.UpdateRuleClassificationRequest{RuleId: id}
94+
if cl.ComponentSet {
95+
update.Component = cl.Component
96+
update.ComponentSet = true
97+
}
98+
if cl.LayerSet {
99+
update.Layer = cl.Layer
100+
update.LayerSet = true
101+
}
102+
if cl.ComponentFromSet {
103+
update.ComponentFrom = cl.ComponentFrom
104+
update.ComponentFromSet = true
105+
}
106+
if cl.LayerFromSet {
107+
update.LayerFrom = cl.LayerFrom
108+
update.LayerFromSet = true
109+
}
110+
111+
if update.ComponentSet || update.LayerSet || update.ComponentFromSet || update.LayerFromSet {
112+
if err := hr.managementClient.UpdateAlertRuleClassification(req.Context(), update); err != nil {
113+
status, message := parseError(err)
114+
results = append(results, UpdateAlertRuleResult{
115+
Id: id,
116+
StatusCode: int32(status),
117+
Message: &message,
118+
})
119+
continue
120+
}
121+
}
122+
}
123+
124+
if payload.Labels != nil {
125+
currentRule, err := hr.managementClient.GetRuleById(req.Context(), id)
126+
if err != nil {
127+
status, message := parseError(err)
128+
results = append(results, UpdateAlertRuleResult{
129+
Id: id,
130+
StatusCode: int32(status),
131+
Message: &message,
132+
})
133+
continue
134+
}
135+
136+
// platformLabels uses "" to signal "drop this label"; the management
137+
// layer's UpdatePlatformAlertRule interprets "" as a delete directive.
138+
// userLabels is the fully-merged map for user-defined rules where we
139+
// simply omit deleted keys rather than set them to "".
140+
platformLabels := make(map[string]string)
141+
userLabels := make(map[string]string)
142+
for k, v := range currentRule.Labels {
143+
userLabels[k] = v
144+
}
145+
for k, pv := range *payload.Labels {
146+
if pv == nil || *pv == "" {
147+
platformLabels[k] = ""
148+
delete(userLabels, k)
149+
} else {
150+
platformLabels[k] = *pv
151+
userLabels[k] = *pv
152+
}
153+
}
154+
155+
updatedPlatformRule := monitoringv1.Rule{Labels: platformLabels}
156+
157+
err = hr.managementClient.UpdatePlatformAlertRule(req.Context(), id, updatedPlatformRule)
158+
if err != nil {
159+
var ve *management.ValidationError
160+
var nf *management.NotFoundError
161+
if errors.As(err, &ve) || errors.As(err, &nf) {
162+
status, message := parseError(err)
163+
results = append(results, UpdateAlertRuleResult{
164+
Id: id,
165+
StatusCode: int32(status),
166+
Message: &message,
167+
})
168+
continue
169+
}
170+
171+
var na *management.NotAllowedError
172+
if errors.As(err, &na) {
173+
updatedUserRule := currentRule
174+
updatedUserRule.Labels = userLabels
175+
176+
newRuleId, err := hr.managementClient.UpdateUserDefinedAlertRule(req.Context(), id, updatedUserRule)
177+
if err != nil {
178+
status, message := parseError(err)
179+
results = append(results, UpdateAlertRuleResult{
180+
Id: id,
181+
StatusCode: int32(status),
182+
Message: &message,
183+
})
184+
continue
185+
}
186+
results = append(results, UpdateAlertRuleResult{
187+
Id: newRuleId,
188+
StatusCode: int32(http.StatusNoContent),
189+
})
190+
continue
191+
}
192+
193+
status, message := parseError(err)
194+
results = append(results, UpdateAlertRuleResult{
195+
Id: id,
196+
StatusCode: int32(status),
197+
Message: &message,
198+
})
199+
continue
200+
}
201+
}
202+
203+
if notAllowedEnabled && payload.Labels == nil && payload.Classification == nil {
204+
results = append(results, UpdateAlertRuleResult{
205+
Id: id,
206+
StatusCode: int32(http.StatusMethodNotAllowed),
207+
})
208+
continue
209+
}
210+
211+
results = append(results, UpdateAlertRuleResult{
212+
Id: id,
213+
StatusCode: int32(http.StatusNoContent),
214+
})
215+
}
216+
217+
w.Header().Set("Content-Type", "application/json")
218+
w.WriteHeader(http.StatusOK)
219+
if err := json.NewEncoder(w).Encode(BulkUpdateAlertRulesResponse{Rules: results}); err != nil {
220+
log.WithError(err).Warn("failed to encode bulk update response")
221+
}
222+
}

0 commit comments

Comments
 (0)