Skip to content
Open
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
139 changes: 139 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@ servers:

paths:
/rules:
patch:
operationId: BulkUpdateAlertRules
summary: Bulk update alert rules
description: >
Updates one or more alert rules by their stable IDs. Each rule is
updated independently; per-rule status is returned in the response
so partial success is visible to the caller.
Supports label overrides, drop/restore toggles (platform rules only),
and classification label updates.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/BulkUpdateAlertRulesRequest"
responses:
"200":
description: Update results (may include per-rule errors)
content:
application/json:
schema:
$ref: "#/components/schemas/BulkUpdateAlertRulesResponse"
"400":
description: Invalid request body
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
description: Missing or invalid authorization token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"413":
description: Request body exceeds the 1 MB limit
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"500":
description: Unexpected server error
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
delete:
operationId: BulkDeleteUserDefinedAlertRules
summary: Bulk delete user-defined alert rules
Expand Down Expand Up @@ -202,6 +248,9 @@ components:
description: The stable alert rule ID that was processed.
status_code:
type: integer
format: int32
minimum: 100
maximum: 599
description: HTTP status code for this rule's deletion result.
message:
type: string
Expand All @@ -218,6 +267,96 @@ components:
$ref: "#/components/schemas/DeleteAlertRuleResult"
description: Per-rule deletion results.

AlertRuleClassificationUpdate:
type: object
description: >
Partial update for alert rule classification labels.
Each field supports three states: omitted (leave unchanged),
null (clear the override), or a string value (set the override).
The three-state semantics require a custom JSON decoder; the Go
type AlertRuleClassificationPatch is used at runtime instead of
the generated struct.
x-go-type: AlertRuleClassificationPatch
properties:
openshift_io_alert_rule_component:
type: string
nullable: true
description: Component classification label override.
openshift_io_alert_rule_layer:
type: string
nullable: true
description: Layer classification label override.
openshift_io_alert_rule_component_from:
type: string
nullable: true
description: Dynamic component source label key.
openshift_io_alert_rule_layer_from:
type: string
nullable: true
description: Dynamic layer source label key.

BulkUpdateAlertRulesRequest:
type: object
required:
- ruleIds
properties:
ruleIds:
type: array
minItems: 1
items:
type: string
description: List of stable alert rule IDs to update.
labels:
type: object
additionalProperties:
type: string
nullable: true
description: >
Label key/value pairs to set. A null or empty-string value removes
the label. Omitting this field leaves existing labels unchanged.
alertingRuleEnabled:
type: boolean
nullable: true
description: >
When false, drops (silences) the platform alert rule.
When true, restores a previously dropped rule.
Not applicable to user-defined rules; if set on a user-defined
rule alongside other update fields (labels, classification) that
succeed, the toggle rejection is silently absorbed and the
overall per-rule result is still 204.
classification:
$ref: "#/components/schemas/AlertRuleClassificationUpdate"

UpdateAlertRuleResult:
type: object
required:
- id
- status_code
properties:
id:
type: string
description: The stable alert rule ID that was processed.
status_code:
type: integer
format: int32
minimum: 100
maximum: 599
description: HTTP status code for this rule's update result.
message:
type: string
description: Error message if update failed; omitted on success.

BulkUpdateAlertRulesResponse:
type: object
required:
- rules
properties:
rules:
type: array
items:
$ref: "#/components/schemas/UpdateAlertRuleResult"
description: Per-rule update results.

ErrorResponse:
type: object
required:
Expand Down
218 changes: 218 additions & 0 deletions internal/managementrouter/alert_rule_bulk_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package managementrouter

import (
"encoding/json"
"errors"
"io"
"net/http"

monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"

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

func (hr *httpRouter) BulkUpdateAlertRules(w http.ResponseWriter, req *http.Request) {
req.Body = http.MaxBytesReader(w, req.Body, maxRequestBodyBytes)

body, err := io.ReadAll(req.Body)
if err != nil {
writeError(w, http.StatusRequestEntityTooLarge, "request body too large")
return
}

// BulkUpdateAlertRulesRequest.Classification is typed as
// *AlertRuleClassificationPatch (via x-go-type in the spec), so the
// three-state omitted/null/string semantics are preserved on decode.
var payload BulkUpdateAlertRulesRequest
if err := json.Unmarshal(body, &payload); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}

if len(payload.RuleIds) == 0 {
writeError(w, http.StatusBadRequest, "ruleIds is required")
return
}

if payload.AlertingRuleEnabled == nil && payload.Labels == nil && payload.Classification == nil {
writeError(w, http.StatusBadRequest, "alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required")
return
}

var haveToggle bool
var enabled bool
if payload.AlertingRuleEnabled != nil {
enabled = *payload.AlertingRuleEnabled
haveToggle = true
}

results := make([]UpdateAlertRuleResult, 0, len(payload.RuleIds))

for _, rawId := range payload.RuleIds {
id, err := parseParam(rawId, "ruleId")
if err != nil {
msg := err.Error()
results = append(results, UpdateAlertRuleResult{
Id: rawId,
StatusCode: int32(http.StatusBadRequest),
Message: &msg,
})
continue
}

notAllowedEnabled := false
if haveToggle {
var derr error
if !enabled {
derr = hr.managementClient.DropPlatformAlertRule(req.Context(), id)
} else {
derr = hr.managementClient.RestorePlatformAlertRule(req.Context(), id)
}
if derr != nil {
var na *management.NotAllowedError
if errors.As(derr, &na) {
notAllowedEnabled = true
} else {
status, message := parseError(derr)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}
}
}

if payload.Classification != nil {
cl := payload.Classification
update := management.UpdateRuleClassificationRequest{RuleId: id}
if cl.ComponentSet {
update.Component = cl.Component
update.ComponentSet = true
}
if cl.LayerSet {
update.Layer = cl.Layer
update.LayerSet = true
}
if cl.ComponentFromSet {
update.ComponentFrom = cl.ComponentFrom
update.ComponentFromSet = true
}
if cl.LayerFromSet {
update.LayerFrom = cl.LayerFrom
update.LayerFromSet = true
}

if update.ComponentSet || update.LayerSet || update.ComponentFromSet || update.LayerFromSet {
if err := hr.managementClient.UpdateAlertRuleClassification(req.Context(), update); err != nil {
status, message := parseError(err)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}
}
}

if payload.Labels != nil {
currentRule, err := hr.managementClient.GetRuleById(req.Context(), id)
if err != nil {
status, message := parseError(err)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}

// platformLabels uses "" to signal "drop this label"; the management
// layer's UpdatePlatformAlertRule interprets "" as a delete directive.
// userLabels is the fully-merged map for user-defined rules where we
// simply omit deleted keys rather than set them to "".
platformLabels := make(map[string]string)
userLabels := make(map[string]string)
for k, v := range currentRule.Labels {
userLabels[k] = v
}
for k, pv := range *payload.Labels {
if pv == nil || *pv == "" {
platformLabels[k] = ""
delete(userLabels, k)
} else {
platformLabels[k] = *pv
userLabels[k] = *pv
}
}

updatedPlatformRule := monitoringv1.Rule{Labels: platformLabels}

err = hr.managementClient.UpdatePlatformAlertRule(req.Context(), id, updatedPlatformRule)
if err != nil {
var ve *management.ValidationError
var nf *management.NotFoundError
if errors.As(err, &ve) || errors.As(err, &nf) {
status, message := parseError(err)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}

var na *management.NotAllowedError
if errors.As(err, &na) {
updatedUserRule := currentRule
updatedUserRule.Labels = userLabels

newRuleId, err := hr.managementClient.UpdateUserDefinedAlertRule(req.Context(), id, updatedUserRule)
if err != nil {
status, message := parseError(err)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}
results = append(results, UpdateAlertRuleResult{
Id: newRuleId,
StatusCode: int32(http.StatusNoContent),
})
continue
}

status, message := parseError(err)
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(status),
Message: &message,
})
continue
}
}

if notAllowedEnabled && payload.Labels == nil && payload.Classification == nil {
results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(http.StatusMethodNotAllowed),
})
continue
}

results = append(results, UpdateAlertRuleResult{
Id: id,
StatusCode: int32(http.StatusNoContent),
})
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(BulkUpdateAlertRulesResponse{Rules: results}); err != nil {
log.WithError(err).Warn("failed to encode bulk update response")
}
}
Loading