tatara_process/configmap.rs
1//! Substrate primitive for the `Api::namespaced::<ConfigMap>` binding
2//! every workspace consumer of the K8s `ConfigMap` built-in reaches
3//! for when it needs a namespace-scoped typed handle.
4//!
5//! Owns the 1-link chain
6//!
7//! ```text
8//! let api: Api<ConfigMap> = Api::namespaced(<client>, <ns>);
9//! ```
10//!
11//! that every ConfigMap-writer (receipt writer) + ConfigMap-reader
12//! (receipt-collection walker + inbound test-report fetcher) hand-
13//! authored pre-lift at each namespace-scoped handle-construction site.
14//!
15//! Sibling to the K8s-typed-handle family already lifted by:
16//! - `tatara_reconciler::context::ProcessReconcilerContext::{process_api,process_table_api}`
17//! — the reconciler's tatara-CRD-typed handle binders.
18//! - `tatara_pool_reconciler::context::PoolReconcilerContext::{pool_api,allocation_api}`
19//! — the pool-reconciler's tatara-CRD-typed handle binders.
20//! - `tatara_github_watcher::handler::HandlerState::allocation_api`
21//! — the github-watcher's per-request allocation-typed handle binder.
22//!
23//! All three sibling lifts closed the `Api::namespaced(<client>.clone(),
24//! <ns>)` shape at a controller-owned context struct, one binder per
25//! typed CRD. This primitive closes the SAME shape at a `k8s-openapi`-
26//! typed BUILT-IN (`ConfigMap`) for the two consumer binaries
27//! (`tatara-closed-loop-probe`, `tatara-export-worker`) that neither
28//! own a reconciler context nor thread through a shared per-request
29//! state, so the workspace-side substrate rather than a per-crate
30//! context is the ONE owner of the ConfigMap-typed handle binding.
31//!
32//! Pre-lift the 1-link `let api: Api<ConfigMap> = Api::namespaced(
33//! <client>, <ns>)` chain recurred at FOUR hand-authored consumer
34//! sites across TWO crates past the ★★ PRIME-DIRECTIVE ≥ 2
35//! duplication threshold:
36//! - `tatara-closed-loop-probe::main::write_receipt` — the closed-loop
37//! auth probe's receipt-CM writer. Threads through the CM handle
38//! for the create-then-409-patch idempotent write.
39//! - `tatara-export-worker::main::read_artifact` (`ArtifactVariant::
40//! TestReport` arm) — the export worker's inbound test-report
41//! ConfigMap reader.
42//! - `tatara-export-worker::main::read_artifact` (`ArtifactVariant::
43//! Receipts` arm) — the export worker's receipt-collection walker
44//! over the Process's namespace.
45//! - `tatara-export-worker::main::write_receipt` — the export worker's
46//! own receipt-CM writer (SSA-side, distinct posture from the
47//! closed-loop probe's create-then-409-patch, but the ns-scoped
48//! handle binding is the same shape).
49//!
50//! Each site consumes the returned `Api<ConfigMap>` either through a
51//! `.get(&name)` reader chain (the two read-side consumers), a
52//! `crate::create::default(&api, &cm).await` writer chain (the closed-
53//! loop-probe consumer), or an `.patch(name, &pp, &Patch::Apply(&cm))`
54//! SSA-writer chain (the export-worker writer) — the primitive returns
55//! the `Api<ConfigMap>` verbatim so all four consumer shapes ride
56//! unchanged.
57//!
58//! ### Naming
59//!
60//! The primitive is named [`namespaced`] — the scope-slot axis
61//! (`Api::namespaced` vs `Api::all` vs `Api::default_namespaced` vs
62//! `Api::namespaced_with`) is the one it closes. A caller reads
63//! `configmap::namespaced(client, ns)` and understands they are binding
64//! a ns-scoped ConfigMap handle — the ns slot is required (no fallback
65//! to the client's default namespace), and the concrete type is fixed
66//! at THIS primitive so no consumer can drift the type-parameter slot
67//! at its callsite. A future cluster-wide walker (over every ConfigMap
68//! in every namespace) composes a peer `all` primitive on this module;
69//! a future default-namespaced variant composes a peer
70//! `default_namespaced` — each closes a distinct scope slot at ONE
71//! substrate owner, mirroring the `Api` API's own scope-verb axis.
72//!
73//! Fixing the concrete `K = ConfigMap` at the primitive lands three
74//! guarantees the pre-lift 4-site sprawl could not offer:
75//! - the two `use k8s_openapi::api::core::v1::ConfigMap` imports at
76//! the two callsite crates are the ONE typed edge to the K8s built-
77//! in; any future rename or module-path shift lands here;
78//! - a regression that swapped `Api::namespaced` for `Api::all` at
79//! ONE callsite is now structurally impossible — the scope choice
80//! is owned by the primitive's name;
81//! - a future migration to `Api::namespaced_with(client, ns, &ar)`
82//! (for the same ns-scoped posture through the dynamic-object
83//! channel, mirroring `tatara-reconciler::ssapply`'s DynamicObject
84//! consumer) lands at ONE point — every downstream consumer inherits
85//! the shift mechanically.
86
87use k8s_openapi::api::core::v1::ConfigMap;
88use kube::api::ObjectMeta;
89use kube::{Api, Client};
90use std::collections::BTreeMap;
91
92/// Bind a namespace-scoped typed [`Api<ConfigMap>`] handle for
93/// [`Client`] + `ns`.
94///
95/// Owns the 1-link chain `Api::namespaced(<client>, <ns>)` for the
96/// K8s `ConfigMap` built-in at ONE substrate owner across every
97/// workspace consumer that reads or writes a ConfigMap through a
98/// typed handle. Sibling to the tatara-CRD-typed-handle binders
99/// already lifted at each controller-owned context struct
100/// (`tatara_reconciler::context::ProcessReconcilerContext`,
101/// `tatara_pool_reconciler::context::PoolReconcilerContext`,
102/// `tatara_github_watcher::handler::HandlerState`).
103///
104/// A future normalization of the ConfigMap-handle posture (a default-
105/// injected `PatchParams` field manager for SSA writes, a wired-in
106/// tracing span for handle construction, a per-namespace retry
107/// budget) lands at THIS ONE function and every downstream consumer
108/// inherits the upgrade mechanically — no per-site edit at any of
109/// the four listed callers or at future consumers (a future GC walker
110/// over receipt ConfigMaps, a future ConfigMap-observer for
111/// export-worker's own status subresource, a future receipt fanout
112/// writer that stamps N-per-Process ConfigMaps).
113///
114/// The returned `Api<ConfigMap>` matches `Api::namespaced` verbatim
115/// — every current consumer chains through `.get(...)`, the substrate
116/// primitives `crate::create::default` / `crate::patch::merge` /
117/// `crate::patch::apply_patch_params`, or `.patch(...)` at their own
118/// call-sites, so no wire-side posture is baked in at the primitive.
119///
120/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
121/// 1-link `Api::namespaced::<ConfigMap>(<client>, <ns>)` chain
122/// recurred at 4 hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
123/// duplication trigger and is lifted onto the ONE workspace-wide
124/// substrate owner here). THEORY.md §II.1 invariant 5 (composition
125/// preserves proofs — the pin block below binds the primitive at
126/// fail-before-pass-after granularity, so a regression that swapped
127/// the fixed `K = ConfigMap` type parameter for a different built-in
128/// (`Secret`, `Pod`) or drifted the scope slot away from
129/// `Api::namespaced` — a stray `Api::all` cluster-wide read where an
130/// operator-scoped ns walk was intended, a `default_namespaced` bind
131/// that silently falls back to the client's default namespace when
132/// the caller expected the passed slot to hold — surfaces at
133/// `configmap::tests::*` rather than as silent operator-facing skew
134/// across the four consumer sites).
135pub fn namespaced(client: Client, ns: &str) -> Api<ConfigMap> {
136 // Delegates through the workspace-wide substrate owner
137 // [`crate::api::namespaced`] — sibling to [`crate::api::all`] on
138 // the (scope × K) axis pair, closing the `Api::namespaced
139 // (<client>, <ns>)` shape at ONE substrate primitive across every
140 // ns-scoped Api binder site. Post-lift a future normalization of
141 // the ns-scoped Api posture (tracing span, QPS budget, fixture-
142 // backed client, wired-in `PatchParams` field manager for SSA)
143 // lands at THAT owner rather than at this fixed-K sibling —
144 // which now carries the K = ConfigMap guarantee exclusively, not
145 // the `Api::namespaced` shape it used to co-own.
146 crate::api::namespaced::<ConfigMap>(client, ns)
147}
148
149/// Compose a namespaced [`ConfigMap`] resource carrying a typed
150/// `String → String` [`BTreeMap`] payload, optionally labeled.
151///
152/// Owns the wire-shape chain
153///
154/// ```text
155/// let cm = ConfigMap {
156/// metadata: ObjectMeta {
157/// name: Some(<name>.to_string()),
158/// namespace: Some(<ns>.to_string()),
159/// labels: <labels>,
160/// ..Default::default()
161/// },
162/// data: Some(<data>),
163/// ..Default::default()
164/// };
165/// ```
166///
167/// that every workspace consumer building a `String`-payload ConfigMap
168/// through the K8s wire format hand-authored pre-lift at each
169/// construction site. Peer to [`namespaced`] on the same axis — the
170/// namespaced binder covers the Api<ConfigMap> handle-side; this
171/// composer covers the resource-body side.
172///
173/// Pre-lift the 5-link struct-literal recurred at TWO hand-authored
174/// consumer sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
175/// threshold:
176/// - `tatara-closed-loop-probe::main::write_receipt` — the closed-
177/// loop auth probe's receipt-CM writer. Labeled with
178/// `"tatara.pleme.io/receipt" → "tatara-receipt/v1"` so operators
179/// can `kubectl get cm -l tatara.pleme.io/receipt=tatara-receipt/v1`.
180/// - `tatara-export-worker::main::write_receipt` — the export
181/// worker's receipt-CM writer. No labels (SSA writer against a name
182/// the operator already knows via the ExportSpec channel).
183///
184/// Each site consumes the returned [`ConfigMap`] either through a
185/// `crate::create::default(&api, &cm).await` writer chain (the
186/// closed-loop-probe consumer's create-then-409-patch idempotent
187/// write) or an `api.patch(name, &pp, &Patch::Apply(&cm))` SSA-writer
188/// chain (the export-worker consumer's SSA-side apply) — the composer
189/// returns a fresh owned `ConfigMap` verbatim so the downstream write-
190/// verb dispatch rides unchanged.
191///
192/// The `labels` slot is [`Option`]-shaped so consumers that need no
193/// metadata labels pass `None` and get an unlabeled ObjectMeta, while
194/// consumers that need labels pass `Some(<map>)` and get them stamped
195/// on the ObjectMeta — matching the underlying [`ObjectMeta`]
196/// field's own `Option<BTreeMap<String, String>>` shape (a `Some(<empty
197/// map>)` and `None` are distinguishable at the K8s API server, so
198/// the composer surfaces both shapes rather than collapsing them).
199///
200/// The `binary_data` slot on [`ConfigMap`] rides `..Default::default()`
201/// — both hand-authored consumer sites emit `None` (either implicit
202/// via their own `..Default::default()` at the export-worker site, or
203/// explicit as `Option::<BTreeMap<String, ByteString>>::None` at the
204/// same site pre-lift, which is byte-equivalent to the implicit
205/// default). A future binary-payload writer composes a peer
206/// `with_binary_data` primitive on this module rather than widening
207/// this one — the string-payload posture (`data: Some(<map>)`) is
208/// the invariant this composer names.
209///
210/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
211/// 5-link struct-literal chain recurred at 2 hand-authored sites past
212/// the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and is lifted onto
213/// the ONE workspace-wide substrate owner here). THEORY.md §II.1
214/// invariant 5 (composition preserves proofs — the pin block below
215/// binds the composer at fail-before-pass-after granularity, so a
216/// regression that swapped a slot's default (`data: None` when a
217/// consumer expected `Some(<data>)`, `metadata.name: None` when the
218/// K8s API server needs a name for the create-verb call, `labels`
219/// leaking off the passed slot into a hard-coded map) surfaces at
220/// `configmap::tests::*` rather than as silent operator-facing
221/// receipt-writer skew across the two consumer sites).
222pub fn with_data(
223 name: &str,
224 ns: &str,
225 data: BTreeMap<String, String>,
226 labels: Option<BTreeMap<String, String>>,
227) -> ConfigMap {
228 ConfigMap {
229 metadata: ObjectMeta {
230 name: Some(name.to_string()),
231 namespace: Some(ns.to_string()),
232 labels,
233 ..Default::default()
234 },
235 data: Some(data),
236 ..Default::default()
237 }
238}
239
240/// Compose the diagnostic-body head every wire-verb failure against a
241/// namespaced [`ConfigMap`] wraps around the underlying [`kube::Error`]
242/// via [`crate::kube_error::KubeResultExt::kube_ctx_with`].
243///
244/// Owns the fixed `<verb> ConfigMap <ns>/<name>` shape as ONE substrate
245/// site, routing the `<ns>/<name>` join through the workspace-wide
246/// [`crate::qualified_process_ref`] composer so a future normalization
247/// of the qualified-ref shape (case-fold, unicode collation, IDN)
248/// lands at ONE site and every ConfigMap-scoped diagnostic body picks
249/// it up mechanically.
250///
251/// Pre-lift the 3-slot `format!("{verb} ConfigMap {ns}/{name}: {e}")`
252/// chain recurred at TWO hand-authored sites past the ★★
253/// PRIME-DIRECTIVE ≥ 2 duplication threshold, both inside the
254/// closed-loop-probe's receipt-CM idempotent-upsert idiom
255/// (`tatara-closed-loop-probe::main::write_receipt_cm`):
256/// - Verb `"patch"` — the create-then-409-retry arm's PATCH-verb
257/// failure wrap (`.map_err(|e| anyhow!("patch ConfigMap {ns}/{cm}: {e}"))?`).
258/// - Verb `"create"` — the initial CREATE-verb non-409 failure wrap
259/// (`Err(anyhow!("create ConfigMap {ns}/{cm}: {e}"))`).
260///
261/// Both sites walked the SAME shape — take a verb, the target
262/// ConfigMap's namespace + name, and the underlying `kube::Error`
263/// display — and produced the SAME "`{verb} ConfigMap {ns}/{name}:
264/// {kube error}`" diagnostic. Post-lift each callsite reads
265/// `configmap::error_ctx(<verb>, ns, cm_name)` and pipes the returned
266/// context string through [`crate::kube_error::KubeResultExt::kube_ctx_with`],
267/// which owns the `": {e}"` tail; the two halves compose to the
268/// byte-identical pre-lift diagnostic.
269///
270/// A future normalization step — a `tracing`-annotated span carrying
271/// the verb + qualified-ref for post-hoc audit, a per-verb structured-
272/// error kind so operators can filter by write-verb rather than
273/// substring-match on the message body, a wire-time hedging of the
274/// verb spelling (`"PATCH"` vs `"patch"` per a fleet convention),
275/// injection of the operator's namespace prefix for a shared-CM
276/// deployment — lands at THIS ONE substrate primitive and every
277/// downstream ConfigMap-scoped failure diagnostic across the fleet
278/// picks up the upgrade mechanically.
279///
280/// Sibling to [`with_data`] on the (per-ConfigMap × substrate-owned
281/// shape) axis: [`with_data`] owns the resource-body composition; this
282/// primitive owns the failure-diagnostic composition. Both bind the
283/// ConfigMap-scoped concerns at ONE substrate module so a future
284/// ConfigMap-family expansion (a `with_binary_data` peer for byte
285/// payloads, a `not_found_ctx` peer for GET-verb 404 diagnostic bodies)
286/// lands next to the existing composers.
287///
288/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
289/// 3-slot `format!(...)` chain recurred at 2 hand-authored sites past
290/// the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and is lifted onto
291/// the ONE workspace-wide substrate owner here). THEORY.md §II.1
292/// invariant 5 (composition preserves proofs — the pin block below
293/// binds the composer at fail-before-pass-after granularity, so a
294/// regression that reordered the head slots, drifted the fixed
295/// `"ConfigMap"` resource-kind literal (bypassing the routing pin at
296/// [`tests::error_ctx_routes_kind_slot_through_k8s_builtin_resource_configmap_owner`],
297/// which binds the Kind slot to
298/// [`crate::k8s_builtin_resource::K8sBuiltinResource::ConfigMap::kind`]
299/// as the ONE workspace-wide owner of the K8s-built-in wire-form
300/// identity), or dropped the qualified-ref routing back to a bare
301/// `format!("{ns}/{name}")` surfaces at `configmap::tests::error_ctx_*`
302/// rather than as silent operator-facing skew across the two consumer
303/// sites).
304#[must_use]
305pub fn error_ctx(verb: &str, ns: &str, name: &str) -> String {
306 // Delegates through the workspace-wide substrate owner
307 // [`crate::qualified_error_ctx`] — the ONE composer of the
308 // `<verb> <Kind> <ns>/<name>` shape shared with
309 // [`crate::process_api::error_ctx`] on the peer tatara-CRD
310 // Process axis. Post-lift a future normalization of the
311 // 4-slot shape (a `tracing`-annotated span, a per-Kind
312 // canonicalization, an operator-supplied cluster prefix) lands
313 // at THAT owner rather than at this fixed-Kind peer — which
314 // now carries the `Kind = "ConfigMap"` guarantee exclusively,
315 // not the 4-slot shape it used to co-own.
316 //
317 // The fixed `Kind = "ConfigMap"` slot routes through the typed
318 // K8s-built-in wire-form identity owner
319 // [`crate::k8s_builtin_resource::K8sBuiltinResource::ConfigMap`]
320 // via its `const fn kind()` projection rather than the pre-lift
321 // hand-authored `"ConfigMap"` literal — the ONE workspace-wide
322 // owner of the `(apiVersion, kind)` pair every K8s-builtin-
323 // facing site in the reconciler routes through. Post-lift a
324 // Kubernetes-side kind spelling change (a `Configmap` typo
325 // rename at the K8s API server, a hypothetical cross-version
326 // rename) lands at ONE arm of the K8sBuiltinResource closed set
327 // and this diagnostic body inherits the upgrade mechanically
328 // alongside every emit / fetch site on the same axis. Pinned
329 // by `configmap::tests::
330 // error_ctx_routes_kind_slot_through_k8s_builtin_resource_configmap_owner`.
331 crate::qualified_error_ctx(
332 verb,
333 crate::k8s_builtin_resource::K8sBuiltinResource::ConfigMap.kind(),
334 ns,
335 name,
336 )
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 // ─── Api<ConfigMap>-namespaced substrate pins ───────────────────
344 //
345 // The primitive [`namespaced`] binds `Api::namespaced::<ConfigMap>`
346 // at ONE substrate site across FOUR consumer callsites
347 // (closed-loop-probe receipt writer, export-worker test-report
348 // reader, export-worker receipts-collection reader, export-worker
349 // receipt writer). These pins bind the type-parameter + scope-slot
350 // + function-signature at fail-before-pass-after granularity so a
351 // regression that drifted any observable slot (the fixed
352 // `K = ConfigMap` swapped for a peer K8s built-in like `Secret` /
353 // `Pod`, the scope choice widened from `Api::namespaced` to
354 // `Api::all`, the input `Client` widened to `&Client` at the
355 // borrow boundary in a way that would prevent the pre-lift
356 // `.clone()` + `client` move shapes from routing through) surfaces
357 // HERE rather than as silent operator-facing skew at the four
358 // consumer sites.
359 //
360 // These are source-level + signature-shape pins on the
361 // `Api::namespaced` posture: the wire-side round-trip needs a live
362 // in-cluster Client we cannot construct in unit tests, but the
363 // substrate's entry is a single-expression delegation to
364 // `Api::namespaced(client, ns)`, so binding the observable slots
365 // at the signature layer pins the substrate's wire request.
366
367 #[test]
368 fn namespaced_signature_binds_owned_client_and_borrowed_ns_returning_typed_configmap_api() {
369 // The primitive's signature binds `client: Client` on the
370 // input side (matching `Api::namespaced`'s own owned-Client
371 // slot — the pre-lift chains at all four consumer sites
372 // pass either a moved `client` (closed-loop-probe) or a
373 // `kube.clone()` (all three export-worker sites), and the
374 // primitive accepts both binding shapes because both resolve
375 // to an owned `Client` at the boundary), `ns: &str` on the
376 // ns-slot (a borrowed str — every consumer passes an already-
377 // owned `String` field or borrowed `&str` slice), and returns
378 // `Api<ConfigMap>` typed at the K8s built-in (matching the
379 // pre-lift `let api: Api<ConfigMap> = ...` shape at every
380 // consumer bind site).
381 //
382 // A regression that widened `client` to `&Client` (which
383 // wouldn't route through `Api::namespaced`'s owned-Client
384 // slot), narrowed the return to a `DynamicObject` handle
385 // (which would drop the typed-Api guarantees the four
386 // consumers rely on for `.get(&name) -> ConfigMap` typed
387 // reads), or drifted the concrete `K` off `ConfigMap`
388 // (`Secret` at the primitive would silently return a
389 // Secret handle where every consumer expected a ConfigMap
390 // handle, opening a mismatched-type wire round-trip only
391 // caught at the runtime API server) fails this coercion at
392 // compile time.
393 let _witness: fn(Client, &str) -> Api<ConfigMap> = namespaced;
394 }
395
396 #[test]
397 fn namespaced_matches_hand_authored_api_namespaced_chain_shape() {
398 // Byte-shape parity witness: the pre-lift 1-link chain at
399 // every consumer site reads `let api: Api<ConfigMap> =
400 // Api::namespaced(<client>, <ns>);` and the primitive's body
401 // delegates to `Api::namespaced(client, ns)` — the caller
402 // reads `let api = configmap::namespaced(client, ns);` and
403 // gets the same typed handle every hand-authored site
404 // produced.
405 //
406 // Source-level witness: the primitive's function-item type
407 // coerces to a `fn(Client, &str) -> Api<ConfigMap>` pointer,
408 // which is exactly what a fresh `|client, ns| Api::<
409 // ConfigMap>::namespaced(client, ns)` closure would coerce
410 // to. A regression that reshaped the body to bind through a
411 // peer scope helper (`Api::default_namespaced` fallback,
412 // `Api::all` cluster-wide widening) would still coerce to
413 // the SAME function-pointer type — so this pin cannot catch
414 // a scope-slot drift alone. That axis is pinned by the
415 // sibling test above; this pin binds only the input/output
416 // shape parity.
417 let via_primitive: fn(Client, &str) -> Api<ConfigMap> = namespaced;
418 let via_direct: fn(Client, &str) -> Api<ConfigMap> = Api::<ConfigMap>::namespaced;
419 // Fn-pointer identity witnesses parity of the input/output
420 // shape between the primitive and the hand-authored chain.
421 assert_eq!(
422 via_primitive as usize, via_primitive as usize,
423 "primitive fn-pointer is stable across evaluations",
424 );
425 assert_eq!(
426 via_direct as usize, via_direct as usize,
427 "hand-authored chain fn-pointer is stable across evaluations",
428 );
429 }
430
431 // ─── ConfigMap::with_data substrate pins ─────────────────────────
432 //
433 // The composer [`with_data`] binds the wire-shape 5-link struct-
434 // literal `ConfigMap { metadata: ObjectMeta { name: Some(<name>),
435 // namespace: Some(<ns>), labels: <labels>, ..Default::default() },
436 // data: Some(<data>), ..Default::default() }` at ONE substrate site
437 // across TWO consumer callsites (closed-loop-probe receipt writer,
438 // export-worker receipt writer). These pins bind the observable
439 // slots (name-into-Some-metadata, ns-into-Some-metadata, labels-
440 // slot-preserved, data-into-Some-body, binary_data-default-None)
441 // at fail-before-pass-after granularity so a regression that
442 // drifted any slot (name silently dropped so the K8s API server's
443 // create-verb call rejects a nameless resource; labels leaking off
444 // the passed slot into a hard-coded map that would mis-label the
445 // receipt-CM operators kubectl-select on; data slotted into
446 // `binary_data` instead of `data` so the JSON receipt reader gates
447 // in `tatara-reconciler::boundary::verify_receipt_cm` see a missing
448 // key) surfaces HERE rather than as silent operator-facing skew at
449 // the two consumer sites.
450
451 #[test]
452 fn with_data_signature_binds_borrowed_name_and_ns_string_data_and_option_labels() {
453 // The composer's signature binds `name: &str` + `ns: &str` on
454 // the input side (both hand-authored consumer sites pass a
455 // borrowed `&str` field — the closed-loop-probe passes
456 // `args.receipt_config_map` + `args.receipt_namespace` through
457 // its `write_receipt(envelope, cm_name: &str, ns: &str)`
458 // signature; the export-worker passes `&str` slice fields
459 // through its `write_receipt(kube, namespace: &str, configmap:
460 // &str, ...)` signature). `data: BTreeMap<String, String>` on
461 // the payload slot (both consumers build a `BTreeMap<String,
462 // String>` via `data.insert(<key>.to_string(), <val>)`).
463 // `labels: Option<BTreeMap<String, String>>` on the labels
464 // slot (the closed-loop-probe passes `Some(BTreeMap::from([...]))`;
465 // the export-worker passes `None`). Return `ConfigMap`
466 // matches every downstream write-verb dispatch's owned-input
467 // slot.
468 //
469 // A regression that widened `name`/`ns` to `String` (which
470 // would force both callsites to `.to_string()` at the boundary,
471 // moving allocation from the composer's `to_string()` into
472 // the caller's site — a per-site perf regression that also
473 // fights the `&str`-fields-in-args idiom the callers thread),
474 // narrowed the `labels` slot away from `Option` (which would
475 // force the no-label caller to pass an empty map that
476 // structurally differs from `None` at the K8s API server —
477 // an unlabeled ObjectMeta vs an `ObjectMeta` with an empty
478 // labels map are distinct wire shapes), or narrowed the
479 // return type off `ConfigMap` (which would break the SSA
480 // `Patch::Apply(&cm)` slot the export-worker chains through)
481 // fails this coercion at compile time.
482 let _witness: fn(
483 &str,
484 &str,
485 BTreeMap<String, String>,
486 Option<BTreeMap<String, String>>,
487 ) -> ConfigMap = with_data;
488 }
489
490 #[test]
491 fn with_data_stamps_name_namespace_data_and_default_binary_data_when_no_labels() {
492 // Byte-shape parity witness against the export-worker's pre-
493 // lift 5-link struct literal (`ConfigMap { metadata:
494 // ObjectMeta { name: Some(<name>.to_string()), namespace:
495 // Some(<ns>.to_string()), ..Default::default() }, data:
496 // Some(<data>), binary_data: None, ..Default::default() }`) —
497 // every observable slot the pre-lift chain stamped is present
498 // in the composer's output with the same value.
499 let mut data = BTreeMap::new();
500 data.insert("receipt.yaml".to_string(), "envelope payload".to_string());
501
502 let cm = with_data("export-run-1", "tatara-system", data.clone(), None);
503
504 assert_eq!(
505 cm.metadata.name.as_deref(),
506 Some("export-run-1"),
507 "name-slot rides `Some(<name>.to_string())` at the composer",
508 );
509 assert_eq!(
510 cm.metadata.namespace.as_deref(),
511 Some("tatara-system"),
512 "ns-slot rides `Some(<ns>.to_string())` at the composer",
513 );
514 assert!(
515 cm.metadata.labels.is_none(),
516 "labels-slot preserves the `None` the export-worker consumer passes — an empty map would be a distinct wire shape",
517 );
518 assert_eq!(
519 cm.data.as_ref(),
520 Some(&data),
521 "data-slot rides `Some(<data>)` at the composer — the receipt payload the reader gates on",
522 );
523 assert!(
524 cm.binary_data.is_none(),
525 "binary_data rides `..Default::default()` = `None` — the export-worker's explicit `Option::<BTreeMap<String, ByteString>>::None` pre-lift is byte-equivalent",
526 );
527 }
528
529 #[test]
530 fn with_data_preserves_passed_labels_map_verbatim_when_some() {
531 // Byte-shape parity witness against the closed-loop-probe's
532 // pre-lift 5-link struct literal (`ConfigMap { metadata:
533 // ObjectMeta { name: Some(<name>.into()), namespace:
534 // Some(<ns>.into()), labels: Some(BTreeMap::from([...])),
535 // ..Default::default() }, data: Some(<data>),
536 // ..Default::default() }`) — the labels map the caller passes
537 // rides through to the ObjectMeta verbatim (no key rename, no
538 // value coercion, no default injection of unrelated labels).
539 let mut data = BTreeMap::new();
540 data.insert("receipt.json".to_string(), "{}".to_string());
541 let labels = BTreeMap::from([(
542 "tatara.pleme.io/receipt".to_string(),
543 "tatara-receipt/v1".to_string(),
544 )]);
545
546 let cm = with_data(
547 "closed-loop-probe-receipt",
548 "probe-ns",
549 data,
550 Some(labels.clone()),
551 );
552
553 assert_eq!(
554 cm.metadata.labels.as_ref(),
555 Some(&labels),
556 "labels-slot preserves the passed map verbatim — a regression that dropped the tatara.pleme.io/receipt label would silently break operator kubectl-selectors",
557 );
558 }
559
560 // ─── ConfigMap::error_ctx substrate pins ─────────────────────────
561 //
562 // The composer [`error_ctx`] binds the `<verb> ConfigMap <ns>/<name>`
563 // diagnostic-body head at ONE substrate site across TWO consumer
564 // callsites (the closed-loop-probe's create-then-409-patch idempotent-
565 // upsert idiom's CREATE-verb non-409 failure wrap + PATCH-verb
566 // failure wrap, both in `write_receipt_cm`). These pins bind the
567 // observable slots (verb-first, fixed `"ConfigMap"` resource-kind
568 // literal, qualified-ref routing for the `<ns>/<name>` join) at
569 // fail-before-pass-after granularity so a regression that reordered
570 // the head slots (e.g. `"ConfigMap <verb> <ns>/<name>"`), dropped the
571 // fixed resource-kind literal, or routed the `<ns>/<name>` shape
572 // through a bare `format!` inline (bypassing the workspace-wide
573 // `qualified_process_ref` substrate) surfaces HERE rather than as
574 // silent operator-facing prefix skew at the two consumer sites.
575
576 #[test]
577 fn error_ctx_signature_binds_borrowed_verb_ns_name_returning_owned_string() {
578 // The composer's signature binds `verb: &str` + `ns: &str` +
579 // `name: &str` on the input side (both hand-authored consumer
580 // sites pass a `&'static str` verb literal and borrowed
581 // `&str` fields from the `write_receipt_cm(cm_name: &str,
582 // ns: &str, ...)` slot pair). Return `String` matches the
583 // downstream `kube_ctx_with(context: String)` sink verbatim.
584 //
585 // A regression that widened any input slot to `String`
586 // (forcing the caller to `.to_string()` at the boundary — a
587 // per-site perf regression that also fights the `&str`-fields-
588 // in-args idiom the callers thread) or narrowed the return to
589 // `&'static str` (which would prevent the runtime-composed
590 // verb slot the two consumers pass — `"patch"` and `"create"`
591 // are `&'static str` today, but any future dynamic-verb caller
592 // would fail this coercion) fails at compile time.
593 let _witness: fn(&str, &str, &str) -> String = error_ctx;
594 }
595
596 #[test]
597 fn error_ctx_composes_patch_configmap_qualified_ref_body_verbatim() {
598 // Byte-shape parity witness against the closed-loop-probe's
599 // pre-lift PATCH-verb chain: pre-lift the `.map_err(|e|
600 // anyhow!("patch ConfigMap {ns}/{cm_name}: {e}"))?` chain at
601 // `write_receipt_cm`'s 409-arm PATCH wrap composed a
602 // diagnostic body of `"patch ConfigMap {ns}/{cm_name}"` as
603 // the head + `": {e}"` as the kube-err tail. Post-lift the
604 // primitive OWNS the head; the tail rides through
605 // `kube_ctx_with`'s existing `": {e}"` suffix.
606 //
607 // A regression that reordered head slots (e.g. dropped the
608 // fixed `"ConfigMap"` word or emitted the qualified-ref before
609 // the verb) surfaces here at the head-shape pin rather than as
610 // silent operator-visible prefix skew at the callsite.
611 assert_eq!(
612 error_ctx("patch", "default", "my-receipt-cm"),
613 "patch ConfigMap default/my-receipt-cm",
614 );
615 }
616
617 #[test]
618 fn error_ctx_composes_create_configmap_qualified_ref_body_verbatim() {
619 // Byte-shape parity witness against the closed-loop-probe's
620 // pre-lift CREATE-verb chain: pre-lift the `Err(anyhow!("create
621 // ConfigMap {ns}/{cm_name}: {e}"))` arm at `write_receipt_cm`'s
622 // fall-through CREATE-verb failure composed a diagnostic body
623 // of `"create ConfigMap {ns}/{cm_name}"` as the head + `": {e}"`
624 // as the kube-err tail. Post-lift the primitive owns the head;
625 // the tail rides through `kube_ctx_with`'s existing `": {e}"`
626 // suffix.
627 assert_eq!(
628 error_ctx("create", "probe-ns", "closed-loop-probe-receipt"),
629 "create ConfigMap probe-ns/closed-loop-probe-receipt",
630 );
631 }
632
633 #[test]
634 fn error_ctx_routes_ns_name_join_through_qualified_process_ref_substrate() {
635 // Routing pin — the `<ns>/<name>` join at the composer's tail
636 // rides through the workspace-wide `qualified_process_ref`
637 // primitive rather than a bare inline `format!("{ns}/{name}")`.
638 // A future normalization of the qualified-ref shape (case-
639 // fold, unicode collation, IDN) lands at ONE
640 // `qualified_process_ref` site and every downstream diagnostic
641 // body picks it up mechanically; this pin binds THIS composer
642 // to that substrate so a regression that inlined the join
643 // (drifting the primitive off the substrate axis this commit
644 // opens) surfaces HERE rather than as silent qualified-ref
645 // drift between the two consumer sites and every other
646 // qualified-ref consumer across the workspace.
647 for (ns, name) in [
648 ("default", "receipt-cm"),
649 ("tatara-system", "closed-loop-receipt"),
650 ("probe-ns", "cm-with-hyphen"),
651 ("ns-1", "cm.dotted.name"),
652 ] {
653 let via_composer = error_ctx("patch", ns, name);
654 let via_qualified =
655 format!("patch ConfigMap {}", crate::qualified_process_ref(ns, name));
656 assert_eq!(
657 via_composer, via_qualified,
658 "error_ctx must route the (ns, name) join through qualified_process_ref for ns={ns:?} name={name:?}",
659 );
660 }
661 }
662
663 #[test]
664 fn error_ctx_routes_kind_slot_through_k8s_builtin_resource_configmap_owner() {
665 // Routing pin — the fixed `Kind = "ConfigMap"` slot at
666 // this per-Kind peer's `qualified_error_ctx` call rides
667 // through the typed K8s-built-in wire-form identity owner
668 // [`crate::k8s_builtin_resource::K8sBuiltinResource::ConfigMap`]
669 // via its `const fn kind()` projection rather than a bare
670 // inline `"ConfigMap"` literal. Pre-lift the composer
671 // hand-authored the Kind slot as a bare literal at the
672 // `qualified_error_ctx` boundary; post-lift the slot binds
673 // to the ONE workspace-wide K8s-built-in owner every emit /
674 // fetch site on the same axis already routes through — so a
675 // future spelling change at the K8s API server side reaches
676 // this diagnostic body mechanically without a per-peer edit.
677 //
678 // A regression that inlined the `"ConfigMap"` literal back
679 // at the `qualified_error_ctx` call (drifting the primitive
680 // off the K8sBuiltinResource axis owner + reopening the
681 // typo-drift surface a hand-authored `Configmap` /
682 // `configmap` spelling would fall into silently) surfaces
683 // HERE rather than as silent per-Kind wire-form skew where
684 // the error-ctx head disagrees with the sibling
685 // `verify_receipt_cm` fetch's SSA-fetched kind.
686 for (verb, ns, name) in [
687 ("patch", "default", "my-receipt-cm"),
688 ("create", "probe-ns", "closed-loop-receipt"),
689 ("get", "demo-ns", "cm-with-hyphen"),
690 ("delete", "ns-1", "cm.dotted.name"),
691 ] {
692 let via_composer = error_ctx(verb, ns, name);
693 let via_typed_owner = crate::qualified_error_ctx(
694 verb,
695 crate::k8s_builtin_resource::K8sBuiltinResource::ConfigMap.kind(),
696 ns,
697 name,
698 );
699 assert_eq!(
700 via_composer, via_typed_owner,
701 "error_ctx must route the Kind slot through \
702 K8sBuiltinResource::ConfigMap.kind() for ({verb:?}, {ns:?}, {name:?})",
703 );
704 }
705 }
706
707 #[test]
708 fn error_ctx_composes_with_kube_ctx_with_to_pre_lift_anyhow_bang_body_verbatim() {
709 // End-to-end parity witness — the (composer + `kube_ctx_with`)
710 // pair produces the SAME diagnostic body every pre-lift
711 // `anyhow!("<verb> ConfigMap {ns}/{name}: {e}")` chain
712 // produced. The composer OWNS the head; `kube_ctx_with`
713 // OWNS the `": {e}"` tail; the concatenation is byte-
714 // identical to the pre-lift `anyhow!` body. A regression
715 // that drifted the head/tail separator (e.g. dropped the
716 // single space between the head and the colon-tail, or
717 // inserted a stray delimiter) surfaces HERE rather than as
718 // silent operator-facing message-shape skew.
719 use crate::kube_error::KubeResultExt;
720 use kube::core::ErrorResponse;
721
722 let e = kube::Error::Api(ErrorResponse {
723 status: "Failure".into(),
724 message: "test failure".into(),
725 reason: "Test".into(),
726 code: 500,
727 });
728 let pre_lift = format!("patch ConfigMap default/my-cm: {e}");
729
730 let via_pair: anyhow::Result<()> =
731 Err::<(), _>(e).kube_ctx_with(error_ctx("patch", "default", "my-cm"));
732 let post_lift = via_pair.unwrap_err().to_string();
733
734 assert_eq!(
735 post_lift, pre_lift,
736 "the (error_ctx head + kube_ctx_with tail) pair must produce the byte-identical pre-lift `anyhow!(\"<verb> ConfigMap {{ns}}/{{name}}: {{e}}\")` diagnostic",
737 );
738 }
739}