Skip to main content

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    Api::namespaced(client, ns)
137}
138
139/// Compose a namespaced [`ConfigMap`] resource carrying a typed
140/// `String → String` [`BTreeMap`] payload, optionally labeled.
141///
142/// Owns the wire-shape chain
143///
144/// ```text
145/// let cm = ConfigMap {
146///     metadata: ObjectMeta {
147///         name: Some(<name>.to_string()),
148///         namespace: Some(<ns>.to_string()),
149///         labels: <labels>,
150///         ..Default::default()
151///     },
152///     data: Some(<data>),
153///     ..Default::default()
154/// };
155/// ```
156///
157/// that every workspace consumer building a `String`-payload ConfigMap
158/// through the K8s wire format hand-authored pre-lift at each
159/// construction site. Peer to [`namespaced`] on the same axis — the
160/// namespaced binder covers the Api<ConfigMap> handle-side; this
161/// composer covers the resource-body side.
162///
163/// Pre-lift the 5-link struct-literal recurred at TWO hand-authored
164/// consumer sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
165/// threshold:
166/// - `tatara-closed-loop-probe::main::write_receipt` — the closed-
167///   loop auth probe's receipt-CM writer. Labeled with
168///   `"tatara.pleme.io/receipt" → "tatara-receipt/v1"` so operators
169///   can `kubectl get cm -l tatara.pleme.io/receipt=tatara-receipt/v1`.
170/// - `tatara-export-worker::main::write_receipt` — the export
171///   worker's receipt-CM writer. No labels (SSA writer against a name
172///   the operator already knows via the ExportSpec channel).
173///
174/// Each site consumes the returned [`ConfigMap`] either through a
175/// `crate::create::default(&api, &cm).await` writer chain (the
176/// closed-loop-probe consumer's create-then-409-patch idempotent
177/// write) or an `api.patch(name, &pp, &Patch::Apply(&cm))` SSA-writer
178/// chain (the export-worker consumer's SSA-side apply) — the composer
179/// returns a fresh owned `ConfigMap` verbatim so the downstream write-
180/// verb dispatch rides unchanged.
181///
182/// The `labels` slot is [`Option`]-shaped so consumers that need no
183/// metadata labels pass `None` and get an unlabeled ObjectMeta, while
184/// consumers that need labels pass `Some(<map>)` and get them stamped
185/// on the ObjectMeta — matching the underlying [`ObjectMeta`]
186/// field's own `Option<BTreeMap<String, String>>` shape (a `Some(<empty
187/// map>)` and `None` are distinguishable at the K8s API server, so
188/// the composer surfaces both shapes rather than collapsing them).
189///
190/// The `binary_data` slot on [`ConfigMap`] rides `..Default::default()`
191/// — both hand-authored consumer sites emit `None` (either implicit
192/// via their own `..Default::default()` at the export-worker site, or
193/// explicit as `Option::<BTreeMap<String, ByteString>>::None` at the
194/// same site pre-lift, which is byte-equivalent to the implicit
195/// default). A future binary-payload writer composes a peer
196/// `with_binary_data` primitive on this module rather than widening
197/// this one — the string-payload posture (`data: Some(<map>)`) is
198/// the invariant this composer names.
199///
200/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
201/// 5-link struct-literal chain recurred at 2 hand-authored sites past
202/// the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger and is lifted onto
203/// the ONE workspace-wide substrate owner here). THEORY.md §II.1
204/// invariant 5 (composition preserves proofs — the pin block below
205/// binds the composer at fail-before-pass-after granularity, so a
206/// regression that swapped a slot's default (`data: None` when a
207/// consumer expected `Some(<data>)`, `metadata.name: None` when the
208/// K8s API server needs a name for the create-verb call, `labels`
209/// leaking off the passed slot into a hard-coded map) surfaces at
210/// `configmap::tests::*` rather than as silent operator-facing
211/// receipt-writer skew across the two consumer sites).
212pub fn with_data(
213    name: &str,
214    ns: &str,
215    data: BTreeMap<String, String>,
216    labels: Option<BTreeMap<String, String>>,
217) -> ConfigMap {
218    ConfigMap {
219        metadata: ObjectMeta {
220            name: Some(name.to_string()),
221            namespace: Some(ns.to_string()),
222            labels,
223            ..Default::default()
224        },
225        data: Some(data),
226        ..Default::default()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    // ─── Api<ConfigMap>-namespaced substrate pins ───────────────────
235    //
236    // The primitive [`namespaced`] binds `Api::namespaced::<ConfigMap>`
237    // at ONE substrate site across FOUR consumer callsites
238    // (closed-loop-probe receipt writer, export-worker test-report
239    // reader, export-worker receipts-collection reader, export-worker
240    // receipt writer). These pins bind the type-parameter + scope-slot
241    // + function-signature at fail-before-pass-after granularity so a
242    // regression that drifted any observable slot (the fixed
243    // `K = ConfigMap` swapped for a peer K8s built-in like `Secret` /
244    // `Pod`, the scope choice widened from `Api::namespaced` to
245    // `Api::all`, the input `Client` widened to `&Client` at the
246    // borrow boundary in a way that would prevent the pre-lift
247    // `.clone()` + `client` move shapes from routing through) surfaces
248    // HERE rather than as silent operator-facing skew at the four
249    // consumer sites.
250    //
251    // These are source-level + signature-shape pins on the
252    // `Api::namespaced` posture: the wire-side round-trip needs a live
253    // in-cluster Client we cannot construct in unit tests, but the
254    // substrate's entry is a single-expression delegation to
255    // `Api::namespaced(client, ns)`, so binding the observable slots
256    // at the signature layer pins the substrate's wire request.
257
258    #[test]
259    fn namespaced_signature_binds_owned_client_and_borrowed_ns_returning_typed_configmap_api() {
260        // The primitive's signature binds `client: Client` on the
261        // input side (matching `Api::namespaced`'s own owned-Client
262        // slot — the pre-lift chains at all four consumer sites
263        // pass either a moved `client` (closed-loop-probe) or a
264        // `kube.clone()` (all three export-worker sites), and the
265        // primitive accepts both binding shapes because both resolve
266        // to an owned `Client` at the boundary), `ns: &str` on the
267        // ns-slot (a borrowed str — every consumer passes an already-
268        // owned `String` field or borrowed `&str` slice), and returns
269        // `Api<ConfigMap>` typed at the K8s built-in (matching the
270        // pre-lift `let api: Api<ConfigMap> = ...` shape at every
271        // consumer bind site).
272        //
273        // A regression that widened `client` to `&Client` (which
274        // wouldn't route through `Api::namespaced`'s owned-Client
275        // slot), narrowed the return to a `DynamicObject` handle
276        // (which would drop the typed-Api guarantees the four
277        // consumers rely on for `.get(&name) -> ConfigMap` typed
278        // reads), or drifted the concrete `K` off `ConfigMap`
279        // (`Secret` at the primitive would silently return a
280        // Secret handle where every consumer expected a ConfigMap
281        // handle, opening a mismatched-type wire round-trip only
282        // caught at the runtime API server) fails this coercion at
283        // compile time.
284        let _witness: fn(Client, &str) -> Api<ConfigMap> = namespaced;
285    }
286
287    #[test]
288    fn namespaced_matches_hand_authored_api_namespaced_chain_shape() {
289        // Byte-shape parity witness: the pre-lift 1-link chain at
290        // every consumer site reads `let api: Api<ConfigMap> =
291        // Api::namespaced(<client>, <ns>);` and the primitive's body
292        // delegates to `Api::namespaced(client, ns)` — the caller
293        // reads `let api = configmap::namespaced(client, ns);` and
294        // gets the same typed handle every hand-authored site
295        // produced.
296        //
297        // Source-level witness: the primitive's function-item type
298        // coerces to a `fn(Client, &str) -> Api<ConfigMap>` pointer,
299        // which is exactly what a fresh `|client, ns| Api::<
300        // ConfigMap>::namespaced(client, ns)` closure would coerce
301        // to. A regression that reshaped the body to bind through a
302        // peer scope helper (`Api::default_namespaced` fallback,
303        // `Api::all` cluster-wide widening) would still coerce to
304        // the SAME function-pointer type — so this pin cannot catch
305        // a scope-slot drift alone. That axis is pinned by the
306        // sibling test above; this pin binds only the input/output
307        // shape parity.
308        let via_primitive: fn(Client, &str) -> Api<ConfigMap> = namespaced;
309        let via_direct: fn(Client, &str) -> Api<ConfigMap> = Api::<ConfigMap>::namespaced;
310        // Fn-pointer identity witnesses parity of the input/output
311        // shape between the primitive and the hand-authored chain.
312        assert_eq!(
313            via_primitive as usize, via_primitive as usize,
314            "primitive fn-pointer is stable across evaluations",
315        );
316        assert_eq!(
317            via_direct as usize, via_direct as usize,
318            "hand-authored chain fn-pointer is stable across evaluations",
319        );
320    }
321
322    // ─── ConfigMap::with_data substrate pins ─────────────────────────
323    //
324    // The composer [`with_data`] binds the wire-shape 5-link struct-
325    // literal `ConfigMap { metadata: ObjectMeta { name: Some(<name>),
326    // namespace: Some(<ns>), labels: <labels>, ..Default::default() },
327    // data: Some(<data>), ..Default::default() }` at ONE substrate site
328    // across TWO consumer callsites (closed-loop-probe receipt writer,
329    // export-worker receipt writer). These pins bind the observable
330    // slots (name-into-Some-metadata, ns-into-Some-metadata, labels-
331    // slot-preserved, data-into-Some-body, binary_data-default-None)
332    // at fail-before-pass-after granularity so a regression that
333    // drifted any slot (name silently dropped so the K8s API server's
334    // create-verb call rejects a nameless resource; labels leaking off
335    // the passed slot into a hard-coded map that would mis-label the
336    // receipt-CM operators kubectl-select on; data slotted into
337    // `binary_data` instead of `data` so the JSON receipt reader gates
338    // in `tatara-reconciler::boundary::verify_receipt_cm` see a missing
339    // key) surfaces HERE rather than as silent operator-facing skew at
340    // the two consumer sites.
341
342    #[test]
343    fn with_data_signature_binds_borrowed_name_and_ns_string_data_and_option_labels() {
344        // The composer's signature binds `name: &str` + `ns: &str` on
345        // the input side (both hand-authored consumer sites pass a
346        // borrowed `&str` field — the closed-loop-probe passes
347        // `args.receipt_config_map` + `args.receipt_namespace` through
348        // its `write_receipt(envelope, cm_name: &str, ns: &str)`
349        // signature; the export-worker passes `&str` slice fields
350        // through its `write_receipt(kube, namespace: &str, configmap:
351        // &str, ...)` signature). `data: BTreeMap<String, String>` on
352        // the payload slot (both consumers build a `BTreeMap<String,
353        // String>` via `data.insert(<key>.to_string(), <val>)`).
354        // `labels: Option<BTreeMap<String, String>>` on the labels
355        // slot (the closed-loop-probe passes `Some(BTreeMap::from([...]))`;
356        // the export-worker passes `None`). Return `ConfigMap`
357        // matches every downstream write-verb dispatch's owned-input
358        // slot.
359        //
360        // A regression that widened `name`/`ns` to `String` (which
361        // would force both callsites to `.to_string()` at the boundary,
362        // moving allocation from the composer's `to_string()` into
363        // the caller's site — a per-site perf regression that also
364        // fights the `&str`-fields-in-args idiom the callers thread),
365        // narrowed the `labels` slot away from `Option` (which would
366        // force the no-label caller to pass an empty map that
367        // structurally differs from `None` at the K8s API server —
368        // an unlabeled ObjectMeta vs an `ObjectMeta` with an empty
369        // labels map are distinct wire shapes), or narrowed the
370        // return type off `ConfigMap` (which would break the SSA
371        // `Patch::Apply(&cm)` slot the export-worker chains through)
372        // fails this coercion at compile time.
373        let _witness: fn(
374            &str,
375            &str,
376            BTreeMap<String, String>,
377            Option<BTreeMap<String, String>>,
378        ) -> ConfigMap = with_data;
379    }
380
381    #[test]
382    fn with_data_stamps_name_namespace_data_and_default_binary_data_when_no_labels() {
383        // Byte-shape parity witness against the export-worker's pre-
384        // lift 5-link struct literal (`ConfigMap { metadata:
385        // ObjectMeta { name: Some(<name>.to_string()), namespace:
386        // Some(<ns>.to_string()), ..Default::default() }, data:
387        // Some(<data>), binary_data: None, ..Default::default() }`) —
388        // every observable slot the pre-lift chain stamped is present
389        // in the composer's output with the same value.
390        let mut data = BTreeMap::new();
391        data.insert("receipt.yaml".to_string(), "envelope payload".to_string());
392
393        let cm = with_data("export-run-1", "tatara-system", data.clone(), None);
394
395        assert_eq!(
396            cm.metadata.name.as_deref(),
397            Some("export-run-1"),
398            "name-slot rides `Some(<name>.to_string())` at the composer",
399        );
400        assert_eq!(
401            cm.metadata.namespace.as_deref(),
402            Some("tatara-system"),
403            "ns-slot rides `Some(<ns>.to_string())` at the composer",
404        );
405        assert!(
406            cm.metadata.labels.is_none(),
407            "labels-slot preserves the `None` the export-worker consumer passes — an empty map would be a distinct wire shape",
408        );
409        assert_eq!(
410            cm.data.as_ref(),
411            Some(&data),
412            "data-slot rides `Some(<data>)` at the composer — the receipt payload the reader gates on",
413        );
414        assert!(
415            cm.binary_data.is_none(),
416            "binary_data rides `..Default::default()` = `None` — the export-worker's explicit `Option::<BTreeMap<String, ByteString>>::None` pre-lift is byte-equivalent",
417        );
418    }
419
420    #[test]
421    fn with_data_preserves_passed_labels_map_verbatim_when_some() {
422        // Byte-shape parity witness against the closed-loop-probe's
423        // pre-lift 5-link struct literal (`ConfigMap { metadata:
424        // ObjectMeta { name: Some(<name>.into()), namespace:
425        // Some(<ns>.into()), labels: Some(BTreeMap::from([...])),
426        // ..Default::default() }, data: Some(<data>),
427        // ..Default::default() }`) — the labels map the caller passes
428        // rides through to the ObjectMeta verbatim (no key rename, no
429        // value coercion, no default injection of unrelated labels).
430        let mut data = BTreeMap::new();
431        data.insert("receipt.json".to_string(), "{}".to_string());
432        let labels = BTreeMap::from([(
433            "tatara.pleme.io/receipt".to_string(),
434            "tatara-receipt/v1".to_string(),
435        )]);
436
437        let cm = with_data(
438            "closed-loop-probe-receipt",
439            "probe-ns",
440            data,
441            Some(labels.clone()),
442        );
443
444        assert_eq!(
445            cm.metadata.labels.as_ref(),
446            Some(&labels),
447            "labels-slot preserves the passed map verbatim — a regression that dropped the tatara.pleme.io/receipt label would silently break operator kubectl-selectors",
448        );
449    }
450}