Skip to content

fix(k8s): retry ResourceQuota admission conflicts instead of treating them as existing objects - #2489

Merged
Brad-Edwards merged 3 commits into
devfrom
fix/k8s-quota-conflict-vs-already-exists
Oct 5, 2026
Merged

Brad-Edwards merged 3 commits into
devfrom
fix/k8s-quota-conflict-vs-already-exists

Conversation

@Brad-Edwards

Copy link
Copy Markdown
Owner

Problem

A CTF participant range failed during concurrent provisioning with Runtime plugin planning failed. GKE audit logs for the plugin namespace at the failure time show:

io.k8s.core.v1.secrets.create  .../secrets/runtime-plugin-pull-<invocation>  code=10
Operation cannot be fulfilled on resourcequotas "plugin-budget": the object has been modified; please apply your changes to the latest version and try again

The API server returns HTTP 409 for both AlreadyExists and a write rejected on an optimistic-concurrency Conflict, here ResourceQuota admission racing when several plugin Jobs and their Secrets are created in one namespace at once. The two are separated only by the Status reason.

  • _pull_secret (runtime plugin image-pull secret) treated every 409 as "already exists" and read the Secret back. After a Conflict nothing was created, so the read 404'd, the invocation reconciler marked planning terminally failed, and the whole range failed.
  • _ensure_sensitive_secret had the same conflation (patching an absent Secret).
  • The Job create path already retried quota conflicts, but only 3 attempts within about 0.3s.

It is a race, so only some ranges failed.

Fix

  • shared/cloud/kubernetes/_helpers.py: is_admission_conflict (409 with Status reason Conflict) and create_with_admission_retry (6 attempts, full-jitter exponential backoff capped 0.1s→1.6s, so racing writers desynchronize).
  • Image-pull secret and sensitive-env secret creates retry admission conflicts; an exhausted Conflict fails loudly and is never treated as "already exists". A real AlreadyExists keeps its existing verify/patch recovery. A 409 with no parseable Status body keeps the historical handling.
  • Job create uses the same budget and jittered backoff (its observe-first logic is unchanged).

Tests

  • tests/engine/services/test_runtime_plugin_pull_secret.py (new; _pull_secret had no direct coverage): classifier distinguishes Conflict/AlreadyExists/missing body; conflicts are retried and never read back; exhausted conflicts raise; an existing matching Secret is reused. Verified the retry test fails without the fix.
  • Sensitive-env secret: Conflict retries the create with no recovery patch; exhausted conflict fails.
  • Job create: four consecutive conflicts (beyond the old budget) now succeed; delays stay within each jitter cap.
  • Runner, plugin, preparation, and launcher suites pass (177); ruff, mypy, bandit, and adr_guard --level ci clean.

… them as existing objects

The API server answers HTTP 409 both for AlreadyExists and for a write it
rejected on an optimistic-concurrency Conflict, notably ResourceQuota admission
when several objects are created in one namespace at once. The runtime plugin
image-pull secret and the sensitive-env secret paths treated every 409 as
'already exists', so a quota Conflict (which created nothing) was followed by a
read/patch of an absent Secret, a 404, and a terminally failed plugin planning
invocation -- failing the whole range when several ranges plan concurrently.

Classify 409s by the Status reason and retry admission Conflicts with bounded
full-jitter exponential backoff (6 attempts) in the image-pull secret, the
sensitive-env secret, and the Job create path (which already retried, but only
3 times within ~0.3s). An exhausted Conflict still fails loudly.
Drops the unreachable trailing raise (and its trailing pragma comment, Sonar
python:S139): the last attempt now propagates any error directly.
@Brad-Edwards
Brad-Edwards merged commit 32f4808 into dev Oct 5, 2026
5 checks passed
@Brad-Edwards
Brad-Edwards deleted the fix/k8s-quota-conflict-vs-already-exists branch October 5, 2026 07:18
@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant