management: add single alert rule endpoints - #1121
Conversation
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>
|
Pipeline controller notification For optional jobs, comment This repository is configured in: LGTM mode |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: sradco The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
df528cb to
d71662e
Compare
WalkthroughAdded 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. ChangesAlert rule management
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
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (13 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (17)
pkg/management/alert_rule_preconditions.go (1)
110-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReduce the repeated GitOps checks.
validateDropRestorePreconditionsrepeats the sameIsExternallyManagedObjectGitOps check forpr,ar, andarc. 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
IsExternallyManagedObjectalready handles typed-nil pointers, so a smallobjOrNilhelper 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 valueConsolidate the near-identical cases into table-driven tests.
TestUpdateUserDefinedAlertRule_BlocksGitOpsManagedandTestUpdateUserDefinedAlertRule_BlocksOperatorManageddiffer only in the managed-by label value and the expected message.TestUpdateUserDefinedAlertRule_PRNotFoundandTestUpdateUserDefinedAlertRule_PRGetErrordiffer only in the mockedGetFuncreturn 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 valueAdd
minProperties: 1toUpdateAlertRuleRequest.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.
validateAlertRuleUpdateFieldsrejects it at runtime with 400. Encode the constraint in the schema to keep the contract self-describing.Note:
UpdateAlertRuleResult.statusCodedeclaresformat: int32whileDeleteAlertRuleResult.statusCode(line 350) does not. This producesint32in one generated struct andintin 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 winConsider rejecting duplicate rule IDs.
The loop processes each entry of
payload.RuleIdsindependently. If the same ID appears twice, the handler applies the mutation twice and returns two result entries with the sameid. 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 valueValidate
ruleIdbefore reading the body.The handler reads and unmarshals the request body, then checks that
ruleIdis non-blank. Move theruleIdcheck 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 winAdd cases for the 405 and 400 branches.
The tests cover 204, 404, and 401.
DeleteAlertRulealso returns 405 when the management client returns aNotAllowedErrorfor a platform or externally managed rule, and 400 whenruleIdis whitespace-only. Both branches are untested here. A whitespace ID such as%20reaches 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 winUse the existing retry wrapper in
mustCreateRule.
createRuleViaAPIWithRetryalready exists at Line 45 for flake tolerance.mustCreateRulecallscreateRuleViaAPIdirectly.TestRBAC_UpdateAlertRulemakes three sequentialmustCreateRulecalls, 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 winAssert that impersonation is cleared.
The doc comment on
buildUserScopedConfigstates that impersonation is stripped. The test does not cover it. AddImpersonateto 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 winMake
retryhonor context cancellation.
retrysleeps for a fixed one second and ignores the caller context.requestServiceAccountTokenandCreateScopedUserboth receive actx, so a canceled or expired context still costs up to two seconds of sleep and two extra API calls. Pass the context intoretryand 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, andCreateAnonymousUserto passctx.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 winReconcile an existing Role and RoleBinding instead of accepting it as-is.
CreateScopedUsertreatsAlreadyExistsas success for theRoleand theRoleBinding. If a previous run left these objects behind with different verbs or resources, the test proceeds with the stale permissions. Every RBAC assertion inTestRBAC_UpdateAlertRule,TestRBAC_DeleteAlertRule, andTestRBAC_CreateAlertRulethen 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 valueReuse the shared
pollhelper.
waitForUpdateCacheSynchand-rolls a deadline loop.test/e2e/helpers_test.goalready exportspoll(interval, timeout, fn), andwaitForCacheSyncintest/e2e/delete_alert_rule_test.gouses 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 winDerive the fixture IDs from one source of truth.
buFixtureIDsredefines the same three rule literals thatnewBUFixturebuilds 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 winAssert the Drop regex against
regexp.QuoteMeta, as the production code produces it.
DropAlertRulesetsRegex: regexp.QuoteMeta(alertRuleId)(seepkg/management/update_platform_alert_rule.goline 342). The test comparesrc.Regex == drPlatformRuleId. The comparison holds only because the current fixture ID contains no regex metacharacter. Compare againstregexp.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
regexpimport.🤖 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 winWrap the
Deleteerror 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 winClarify the four-value return signature and drop the single-element loop.
findARCByAlertRuleIDreturns(string, *osmv1.AlertRelabelConfig, string, error). The twostringresults are the namespace and the ARC name, in that order, and nothing at the call site prevents swapping them. Thenamespacesslice 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 winThese five tests verify
k8s.DetermineManagedBy, notGetRuleById.Each test calls
k8s.DetermineManagedBy, writes the result into the rule throughbuildRuleWithManagedBy, programs the mock to return that rule, and then asserts on the labels of the returned rule.GetRuleByIdonly forwards the cached rule, so the assertions pass or fail based onDetermineManagedByalone. Two consequences follow:
- The managed-by coverage belongs next to
k8s.DetermineManagedByinpkg/k8s.- The five cases differ only in
ObjectMetaand 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 winAdd 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
📒 Files selected for processing (35)
api/openapi.yamldocs/alert-management.mddocs/alert-rule-classification.mdinternal/managementrouter/alert_rule_bulk_update.gointernal/managementrouter/alert_rule_bulk_update_test.gointernal/managementrouter/alert_rule_delete.gointernal/managementrouter/alert_rule_delete_test.gointernal/managementrouter/alert_rule_update.gointernal/managementrouter/alert_rule_update_test.gointernal/managementrouter/api_generated.gointernal/managementrouter/router.gointernal/managementrouter/router_test.gopkg/k8s/const.gopkg/k8s/user_scoped_client.gopkg/k8s/user_scoped_client_test.gopkg/management/alert_rule_preconditions.gopkg/management/client_factory.gopkg/management/delete_user_defined_alert_rule_by_id.gopkg/management/get_rule_by_id.gopkg/management/get_rule_by_id_test.gopkg/management/label_utils.gopkg/management/management.gopkg/management/types.gopkg/management/update_alert_rule_labels.gopkg/management/update_classification.gopkg/management/update_classification_test.gopkg/management/update_platform_alert_rule.gopkg/management/update_platform_alert_rule_test.gopkg/management/update_user_defined_alert_rule.gopkg/management/update_user_defined_alert_rule_test.gotest/e2e/create_alert_rule_test.gotest/e2e/delete_alert_rule_test.gotest/e2e/framework/framework.gotest/e2e/helpers_test.gotest/e2e/update_alert_rule_test.go
- 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>
d71662e to
3f9fdc0
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
test/e2e/framework/poll.go (1)
13-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAccept a
context.Contextparameter instead of usingcontext.Background().
Pollhardcodescontext.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.PollUntilContextTimeoutreturns a context deadline error on timeout, notwait.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
pollwrapper intest/e2e/helpers_test.goand all call sites intest/e2e/framework/framework.go,test/e2e/single_alert_rule_test.go,test/e2e/create_alert_rule_test.go, andtest/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 winRestructure the nested polling in
CreateAnonymousUser.The outer
Polluses a 3-second timeout, andrequestServiceAccountTokenpolls 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
CreateScopedUserdoes, 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 valueRemove the unused
keepIDassignment.
keepIDis discarded with_ = keepID. The later assertion checks the alert nameKeepSingleAlert, 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 winMatch
SourceLabelsinfilterOutDropfor symmetry withensureStampAndDrop.
ensureStampAndDropidentifies the drop entry byAction == "Drop",Regex, andSourceLabels[0] == k8s.AlertRuleLabelId.filterOutDropmatches onlyActionandRegex. A future or externally addedDropentry 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 valueExtract the target-resolution branches from
RestoreAlertRule.The function mixes two resolution strategies with the mutation logic. The pre-declared
errat 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 inRestoreAlertRule.🤖 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 tradeoffConsider a table-driven form for the four platform deletion tests.
TestDeleteAlertRuleById_PlatformRuleNotOperatorManaged,_PlatformRuleGitOpsManaged,_PlatformRuleOperatorManaged, and this new test repeat the sameRelabeledRulesFunc,NamespaceFunc, andPrometheusRulesFuncscaffold. 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
📒 Files selected for processing (24)
api/openapi.yamldocs/alert-management.mddocs/alert-rule-classification.mdinternal/managementrouter/alert_rule_delete_test.gointernal/managementrouter/alert_rule_update.gointernal/managementrouter/alert_rule_update_test.gointernal/managementrouter/api_generated.gointernal/managementrouter/router.gointernal/managementrouter/router_test.gopkg/k8s/user_scoped_client_test.gopkg/management/delete_user_defined_alert_rule_by_id.gopkg/management/delete_user_defined_alert_rule_by_id_test.gopkg/management/types.gopkg/management/update_platform_alert_rule.gopkg/management/update_platform_alert_rule_test.gopkg/management/update_user_defined_alert_rule.gopkg/management/update_user_defined_alert_rule_test.gotest/e2e/create_alert_rule_test.gotest/e2e/delete_alert_rule_test.gotest/e2e/framework/framework.gotest/e2e/framework/poll.gotest/e2e/helpers_test.gotest/e2e/single_alert_rule_test.gotest/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
| **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. | ||
|
|
There was a problem hiding this comment.
📐 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.yamlRepository: 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 -500Repository: 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.
| 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 "" |
There was a problem hiding this comment.
🎯 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.
| 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
| 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) |
There was a problem hiding this comment.
🩺 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.
| 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.
|
@sradco: The following test failed, say
Full PR test history. Your PR dashboard. DetailsInstructions 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. |
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, classificationDELETE /api/v1/alerting/rules/{ruleId}— delete one ruleSingle update reuses the same validation/mutation path as
BulkUpdateAlertRules(addresses prior review ask for one unified update path). Errors returnErrorResponsewith an actionable message (same intent as bulk per-rulemessage).Tests
Docs
docs/alert-management.md— single vs bulk API matrix and response semanticsdocs/alert-rule-classification.md— single PATCH response/error notes aligned with implementationTest plan
go test ./internal/managementrouter/go test -tags e2e ./test/e2e/ -cKeep the bulk PATCH/DELETE /rules APIs and add
per-rule endpoints for easier client use and
reviewability:
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