Skip to content

management: add single alert rule endpoints - #1121

Open
sradco wants to merge 5 commits into
openshift:main-alerts-management-apifrom
sradco:alert-mgmt-single-rule-endpoints
Open

management: add single alert rule endpoints#1121
sradco wants to merge 5 commits into
openshift:main-alerts-management-apifrom
sradco:alert-mgmt-single-rule-endpoints

Conversation

@sradco

@sradco sradco commented Aug 12, 2026

Copy link
Copy Markdown

Summary

Depends on #1047 (and transitively #1066).

Adds single-rule HTTP endpoints while keeping the existing bulk APIs:

  • PATCH /api/v1/alerting/rules/{ruleId} — labels, drop/restore, classification
  • DELETE /api/v1/alerting/rules/{ruleId} — delete one rule

Single update reuses the same validation/mutation path as BulkUpdateAlertRules (addresses prior review ask for one unified update path). Errors return ErrorResponse with an actionable message (same intent as bulk per-rule message).

Tests

  • Unit: single update/delete happy paths + 400/401/404/405/413 edge cases
  • E2E: single drop/restore, classification, single delete
  • E2E RBAC: anonymous / namespace-scoped / cluster-admin for single PATCH and DELETE (Simon feedback parity with create/delete/update bulk)

Docs

  • docs/alert-management.md — single vs bulk API matrix and response semantics
  • docs/alert-rule-classification.md — single PATCH response/error notes aligned with implementation

Test plan

Keep the bulk PATCH/DELETE /rules APIs and add
per-rule endpoints for easier client use and
reviewability:

  • PATCH /rules/{ruleId}
  • DELETE /rules/{ruleId}

Single update shares validation and mutation
logic with BulkUpdateAlertRules. Adds unit and
e2e coverage (including RBAC) plus API docs.

Signed-off-by: Shirly Radco sradco@redhat.com
Co-authored-by: AI Assistant noreply@cursor.com

Summary by CodeRabbit

  • New Features
    • Added single-rule and bulk alert-rule update APIs.
    • Added alert-rule deletion support.
    • Updates can change labels, enablement, restoration status, and platform-rule settings.
    • Bulk operations return per-rule results and support partial-success reporting.
  • Bug Fixes
    • Improved authorization and error responses for alert-rule operations.
    • Prevented unsupported edits to managed rules and user-defined classification updates.
  • Documentation
    • Documented endpoints, limits, errors, ownership restrictions, and supported workflows.

delete endpoints

Add e2e tests verifying that the management
API enforces Kubernetes RBAC for create and
delete alert rule operations. Three user
profiles are tested: anonymous (expects
403), namespace-scoped (succeeds in own
namespace, denied elsewhere), and
cluster-admin (succeeds everywhere).

Also fixes a critical bug in
newUserScopedClientsets: when the base
rest.Config uses client certificates
(common in CI kubeconfigs), CopyConfig
preserved them. Since Kubernetes
authenticates via client certs when both
certs and bearer token are present, user
RBAC was bypassed entirely. Use
AnonymousClientConfig to strip all auth
so the API server authenticates exclusively
via the user's bearer token.

Signed-off-by: Shirly Radco <sradco@redhat.com>
Co-authored-by: AI Assistant <noreply@cursor.com>
@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci
openshift-ci Bot requested review from PeterYurkovich and zhuje August 12, 2026 09:27
@openshift-ci

openshift-ci Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: sradco
Once this PR has been reviewed and has the lgtm label, please assign kyoto for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sradco
sradco force-pushed the alert-mgmt-single-rule-endpoints branch from df528cb to d71662e Compare August 12, 2026 09:32
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Walkthrough

Added bulk and single-rule update APIs and single-rule deletion. Implemented user-defined rule edits, platform label and drop/restore operations, ARC cleanup, ownership validation, Kubernetes authorization mapping, and end-to-end RBAC coverage.

Changes

Alert rule management

Layer / File(s) Summary
API contracts and HTTP handlers
api/openapi.yaml, internal/managementrouter/*
Added bulk and single-rule update schemas, PATCH routes, deletion handling, request validation, per-rule results, and authorization error mapping.
Management contracts and user-rule updates
pkg/management/types.go, pkg/management/get_rule_by_id.go, pkg/management/update_user_defined_alert_rule.go, pkg/management/update_alert_rule_labels.go, pkg/management/alert_rule_preconditions.go
Added rule lookup, label updates, user-defined rule updates, protected-label handling, ownership checks, stable-ID reporting, and rejection of user-defined classification updates.
Platform ARC lifecycle and deletion
pkg/management/update_platform_alert_rule.go, pkg/management/delete_user_defined_alert_rule_by_id.go, docs/alert-management.md, docs/alert-rule-classification.md
Added platform label, drop, restore, and deletion workflows. ARC stamping, preservation, cleanup, and GitOps restrictions are implemented. Documentation describes the supported management model.
Scoped authorization and end-to-end validation
pkg/k8s/user_scoped_client.go, test/e2e/framework/*, test/e2e/*alert_rule_test.go, test/e2e/helpers_test.go
Added sanitized user-scoped clients, ServiceAccount helpers, RBAC tests, update and deletion integration tests, and ARC lifecycle assertions.

Estimated code review effort: 5 (Critical) | ~90+ minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ManagementRouter
  participant ManagementClient
  participant Kubernetes
  Client->>ManagementRouter: Submit alert-rule update
  ManagementRouter->>ManagementClient: Validate and apply update
  ManagementClient->>Kubernetes: Update PrometheusRule or AlertRelabelConfig
  Kubernetes-->>ManagementClient: Return resource result
  ManagementClient-->>ManagementRouter: Return status and effective rule ID
  ManagementRouter-->>Client: Return JSON response
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
No-Sensitive-Data-In-Logs ❌ Error Added production logs expose customer-controlled alert names and PrometheusRule namespace/name in pkg/k8s/relabeled_rules.go at Info/Warn levels. Remove alert and resource values from logs, or emit only redacted/hash identifiers and aggregate counts.
Docstring Coverage ⚠️ Warning Docstring coverage is 29.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change by identifying the addition of single alert-rule management endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed The changed tests use Go testing functions and static t.Run case names; no Ginkgo API or dynamic test titles appear in the PR diff.
Test Structure And Quality ✅ Passed The PR contains standard testing.T tests, not Ginkgo tests; no It, BeforeEach, AfterEach, Eventually, or Consistently constructs exist, so the Ginkgo-specific check is inapplicable.
Microshift Test Compatibility ✅ Passed The PR adds standard Go Test functions in single_alert_rule_test.go, not Ginkgo Describe/Context/It tests; the MicroShift-specific check is therefore inapplicable.
Single Node Openshift (Sno) Test Compatibility ✅ Passed Added e2e tests use Go's testing package, not Ginkgo, and contain no multi-node, HA, scheduling, topology, or node-failure assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed The full branch diff changes API, documentation, management, and test files only; no deployment/controller manifests or topology-sensitive scheduling fields are introduced.
Ote Binary Stdout Contract ✅ Passed The PR adds no main/init/TestMain or suite setup code and no direct stdout or klog writes; logrus logging uses its stderr default, while JSON encoding targets HTTP responses.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed Added e2e tests use standard testing.T, not Ginkgo; searches found no hardcoded IPv4 or public endpoints, and API URLs derive from PLUGIN_URL.
No-Weak-Crypto ✅ Passed The PR diff adds HTTP/API update and delete paths only; no MD5, SHA-1, DES, RC4, Blowfish, ECB, custom crypto, or secret comparisons were introduced. Existing hashes are SHA-256.
Container-Privileges ✅ Passed The diff changes OpenAPI, documentation, Go handlers, and e2e tests only; it adds no Kubernetes/container manifest privilege settings or root execution configuration.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (17)
pkg/management/alert_rule_preconditions.go (1)

110-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce the repeated GitOps checks.

validateDropRestorePreconditions repeats the same IsExternallyManagedObject GitOps check for pr, ar, and arc. Iterate over the objects instead. This keeps the behavior identical and makes a future added resource harder to miss.

♻️ Proposed refactor
 	if isRuleManagedByGitOpsLabel(relabeled) {
 		return notAllowedGitOpsEdit()
 	}
-	if pr != nil {
-		if gitOpsManaged, _ := k8s.IsExternallyManagedObject(pr); gitOpsManaged {
-			return notAllowedGitOpsEdit()
-		}
-	}
-	if ar != nil {
-		if gitOpsManaged, _ := k8s.IsExternallyManagedObject(ar); gitOpsManaged {
-			return notAllowedGitOpsEdit()
-		}
-	}
-	if arc != nil {
-		if gitOpsManaged, _ := k8s.IsExternallyManagedObject(arc); gitOpsManaged {
-			return notAllowedGitOpsEdit()
-		}
-	}
+	for _, obj := range []metav1.Object{objOrNil(pr), objOrNil(ar), objOrNil(arc)} {
+		if obj == nil {
+			continue
+		}
+		if gitOpsManaged, _ := k8s.IsExternallyManagedObject(obj); gitOpsManaged {
+			return notAllowedGitOpsEdit()
+		}
+	}
 	return nil

IsExternallyManagedObject already handles typed-nil pointers, so a small objOrNil helper or direct nil checks both work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/alert_rule_preconditions.go` around lines 110 - 130, Refactor
validateDropRestorePreconditions to consolidate the repeated
IsExternallyManagedObject checks for pr, ar, and arc into a single iteration
over the provided resources, while skipping nil values. Preserve the existing
relabeled GitOps check and return notAllowedGitOpsEdit as soon as any resource
is externally managed.
pkg/management/update_user_defined_alert_rule_test.go (1)

94-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consolidate the near-identical cases into table-driven tests.

TestUpdateUserDefinedAlertRule_BlocksGitOpsManaged and TestUpdateUserDefinedAlertRule_BlocksOperatorManaged differ only in the managed-by label value and the expected message. TestUpdateUserDefinedAlertRule_PRNotFound and TestUpdateUserDefinedAlertRule_PRGetError differ only in the mocked GetFunc return and the expected error. A table for each pair reduces duplication and makes new cases cheap to add.

As per coding guidelines: "{cmd,pkg}/**/*_test.go: Co-locate Go tests with implementation files and use table-driven tests when feasible."

Also applies to: 145-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_user_defined_alert_rule_test.go` around lines 94 - 114,
Consolidate TestUpdateUserDefinedAlertRule_BlocksGitOpsManaged and
TestUpdateUserDefinedAlertRule_BlocksOperatorManaged into one table-driven test,
parameterizing the managed-by label and expected error message while preserving
each mock setup and assertion. Apply the same table-driven consolidation to
TestUpdateUserDefinedAlertRule_PRNotFound and
TestUpdateUserDefinedAlertRule_PRGetError, parameterizing the mocked GetFunc
result and expected error.

Source: Coding guidelines

api/openapi.yaml (1)

427-471: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add minProperties: 1 to UpdateAlertRuleRequest.

The description states that at least one field is required. The schema does not encode that rule, so generated validators and clients accept an empty object. validateAlertRuleUpdateFields rejects it at runtime with 400. Encode the constraint in the schema to keep the contract self-describing.

Note: UpdateAlertRuleResult.statusCode declares format: int32 while DeleteAlertRuleResult.statusCode (line 350) does not. This produces int32 in one generated struct and int in the other. Align the two for consumer consistency.

📘 Proposed schema change
     UpdateAlertRuleRequest:
       type: object
+      minProperties: 1
       description: >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/openapi.yaml` around lines 427 - 471, Add minProperties: 1 to the
UpdateAlertRuleRequest schema so empty update objects are rejected by generated
validators, matching validateAlertRuleUpdateFields. Also align
UpdateAlertRuleResult.statusCode with DeleteAlertRuleResult.statusCode by
removing the inconsistent int32 format declaration.
internal/managementrouter/alert_rule_bulk_update.go (1)

49-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider rejecting duplicate rule IDs.

The loop processes each entry of payload.RuleIds independently. If the same ID appears twice, the handler applies the mutation twice and returns two result entries with the same id. For label updates the second call operates on a rule whose ID already changed, so it reports a not-found error for an operation that succeeded. Deduplicate the IDs before the loop, or reject duplicates with 400.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/managementrouter/alert_rule_bulk_update.go` around lines 49 - 75,
The bulk update loop should reject duplicate rule IDs before applying mutations.
Track normalized IDs while processing payload.RuleIds, return a 400 result for
repeated IDs (without calling applyAlertRuleUpdate), and preserve the existing
handling for unique, empty, and whitespace-trimmed IDs.
internal/managementrouter/alert_rule_update.go (1)

85-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Validate ruleId before reading the body.

The handler reads and unmarshals the request body, then checks that ruleId is non-blank. Move the ruleId check above the body read so a request with a blank ID fails without consuming up to 1 MB.

♻️ Proposed reorder
 func (hr *httpRouter) UpdateAlertRule(w http.ResponseWriter, req *http.Request, ruleId string) {
+	id := strings.TrimSpace(ruleId)
+	if id == "" {
+		writeError(w, http.StatusBadRequest, "ruleId is required")
+		return
+	}
+
 	req.Body = http.MaxBytesReader(w, req.Body, maxRequestBodyBytes)
 
 	body, err := io.ReadAll(req.Body)
@@
 	var payload UpdateAlertRuleRequest
 	if err := json.Unmarshal(body, &payload); err != nil {
 		writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
 		return
 	}
 
-	id := strings.TrimSpace(ruleId)
-	if id == "" {
-		writeError(w, http.StatusBadRequest, "ruleId is required")
-		return
-	}
-
 	fields := alertRuleUpdateFields{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/managementrouter/alert_rule_update.go` around lines 85 - 101, In the
alert rule update handler, move the strings.TrimSpace(ruleId) validation and
empty-ID error response before io.ReadAll(req.Body) and JSON unmarshalling.
Preserve the existing “ruleId is required” response and leave body processing
unchanged for valid IDs.
internal/managementrouter/alert_rule_delete_test.go (1)

24-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add cases for the 405 and 400 branches.

The tests cover 204, 404, and 401. DeleteAlertRule also returns 405 when the management client returns a NotAllowedError for a platform or externally managed rule, and 400 when ruleId is whitespace-only. Both branches are untested here. A whitespace ID such as %20 reaches the handler through the mux route and exercises the 400 path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/managementrouter/alert_rule_delete_test.go` around lines 24 - 53,
Add test cases alongside TestDeleteAlertRule_Succeeds,
TestDeleteAlertRule_NotFound, and TestDeleteAlertRule_MissingAuth for the
remaining DeleteAlertRule branches: configure the management client fixture to
return NotAllowedError for a platform or externally managed rule and assert HTTP
405, then send a DELETE request whose routed ruleId is whitespace (for example,
encoded as %20) and assert HTTP 400.
test/e2e/helpers_test.go (1)

132-158: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use the existing retry wrapper in mustCreateRule.

createRuleViaAPIWithRetry already exists at Line 45 for flake tolerance. mustCreateRule calls createRuleViaAPI directly. TestRBAC_UpdateAlertRule makes three sequential mustCreateRule calls, so one transient API error fails the whole test before any RBAC case runs.

♻️ Proposed refactor
-	id, err := createRuleViaAPI(ctx, f, managementrouter.CreateAlertRuleRequest{
+	id, err := createRuleViaAPIWithRetry(ctx, f, managementrouter.CreateAlertRuleRequest{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/helpers_test.go` around lines 132 - 158, Update mustCreateRule to
call the existing createRuleViaAPIWithRetry wrapper instead of createRuleViaAPI,
preserving the current request construction, error handling, and returned ID
behavior.
pkg/k8s/user_scoped_client_test.go (1)

10-49: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that impersonation is cleared.

The doc comment on buildUserScopedConfig states that impersonation is stripped. The test does not cover it. Add Impersonate to the base config and assert the derived config carries no impersonation. This protects the security guarantee against a future change of the sanitization helper.

💚 Proposed test addition
 	base := &rest.Config{
 		Host:            "https://api.example.com:6443",
 		BearerToken:     "sa-token",
 		BearerTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token",
+		Impersonate: rest.ImpersonationConfig{
+			UserName: "system:admin",
+			Groups:   []string{"system:masters"},
+		},
 		TLSClientConfig: rest.TLSClientConfig{
 	if cfg.KeyFile != "" {
 		t.Errorf("derived KeyFile = %q, want empty", cfg.KeyFile)
 	}
+	if cfg.Impersonate.UserName != "" || len(cfg.Impersonate.Groups) != 0 {
+		t.Errorf("derived Impersonate = %+v, want empty", cfg.Impersonate)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/k8s/user_scoped_client_test.go` around lines 10 - 49, Extend the test
around buildUserScopedConfig by setting the base rest.Config.Impersonate field
to a non-empty value, then assert the derived config’s Impersonate field is
empty. Preserve the existing checks for user credentials and copied connection
settings.
test/e2e/framework/framework.go (2)

256-269: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make retry honor context cancellation.

retry sleeps for a fixed one second and ignores the caller context. requestServiceAccountToken and CreateScopedUser both receive a ctx, so a canceled or expired context still costs up to two seconds of sleep and two extra API calls. Pass the context into retry and return early when it is done.

♻️ Proposed refactor
-// retry calls fn up to maxAttempts times with a 1-second pause between attempts.
-// It returns nil on the first successful call or the last error after exhaustion.
-func retry(maxAttempts int, fn func() error) error {
+// retry calls fn up to maxAttempts times with a 1-second pause between attempts.
+// It returns nil on the first successful call, the context error if ctx is done,
+// or the last error after exhaustion.
+func retry(ctx context.Context, maxAttempts int, fn func() error) error {
 	var err error
 	for i := range maxAttempts {
 		if err = fn(); err == nil {
 			return nil
 		}
 		if i < maxAttempts-1 {
-			time.Sleep(time.Second)
+			select {
+			case <-ctx.Done():
+				return ctx.Err()
+			case <-time.After(time.Second):
+			}
 		}
 	}
 	return err
 }

Update the four call sites in requestServiceAccountToken, CreateScopedUser, and CreateAnonymousUser to pass ctx.

As per path instructions: "context.Context for cancellation and timeouts".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/framework/framework.go` around lines 256 - 269, Update retry to
accept a context.Context, check for cancellation before each attempt, and
replace the fixed time.Sleep with a cancellation-aware wait that returns
ctx.Err(). Update all four retry call sites in requestServiceAccountToken,
CreateScopedUser, and CreateAnonymousUser to pass their ctx values, preserving
the existing retry and final-error behavior when the context remains active.

Source: Path instructions


304-335: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reconcile an existing Role and RoleBinding instead of accepting it as-is.

CreateScopedUser treats AlreadyExists as success for the Role and the RoleBinding. If a previous run left these objects behind with different verbs or resources, the test proceeds with the stale permissions. Every RBAC assertion in TestRBAC_UpdateAlertRule, TestRBAC_DeleteAlertRule, and TestRBAC_CreateAlertRule then measures the wrong policy, and the failure looks like a product bug. Update the object when it already exists.

🛡️ Proposed fix for the Role (apply the same pattern to the RoleBinding)
 	err = retry(3, func() error {
 		_, err := f.Clientset.RbacV1().Roles(namespace).Create(ctx, role, metav1.CreateOptions{})
 		if apierrors.IsAlreadyExists(err) {
-			return nil
+			_, err = f.Clientset.RbacV1().Roles(namespace).Update(ctx, role, metav1.UpdateOptions{})
+			return err
 		}
 		return err
 	})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/framework/framework.go` around lines 304 - 335, Update
CreateScopedUser’s Role and RoleBinding setup to reconcile existing objects
instead of treating apierrors.IsAlreadyExists as success. When Create returns
AlreadyExists, retrieve or use the existing object, apply the desired role rules
or binding subjects/RoleRef, and update it through the RBAC client; retain retry
behavior and return other errors unchanged.
test/e2e/update_alert_rule_test.go (1)

237-257: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the shared poll helper.

waitForUpdateCacheSync hand-rolls a deadline loop. test/e2e/helpers_test.go already exports poll(interval, timeout, fn), and waitForCacheSync in test/e2e/delete_alert_rule_test.go uses it for the same purpose. Reuse it so both cache-sync helpers behave the same way.

♻️ Proposed refactor
 func waitForUpdateCacheSync(t *testing.T, f *framework.Framework, ctx context.Context, token, ruleID string) {
 	t.Helper()
-	const timeout = 30 * time.Second
-	const interval = time.Second
-	deadline := time.Now().Add(timeout)
-	for {
-		status, err := tryUpdateAlertRule(f, ctx, token, ruleID)
-		if err == nil && (status == http.StatusForbidden || status == http.StatusNoContent) {
-			return
-		}
-		if time.Now().After(deadline) {
-			t.Fatalf("Cache sync timed out after %v (last status=%d, err=%v)", timeout, status, err)
-		}
-		if err != nil {
-			t.Logf("Cache sync: %v, retrying...", err)
-		} else {
-			t.Logf("Cache sync: per-rule status %d, retrying...", status)
-		}
-		time.Sleep(interval)
-	}
+	err := poll(time.Second, 30*time.Second, func() error {
+		status, err := tryUpdateAlertRule(f, ctx, token, ruleID)
+		if err != nil {
+			return err
+		}
+		if status == http.StatusForbidden || status == http.StatusNoContent {
+			return nil
+		}
+		return fmt.Errorf("per-rule status %d, waiting for cache sync", status)
+	})
+	if err != nil {
+		t.Fatalf("Cache sync timed out for rule %s: %v", ruleID, err)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/update_alert_rule_test.go` around lines 237 - 257, Refactor
waitForUpdateCacheSync to use the shared poll helper from helpers_test.go
instead of its local deadline, retry, logging, and sleep loop. Preserve the
existing tryUpdateAlertRule success condition for StatusForbidden or
StatusNoContent, and adapt timeout or retry errors to poll’s callback contract
while retaining the timeout failure behavior.
internal/managementrouter/alert_rule_bulk_update_test.go (1)

143-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the fixture IDs from one source of truth.

buFixtureIDs redefines the same three rule literals that newBUFixture builds at Lines 41-60. If one copy changes, the computed IDs diverge from the fixture data and the failure is hard to diagnose. Extract the three rules into package-level vars and use them in both functions.

♻️ Proposed refactor
+var (
+	buUserRule1 = monitoringv1.Rule{Alert: "user-alert-1", Expr: intstr.FromString("up == 0"), Labels: map[string]string{"severity": "warning"}}
+	buUserRule2 = monitoringv1.Rule{Alert: "user-alert-2", Expr: intstr.FromString("cpu > 80"), Labels: map[string]string{"severity": "info"}}
+	buPlatformRule = monitoringv1.Rule{Alert: "platform-alert", Expr: intstr.FromString("memory > 90"), Labels: map[string]string{"severity": "critical"}}
+)
+
 func buFixtureIDs() (user1, user2, platform string) {
-	r1 := monitoringv1.Rule{Alert: "user-alert-1", Expr: intstr.FromString("up == 0"), Labels: map[string]string{"severity": "warning"}}
-	r2 := monitoringv1.Rule{Alert: "user-alert-2", Expr: intstr.FromString("cpu > 80"), Labels: map[string]string{"severity": "info"}}
-	rp := monitoringv1.Rule{Alert: "platform-alert", Expr: intstr.FromString("memory > 90"), Labels: map[string]string{"severity": "critical"}}
-	return alertrule.GetAlertingRuleId(&r1), alertrule.GetAlertingRuleId(&r2), alertrule.GetAlertingRuleId(&rp)
+	return alertrule.GetAlertingRuleId(&buUserRule1),
+		alertrule.GetAlertingRuleId(&buUserRule2),
+		alertrule.GetAlertingRuleId(&buPlatformRule)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/managementrouter/alert_rule_bulk_update_test.go` around lines 143 -
150, Extract the three rule definitions currently duplicated by newBUFixture and
buFixtureIDs into package-level variables. Update both functions to reuse those
shared rule values while preserving the existing user1, user2, platform ordering
and fixture behavior.
pkg/management/update_platform_alert_rule_test.go (1)

686-692: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the Drop regex against regexp.QuoteMeta, as the production code produces it.

DropAlertRule sets Regex: regexp.QuoteMeta(alertRuleId) (see pkg/management/update_platform_alert_rule.go line 342). The test compares rc.Regex == drPlatformRuleId. The comparison holds only because the current fixture ID contains no regex metacharacter. Compare against regexp.QuoteMeta(drPlatformRuleId) so the test stays valid for any ID.

♻️ Proposed fix
 		case "Drop":
-			if len(rc.SourceLabels) == 1 && string(rc.SourceLabels[0]) == "openshift_io_alert_rule_id" && rc.Regex == drPlatformRuleId {
+			if len(rc.SourceLabels) == 1 && string(rc.SourceLabels[0]) == "openshift_io_alert_rule_id" && rc.Regex == regexp.QuoteMeta(drPlatformRuleId) {
 				hasDrop = true
 			}

Add the regexp import.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_platform_alert_rule_test.go` around lines 686 - 692,
Update the Drop receiver assertion in the test to compare rc.Regex against
regexp.QuoteMeta(drPlatformRuleId), matching DropAlertRule’s production
behavior, and add the regexp import required for the assertion.
pkg/management/delete_user_defined_alert_rule_by_id.go (1)

163-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrap the Delete error and align the doc comment with the behavior.

Line 177 returns the raw error from AlertRelabelConfigs().Delete. Every other failure in this file carries namespace and name context. The doc comment on line 164 also says the operation is "best-effort", but the function propagates lookup and delete failures, which aborts the caller after the rule is already removed.

♻️ Proposed fix
 // deleteAssociatedARC removes the AlertRelabelConfig associated with an alert rule, if it exists.
-// This is best-effort: if the ARC does not exist or is GitOps-managed, it is silently skipped.
+// If the ARC does not exist or is GitOps-managed, it is skipped. Lookup and delete
+// failures are returned to the caller.
 func (c *client) deleteAssociatedARC(ctx context.Context, namespace, prName, alertRuleId string) error {
@@
-	return c.k8sClient.AlertRelabelConfigs().Delete(ctx, namespace, arcName)
+	if err := c.k8sClient.AlertRelabelConfigs().Delete(ctx, namespace, arcName); err != nil {
+		return fmt.Errorf("failed to delete AlertRelabelConfig %s/%s: %w", namespace, arcName, err)
+	}
+	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/delete_user_defined_alert_rule_by_id.go` around lines 163 -
177, Update deleteAssociatedARC to wrap errors returned by
AlertRelabelConfigs().Delete with the namespace and ARC name, matching the
context used by the lookup error. Revise the function comment to state that
missing or GitOps-managed ARCs are skipped while lookup and deletion failures
are returned to the caller.
pkg/management/update_platform_alert_rule.go (1)

452-468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the four-value return signature and drop the single-element loop.

findARCByAlertRuleID returns (string, *osmv1.AlertRelabelConfig, string, error). The two string results are the namespace and the ARC name, in that order, and nothing at the call site prevents swapping them. The namespaces slice always holds exactly one element, so the loop adds no behavior.

Name the results, or return a small struct, and query the single namespace directly.

♻️ Proposed refactor
-func (c *client) findARCByAlertRuleID(ctx context.Context, alertRuleId string) (string, *osmv1.AlertRelabelConfig, string, error) {
-	namespaces := []string{k8s.ClusterMonitoringNamespace}
-	for _, ns := range namespaces {
-		arcs, err := c.k8sClient.AlertRelabelConfigs().List(ctx, ns)
-		if err != nil {
-			return "", nil, "", fmt.Errorf("failed to list AlertRelabelConfigs in %s: %w", ns, err)
-		}
-		for i := range arcs {
-			arc := arcs[i]
-			if arc.Annotations != nil && arc.Annotations[managementlabels.ARCAnnotationAlertRuleIDKey] == alertRuleId {
-				arcCopy := arc
-				return ns, &arcCopy, arc.Name, nil
-			}
-		}
-	}
-	return "", nil, "", nil
-}
+func (c *client) findARCByAlertRuleID(ctx context.Context, alertRuleId string) (namespace string, arc *osmv1.AlertRelabelConfig, name string, err error) {
+	ns := k8s.ClusterMonitoringNamespace
+	arcs, err := c.k8sClient.AlertRelabelConfigs().List(ctx, ns)
+	if err != nil {
+		return "", nil, "", fmt.Errorf("failed to list AlertRelabelConfigs in %s: %w", ns, err)
+	}
+	for i := range arcs {
+		if arcs[i].Annotations[managementlabels.ARCAnnotationAlertRuleIDKey] == alertRuleId {
+			found := arcs[i]
+			return ns, &found, found.Name, nil
+		}
+	}
+	return "", nil, "", nil
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_platform_alert_rule.go` around lines 452 - 468,
Refactor findARCByAlertRuleID to make the return values’ meanings
explicit—namespace first, ARC name second—and remove the unnecessary loop over
the single-element namespaces slice by querying k8s.ClusterMonitoringNamespace
directly. Preserve the existing lookup, error wrapping, and not-found behavior.
pkg/management/get_rule_by_id_test.go (1)

170-377: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These five tests verify k8s.DetermineManagedBy, not GetRuleById.

Each test calls k8s.DetermineManagedBy, writes the result into the rule through buildRuleWithManagedBy, programs the mock to return that rule, and then asserts on the labels of the returned rule. GetRuleById only forwards the cached rule, so the assertions pass or fail based on DetermineManagedBy alone. Two consequences follow:

  • The managed-by coverage belongs next to k8s.DetermineManagedBy in pkg/k8s.
  • The five cases differ only in ObjectMeta and in the expected labels, so a single table-driven test would remove the repeated mock setup.

The coding guidelines require table-driven tests where feasible: "Co-locate Go tests with implementation files and use table-driven tests when feasible."

♻️ Sketch of a table-driven replacement
func TestDetermineManagedByLabels(t *testing.T) {
	tests := []struct {
		name                  string
		promRule              *monitoringv1.PrometheusRule
		arc                   *testutils.MockAlertRelabelConfigInterface
		clusterMonitoringNS   bool
		wantRuleManagedBy     string
		wantRelabelManagedBy  string
	}{
		{name: "operator owner reference", /* ... */},
		{name: "no owner reference", /* ... */},
		{name: "gitops annotated ARC", /* ... */},
		{name: "gitops annotated PrometheusRule", /* ... */},
		{name: "plain ARC", /* ... */},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			mockNS := &testutils.MockNamespaceInterface{
				IsClusterMonitoringNamespaceFunc: func(string) bool { return tc.clusterMonitoringNS },
			}
			ruleManagedBy, relabelManagedBy := k8s.DetermineManagedBy(context.Background(), tc.arc, mockNS, tc.promRule, grTestRuleId)
			if ruleManagedBy != tc.wantRuleManagedBy {
				t.Errorf("ruleManagedBy: got %q, want %q", ruleManagedBy, tc.wantRuleManagedBy)
			}
			if relabelManagedBy != tc.wantRelabelManagedBy {
				t.Errorf("relabelManagedBy: got %q, want %q", relabelManagedBy, tc.wantRelabelManagedBy)
			}
		})
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/get_rule_by_id_test.go` around lines 170 - 377, Move the five
managed-by scenarios out of the GetRuleById tests and co-locate them with
DetermineManagedBy tests under pkg/k8s. Replace the repeated TestGetRuleById_*
cases with one table-driven TestDetermineManagedByLabels covering each
ObjectMeta, ARC, namespace, and expected-label combination, asserting
DetermineManagedBy results directly without buildRuleWithManagedBy or
GetRuleById setup.

Source: Coding guidelines

pkg/management/update_alert_rule_labels.go (1)

34-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add focused management tests for UpdateAlertRuleLabels.

Router tests cover user label setting/removal and mixed routing. Add tests that assert platform label set/drop payloads and preservation of unchanged user labels.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_alert_rule_labels.go` around lines 34 - 85, Add focused
management tests for UpdateAlertRuleLabels covering platform label set and
removal payloads, mixed platform/user routing, and preservation of unchanged
user labels. Exercise updatePlatformRuleLabels and updateUserRuleLabels through
the public method, asserting the platform update payload and PrometheusRule
update retain unaffected labels while applying set/drop semantics.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/openapi.yaml`:
- Around line 178-256: Update the OpenAPI response definitions for both the
update and DeleteAlertRule operations to document 400, 403, and 409 responses,
each using ErrorResponse. Preserve the existing descriptions and responses, and
describe 400 as invalid or blank ruleId/request input, 403 as forbidden
authorization, and 409 as a conflicting mutation.

In `@internal/managementrouter/alert_rule_update.go`:
- Around line 41-78: The applyAlertRuleUpdate flow should report partial success
when classification is persisted but UpdateAlertRuleLabels fails, rather than
returning the unchanged id as if no mutation occurred. Update the error/result
handling around UpdateAlertRuleClassification and UpdateAlertRuleLabels so
callers can identify the applied mutation state, or document this non-atomic
behavior for both PATCH rule endpoints in the existing OpenAPI definitions.

In `@pkg/management/delete_user_defined_alert_rule_by_id.go`:
- Around line 67-69: Prevent the fallback in deletePlatformAlertRuleById from
mutating operator-managed platform PrometheusRule objects when the owning
AlertingRule is absent. Before delegating to deleteUserAlertRuleById, reject
externally managed objects using k8s.IsExternallyManagedObject, or restore the
existing NotFoundError behavior; ensure the fallback cannot edit or delete the
entire platform rule.

In `@pkg/management/update_alert_rule_labels.go`:
- Around line 12-14: Update the Client interface documentation in types.go to
state that both nil and empty-string label values remove the label, while
non-empty values set it; keep the implementation in UpdateAlertRuleLabels
unchanged.

In `@pkg/management/update_platform_alert_rule_test.go`:
- Around line 549-571: The test must verify that UpdatePlatformAlertRule
excludes the protected openshift_io_alert_rule_id label from the created ARC.
Replace the unused _ = createdARC with an assertion that createdARC contains no
relabel target for that label with value "fake", while preserving the existing
no-error check.

In `@pkg/management/update_platform_alert_rule.go`:
- Around line 340-344: Update the dropCfg initialization in ensureStampAndDrop
to use k8s.AlertRuleLabelId for SourceLabels instead of the hardcoded
"openshift_io_alert_rule_id" value, keeping the existing regex and action
unchanged.
- Around line 437-446: Deep-copy the informer-backed object before mutation in
the AlertRelabelConfig update flow: replace the assignment of arc from
existingArc with existingArc.DeepCopy(), then continue modifying Spec and
Annotations and passing the copy to Update.

In `@pkg/management/update_user_defined_alert_rule_test.go`:
- Around line 312-320: Guard savedPR with a fatal nil check before accessing
savedPR.Spec in TestUpdateUserDefinedAlertRule_MultipleGroups, matching the
existing guard in TestUpdateUserDefinedAlertRule_UpdatesRule; add the same
protection before the assertions around the multiple-rules validation and before
the corresponding assertions near the second group.

In `@pkg/management/update_user_defined_alert_rule.go`:
- Line 14: Document the exported client.UpdateUserDefinedAlertRule method with a
Go doc comment beginning exactly with “UpdateUserDefinedAlertRule” and briefly
describing its behavior.

In `@test/e2e/update_alert_rule_test.go`:
- Around line 364-374: Update hasClassificationForRule to track whether both the
component and layer relabel configurations match, and return true only after
both classifications have been found. Preserve iterating through
arc.Spec.Configs and return false when either label is missing.
- Line 209: Update the cache synchronization setup in the alert-rule update test
to call waitForUpdateCacheSync for every rule ID used by the subtests, including
ruleInY, ruleInZ, and ruleInY2. Ensure each rule is synchronized before the
endpoint assertions run, following the pattern used by TestRBAC_DeleteAlertRule.

---

Nitpick comments:
In `@api/openapi.yaml`:
- Around line 427-471: Add minProperties: 1 to the UpdateAlertRuleRequest schema
so empty update objects are rejected by generated validators, matching
validateAlertRuleUpdateFields. Also align UpdateAlertRuleResult.statusCode with
DeleteAlertRuleResult.statusCode by removing the inconsistent int32 format
declaration.

In `@internal/managementrouter/alert_rule_bulk_update_test.go`:
- Around line 143-150: Extract the three rule definitions currently duplicated
by newBUFixture and buFixtureIDs into package-level variables. Update both
functions to reuse those shared rule values while preserving the existing user1,
user2, platform ordering and fixture behavior.

In `@internal/managementrouter/alert_rule_bulk_update.go`:
- Around line 49-75: The bulk update loop should reject duplicate rule IDs
before applying mutations. Track normalized IDs while processing
payload.RuleIds, return a 400 result for repeated IDs (without calling
applyAlertRuleUpdate), and preserve the existing handling for unique, empty, and
whitespace-trimmed IDs.

In `@internal/managementrouter/alert_rule_delete_test.go`:
- Around line 24-53: Add test cases alongside TestDeleteAlertRule_Succeeds,
TestDeleteAlertRule_NotFound, and TestDeleteAlertRule_MissingAuth for the
remaining DeleteAlertRule branches: configure the management client fixture to
return NotAllowedError for a platform or externally managed rule and assert HTTP
405, then send a DELETE request whose routed ruleId is whitespace (for example,
encoded as %20) and assert HTTP 400.

In `@internal/managementrouter/alert_rule_update.go`:
- Around line 85-101: In the alert rule update handler, move the
strings.TrimSpace(ruleId) validation and empty-ID error response before
io.ReadAll(req.Body) and JSON unmarshalling. Preserve the existing “ruleId is
required” response and leave body processing unchanged for valid IDs.

In `@pkg/k8s/user_scoped_client_test.go`:
- Around line 10-49: Extend the test around buildUserScopedConfig by setting the
base rest.Config.Impersonate field to a non-empty value, then assert the derived
config’s Impersonate field is empty. Preserve the existing checks for user
credentials and copied connection settings.

In `@pkg/management/alert_rule_preconditions.go`:
- Around line 110-130: Refactor validateDropRestorePreconditions to consolidate
the repeated IsExternallyManagedObject checks for pr, ar, and arc into a single
iteration over the provided resources, while skipping nil values. Preserve the
existing relabeled GitOps check and return notAllowedGitOpsEdit as soon as any
resource is externally managed.

In `@pkg/management/delete_user_defined_alert_rule_by_id.go`:
- Around line 163-177: Update deleteAssociatedARC to wrap errors returned by
AlertRelabelConfigs().Delete with the namespace and ARC name, matching the
context used by the lookup error. Revise the function comment to state that
missing or GitOps-managed ARCs are skipped while lookup and deletion failures
are returned to the caller.

In `@pkg/management/get_rule_by_id_test.go`:
- Around line 170-377: Move the five managed-by scenarios out of the GetRuleById
tests and co-locate them with DetermineManagedBy tests under pkg/k8s. Replace
the repeated TestGetRuleById_* cases with one table-driven
TestDetermineManagedByLabels covering each ObjectMeta, ARC, namespace, and
expected-label combination, asserting DetermineManagedBy results directly
without buildRuleWithManagedBy or GetRuleById setup.

In `@pkg/management/update_alert_rule_labels.go`:
- Around line 34-85: Add focused management tests for UpdateAlertRuleLabels
covering platform label set and removal payloads, mixed platform/user routing,
and preservation of unchanged user labels. Exercise updatePlatformRuleLabels and
updateUserRuleLabels through the public method, asserting the platform update
payload and PrometheusRule update retain unaffected labels while applying
set/drop semantics.

In `@pkg/management/update_platform_alert_rule_test.go`:
- Around line 686-692: Update the Drop receiver assertion in the test to compare
rc.Regex against regexp.QuoteMeta(drPlatformRuleId), matching DropAlertRule’s
production behavior, and add the regexp import required for the assertion.

In `@pkg/management/update_platform_alert_rule.go`:
- Around line 452-468: Refactor findARCByAlertRuleID to make the return values’
meanings explicit—namespace first, ARC name second—and remove the unnecessary
loop over the single-element namespaces slice by querying
k8s.ClusterMonitoringNamespace directly. Preserve the existing lookup, error
wrapping, and not-found behavior.

In `@pkg/management/update_user_defined_alert_rule_test.go`:
- Around line 94-114: Consolidate
TestUpdateUserDefinedAlertRule_BlocksGitOpsManaged and
TestUpdateUserDefinedAlertRule_BlocksOperatorManaged into one table-driven test,
parameterizing the managed-by label and expected error message while preserving
each mock setup and assertion. Apply the same table-driven consolidation to
TestUpdateUserDefinedAlertRule_PRNotFound and
TestUpdateUserDefinedAlertRule_PRGetError, parameterizing the mocked GetFunc
result and expected error.

In `@test/e2e/framework/framework.go`:
- Around line 256-269: Update retry to accept a context.Context, check for
cancellation before each attempt, and replace the fixed time.Sleep with a
cancellation-aware wait that returns ctx.Err(). Update all four retry call sites
in requestServiceAccountToken, CreateScopedUser, and CreateAnonymousUser to pass
their ctx values, preserving the existing retry and final-error behavior when
the context remains active.
- Around line 304-335: Update CreateScopedUser’s Role and RoleBinding setup to
reconcile existing objects instead of treating apierrors.IsAlreadyExists as
success. When Create returns AlreadyExists, retrieve or use the existing object,
apply the desired role rules or binding subjects/RoleRef, and update it through
the RBAC client; retain retry behavior and return other errors unchanged.

In `@test/e2e/helpers_test.go`:
- Around line 132-158: Update mustCreateRule to call the existing
createRuleViaAPIWithRetry wrapper instead of createRuleViaAPI, preserving the
current request construction, error handling, and returned ID behavior.

In `@test/e2e/update_alert_rule_test.go`:
- Around line 237-257: Refactor waitForUpdateCacheSync to use the shared poll
helper from helpers_test.go instead of its local deadline, retry, logging, and
sleep loop. Preserve the existing tryUpdateAlertRule success condition for
StatusForbidden or StatusNoContent, and adapt timeout or retry errors to poll’s
callback contract while retaining the timeout failure behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ea6da1e-524e-444d-adbb-aa80c26bfa16

📥 Commits

Reviewing files that changed from the base of the PR and between 3397ba9 and df528cb.

📒 Files selected for processing (35)
  • api/openapi.yaml
  • docs/alert-management.md
  • docs/alert-rule-classification.md
  • internal/managementrouter/alert_rule_bulk_update.go
  • internal/managementrouter/alert_rule_bulk_update_test.go
  • internal/managementrouter/alert_rule_delete.go
  • internal/managementrouter/alert_rule_delete_test.go
  • internal/managementrouter/alert_rule_update.go
  • internal/managementrouter/alert_rule_update_test.go
  • internal/managementrouter/api_generated.go
  • internal/managementrouter/router.go
  • internal/managementrouter/router_test.go
  • pkg/k8s/const.go
  • pkg/k8s/user_scoped_client.go
  • pkg/k8s/user_scoped_client_test.go
  • pkg/management/alert_rule_preconditions.go
  • pkg/management/client_factory.go
  • pkg/management/delete_user_defined_alert_rule_by_id.go
  • pkg/management/get_rule_by_id.go
  • pkg/management/get_rule_by_id_test.go
  • pkg/management/label_utils.go
  • pkg/management/management.go
  • pkg/management/types.go
  • pkg/management/update_alert_rule_labels.go
  • pkg/management/update_classification.go
  • pkg/management/update_classification_test.go
  • pkg/management/update_platform_alert_rule.go
  • pkg/management/update_platform_alert_rule_test.go
  • pkg/management/update_user_defined_alert_rule.go
  • pkg/management/update_user_defined_alert_rule_test.go
  • test/e2e/create_alert_rule_test.go
  • test/e2e/delete_alert_rule_test.go
  • test/e2e/framework/framework.go
  • test/e2e/helpers_test.go
  • test/e2e/update_alert_rule_test.go

Comment thread api/openapi.yaml
Comment thread internal/managementrouter/alert_rule_update.go
Comment thread pkg/management/delete_user_defined_alert_rule_by_id.go
Comment thread pkg/management/update_alert_rule_labels.go
Comment thread pkg/management/update_platform_alert_rule_test.go
Comment thread pkg/management/update_platform_alert_rule.go Outdated
Comment thread pkg/management/update_user_defined_alert_rule_test.go
Comment thread pkg/management/update_user_defined_alert_rule.go
Comment thread test/e2e/update_alert_rule_test.go
Comment thread test/e2e/update_alert_rule_test.go
sradco and others added 4 commits August 12, 2026 14:20
- Move Poll into the e2e framework and use it
  instead of a custom retry helper
- Consolidate create/delete RBAC coverage as
  subtests of the main functional tests
- Use idiomatic errors.As with *T targets for
  pointer-receiver management errors

Signed-off-by: Shirly Radco <sradco@redhat.com>
Co-authored-by: AI Assistant <noreply@cursor.com>
Signed-off-by: Shirly Radco <sradco@redhat.com>
Co-authored-by: AI Assistant <noreply@cursor.com>
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. 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)
- 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>
Co-authored-by: AI Assistant <noreply@cursor.com>
Keep the bulk PATCH/DELETE /rules APIs and add
per-rule endpoints for easier client use and
reviewability:

- PATCH /rules/{ruleId}
- DELETE /rules/{ruleId}

Single update shares validation and mutation
logic with BulkUpdateAlertRules. Adds unit and
e2e coverage (including RBAC) plus API docs.

Signed-off-by: Shirly Radco <sradco@redhat.com>
Co-authored-by: AI Assistant <noreply@cursor.com>
@sradco
sradco force-pushed the alert-mgmt-single-rule-endpoints branch from d71662e to 3f9fdc0 Compare August 12, 2026 14:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
test/e2e/framework/poll.go (1)

13-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Accept a context.Context parameter instead of using context.Background().

Poll hardcodes context.Background(), so callers cannot cancel or shorten a wait. Every current caller already holds a test context. Pass that context through.

Also verify the doc comment. wait.PollUntilContextTimeout returns a context deadline error on timeout, not wait.ErrWaitTimeout, so the comment can mislead readers about the wrapped sentinel.

♻️ Proposed signature change
-// Poll calls f every interval until it returns nil or timeout elapses.
-// On timeout the last observed error is wrapped with wait.ErrWaitTimeout.
-func Poll(interval, timeout time.Duration, f func() error) error {
+// Poll calls f every interval until it returns nil, timeout elapses, or ctx is done.
+// On timeout the returned error wraps the timeout error and the last observed error from f.
+func Poll(ctx context.Context, interval, timeout time.Duration, f func() error) error {
 	var lastErr error
-	err := wait.PollUntilContextTimeout(context.Background(), interval, timeout, true, func(context.Context) (bool, error) {
+	err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(context.Context) (bool, error) {
 		if lastErr = f(); lastErr != nil {
 			return false, nil
 		}
 		return true, nil
 	})

Update the poll wrapper in test/e2e/helpers_test.go and all call sites in test/e2e/framework/framework.go, test/e2e/single_alert_rule_test.go, test/e2e/create_alert_rule_test.go, and test/e2e/delete_alert_rule_test.go.

As per path instructions: "context.Context for cancellation and timeouts".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/framework/poll.go` around lines 13 - 27, Update Poll to accept a
context.Context parameter and pass it to wait.PollUntilContextTimeout instead of
context.Background(), then propagate the existing caller contexts through the
poll wrapper in helpers_test.go and all listed call sites. Correct the Poll doc
comment to describe the actual context deadline error returned on timeout rather
than wait.ErrWaitTimeout.

Source: Path instructions

test/e2e/framework/framework.go (1)

337-367: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restructure the nested polling in CreateAnonymousUser.

The outer Poll uses a 3-second timeout, and requestServiceAccountToken polls with its own 3-second timeout inside the same closure. The inner poll consumes the whole outer budget, so the outer retry for ServiceAccount creation almost never runs. The result is a single effective attempt with a confusing retry structure.

Split the two steps like CreateScopedUser does, and give the token request a longer budget. ServiceAccount token issuance can lag on a loaded cluster, so a 3-second cap can make these e2e tests flaky.

♻️ Proposed restructure
 func (f *Framework) CreateAnonymousUser(ctx context.Context, name, namespace string) (*ScopedUser, error) {
-	var user *ScopedUser
-	err := Poll(time.Second, 3*time.Second, func() error {
-		sa := &corev1.ServiceAccount{
-			ObjectMeta: metav1.ObjectMeta{Name: name},
-		}
-		_, err := f.Clientset.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{})
-		if err != nil && !apierrors.IsAlreadyExists(err) {
-			return fmt.Errorf("creating service account %s/%s: %w", namespace, name, err)
-		}
-
-		token, err := f.requestServiceAccountToken(ctx, namespace, name)
-		if err != nil {
-			_ = f.Clientset.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{})
-			return err
-		}
-
-		user = &ScopedUser{
-			Token: token,
-			Cleanup: func() error {
-				_ = f.Clientset.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{})
-				return nil
-			},
-		}
-		return nil
-	})
-	if err != nil {
+	sa := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{Name: name},
+	}
+	err := Poll(time.Second, 30*time.Second, func() error {
+		_, err := f.Clientset.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{})
+		if apierrors.IsAlreadyExists(err) {
+			return nil
+		}
+		return err
+	})
+	if err != nil {
+		return nil, fmt.Errorf("creating service account %s/%s: %w", namespace, name, err)
+	}
+
+	token, err := f.requestServiceAccountToken(ctx, namespace, name)
+	if err != nil {
+		_ = f.Clientset.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{})
 		return nil, err
 	}
-	return user, nil
+
+	return &ScopedUser{
+		Token: token,
+		Cleanup: func() error {
+			return f.Clientset.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, metav1.DeleteOptions{})
+		},
+	}, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/framework/framework.go` around lines 337 - 367, Restructure
CreateAnonymousUser so ServiceAccount creation and token acquisition are no
longer performed inside the same outer Poll closure. Follow the separate-step
pattern used by CreateScopedUser: poll ServiceAccount creation independently,
then call requestServiceAccountToken with a longer timeout suitable for delayed
token issuance, preserving cleanup on token failure and the existing ScopedUser
result.
test/e2e/single_alert_rule_test.go (1)

184-186: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused keepID assignment.

keepID is discarded with _ = keepID. The later assertion checks the alert name KeepSingleAlert, so the ID is not needed. Create the rule without binding the return value.

♻️ Proposed cleanup
-	keepID := mustCreateRule(ctx, t, f, ns, "KeepSingleAlert", "e2e-delete-single-pr")
+	mustCreateRule(ctx, t, f, ns, "KeepSingleAlert", "e2e-delete-single-pr")
 	deleteID := mustCreateRule(ctx, t, f, ns, "DeleteSingleAlert", "e2e-delete-single-pr")
-	_ = keepID
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/single_alert_rule_test.go` around lines 184 - 186, Remove the unused
keepID binding in the test setup and call mustCreateRule directly for
KeepSingleAlert without assigning its return value or retaining the _ = keepID
statement; leave deleteID and the existing assertions unchanged.
pkg/management/update_platform_alert_rule.go (2)

255-267: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Match SourceLabels in filterOutDrop for symmetry with ensureStampAndDrop.

ensureStampAndDrop identifies the drop entry by Action == "Drop", Regex, and SourceLabels[0] == k8s.AlertRuleLabelId. filterOutDrop matches only Action and Regex. A future or externally added Drop entry that uses the same regex on a different source label is removed during restore. Align both predicates.

♻️ Proposed alignment
 	for _, rc := range configs {
-		if rc.Action == "Drop" && (rc.Regex == target || rc.Regex == alertRuleId) {
+		if rc.Action == "Drop" && (rc.Regex == target || rc.Regex == alertRuleId) &&
+			len(rc.SourceLabels) == 1 && rc.SourceLabels[0] == k8s.AlertRuleLabelId {
 			removed = true
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_platform_alert_rule.go` around lines 255 - 267, Update
filterOutDrop to match SourceLabels[0] against k8s.AlertRuleLabelId in addition
to the existing Drop action and regex checks, mirroring the predicate used by
ensureStampAndDrop. Preserve all non-matching relabel configurations.

362-422: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the target-resolution branches from RestoreAlertRule.

The function mixes two resolution strategies with the mutation logic. The pre-declared err at Line 367 is also shadowed inside the branches, which makes the control flow harder to follow. Move each branch into a helper that returns (arcNamespace, arcName, *osmv1.AlertRelabelConfig, error), then keep only the filter-and-write logic in RestoreAlertRule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/update_platform_alert_rule.go` around lines 362 - 422, Extract
the relabeled-cache and annotation-scan target-resolution branches from
RestoreAlertRule into separate helpers, each returning (arcNamespace, arcName,
*osmv1.AlertRelabelConfig, error). Move their lookup and
validateDropRestorePreconditions logic into the helpers, eliminate the
pre-declared err and branch-local shadowing, and leave RestoreAlertRule
responsible only for invoking resolution and performing the existing
filter-and-write mutation flow.
pkg/management/delete_user_defined_alert_rule_by_id_test.go (1)

245-297: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a table-driven form for the four platform deletion tests.

TestDeleteAlertRuleById_PlatformRuleNotOperatorManaged, _PlatformRuleGitOpsManaged, _PlatformRuleOperatorManaged, and this new test repeat the same RelabeledRulesFunc, NamespaceFunc, and PrometheusRulesFunc scaffold. Only the ownership metadata and the expected outcome differ. Extract the shared mock setup into a helper and drive the cases from a table. The coding guidelines ask for table-driven tests when feasible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/management/delete_user_defined_alert_rule_by_id_test.go` around lines 245
- 297, Refactor the four platform deletion
tests—TestDeleteAlertRuleById_PlatformRuleNotOperatorManaged,
TestDeleteAlertRuleById_PlatformRuleGitOpsManaged,
TestDeleteAlertRuleById_PlatformRuleOperatorManaged, and
TestDeleteAlertRuleById_PlatformFallbackRejectsOperatorManagedPR—into a
table-driven test. Extract their repeated RelabeledRulesFunc, NamespaceFunc, and
PrometheusRulesFunc setup into a shared helper, parameterize ownership metadata
and expected update/delete outcomes, and retain each case’s distinct behavior
assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/alert-management.md`:
- Around line 51-77: Update the “Bulk delete” section to state that its request
body accepts 1–100 ruleIds, matching the documented bulk update limit and API
contract.

In `@internal/managementrouter/alert_rule_update.go`:
- Around line 21-28: The validateAlertRuleUpdateFields function must reject
non-nil but empty Classification values. When Classification is provided,
require at least one of ComponentSet, LayerSet, ComponentFromSet, or
LayerFromSet; otherwise return a validation error before the update logic runs.
Add a regression test covering a request with an empty classification object and
ensure it is rejected.

In `@test/e2e/single_alert_rule_test.go`:
- Around line 141-145: Update the setup in the table-driven test around ruleInY,
ruleInZ, and ruleInY2 to call waitForSingleUpdateCacheSync for all three rule
IDs before executing cases, matching the pattern used by
TestRBAC_DeleteAlertRule_Single. Preserve the existing synchronization behavior
for ruleInY.

---

Nitpick comments:
In `@pkg/management/delete_user_defined_alert_rule_by_id_test.go`:
- Around line 245-297: Refactor the four platform deletion
tests—TestDeleteAlertRuleById_PlatformRuleNotOperatorManaged,
TestDeleteAlertRuleById_PlatformRuleGitOpsManaged,
TestDeleteAlertRuleById_PlatformRuleOperatorManaged, and
TestDeleteAlertRuleById_PlatformFallbackRejectsOperatorManagedPR—into a
table-driven test. Extract their repeated RelabeledRulesFunc, NamespaceFunc, and
PrometheusRulesFunc setup into a shared helper, parameterize ownership metadata
and expected update/delete outcomes, and retain each case’s distinct behavior
assertions.

In `@pkg/management/update_platform_alert_rule.go`:
- Around line 255-267: Update filterOutDrop to match SourceLabels[0] against
k8s.AlertRuleLabelId in addition to the existing Drop action and regex checks,
mirroring the predicate used by ensureStampAndDrop. Preserve all non-matching
relabel configurations.
- Around line 362-422: Extract the relabeled-cache and annotation-scan
target-resolution branches from RestoreAlertRule into separate helpers, each
returning (arcNamespace, arcName, *osmv1.AlertRelabelConfig, error). Move their
lookup and validateDropRestorePreconditions logic into the helpers, eliminate
the pre-declared err and branch-local shadowing, and leave RestoreAlertRule
responsible only for invoking resolution and performing the existing
filter-and-write mutation flow.

In `@test/e2e/framework/framework.go`:
- Around line 337-367: Restructure CreateAnonymousUser so ServiceAccount
creation and token acquisition are no longer performed inside the same outer
Poll closure. Follow the separate-step pattern used by CreateScopedUser: poll
ServiceAccount creation independently, then call requestServiceAccountToken with
a longer timeout suitable for delayed token issuance, preserving cleanup on
token failure and the existing ScopedUser result.

In `@test/e2e/framework/poll.go`:
- Around line 13-27: Update Poll to accept a context.Context parameter and pass
it to wait.PollUntilContextTimeout instead of context.Background(), then
propagate the existing caller contexts through the poll wrapper in
helpers_test.go and all listed call sites. Correct the Poll doc comment to
describe the actual context deadline error returned on timeout rather than
wait.ErrWaitTimeout.

In `@test/e2e/single_alert_rule_test.go`:
- Around line 184-186: Remove the unused keepID binding in the test setup and
call mustCreateRule directly for KeepSingleAlert without assigning its return
value or retaining the _ = keepID statement; leave deleteID and the existing
assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: ae20873f-5044-4a84-a450-3a448aeca227

📥 Commits

Reviewing files that changed from the base of the PR and between df528cb and 3f9fdc0.

📒 Files selected for processing (24)
  • api/openapi.yaml
  • docs/alert-management.md
  • docs/alert-rule-classification.md
  • internal/managementrouter/alert_rule_delete_test.go
  • internal/managementrouter/alert_rule_update.go
  • internal/managementrouter/alert_rule_update_test.go
  • internal/managementrouter/api_generated.go
  • internal/managementrouter/router.go
  • internal/managementrouter/router_test.go
  • pkg/k8s/user_scoped_client_test.go
  • pkg/management/delete_user_defined_alert_rule_by_id.go
  • pkg/management/delete_user_defined_alert_rule_by_id_test.go
  • pkg/management/types.go
  • pkg/management/update_platform_alert_rule.go
  • pkg/management/update_platform_alert_rule_test.go
  • pkg/management/update_user_defined_alert_rule.go
  • pkg/management/update_user_defined_alert_rule_test.go
  • test/e2e/create_alert_rule_test.go
  • test/e2e/delete_alert_rule_test.go
  • test/e2e/framework/framework.go
  • test/e2e/framework/poll.go
  • test/e2e/helpers_test.go
  • test/e2e/single_alert_rule_test.go
  • test/e2e/update_alert_rule_test.go
🚧 Files skipped from review as they are similar to previous changes (13)
  • internal/managementrouter/router_test.go
  • pkg/k8s/user_scoped_client_test.go
  • test/e2e/helpers_test.go
  • internal/managementrouter/router.go
  • pkg/management/delete_user_defined_alert_rule_by_id.go
  • pkg/management/types.go
  • pkg/management/update_user_defined_alert_rule_test.go
  • pkg/management/update_user_defined_alert_rule.go
  • test/e2e/update_alert_rule_test.go
  • pkg/management/update_platform_alert_rule_test.go
  • api/openapi.yaml
  • docs/alert-rule-classification.md
  • internal/managementrouter/api_generated.go

Comment thread docs/alert-management.md
Comment on lines +51 to +77
**Single update** (`PATCH /rules/{ruleId}`):
- Request body uses `UpdateAlertRuleRequest` (labels and/or classification, or
`alertingRuleEnabled` alone for drop/restore).
- Success: HTTP `200` with `UpdateAlertRuleResult` (`statusCode: 204`). The
returned `id` may differ from the path `ruleId` when labels change the stable ID.
- Failure: standard `ErrorResponse` with the corresponding HTTP status
(400/401/403/404/405/409/413/500). Errors include a message so callers can act on them.
- Non-atomic combined updates: when both `classification` and `labels` are set,
classification is applied first, then labels. If the label step fails, the
classification change may already be persisted and the request still returns
an error. Retry or inspect cluster state before re-applying classification.

**Bulk update** (`PATCH /rules`):
- Request body includes `ruleIds` (1–100) plus the same mutation fields.
- Always returns HTTP `200` with per-rule `statusCode`/`message` entries so
partial success is visible.
- Same non-atomic classification-then-labels behavior as single update; a failed
label step is reported on that rule's result while classification may remain.

**Single delete** (`DELETE /rules/{ruleId}`):
- Success: HTTP `204`.
- Failure: `ErrorResponse` with HTTP status (400/401/403/404/405/409/500).

**Bulk delete** (`DELETE /rules`):
- Request body includes `ruleIds`.
- Always returns HTTP `200` with per-rule results.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
echo '--- alerting rules paths and responses ---'
rg -n -A40 '/alerting/rules' api/openapi.yaml | head -300
echo '--- ruleIds constraints ---'
rg -n -B3 -A12 'ruleIds' api/openapi.yaml

Repository: openshift/monitoring-plugin

Length of output: 1270


🏁 Script executed:

#!/bin/bash
set -eu
echo '--- OpenAPI paths containing alert rules ---'
rg -n -i -B3 -A35 'rules' api/openapi.yaml | head -500
echo '--- response definitions and status references ---'
rg -n -i -B3 -A12 'responses:|statusCode|ErrorResponse' api/openapi.yaml | head -500

Repository: openshift/monitoring-plugin

Length of output: 28662


Document the bulk delete ruleIds limit.

The single-update and single-delete status lists match api/openapi.yaml. State that bulk delete accepts 1–100 ruleIds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/alert-management.md` around lines 51 - 77, Update the “Bulk delete”
section to state that its request body accepts 1–100 ruleIds, matching the
documented bulk update limit and API contract.

Comment on lines +21 to +28
func validateAlertRuleUpdateFields(f alertRuleUpdateFields) string {
if f.AlertingRuleEnabled == nil && f.Labels == nil && f.Classification == nil {
return "one of alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required"
}
if f.AlertingRuleEnabled != nil && (f.Labels != nil || f.Classification != nil) {
return "alertingRuleEnabled cannot be combined with labels or classification in the same request"
}
return ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject an empty classification object.

{"classification":{}} passes this validation. Lines 45-69 then perform no classification update, and the handler returns success.

Require at least one of ComponentSet, LayerSet, ComponentFromSet, or LayerFromSet when Classification is non-nil. Add a regression test for this request body.

Proposed fix
 func validateAlertRuleUpdateFields(f alertRuleUpdateFields) string {
   if f.AlertingRuleEnabled == nil && f.Labels == nil && f.Classification == nil {
     return "one of alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required"
   }
   if f.AlertingRuleEnabled != nil && (f.Labels != nil || f.Classification != nil) {
     return "alertingRuleEnabled cannot be combined with labels or classification in the same request"
   }
+  if f.Classification != nil &&
+    !f.Classification.ComponentSet &&
+    !f.Classification.LayerSet &&
+    !f.Classification.ComponentFromSet &&
+    !f.Classification.LayerFromSet {
+    return "classification must set at least one field"
+  }
   return ""
 }

As per coding guidelines, “Add unit tests for utility functions, business logic, bug fixes, and backend API handlers.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func validateAlertRuleUpdateFields(f alertRuleUpdateFields) string {
if f.AlertingRuleEnabled == nil && f.Labels == nil && f.Classification == nil {
return "one of alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required"
}
if f.AlertingRuleEnabled != nil && (f.Labels != nil || f.Classification != nil) {
return "alertingRuleEnabled cannot be combined with labels or classification in the same request"
}
return ""
func validateAlertRuleUpdateFields(f alertRuleUpdateFields) string {
if f.AlertingRuleEnabled == nil && f.Labels == nil && f.Classification == nil {
return "one of alertingRuleEnabled (toggle drop/restore) or labels (set/unset) or classification is required"
}
if f.AlertingRuleEnabled != nil && (f.Labels != nil || f.Classification != nil) {
return "alertingRuleEnabled cannot be combined with labels or classification in the same request"
}
if f.Classification != nil &&
!f.Classification.ComponentSet &&
!f.Classification.LayerSet &&
!f.Classification.ComponentFromSet &&
!f.Classification.LayerFromSet {
return "classification must set at least one field"
}
return ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/managementrouter/alert_rule_update.go` around lines 21 - 28, The
validateAlertRuleUpdateFields function must reject non-nil but empty
Classification values. When Classification is provided, require at least one of
ComponentSet, LayerSet, ComponentFromSet, or LayerFromSet; otherwise return a
validation error before the update logic runs. Add a regression test covering a
request with an empty classification object and ensure it is rejected.

Source: Coding guidelines

Comment on lines +141 to +145
ruleInY := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY", "e2e-rbac-upd1-pr")
ruleInZ := mustCreateRule(ctx, t, f, nsZ, "RBACUpd1AlertZ", "e2e-rbac-upd1-pr")
ruleInY2 := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY2", "e2e-rbac-upd1-pr")

waitForSingleUpdateCacheSync(ctx, t, f, anonymousUser.Token, ruleInY)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wait for cache sync on every rule ID used by the table.

The test synchronizes only ruleInY, but the cases also target ruleInZ and ruleInY2. If the plugin cache has not yet observed those rules, the API returns 404 instead of the expected 403 or 200, and the subtests fail intermittently. TestRBAC_DeleteAlertRule_Single at Lines 263-265 loops over all three IDs. Apply the same pattern here.

🐛 Proposed fix
-	waitForSingleUpdateCacheSync(ctx, t, f, anonymousUser.Token, ruleInY)
+	for _, ruleID := range []string{ruleInY, ruleInY2, ruleInZ} {
+		waitForSingleUpdateCacheSync(ctx, t, f, anonymousUser.Token, ruleID)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ruleInY := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY", "e2e-rbac-upd1-pr")
ruleInZ := mustCreateRule(ctx, t, f, nsZ, "RBACUpd1AlertZ", "e2e-rbac-upd1-pr")
ruleInY2 := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY2", "e2e-rbac-upd1-pr")
waitForSingleUpdateCacheSync(ctx, t, f, anonymousUser.Token, ruleInY)
ruleInY := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY", "e2e-rbac-upd1-pr")
ruleInZ := mustCreateRule(ctx, t, f, nsZ, "RBACUpd1AlertZ", "e2e-rbac-upd1-pr")
ruleInY2 := mustCreateRule(ctx, t, f, nsY, "RBACUpd1AlertY2", "e2e-rbac-upd1-pr")
for _, ruleID := range []string{ruleInY, ruleInY2, ruleInZ} {
waitForSingleUpdateCacheSync(ctx, t, f, anonymousUser.Token, ruleID)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/single_alert_rule_test.go` around lines 141 - 145, Update the setup
in the table-driven test around ruleInY, ruleInZ, and ruleInY2 to call
waitForSingleUpdateCacheSync for all three rule IDs before executing cases,
matching the pattern used by TestRBAC_DeleteAlertRule_Single. Preserve the
existing synchronization behavior for ruleInY.

@openshift-ci

openshift-ci Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@sradco: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/security 3f9fdc0 link false /test security

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant