Skip to main content

tatara_process/
table.rs

1//! `ProcessTable` — cluster-scoped `/proc` registry.
2
3use chrono::{DateTime, Utc};
4use kube::CustomResource;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9use crate::phase::ProcessPhase;
10
11/// ProcessTable — the cluster-wide `/proc` equivalent.
12///
13/// One per cluster (singleton by convention, name `"proc"`).
14/// Aggregates every `Process` status and hands out PIDs.
15#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
16#[kube(
17    group = "tatara.pleme.io",
18    version = "v1alpha1",
19    kind = "ProcessTable",
20    plural = "processtables",
21    shortname = "pt",
22    status = "ProcessTableStatus",
23    printcolumn = r#"{"name":"Procs","type":"integer","jsonPath":".status.processCount"}"#,
24    printcolumn = r#"{"name":"Ready","type":"integer","jsonPath":".status.readyCount"}"#,
25    printcolumn = r#"{"name":"NextPID","type":"integer","jsonPath":".spec.nextSequence"}"#,
26    printcolumn = r#"{"name":"Depth","type":"integer","jsonPath":".spec.maxDepth"}"#,
27    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
28)]
29#[serde(rename_all = "camelCase")]
30pub struct ProcessTableSpec {
31    /// Next hierarchical sequence number to hand out at this level.
32    #[serde(default = "default_next_seq")]
33    pub next_sequence: u32,
34
35    /// PID path of this cluster's parent (None at the root).
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub parent_pid: Option<String>,
38
39    /// DNS domain (e.g., `quero.lol`).
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub dns_domain: Option<String>,
42
43    /// DNS zone id (e.g., Route53 zone id).
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub dns_zone_id: Option<String>,
46
47    /// Max recursion depth from this node (0 = unlimited).
48    #[serde(default)]
49    pub max_depth: u32,
50
51    /// Max concurrent direct children (0 = unlimited).
52    #[serde(default)]
53    pub max_children: u32,
54
55    /// Grace window before escalating SIGTERM → SIGKILL (seconds).
56    #[serde(default = "default_sigterm_timeout")]
57    pub sigterm_timeout_seconds: u32,
58
59    /// After this long in Zombie, force-reap.
60    #[serde(default = "default_zombie_timeout")]
61    pub zombie_timeout_seconds: u32,
62
63    /// When true, PID 1 adopts and terminates orphaned Processes.
64    #[serde(default = "default_true")]
65    pub orphan_reaping_enabled: bool,
66}
67
68fn default_next_seq() -> u32 {
69    1
70}
71fn default_sigterm_timeout() -> u32 {
72    480
73}
74fn default_zombie_timeout() -> u32 {
75    600
76}
77fn default_true() -> bool {
78    true
79}
80
81#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
82#[serde(rename_all = "camelCase")]
83pub struct ProcessTableStatus {
84    pub process_count: u32,
85    pub ready_count: u32,
86    #[serde(default)]
87    pub processes: Vec<ProcessEntry>,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub last_reconciled: Option<DateTime<Utc>>,
90
91    /// **R6** — Stable-name claim registry. Key:
92    /// `${cluster}/${app}` (e.g., `pleme-dev/gator`). Value: the
93    /// Process currently holding the unprefixed-form DNS for that
94    /// (cluster, app) tuple. At most one Process per key; transfer
95    /// is atomic on the holder's Failed/Zombie/Reaped transition.
96    ///
97    /// The reconciler's claim arbiter (`tatara-reconciler::claim`)
98    /// is the sole writer; every other actor reads.
99    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
100    pub claims: BTreeMap<String, ClaimRecord>,
101}
102
103/// One stable-name claim record. Cited by holder Process's
104/// namespace/name + content-hash PID + when granted + priority that
105/// won the arbitration. Used by [`ProcessTableStatus::claims`].
106#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
107#[serde(rename_all = "camelCase")]
108pub struct ClaimRecord {
109    /// `${namespace}/${name}` of the holder Process. Reconciler uses
110    /// this to resolve the holder during render — every emitted
111    /// stable-form Ingress carries an ownerRef back to this Process.
112    pub holder: String,
113
114    /// PID path of the holder (mirror of the Process's
115    /// `status.pid`). Lets observers detect a stale claim if the
116    /// holder is reaped but the registry update lags.
117    pub pid: String,
118
119    /// When the claim was granted to the current holder. Operator-
120    /// visible "how long has this been the claim holder?" metric.
121    pub granted_at: DateTime<Utc>,
122
123    /// Priority the holder declared (from `RoutingSpec.priority`).
124    /// Stamped so a new candidate-Process can compare without
125    /// reading the holder's spec.
126    pub priority: i32,
127}
128
129/// One row of `/proc`.
130#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
131#[serde(rename_all = "camelCase")]
132pub struct ProcessEntry {
133    pub name: String,
134    pub namespace: String,
135    pub pid: String,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub parent_pid: Option<String>,
138    pub phase: ProcessPhase,
139    /// Serialized `ConvergencePointType` (e.g., `"Gate"`).
140    pub point_type: String,
141    /// Serialized `SubstrateType` (e.g., `"Observability"`).
142    pub substrate: String,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub content_hash: Option<String>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub attestation_root: Option<String>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub last_updated: Option<DateTime<Utc>>,
149}