OCPBUGS-47508: Deny superseded duplicate node CSRs and remove approve-stop - #310
OCPBUGS-47508: Deny superseded duplicate node CSRs and remove approve-stop#310RadekManak wants to merge 1 commit into
Conversation
|
@RadekManak: This pull request references Jira Issue OCPBUGS-47508, which is valid. 3 validation(s) were run on this bug
Requesting review from QA contact: The bug has been updated to refer to the pull request using the external bug tracker. DetailsIn response to this:
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 openshift-eng/jira-lifecycle-plugin repository. |
WalkthroughThe controller now detects superseded pending node CSRs, denies older duplicates, excludes terminal CSRs from pending classification, refreshes pending metrics, and exposes duplicate-denial metrics with alerting and documentation. ChangesCSR denial and observability
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Reconcile
participant CSRSelection
participant CertificatesAPI
participant Prometheus
Reconcile->>CSRSelection: identify superseded pending node CSRs
Reconcile->>CertificatesAPI: update superseded CSRs with denial conditions
CertificatesAPI-->>Reconcile: return denial results
Reconcile->>Prometheus: increment duplicate-denial counter
Reconcile->>Prometheus: refresh pending CSR gauges
🚥 Pre-merge checks | ✅ 14 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (14 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@openshift-ci-robot: GitHub didn't allow me to request PR reviews from the following users: sunzhaohua2. Note that only openshift members and repo collaborators can review this PR, and authors cannot review their own PRs. DetailsIn response to this:
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. |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: 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 |
10f47c2 to
0b49426
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
docs/dev/metrics.md (2)
11-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a language to the fenced code block.
Static analysis (MD040) flags this fence as missing a language hint.
📝 Proposed fix
-``` +```text # HELP mapi_current_pending_csr Count of recently pending node CSRs at the cluster level🤖 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/dev/metrics.md` around lines 11 - 21, Update the fenced metrics example in the documentation to specify the text language, using the existing metrics block content unchanged.Source: Linters/SAST tools
18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winHELP text doesn't match the metric's actual
Helpstring.
csr_check.godefinesDuplicateCSRDeniedTotal's help as "Total number of pending node CSRs denied because they were superseded by a newer CSR for the same node and signer", but this line reads "Count of pending node CSRs denied because a newer CSR exists for the same node and signer" — unlike the two lines above it, which match their sourceDesc/CounterOptsverbatim.📝 Proposed fix
-# HELP mapi_duplicate_csr_denied_total Count of pending node CSRs denied because a newer CSR exists for the same node and signer +# HELP mapi_duplicate_csr_denied_total Total number of pending node CSRs denied because they were superseded by a newer CSR for the same node and signer🤖 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/dev/metrics.md` at line 18, Update the mapi_duplicate_csr_denied_total HELP text in the metrics documentation to exactly match the Help string defined for DuplicateCSRDeniedTotal in csr_check.go, including the wording about CSRs being superseded by a newer CSR.pkg/controller/controller.go (2)
389-406: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
deny()usescontext.Background()instead of the caller's context.
UpdateApprovalis a blocking external API call made with no deadline/cancellation tied toReconcile'sctx. ThreadingctxthroughdenySupersededDuplicates→deny()would let this call be cancelled/timed-out consistently with the rest of the reconcile loop.As per path instructions, Go security guidance requires "context.Context for cancellation and timeouts."
♻️ Proposed fix
-func deny(certClient certificatesv1client.CertificateSigningRequestInterface, csr *certificatesv1.CertificateSigningRequest) error { +func deny(ctx context.Context, certClient certificatesv1client.CertificateSigningRequestInterface, csr *certificatesv1.CertificateSigningRequest) error { now := metav1.Now() csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{ Type: certificatesv1.CertificateDenied, Reason: csrConditionDenyReason, Message: csrConditionDenyMessage, LastUpdateTime: now, LastTransitionTime: now, Status: corev1.ConditionTrue, }) - if _, err := certClient.UpdateApproval(context.Background(), csr.Name, csr, metav1.UpdateOptions{}); err != nil { + if _, err := certClient.UpdateApproval(ctx, csr.Name, csr, metav1.UpdateOptions{}); err != nil { return err } DuplicateCSRDeniedTotal.Inc() return nil }Note: this also requires threading
ctxthroughdenySupersededDuplicatesand updating the twodeny(...)call sites incsr_check_test.go.🤖 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/controller/controller.go` around lines 389 - 406, Update deny to accept a context.Context parameter and pass it to UpdateApproval instead of context.Background(). Thread the caller’s ctx through denySupersededDuplicates and both deny call sites in csr_check_test.go, preserving existing denial behavior while enabling cancellation and deadlines.Source: Path instructions
207-217: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winMetrics aren't refreshed when
denySupersededDuplicateserrors, even though the data is already available.
csrs,machines, andnodesare all fetched before this call, sorefreshPendingCSRMetricscould still run on this error path (e.g. viadefer) instead of skipping it entirely. This is the one place the "updates metrics on error paths" goal isn't fully realized.♻️ Proposed fix
+ defer func() { + // csrs reflects approve/deny from this pass, so gauges can use this list + // even if denial or reconciliation returned an error. + refreshPendingCSRMetrics(req.Name, machines, nodes, csrs) + }() + parsedPending, err := m.denySupersededDuplicates(csrs) if err != nil { klog.Errorf("%v: failed to deny superseded CSRs: %v", req.Name, err) return reconcile.Result{}, fmt.Errorf("%v: failed to deny superseded CSRs: %w", req.Name, err) } reconcileErr := m.reconcileNamedCSR(req.Name, csrs, parsedPending, machines) - // csrs reflects approve/deny from this pass, so gauges can use this list. - refreshPendingCSRMetrics(req.Name, machines, nodes, csrs) return reconcile.Result{}, reconcileErr🤖 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/controller/controller.go` around lines 207 - 217, Ensure refreshPendingCSRMetrics runs when denySupersededDuplicates returns an error, using the already fetched csrs, machines, and nodes before returning from the error path. Update the surrounding reconciliation flow so metrics are refreshed exactly once for both success and failure outcomes, without changing the existing error logging or returned error.pkg/controller/csr_check_test.go (1)
2680-2834: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a case for an approved-but-recently-approved CSR to lock in the fix for the major issue in
csr_check.go.Coverage here is solid, but none of the cases exercise a CSR that already carries an
Approvedcondition withinisRecentlyApproved's window alongside a newer duplicate pending CSR — the scenario flagged incsr_check.go'ssupersededPendingNodeCSRsreview comment where such a CSR could wrongly end up inwantDenied.🤖 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/controller/csr_check_test.go` around lines 2680 - 2834, Add a test case to TestSupersededPendingNodeCSRs with an older CSR carrying an Approved condition within the isRecentlyApproved window and a newer duplicate pending CSR; assert that neither CSR is included in wantDenied. Reuse the existing pendingClient fixture and condition setup, preserving the test’s current name-based result comparison.
🤖 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 `@pkg/controller/csr_check.go`:
- Around line 93-134: Update supersededPendingNodeCSRs to exclude CSRs with
terminal Approved, Denied, or Failed conditions before appending indexes to
pendingIdx. Preserve pendingNodeCertFilter for other eligibility checks,
ensuring only non-terminal candidates can reach the superseded list and
subsequent deny flow.
---
Nitpick comments:
In `@docs/dev/metrics.md`:
- Around line 11-21: Update the fenced metrics example in the documentation to
specify the text language, using the existing metrics block content unchanged.
- Line 18: Update the mapi_duplicate_csr_denied_total HELP text in the metrics
documentation to exactly match the Help string defined for
DuplicateCSRDeniedTotal in csr_check.go, including the wording about CSRs being
superseded by a newer CSR.
In `@pkg/controller/controller.go`:
- Around line 389-406: Update deny to accept a context.Context parameter and
pass it to UpdateApproval instead of context.Background(). Thread the caller’s
ctx through denySupersededDuplicates and both deny call sites in
csr_check_test.go, preserving existing denial behavior while enabling
cancellation and deadlines.
- Around line 207-217: Ensure refreshPendingCSRMetrics runs when
denySupersededDuplicates returns an error, using the already fetched csrs,
machines, and nodes before returning from the error path. Update the surrounding
reconciliation flow so metrics are refreshed exactly once for both success and
failure outcomes, without changing the existing error logging or returned error.
In `@pkg/controller/csr_check_test.go`:
- Around line 2680-2834: Add a test case to TestSupersededPendingNodeCSRs with
an older CSR carrying an Approved condition within the isRecentlyApproved window
and a newer duplicate pending CSR; assert that neither CSR is included in
wantDenied. Reuse the existing pendingClient fixture and condition setup,
preserving the test’s current name-based result comparison.
🪄 Autofix (Beta)
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: openshift/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: ed78e3fb-f48d-4fd3-8c79-df485ad5b164
📒 Files selected for processing (6)
docs/dev/metrics.mdmanifests/0000_90_cluster-machine-approver_04_alertrules.yamlpkg/controller/controller.gopkg/controller/csr_check.gopkg/controller/csr_check_test.gopkg/metrics/metrics.go
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/dev/metrics.md`:
- Around line 11-21: Add the text language identifier to the fenced metrics
block in the documentation by changing its opening fence to use text, while
leaving the metrics contents unchanged.
In `@pkg/controller/controller.go`:
- Around line 389-401: Propagate the reconciliation context through
denySupersededDuplicates into deny, updating both method signatures and their
call sites as needed. In deny, replace context.Background() in the
certClient.UpdateApproval call with the received ctx while preserving the
existing denial behavior.
🪄 Autofix (Beta)
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: openshift/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: fc21e132-35e6-463d-b0b0-f5ba8f7f50e9
📒 Files selected for processing (7)
docs/dev/metrics.mdgo.modmanifests/0000_90_cluster-machine-approver_04_alertrules.yamlpkg/controller/controller.gopkg/controller/csr_check.gopkg/controller/csr_check_test.gopkg/metrics/metrics.go
🚧 Files skipped from review as they are similar to previous changes (4)
- manifests/0000_90_cluster-machine-approver_04_alertrules.yaml
- pkg/metrics/metrics.go
- pkg/controller/csr_check.go
- pkg/controller/csr_check_test.go
|
/retest |
ce54b16 to
e373d08
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@pkg/controller/csr_check.go`:
- Around line 100-147: The supersededPendingNodeCSRs path repeatedly parses
unchanged pending CSRs across reconciles, causing excessive x509 work. Add a
cache keyed by CSR name and resourceVersion that stores the parsed CN/signer
result, reuse it from supersededPendingNodeCSRs, and invalidate or replace
entries when the resourceVersion changes while preserving existing handling for
parse failures and non-node CSRs.
🪄 Autofix (Beta)
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: openshift/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: e3d640af-9a48-4ad4-999d-4f6adde9fee9
📒 Files selected for processing (7)
docs/dev/metrics.mdgo.modmanifests/0000_90_cluster-machine-approver_04_alertrules.yamlpkg/controller/controller.gopkg/controller/csr_check.gopkg/controller/csr_check_test.gopkg/metrics/metrics.go
🚧 Files skipped from review as they are similar to previous changes (5)
- docs/dev/metrics.md
- pkg/metrics/metrics.go
- go.mod
- manifests/0000_90_cluster-machine-approver_04_alertrules.yaml
- pkg/controller/csr_check_test.go
Exclude Denied/Failed from the pending filter and count, prune older pending CSRs per (system:node CN, signer), keep approving when pending exceeds max, and alert after 15m of sustained superseded denials.
e373d08 to
dd3812c
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
pkg/controller/controller.go (2)
398-416: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
denydiscards the server-returned updated object; consider naming it for its actual scope.Two small points:
UpdateApproval's return value (which carries the server-assignedResourceVersionafter the write) is discarded, andcsr.Statusis instead spliced from the locally-builtupdatedobject with the pre-updateResourceVersion. Currently harmless sincecsrisn't written again in this pass, but fragile if a future change reuses this object for another update.denyunconditionally incrementsDuplicateCSRDeniedTotal, but the namedenyis generic; if it's ever reused for a different denial reason, the metric would be mislabeled.♻️ Optional tweaks
- if _, err := certClient.UpdateApproval(ctx, updated.Name, updated, metav1.UpdateOptions{}); err != nil { - return err - } - csr.Status = updated.Status + result, err := certClient.UpdateApproval(ctx, updated.Name, updated, metav1.UpdateOptions{}) + if err != nil { + return err + } + *csr = *result🤖 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/controller/controller.go` around lines 398 - 416, Update deny to retain the object returned by UpdateApproval and copy its returned Status onto csr, preserving the server-assigned ResourceVersion. Rename deny to a duplicate-CSR-specific name, or otherwise ensure DuplicateCSRDeniedTotal is only incremented for that denial reason. Update all call sites to use the renamed function.
244-268: 🚀 Performance & Scalability | 🔵 TrivialDuplicate-detection pass re-runs in full on every single CSR reconcile.
denySupersededDuplicatesrecomputessupersededPendingNodeCSRsover the entire CSR list on every reconcile invocation (triggered per watched CSR event). During the exact large scale-up scenario this PR targets, this repeats O(n) grouping/denial work for every one of the n incoming CSR events, layered on top of the existing fulllistNodeCSRsper reconcile. Worth confirming this doesn't reintroduce meaningful overhead or excessive API conflict errors (stale watch-cache resourceVersions) during a scale-up burst, e.g., via rate-limiting/batching the duplicate-denial pass instead of running it unconditionally on every event.🤖 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/controller/controller.go` around lines 244 - 268, Change the reconciliation flow around denySupersededDuplicates so duplicate detection and denial are coalesced or rate-limited instead of running for every CSR event. Ensure a scheduled pass processes the latest CSR state, retains aggregated denial errors, and avoids repeated API conflicts during scale-up while preserving eventual denial of superseded CSRs.
🤖 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.
Nitpick comments:
In `@pkg/controller/controller.go`:
- Around line 398-416: Update deny to retain the object returned by
UpdateApproval and copy its returned Status onto csr, preserving the
server-assigned ResourceVersion. Rename deny to a duplicate-CSR-specific name,
or otherwise ensure DuplicateCSRDeniedTotal is only incremented for that denial
reason. Update all call sites to use the renamed function.
- Around line 244-268: Change the reconciliation flow around
denySupersededDuplicates so duplicate detection and denial are coalesced or
rate-limited instead of running for every CSR event. Ensure a scheduled pass
processes the latest CSR state, retains aggregated denial errors, and avoids
repeated API conflicts during scale-up while preserving eventual denial of
superseded CSRs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 1c977ab5-cc97-4919-848c-e85306844cfe
📒 Files selected for processing (7)
docs/dev/metrics.mdgo.modmanifests/0000_90_cluster-machine-approver_04_alertrules.yamlpkg/controller/controller.gopkg/controller/csr_check.gopkg/controller/csr_check_test.gopkg/metrics/metrics.go
🚧 Files skipped from review as they are similar to previous changes (6)
- manifests/0000_90_cluster-machine-approver_04_alertrules.yaml
- go.mod
- pkg/metrics/metrics.go
- docs/dev/metrics.md
- pkg/controller/csr_check.go
- pkg/controller/csr_check_test.go
|
/test unit |
|
@RadekManak: The following tests 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
On large Machine scale-ups (hundreds–thousands of nodes), Cluster Machine Approver (CMA) would stop approving CSRs entirely. Kubelet abandons a pending CSR after ~15 minutes and issues a new one for the same node; CMA never denied the abandoned ones, so they piled up. Once "recently pending" CSRs crossed
max(machines, nodes) + 100, CMA's safety check stopped reconciling all CSRs — including ones it could otherwise approve — until enough of them aged out of the 1h window or an operator intervened.This PR fixes CMA's side of that: prune the abandoned duplicates as they appear instead of letting them accumulate, and remove the breaker that turned "some stale CSRs" into "no approvals at all." (A companion MAO/MAPA PR raises AWS machine-controller concurrency to 10, so
Machine.status.addressesstops lagging far enough behind instance creation to trigger this in the first place — that's the actual root cause; this PR makes CMA resilient to it regardless.)Changes
Deny superseded duplicates, every reconcile. For each unsigned node CSR, group by
(parsed system:node CN, signerName), keep the newest per group, deny the rest — regardless of whether the newest one is currently approvable. Serving CSRs also requireparsed CN == Spec.Username(same identity bar as approval) before they can keep or supersede a group. This runs unconditionally, so a long CMA outage or address-lag window still cleans itself up once CMA resumes. Individual deny failures are aggregated so a partial prune does not skip the named CSR approval attempt.Remove the approve-stop.
reconcileLimitsno longer short-circuits reconciliation. CMA keeps approving/denying at any pending count; only alerting reacts to "too many pending."Separate wake vs unsigned predicates.
pendingNodeCertFilterstill wakes reconcile for unsigned node CSRs and for recently foreign-approved ones (metrics / catch-up). Supersede selection andrecentlyPendingNodeCSRsuseisUnsignedPendingNodeCSR(!Approved && !Denied && !Failedplus signer/group checks), so Approved/Denied/Failed CSRs cannot enter the deny pool or inflate the pending gauge.New metric + alert:
mapi_duplicate_csr_denied_total. A counter incremented every timedeny()successfully denies a superseded CSR, registered alongside the existing gauges. Backing alert:This is now the primary "CMA is falling behind" signal, replacing the old freeze-and-notice behavior.
for: 15mtolerates a brief burst of denials during normal address-population lag and only pages on sustained pressure.MachineApproverMaxPendingCSRsReached(mapi_current_pending_csr > mapi_max_pending_csr) is kept as-is for non-duplicate overload — pruning + unsigned-only counting now keeps the raw pending count tracking live demand instead of accumulated garbage, so no new "unique pending" metric was needed.Metrics simplification this unlocks
The old code called
reconcileLimitstwice per CSR: once at the top ofReconcile(cached, to decide whether to short-circuit) and once at the end viareconcileLimitsUncached(an extra uncached List, because the just-approved CSR wasn't visible in the cache yet). Removing the short-circuit removes the reason for the first call. And oncedenySupersededDuplicates/reconcileCSRmutate the CSR objects in place in thecsrsslice passed throughReconcile, that local slice already reflects this pass's successful approve/deny by the time metrics are read — so the second call no longer needs to be uncached either:Net effect: one cached metrics update per reconcile (down from a cached check plus an extra uncached List), and it now runs even when deny or approve fails, so the pending count stays accurate instead of waiting for the next reconcile.
Testing
Added table-driven unit tests in
pkg/controller/csr_check_test.go: superseded selection per(CN, signer), Approved/Denied/Failed exclusion from supersede candidates, serving CSRs withCN != Usernameignored for supersede, Denied/Failed exclusion from the pending count, and counter increment on successful deny (no increment whenUpdateApprovalfails). No new envtest/controller harness — full verification is the large scale-up run once this, the MAO concurrency PR, and the MAPA flag PR are all merged.Context & Patterns
Why in-place mutation instead of tracking denied names separately —
denySupersededDuplicatesandreconcileCSRmutate thecertificatesv1.CertificateSigningRequeststructs inside the samecsrsslice passed to the metrics refresh at the end ofReconcile. An earlier version tracked denied names in a side set and re-listed CSRs uncached to get fresh state for metrics. Since the CSR objects already carry their new conditions in place after a successful API update, there's nothing to look up — the slice is the fresh state, so the extra bookkeeping and extra API call both disappear.Why
denydoesn't needapprove's idempotency guard —approvecan be called on a CSR that's already approved (e.g. by another controller), so it checkshasSameStatebefore updating.denyis only ever invoked fromdenySupersededDuplicateson CSRs that just passedisUnsignedPendingNodeCSRin the same pass, so it can't already be in a terminal state — the guard would be dead code.Why
for: 15mon the new alert instead of firing on any denial — a single denied CSR during normal address-population lag is tolerated background noise, not an incident. Requiring 15 minutes of sustained denials (vs. the 5-minute window used for theincrease()itself) distinguishes "CMA briefly pruned one duplicate" from "CMA has been unable to keep up for a while," which is the actual page-worthy condition.Summary by CodeRabbit
MachineApproverDuplicateCSRDenied) when duplicate CSR denials persist for 15 minutes; brief one-off denials are ignored, with guidance on where to investigate.