Skip to main content

Module kube_error

Module kube_error 

Source
Expand description

Substrate primitives over kube::Error — the semantic layer every controller’s match … { Err(kube::Error::Api(e)) if e.code == <N> => … } guard AND every consumer’s .map_err(|e| anyhow::anyhow!("<ctx>: {e}"))? error-wrap restates by hand pre-lift.

Owns two closed-set predicates over the kube::Error::Api sub-variant’s HTTP status code:

  • is_conflict — HTTP 409 (Conflict) — the K8s API server refused a create because a resource with the same key already exists, or refused a patch because of an optimistic-concurrency generation mismatch. Every controller’s create-branch reads this arm as “someone else got here first; treat the intended write as already-done” or “refresh via PATCH”.
  • is_not_found — HTTP 404 (Not Found) — the K8s API server has no resource with the given key. Every controller’s delete-branch reads this arm as “already deleted / never existed; the intended state (absence) is already true”.

Plus the KubeResultExt extension trait on Result<T, kube::Error> that owns the display-prefix wrap the phase-machine / signal-effect / pool-reconciler consumers thread every K8s round-trip through into their anyhow-returning handler:

  • KubeResultExt::kube_ctx — attach a &'static str context slug to a Result<_, kube::Error> and get an anyhow::Result whose Display reads "<ctx>: <kube::Error display>" (byte-identical to the pre-lift .map_err(|e| anyhow!("<ctx>: {e}")) chain).
  • KubeResultExt::kube_ctx_with — owned-String peer for consumers that compose the slug via format! at runtime.

See the trait’s own docstring for the full consumer inventory + the naming rationale (why kube_ctx and not anyhow::Context::context).

Both predicates lift the 2-link matches!(err, kube::Error::Api(e) if e.code == <N>) shape past the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger. Pre-lift the SAME chain was hand-authored at FIVE workspace-wide sites, each interpreting the same HTTP status code with the same semantic:

  • tatara-closed-loop-probe::write_receipt_configmap — 409 arm on api.create(...) → falls through to a merge-patch of the data field so the receipt payload lands idempotently.
  • tatara-github-watcher::handler::handle_pr_event — 404 arm on api.delete(...) → returns 200 OK "allocation already gone" so a closed-PR event is a no-op past the first delivery.
  • tatara-github-watcher::handler::handle_pr_event — 409 arm on api.create(...) → returns 200 OK "allocation already exists (synchronize)" so a re-delivery of an opened PR event maps onto the existing allocation.
  • tatara-pool-reconciler::controller_pool (spawn branch, spawn loop) — 409 arm on process_api.create(...) → treats the race as a successful spawn, incrementing spawned past the arm.
  • tatara-pool-reconciler::controller_pool (desired-loop branch) — 409 arm on process_api.create(...) → treats the race as a no-op so the next reconcile picks up the existing Process.

All FIVE sites walked the SAME two-link shape — destructure the kube::Error::Api sub-variant, guard on e.code == <N> — and interpret the code identically (“write already succeeded” / “delete already succeeded”). The e: ErrorResponse binding is bound but unused at every callsite; the body reads the SEMANTIC (conflict / not-found) rather than the specific fields (e.reason, e.message). Post-lift each callsite reads Err(ref e) if kube_error::is_conflict(e) => { ... } (or is_not_found), and the two-link shape lives at ONE substrate owner.

§Semantic axis (why predicates, not raw codes)

The K8s API server sends the same HTTP status code for a set of semantically identical outcomes (a 404 on get and a 404 on delete both mean “the resource is not present”); it also occasionally sends the same code for OTHER outcomes with subtly different meanings (a 404 on a subresource whose parent exists, for instance). Lifting the raw-code check to a NAMED predicate moves every consumer onto the semantic axis, so a future normalization (a version of is_not_found that also matches kube::Error::Api(ErrorResponse { reason: "NotFound", .. }) for servers that stamp the reason but not the code, or a version of is_conflict that folds the AlreadyExists, Conflict, and generation-mismatch reasons together) lands at THIS ONE substrate owner and every downstream idempotent-write consumer inherits the upgrade mechanically — no per-site edit at any of the FIVE listed callers or at future consumers (an allocation delete-branch, a pool-owned Process reap idempotent gate, a table-controller stale- claim strip that must survive a race with cluster-side GC).

§#[must_use]

Every consumer either drives a match-arm guard on the returned bool or short-circuits a fallthrough branch on it. Dropping the return means the predicate was computed for no observable reason — the attribute surfaces that as a warning at every call site.

Traits§

KubeResultExt
Substrate extension trait over Result<T, kube::Error> — the ONE substrate owner of the .map_err(|e| anyhow::anyhow!("<ctx>: {e}")) wrap-shape every reconciler consumer restates by hand at the K8s round-trip → anyhow error boundary.

Functions§

is_conflict
The kube error names an HTTP 409 Conflict response — a create refused because the resource already exists, or a patch refused because of an optimistic-concurrency generation mismatch.
is_not_found
The kube error names an HTTP 404 Not Found response — the K8s API server has no resource with the given key.