ISR-11280 Fix unbounded Role status.conditions growth and GrantStatement REVOKE-without-regrant bug#13
Merged
unni-facets merged 2 commits intoJul 22, 2026
Conversation
appendRoleStatusCondition trimmed conditions to the last 5 entries into a local variable (roleStatusConditions) but then appended the new condition onto the original, untrimmed role.Status.Conditions field. The trim was silently discarded every time, so conditions accumulated forever instead of being capped at 5. In production this caused individual Role objects to grow to 8,000+ conditions (900KB+), exceeding etcd's request size limit and making every subsequent status update fail with "etcdserver: request is too large" - which in turn caused the controller to retry rapidly, making the object grow even faster. Fix: append onto the already-trimmed roleStatusConditions instead of the untrimmed role.Status.Conditions, matching the pattern already used correctly in GrantReconciler.appendGrantStatusCondition and GrantStatementReconciler.appendGrantStatementStatusCondition.
…E ALL The skip-if-unchanged check (isLastConditionSuccess + !hasGrantStatementChanged) was evaluated against the manager's cached client. A reconcile that had just executed REVOKE ALL could be immediately followed by another reconcile reading the pre-revoke cached status, which incorrectly concluded nothing had changed and skipped re-applying the declared GRANT statements - leaving the role with zero privileges until the GrantStatement's spec changed again. Root-caused during a 21 July 2026 production incident: canvas-server-user lost all privileges on common_auth after its GrantStatement controller reconciled for the first time in months (once a stuck connectivity issue was resolved), hitting exactly this REVOKE-then-skip sequence. Fix: read the GrantStatement live via the manager's APIReader (bypassing the cache) before making the skip decision, so it always reflects the latest status.
unni-facets
approved these changes
Jul 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related reliability bugs found and fixed during a production incident investigation on 21-22 July 2026:
1.
RoleReconciler- unboundedstatus.conditionsgrowthappendRoleStatusConditioninrole_controller.gocomputes a trimmed slice (last 5 conditions) into a local variable, but then appends the new condition onto the original, untrimmedrole.Status.Conditionsfield - silently discarding the trim on every call.Roleobject accumulate 8,000+ conditions (900KB+), exceeding etcd's request size limit. Every subsequent status write then failed withetcdserver: request is too large, and the resulting error-driven requeue caused the object to grow even faster - a self-reinforcing loop that never recovers on its own.GrantReconciler.appendGrantStatusCondition,GrantStatementReconciler.appendGrantStatementStatusCondition) already implement this trim correctly - this PR bringsRoleReconcilerin line with that existing, correct pattern.2.
GrantStatementReconciler- skips re-GRANT after REVOKE ALL on a stale cache readisLastConditionSuccess(...) && !hasGrantStatementChanged(...)) was evaluated against the manager's cached client.database-operatorKubernetes Controller #1 above was fixed and several long-stuckRole/GrantStatementobjects finally became reconcilable again, a reconcile that had just executedREVOKE ALLcould be immediately followed by another reconcile reading the pre-revoke cached status, which incorrectly concluded nothing had changed and skipped re-applying the declaredGRANTstatements - leaving the role with zero privileges until theGrantStatement's spec changed again.canvas-server-userlost all privileges oncommon_authafter itsGrantStatementcontroller reconciled for the first time in months (once a stuck connectivity issue was resolved), hitting exactly this REVOKE-then-skip sequence. The application failed withpermission denied for table instancesas a direct result.Fix
appendRoleStatusCondition: append onto the already-trimmedroleStatusConditionslocal variable instead of the untrimmedrole.Status.Conditionsfield.GrantStatementReconcilernow reads theGrantStatementobject live via the manager'sAPIReader(bypassing the cache) before making the skip decision, so the skip check always reflects the latest status rather than a potentially stale, pre-revoke cached copy.Test plan
kubectl get roles.postgresql.facets.cloud -Ain a live cluster that multipleRoleobjects had condition counts in the 8,000-8,900 range and object sizes near 1MB..status.conditionson the affected objects (via a direct PATCH to the/statussubresource) to unblock reconciliation immediately.REVOKE ALL ...immediately followed by"already been successfully executed, skipping execution"for the same object) before applying the fix.go build, butclient.Reader/client.Clientand the[]metav1.Conditionswap cannot introduce a compile error).len(role.Status.Conditions) <= 6after repeated calls toappendRoleStatusConditionwith alternating reasons.GrantStatementReconciler.Reconcilethat exercises the REVOKE-then-reconcile-again sequence against a stale cache, once test scaffolding (flag registration, fake DB) exists for this controller.🤖 Generated with Claude Code