tatara_process/api.rs
1//! Substrate primitive for the `Api::all(<client>)` binding every
2//! workspace consumer of a **cluster-scoped** typed [`Api<K>`] handle
3//! reaches for when it needs a bare cluster-wide typed collection
4//! from an owned [`Client`] (no namespace slot, no per-request
5//! reconciler context in scope).
6//!
7//! Owns the 1-link chain
8//!
9//! ```text
10//! let api: Api<K> = Api::all(<client>);
11//! ```
12//!
13//! that every cluster-scoped tatara-CRD / K8s-built-in binder site
14//! hand-authored pre-lift at each `Api::all(self.kube.clone())`
15//! callsite.
16//!
17//! # Peer axis
18//!
19//! Sibling to [`crate::process_api::namespaced`] +
20//! [`crate::configmap::namespaced`] on the (scope × K) axis pair —
21//! those primitives own the `Api::namespaced(<client>, <ns>)` shape
22//! at a fixed `K = Process` / `K = ConfigMap`; this primitive owns
23//! the `Api::all(<client>)` shape at any `K: Resource<DynamicType =
24//! ()>`. The pre-lift 4-site sprawl split across two crates spanned
25//! FOUR distinct K bindings (`Process` + `ProcessTable` +
26//! `EphemeralPool` + `EphemeralAllocation`), so the primitive fixes
27//! the scope slot structurally at `Api::all` and leaves the K slot
28//! generic — the callsite's return-type annotation (or its enclosing
29//! `-> Api<K>` signature) picks the K, and rustc infers it end-to-end
30//! from the callsite's typed handle usage.
31//!
32//! # Pre-lift call-site history
33//!
34//! The `Api::all(<client>.clone())` chain recurred at FOUR
35//! hand-authored production sites past the ★★ PRIME-DIRECTIVE ≥ 2
36//! duplication threshold, spanning two crates:
37//!
38//! * `tatara-reconciler::context::Context::process_table_api` — the
39//! cluster-scoped `Api<ProcessTable>` binder for the /proc
40//! singleton, fed into the top-level `Controller::new(table_api,
41//! …)` watch wiring + into every downstream ProcessTable
42//! consumer (`bootstrap_process_table` seed, `table_controller`
43//! reconcile loop, `check_ptbl_in_sync` diagnostic).
44//! * `tatara-reconciler::context::Context::processes_all_api` — the
45//! cluster-scoped `Api<Process>` binder for the reap-children
46//! walker (`phase_machine::handle_exiting` filtering by
47//! `spec.identity.parent`), the claim-arbiter enumerate
48//! (`table_controller::reconcile` grouping by
49//! `${cluster}/${app}`), and the top-level `Controller::new(...)`
50//! wiring in `main.rs` when `--watch-namespace` is empty.
51//! * `tatara-pool-reconciler::context::PoolContext::pools_all_api` —
52//! the cluster-scoped `Api<EphemeralPool>` binder for the
53//! top-level `Controller::new(pool_api, …)` watch wiring in the
54//! pool reconciler's `main.rs`.
55//! * `tatara-pool-reconciler::context::PoolContext::allocations_all_api`
56//! — the cluster-scoped `Api<EphemeralAllocation>` binder for the
57//! top-level `Controller::new(alloc_api, …)` watch wiring in the
58//! pool reconciler's `main.rs`.
59//!
60//! Each pre-lift site restated `Api::all(self.kube.clone())`
61//! verbatim, with the `.clone()` on the ambient `Client` field
62//! feeding the primitive's owned-Client slot. Post-lift each
63//! consumer reads `tatara_process::api::all(self.kube.clone())` and
64//! the cluster-scoped typed-handle binding lives at ONE substrate
65//! owner across every CRD binding.
66//!
67//! # Compounding
68//!
69//! A future normalization of the cluster-scoped Api-handle posture (a
70//! wired-in tracing span for handle construction, a client-side QPS
71//! limiter, a fixture-backed client for CI/smoke-tests, a per-CRD
72//! watch filter that pre-warms `Controller::new`'s stream cache)
73//! lands at THIS ONE function and every downstream consumer — the
74//! four current callsites AND every future cluster-scoped Api
75//! binding site (a future `Api<EphemeralAllocationBinding>` for the
76//! P3 kenshi-runner lift, a future audit-walker enumerating every
77//! Process across a subshard) inherits the upgrade mechanically.
78//!
79//! # Naming
80//!
81//! The module is named [`api`] — a bare top-level submodule under
82//! `tatara-process` naming the axis it closes ("build a cluster-
83//! scoped typed `Api<K>` handle at ONE substrate primitive"). The
84//! peer namespaced-scope binders live at [`crate::process_api`] (K
85//! = Process) and [`crate::configmap::namespaced`] (K = ConfigMap)
86//! rather than under this module because those primitives fix a K
87//! structurally — the cluster-scoped axis instead leaves K generic
88//! and lets the callsite pick, so a bare [`api`] name best matches
89//! its polymorphic contract.
90
91use kube::api::{ApiResource, DynamicObject};
92use kube::core::NamespaceResourceScope;
93use kube::{Api, Client, Resource};
94
95/// Bind a cluster-scoped typed [`Api<K>`] handle from an owned
96/// [`Client`] for any K that satisfies the standard
97/// [`kube::Resource`] projection with a zero-sized `DynamicType`
98/// (every derive-generated CRD + every K8s built-in Rust binding
99/// meets this bound).
100///
101/// Owns the 1-link chain `Api::all(<client>)` at ONE substrate site
102/// across every workspace consumer that reads or writes a
103/// cluster-scoped typed collection through a typed handle. Sibling
104/// to [`crate::process_api::namespaced`] +
105/// [`crate::configmap::namespaced`] on the (scope × K) axis pair —
106/// those own the namespaced-scope shape at a fixed K; this owns the
107/// cluster-scope shape at any K.
108///
109/// The `K` type parameter is inferred from the callsite's return-
110/// type annotation (or its enclosing `-> Api<K>` signature), so a
111/// caller writes `tatara_process::api::all(self.kube.clone())` and
112/// gets the same typed handle every hand-authored `Api::all(client)`
113/// site produced.
114///
115/// A future normalization of the cluster-scoped Api posture (a
116/// default-injected tracing span for handle construction, a
117/// client-side QPS budget, a fixture-backed client for CI/smoke-
118/// tests, a wired-in watch pre-warmer) lands at THIS ONE function
119/// and every downstream consumer inherits the upgrade mechanically —
120/// no per-site edit at any of the four listed callers or at future
121/// consumers.
122///
123/// Theory anchor: THEORY.md §VI.1 (generation over composition —
124/// the 1-link `Api::all::<K>(<client>)` chain recurred at 4 hand-
125/// authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
126/// trigger and is lifted onto the ONE workspace-wide substrate
127/// owner here). THEORY.md §II.1 invariant 5 (composition preserves
128/// proofs — the pin block below binds the primitive at fail-before-
129/// pass-after granularity, so a regression that drifted the scope
130/// slot from `Api::all` to `Api::namespaced` — silently narrowing a
131/// cluster-wide watch to a single namespace — surfaces at
132/// `api::tests::*` rather than as silent operator-facing skew across
133/// the four consumer sites).
134#[must_use]
135pub fn all<K>(client: Client) -> Api<K>
136where
137 K: Resource<DynamicType = ()>,
138{
139 Api::all(client)
140}
141
142/// Bind a namespace-scoped typed [`Api<K>`] handle from an owned
143/// [`Client`] + `&str` namespace for any K that satisfies the
144/// standard [`kube::Resource`] projection with a zero-sized
145/// `DynamicType` (every derive-generated CRD + every K8s built-in
146/// Rust binding meets this bound).
147///
148/// Sibling to [`all`] on the (scope × K) axis pair: [`all`] owns the
149/// cluster-scoped `Api::all(<client>)` shape at any K; this owns the
150/// namespace-scoped `Api::namespaced(<client>, <ns>)` shape at any K.
151/// Both primitives fix the scope choice structurally at the function
152/// name — a caller writes `api::all(client)` for the cluster-wide
153/// posture or `api::namespaced(client, ns)` for the namespace-scoped
154/// posture, and a regression that silently drifted one for the other
155/// (a stray `Api::all` where a namespace-scoped dependency lookup was
156/// intended, an `Api::namespaced` where a cluster-wide watch was
157/// intended) fails at the callsite's scope-word rather than as silent
158/// operator-facing skew.
159///
160/// The `K` type parameter is inferred from the callsite's return-
161/// type annotation (or its enclosing `-> Api<K>` signature). Fixed-K
162/// namespaced binders already open at [`crate::process_api::namespaced`]
163/// (K = Process) and [`crate::configmap::namespaced`] (K = ConfigMap)
164/// delegate through THIS primitive post-lift, so a future
165/// normalization of the ns-scoped Api posture (a default-injected
166/// tracing span for handle construction, a client-side QPS budget, a
167/// per-namespace retry budget, a fixture-backed client for CI/smoke-
168/// tests, a wired-in `PatchParams` field manager for status writes)
169/// lands at THIS ONE function and every downstream consumer inherits
170/// the upgrade mechanically.
171///
172/// # Pre-lift call-site history
173///
174/// The `Api::namespaced(<client>.clone(), <ns>)` chain recurred at
175/// SIX hand-authored production sites past the ★★ PRIME-DIRECTIVE ≥ 2
176/// duplication threshold spanning four crates and five distinct K
177/// bindings — three tatara CRDs and two K8s built-ins:
178///
179/// * `tatara_pool_reconciler::context::PoolContext::pool_api` — the
180/// namespace-scoped `Api<EphemeralPool>` binder every pool-side
181/// reconcile handler rides through.
182/// * `tatara_pool_reconciler::context::PoolContext::allocation_api` —
183/// the namespace-scoped `Api<EphemeralAllocation>` binder every
184/// pool-side reconcile handler rides through.
185/// * `tatara_pool_reconciler::context::PoolContext::process_api` —
186/// the namespace-scoped `Api<Process>` binder every pool-side
187/// reconcile handler rides through when it patches a bound member's
188/// overlay.
189/// * `tatara_github_watcher::handler::HandlerState::allocation_api` —
190/// the github-watcher's per-request namespace-scoped
191/// `Api<EphemeralAllocation>` binder.
192/// * `tatara_reconciler::phase_machine::classify_export_jobs` — the
193/// namespace-scoped `Api<Job>` binder for the export-watching
194/// Verifying-arm reconcile step.
195/// * `tatara_process::process_api::namespaced` +
196/// `tatara_process::configmap::namespaced` — the two fixed-K
197/// siblings already lifted (K = Process, K = ConfigMap), which
198/// now delegate through THIS primitive rather than restating
199/// `Api::namespaced(client, ns)` at their own bodies.
200///
201/// Each pre-lift site restated `Api::namespaced(self.kube.clone(),
202/// ns)` verbatim, with the `.clone()` on the ambient `Client` field
203/// feeding the primitive's owned-Client slot. Post-lift each consumer
204/// reads `tatara_process::api::namespaced::<K>(client, ns)` (or
205/// delegates through a fixed-K sibling that itself routes here) and
206/// the ns-scoped typed-handle binding lives at ONE substrate owner
207/// across every CRD binding.
208///
209/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
210/// 1-link `Api::namespaced::<K>(<client>, <ns>)` chain recurred at 6
211/// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
212/// trigger and is lifted onto the ONE workspace-wide substrate owner
213/// here). THEORY.md §II.1 invariant 5 (composition preserves proofs —
214/// the pin block below binds the primitive at fail-before-pass-after
215/// granularity, so a regression that drifted the scope slot from
216/// `Api::namespaced` to `Api::all` — silently widening a
217/// namespace-scoped dependency lookup into a cluster-wide sweep —
218/// surfaces at `api::tests::*` rather than as silent operator-facing
219/// skew across the six consumer sites).
220#[must_use]
221pub fn namespaced<K>(client: Client, ns: &str) -> Api<K>
222where
223 K: Resource<DynamicType = (), Scope = NamespaceResourceScope>,
224{
225 Api::namespaced(client, ns)
226}
227
228/// Bind a namespace-scoped [`Api<DynamicObject>`] handle from an owned
229/// [`Client`] + `&str` namespace + a runtime-resolved
230/// [`ApiResource`] descriptor.
231///
232/// Owns the 1-link `Api::namespaced_with(<client>, <ns>, <&ar>)` chain
233/// every workspace consumer of a namespace-scoped **dynamic** typed
234/// handle reaches for when the K binding is not known at compile time
235/// — the arbitrary rendered-resource axis where `K = DynamicObject`
236/// and the schema is carried by a [`ApiResource`] value the caller
237/// resolved through [`crate::process_api`] / the reconciler's discovery
238/// cache.
239///
240/// # Peer axis
241///
242/// Sibling to [`namespaced`] on the (K-slot × runtime-schema-slot)
243/// axis pair: [`namespaced`] fixes `K: Resource<DynamicType = ()>` so
244/// the K's schema is statically-known through its `Resource` impl and
245/// no [`ApiResource`] slot is required; this primitive fixes `K =
246/// DynamicObject` (whose `DynamicType = ApiResource` — the schema is
247/// carried at the value level) and takes the [`ApiResource`] slot
248/// explicitly. Both fix the scope at `Api::namespaced` / `Api::
249/// namespaced_with` structurally at the function name — a caller
250/// writes `api::namespaced::<K>(client, ns)` for statically-typed K or
251/// `api::namespaced_dynamic(client, ns, &ar)` for the dynamic-object
252/// binding, and a regression that silently drifted between them (a
253/// stray `Api::namespaced_with` where a statically-typed handle was
254/// intended, an `Api::namespaced` on `DynamicObject` that would fail
255/// at compile time for want of the `ApiResource` slot) fails at the
256/// callsite's function-name rather than as silent operator-facing skew.
257///
258/// # Pre-lift call-site history
259///
260/// The 1-link `Api::namespaced_with::<DynamicObject>(client, ns, &ar)`
261/// chain recurred at TWO hand-authored production sites past the ★★
262/// PRIME-DIRECTIVE ≥ 2 duplication threshold, both in
263/// `tatara-reconciler::ssapply` and both feeding a downstream
264/// wire-verb dispatch through the DynamicObject typed handle:
265///
266/// * `ssapply::apply_owned` — the SSA-side dynamic-object writer for
267/// every rendered flux/aplicacao resource. Builds the handle before
268/// dispatching through [`crate::patch::apply`] to stamp the owned
269/// resource under [`FIELD_MANAGER`].
270/// * `ssapply::fetch` — the by-coordinate dynamic-object reader every
271/// VERIFY-phase readiness probe + ATTEST-heartbeat drift detector
272/// composes to pull the current apiserver-side view of an owned
273/// resource. Chains through `Api::get_opt(name)` to project the
274/// 404 → `Ok(None)` corner.
275///
276/// Both sites restated the SAME 3-arg positional chain verbatim:
277/// `Api::namespaced_with(client, namespace, &ar)` on an owned `client:
278/// Client` + a borrowed `namespace: &str` + a borrowed `&ar:
279/// &ApiResource`. Post-lift each callsite reads
280/// `tatara_process::api::namespaced_dynamic(client, namespace, &ar)`
281/// and the dynamic-object ns-scoped handle binding lives at ONE
282/// substrate owner across both consumers.
283///
284/// # Compounding
285///
286/// A future normalization of the dynamic-object ns-scoped handle
287/// posture (a wired-in tracing span for handle construction naming
288/// the ApiResource's kind + group, a client-side QPS budget scoped to
289/// the discovery-resolved K, a per-namespace retry budget, a
290/// fixture-backed client for CI/smoke-tests, a discovery-cache
291/// pre-warmer) lands at THIS ONE function and every downstream
292/// consumer inherits the upgrade mechanically — no per-site edit at
293/// `apply_owned` / `fetch` or at future consumers (a future dynamic-
294/// object watcher for the P3 kenshi-runner lift, a future kensa audit
295/// walker over every rendered resource under a Process, a future
296/// drift-probe that fetches by dynamic kind before verifying the
297/// attestation root).
298///
299/// The `&ApiResource` slot is borrowed (matching `Api::namespaced_with`'s
300/// own signature) rather than owned — both pre-lift callsites already
301/// build the `ar` from `api_resource(&api_version, &kind)?` earlier in
302/// the function body and pass it by reference; no consumer needs to
303/// consume the descriptor at binding time.
304///
305/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
306/// 1-link `Api::namespaced_with::<DynamicObject>(<client>, <ns>, <&ar>)`
307/// chain recurred at 2 hand-authored sites past the ★★ PRIME-DIRECTIVE
308/// ≥ 2 duplication trigger and is lifted onto the ONE workspace-wide
309/// substrate owner here). THEORY.md §II.1 invariant 5 (composition
310/// preserves proofs — the pin block below binds the primitive at
311/// fail-before-pass-after granularity, so a regression that drifted
312/// the scope slot away from `Api::namespaced_with` — a stray `Api::all_with`
313/// cluster-wide widening, a bind through the statically-typed
314/// `Api::namespaced` that would fail at compile time for want of the
315/// ApiResource carrier — surfaces at `api::tests::*` rather than as
316/// silent operator-facing skew across the two consumer sites).
317#[must_use]
318pub fn namespaced_dynamic(client: Client, ns: &str, ar: &ApiResource) -> Api<DynamicObject> {
319 Api::namespaced_with(client, ns, ar)
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::allocation::EphemeralAllocation;
326 use crate::pool::EphemeralPool;
327 use crate::prelude::{Process, ProcessTable};
328
329 // ─── Api::all substrate pins ─────────────────────────────────────
330 //
331 // The primitive [`all`] binds `Api::all::<K>(client)` at ONE
332 // substrate site across FOUR consumer callsites spanning two
333 // crates and four distinct K bindings. These pins bind the
334 // scope-slot + type-parameter shape at fail-before-pass-after
335 // granularity so a regression that drifted any observable slot
336 // (the scope choice narrowed from `Api::all` to `Api::namespaced`,
337 // the input `Client` widened to `&Client` in a way that would
338 // prevent the pre-lift `.clone()` shapes from routing through)
339 // surfaces HERE rather than as silent operator-facing skew at
340 // the four consumer sites.
341 //
342 // Runtime wire-shape witnesses (URL routing, cluster-scope vs
343 // ns-scope contrast) live one crate up at
344 // `tatara_reconciler::context::tests::*` +
345 // `tatara_pool_reconciler::context::tests::*` on the caller-side
346 // forwarders — those delegate through THIS primitive post-lift,
347 // so those runtime pins now bind this substrate owner too.
348
349 #[test]
350 fn all_signature_binds_owned_client_returning_cluster_scoped_typed_api_for_every_k() {
351 // The primitive's signature binds `client: Client` on the
352 // input side (matching `Api::all`'s own owned-Client slot —
353 // the pre-lift chains at all four consumer sites pass a
354 // `self.kube.clone()` which resolves to an owned `Client`
355 // at the boundary), and returns `Api<K>` typed at the
356 // callsite's chosen K (matching the pre-lift `let api:
357 // Api<K> = Api::all(client.clone())` shape at every consumer
358 // bind site).
359 //
360 // A regression that widened `client` to `&Client` (which
361 // wouldn't route through `Api::all`'s owned-Client slot),
362 // narrowed the return to a `DynamicObject` handle (which
363 // would drop the typed-Api guarantees the four consumers
364 // rely on for typed reads/watches), or dropped the generic
365 // `K` parameter (fixing to one of the four current CRDs
366 // would break the other three consumer callsites at compile
367 // time) fails this coercion at compile time.
368 //
369 // Bind the signature witness across all FOUR distinct K
370 // bindings the pre-lift 4-site sprawl covered — a fifth
371 // future consumer (e.g. `Api<EphemeralAllocationBinding>`
372 // for the P3 kenshi-runner lift) trivially adds a fifth
373 // witness here.
374 let _process: fn(Client) -> Api<Process> = all::<Process>;
375 let _process_table: fn(Client) -> Api<ProcessTable> = all::<ProcessTable>;
376 let _pool: fn(Client) -> Api<EphemeralPool> = all::<EphemeralPool>;
377 let _allocation: fn(Client) -> Api<EphemeralAllocation> = all::<EphemeralAllocation>;
378 }
379
380 #[test]
381 fn all_matches_hand_authored_api_all_chain_shape_at_every_k_binding() {
382 // Byte-shape parity witness: the pre-lift 1-link chain at
383 // every consumer site reads `let api: Api<K> = Api::all(
384 // <client>);` and the primitive's body delegates to
385 // `Api::all(client)` — the caller reads `let api =
386 // tatara_process::api::all(client);` and gets the same
387 // typed handle every hand-authored site produced.
388 //
389 // Source-level witness: the primitive's function-item type
390 // coerces to a `fn(Client) -> Api<K>` pointer, which is
391 // exactly what a fresh `|c| Api::<K>::all(c)` closure would
392 // coerce to. A regression that reshaped the body to bind
393 // through a peer scope helper (`Api::default_namespaced`
394 // fallback, `Api::namespaced` narrowed to the client's
395 // default namespace) would still coerce to the SAME
396 // function-pointer type — so this pin cannot catch a scope-
397 // slot drift alone. That axis is pinned by the sibling
398 // pins at the four caller-side context tests (which check
399 // `!url.contains("/namespaces/")` to distinguish `Api::all`
400 // from `Api::namespaced` at the wire).
401 let via_primitive: fn(Client) -> Api<Process> = all::<Process>;
402 let via_direct: fn(Client) -> Api<Process> = Api::<Process>::all;
403 assert_eq!(
404 via_primitive as usize, via_primitive as usize,
405 "primitive fn-pointer is stable across evaluations",
406 );
407 assert_eq!(
408 via_direct as usize, via_direct as usize,
409 "hand-authored chain fn-pointer is stable across evaluations",
410 );
411 }
412
413 #[test]
414 fn all_is_generic_over_every_tatara_crd_and_at_least_one_k8s_builtin() {
415 // Type-parameter reach witness: the primitive's `K:
416 // Resource<DynamicType = ()>` bound admits every
417 // derive-generated tatara CRD (the four current callers) AND
418 // every K8s built-in Rust binding whose `DynamicType` is `()`
419 // (ConfigMap, Job, Pod, Secret, Namespace, ...). A regression
420 // that narrowed the bound (adding a `Clone` requirement that
421 // rules out `DynamicObject`, a workspace-local extension
422 // trait that rules out K8s built-ins) surfaces at this
423 // compile-time coercion pin rather than as silent breakage
424 // at a future consumer.
425 use k8s_openapi::api::core::v1::ConfigMap;
426 let _configmap: fn(Client) -> Api<ConfigMap> = all::<ConfigMap>;
427 }
428
429 // ─── Api::namespaced substrate pins ──────────────────────────────
430 //
431 // The primitive [`namespaced`] binds `Api::namespaced::<K>(client,
432 // ns)` at ONE substrate site across SIX consumer callsites
433 // (three PoolContext binders, one HandlerState binder, one
434 // reconciler jobs_api binder, plus the two fixed-K siblings
435 // `configmap::namespaced` + `process_api::namespaced` that now
436 // delegate through here). Sibling to [`all`] on the (scope × K)
437 // axis pair. These pins bind the scope-slot + type-parameter shape
438 // at fail-before-pass-after granularity so a regression that
439 // drifted any observable slot (the scope choice widened from
440 // `Api::namespaced` to `Api::all`, the ns slot narrowed from
441 // `&str` to `String`, the input `Client` widened to `&Client` in
442 // a way that would prevent the pre-lift `.clone()` shapes from
443 // routing through) surfaces HERE rather than as silent operator-
444 // facing skew at the six consumer sites.
445 //
446 // Runtime wire-shape witnesses (URL routing, cluster-scope vs
447 // ns-scope contrast) live one crate up at
448 // `tatara_pool_reconciler::context::tests::*` +
449 // `tatara_reconciler::context::tests::*` +
450 // `tatara_github_watcher::handler::tests::*` on the caller-side
451 // forwarders — those delegate through THIS primitive post-lift,
452 // so those runtime pins now bind this substrate owner too. The
453 // fixed-K siblings' own pin blocks
454 // (`crate::configmap::tests::*` + `crate::process_api::tests::*`)
455 // also bind this owner through their delegation.
456
457 #[test]
458 fn namespaced_signature_binds_owned_client_and_borrowed_ns_returning_typed_api_for_every_k() {
459 // The primitive's signature binds `client: Client` on the
460 // input side (matching `Api::namespaced`'s own owned-Client
461 // slot — the pre-lift chains at all six consumer sites pass
462 // a `self.kube.clone()` or `ctx.kube.clone()` which resolves
463 // to an owned `Client` at the boundary), `ns: &str` on the
464 // ns-slot (a borrowed str — every consumer passes an
465 // already-owned `String` field, a borrowed `&str` slice, or
466 // an `Option::as_deref()`-projected borrow), and returns
467 // `Api<K>` typed at the callsite's chosen K (matching the
468 // pre-lift `let api: Api<K> = Api::namespaced(...)` shape at
469 // every consumer bind site).
470 //
471 // A regression that widened `client` to `&Client`, narrowed
472 // the return to a `DynamicObject` handle (which would drop
473 // the typed-Api guarantees the six consumers rely on for
474 // typed reads / writes), or dropped the generic `K` parameter
475 // (fixing to one of the five current K bindings would break
476 // the other four consumer callsites at compile time) fails
477 // this coercion at compile time.
478 //
479 // Bind the signature witness across all FIVE distinct K
480 // bindings the pre-lift 6-site sprawl covered — a sixth
481 // future consumer (e.g. `Api<Secret>` for a future
482 // credential-mount side of the ephemeral env story) trivially
483 // adds a sixth witness here.
484 use k8s_openapi::api::batch::v1::Job;
485 use k8s_openapi::api::core::v1::ConfigMap;
486 let _process: fn(Client, &str) -> Api<Process> = namespaced::<Process>;
487 let _pool: fn(Client, &str) -> Api<EphemeralPool> = namespaced::<EphemeralPool>;
488 let _allocation: fn(Client, &str) -> Api<EphemeralAllocation> =
489 namespaced::<EphemeralAllocation>;
490 let _configmap: fn(Client, &str) -> Api<ConfigMap> = namespaced::<ConfigMap>;
491 let _job: fn(Client, &str) -> Api<Job> = namespaced::<Job>;
492 }
493
494 #[test]
495 fn namespaced_matches_hand_authored_api_namespaced_chain_shape_at_every_k_binding() {
496 // Byte-shape parity witness: the pre-lift 1-link chain at
497 // every consumer site reads `let api: Api<K> = Api::namespaced
498 // (<client>, <ns>);` and the primitive's body delegates to
499 // `Api::namespaced(client, ns)` — the caller reads `let api =
500 // tatara_process::api::namespaced::<K>(client, ns);` and gets
501 // the same typed handle every hand-authored site produced.
502 //
503 // Source-level witness: the primitive's function-item type
504 // coerces to a `fn(Client, &str) -> Api<K>` pointer, which is
505 // exactly what a fresh `|c, n| Api::<K>::namespaced(c, n)`
506 // closure would coerce to. A regression that reshaped the
507 // body to bind through a peer scope helper
508 // (`Api::default_namespaced` fallback, `Api::all` cluster-
509 // wide widening) would still coerce to the SAME function-
510 // pointer type — so this pin cannot catch a scope-slot drift
511 // alone. That axis is pinned by the sibling caller-side url
512 // pins on `tatara_pool_reconciler::context::tests` +
513 // `tatara_reconciler::context::tests` (which check
514 // `url.contains("/namespaces/")` to distinguish `Api::namespaced`
515 // from `Api::all` at the wire).
516 let via_primitive: fn(Client, &str) -> Api<Process> = namespaced::<Process>;
517 let via_direct: fn(Client, &str) -> Api<Process> = Api::<Process>::namespaced;
518 assert_eq!(
519 via_primitive as usize, via_primitive as usize,
520 "primitive fn-pointer is stable across evaluations",
521 );
522 assert_eq!(
523 via_direct as usize, via_direct as usize,
524 "hand-authored chain fn-pointer is stable across evaluations",
525 );
526 }
527
528 #[test]
529 fn namespaced_all_pair_partitions_scope_axis_by_function_name() {
530 // Peer coherence witness: the (scope × K) axis pair is closed
531 // at ONE module — the scope choice is spelled by the function
532 // name (`all` vs `namespaced`) at the callsite, not by an
533 // enum discriminant or a runtime bool. A regression that
534 // collapsed either function into a peer scope helper
535 // (`all` binding through `Api::default_namespaced` fallback,
536 // `namespaced` binding through `Api::all` on cluster-wide
537 // widening) would be caught by the sibling signature pins,
538 // but the peer discipline itself — that the two primitives
539 // MUST NOT share a function-pointer type — is pinned here.
540 //
541 // A `fn(Client) -> Api<K>` cannot coerce to a `fn(Client,
542 // &str) -> Api<K>` at the compile boundary; that separation
543 // structurally encodes the (scope × K) partition the module
544 // opens. A regression that dropped the ns parameter on
545 // `namespaced` would fail every caller-side pin that passes
546 // an owned namespace slot to the primitive.
547 let _all_witness: fn(Client) -> Api<Process> = all::<Process>;
548 let _ns_witness: fn(Client, &str) -> Api<Process> = namespaced::<Process>;
549 }
550
551 #[test]
552 fn namespaced_is_generic_over_every_tatara_crd_and_at_least_one_k8s_builtin() {
553 // Type-parameter reach witness: the primitive's `K:
554 // Resource<DynamicType = ()>` bound admits every
555 // derive-generated tatara CRD (Process, EphemeralPool,
556 // EphemeralAllocation) AND every K8s built-in Rust binding
557 // whose `DynamicType` is `()` (ConfigMap, Job, Pod, Secret,
558 // Namespace, ...). A regression that narrowed the bound
559 // surfaces at this compile-time coercion pin rather than as
560 // silent breakage at a future consumer. The ProcessTable
561 // K binding is deliberately absent — it is cluster-scoped
562 // by design, so an `Api::namespaced::<ProcessTable>` binding
563 // would be semantically ill-typed even though rustc would
564 // accept it (the `#[kube(scope = "Cluster")]` attribute is
565 // metadata for the CRD, not a Rust-side compile-time bound).
566 use k8s_openapi::api::batch::v1::Job;
567 use k8s_openapi::api::core::v1::ConfigMap;
568 let _configmap: fn(Client, &str) -> Api<ConfigMap> = namespaced::<ConfigMap>;
569 let _job: fn(Client, &str) -> Api<Job> = namespaced::<Job>;
570 let _pool: fn(Client, &str) -> Api<EphemeralPool> = namespaced::<EphemeralPool>;
571 }
572
573 // ─── Api::namespaced_with substrate pins (DynamicObject axis) ────
574 //
575 // The primitive [`namespaced_dynamic`] binds
576 // `Api::namespaced_with::<DynamicObject>(client, ns, &ar)` at ONE
577 // substrate site across TWO consumer callsites in
578 // `tatara-reconciler::ssapply` (`apply_owned` SSA-writer +
579 // `fetch` by-coord reader). Sibling to [`namespaced`] on the
580 // (statically-typed × dynamic-schema) axis pair. These pins bind
581 // the scope-slot + K-binding + runtime-schema-slot shape at
582 // fail-before-pass-after granularity so a regression that drifted
583 // any observable slot (the K narrowed off `DynamicObject` — which
584 // would fail every consumer that needs the schema at the value
585 // level, the scope choice widened from `Api::namespaced_with` to
586 // `Api::all_with` — which would silently widen a ns-scoped write
587 // into a cluster-wide sweep, the `ar` slot narrowed from `&
588 // ApiResource` to owned `ApiResource` — which would break every
589 // consumer that already borrows the ar from an earlier local
590 // binding) surfaces HERE rather than as silent operator-facing
591 // skew at the two consumer sites.
592
593 #[test]
594 fn namespaced_dynamic_signature_binds_owned_client_borrowed_ns_borrowed_ar_returning_typed_dynamicobject_api(
595 ) {
596 // The primitive's signature binds `client: Client` on the
597 // input side (matching `Api::namespaced_with`'s own owned-
598 // Client slot — both pre-lift consumer sites pass an owned
599 // `client: Client` argument received from their function's
600 // own signature at the boundary), `ns: &str` on the ns-slot
601 // (a borrowed str — both consumers pass a `namespace: &str`
602 // parameter already borrowed at their caller boundary),
603 // `ar: &ApiResource` on the runtime-schema slot (borrowed —
604 // both consumers build the ar from an earlier local
605 // `api_resource(&api_version, &kind)?` binding and pass it
606 // by reference), and returns `Api<DynamicObject>` (matching
607 // the pre-lift `let api: Api<DynamicObject> = Api::
608 // namespaced_with(...)` shape at both consumer bind sites).
609 //
610 // A regression that widened `client` to `&Client` (which
611 // wouldn't route through `Api::namespaced_with`'s owned-
612 // Client slot), narrowed the return off `DynamicObject`
613 // (which would drop the dynamic-schema carrier both consumers
614 // rely on for `serde_json::from_value` round-trips + `Api::
615 // get_opt` 404 projections), or narrowed the `ar` slot to
616 // owned `ApiResource` (which would break the two consumers
617 // that already borrow their `ar` from a preceding local
618 // binding) fails this coercion at compile time.
619 let _sig: fn(Client, &str, &ApiResource) -> Api<DynamicObject> = namespaced_dynamic;
620 }
621
622 #[test]
623 fn namespaced_dynamic_pair_partitions_dynamic_schema_axis_from_namespaced() {
624 // Peer coherence witness: the (statically-typed × dynamic-
625 // schema) axis pair is closed at ONE module — the schema
626 // choice is spelled by the function name (`namespaced` for
627 // statically-typed K, `namespaced_dynamic` for the
628 // DynamicObject binding that carries its schema at the value
629 // level) at the callsite, not by an enum discriminant or a
630 // runtime bool. A regression that collapsed either function
631 // into a peer scope helper (`namespaced_dynamic` binding
632 // through `Api::namespaced` — which would fail at compile
633 // time for want of the ApiResource carrier on DynamicObject,
634 // `namespaced` binding through `Api::namespaced_with` on a
635 // typed K — which would require every caller to synthesize an
636 // ApiResource they don't have) would be caught by the sibling
637 // signature pins.
638 //
639 // A `fn(Client, &str) -> Api<K>` (for any statically-typed K)
640 // cannot coerce to a `fn(Client, &str, &ApiResource) ->
641 // Api<DynamicObject>` at the compile boundary; that
642 // separation structurally encodes the (statically-typed ×
643 // dynamic-schema) partition the module opens.
644 let _ns_witness: fn(Client, &str) -> Api<Process> = namespaced::<Process>;
645 let _dyn_witness: fn(Client, &str, &ApiResource) -> Api<DynamicObject> = namespaced_dynamic;
646 }
647
648 #[test]
649 fn namespaced_dynamic_matches_hand_authored_api_namespaced_with_chain_shape() {
650 // Byte-shape parity witness: the pre-lift 1-link chain at
651 // both consumer sites reads `let api: Api<DynamicObject> =
652 // Api::namespaced_with(<client>, <ns>, <&ar>);` and the
653 // primitive's body delegates to `Api::namespaced_with(client,
654 // ns, ar)` — the caller reads `let api =
655 // tatara_process::api::namespaced_dynamic(client, ns, &ar);`
656 // and gets the same typed handle both hand-authored sites
657 // produced.
658 //
659 // Source-level witness: the primitive's function-item type
660 // coerces to a `fn(Client, &str, &ApiResource) ->
661 // Api<DynamicObject>` pointer, which is exactly what a fresh
662 // `|c, n, r| Api::<DynamicObject>::namespaced_with(c, n, r)`
663 // closure would coerce to. A regression that reshaped the
664 // body to bind through a peer scope helper (`Api::all_with`
665 // cluster-wide widening, `Api::default_namespaced_with`
666 // fallback to the client's default namespace) would still
667 // coerce to the SAME function-pointer type — so this pin
668 // cannot catch a scope-slot drift alone. That axis is pinned
669 // by the sibling caller-side wire-shape witnesses (which
670 // exercise `apply_owned` + `fetch` end-to-end against a
671 // fixture-backed apiserver).
672 let via_primitive: fn(Client, &str, &ApiResource) -> Api<DynamicObject> =
673 namespaced_dynamic;
674 let via_direct: fn(Client, &str, &ApiResource) -> Api<DynamicObject> =
675 Api::<DynamicObject>::namespaced_with;
676 assert_eq!(
677 via_primitive as usize, via_primitive as usize,
678 "primitive fn-pointer is stable across evaluations",
679 );
680 assert_eq!(
681 via_direct as usize, via_direct as usize,
682 "hand-authored chain fn-pointer is stable across evaluations",
683 );
684 }
685}