Skip to main content

tatara_process/
crd.rs

1//! The `Process` CRD — `tatara.pleme.io/v1alpha1`.
2
3use chrono::{DateTime, Utc};
4use kube::CustomResource;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use tatara_lisp::DeriveTataraDomain;
8
9use crate::attestation::ProcessAttestation;
10use crate::boundary::Boundary;
11use crate::classification::Classification;
12use crate::compliance::ComplianceSpec;
13use crate::encapsulates::EncapsulatesSpec;
14use crate::identity::Identity;
15use crate::intent::Intent;
16use crate::lifetime::Lifetime;
17use crate::phase::ProcessPhase;
18use crate::routing::RoutingSpec;
19use crate::signal::ProcessSignal;
20use crate::spec::{DependsOn, IdentitySpec, SignalPolicy};
21use crate::status::{BoundaryStatus, ComplianceStatus, FluxResourceRef, ProcessCondition};
22
23/// Process — one element of the tatara convergence lattice, reconciled as a Unix process.
24///
25/// ```yaml
26/// apiVersion: tatara.pleme.io/v1alpha1
27/// kind: Process
28/// metadata:
29///   name: observability-stack
30///   namespace: seph
31/// spec:
32///   identity:
33///     parent: seph.1
34///   classification:
35///     pointType: Gate
36///     substrate: Observability
37///   intent:
38///     nix:
39///       flakeRef: github:pleme-io/k8s?dir=shared/infrastructure
40///       attribute: observability
41///   compliance:
42///     baseline: fedramp-moderate
43///     bindings:
44///       - framework: nist-800-53
45///         controlId: SC-7
46///         phase: AtBoundary
47///   dependsOn:
48///     - name: akeyless-injection
49/// ```
50#[derive(CustomResource, DeriveTataraDomain, Clone, Debug, Deserialize, Serialize, JsonSchema)]
51#[kube(
52    group = "tatara.pleme.io",
53    version = "v1alpha1",
54    kind = "Process",
55    plural = "processes",
56    shortname = "proc",
57    namespaced,
58    status = "ProcessStatus",
59    printcolumn = r#"{"name":"PID","type":"string","jsonPath":".status.pid"}"#,
60    printcolumn = r#"{"name":"Phase","type":"string","jsonPath":".status.phase"}"#,
61    printcolumn = r#"{"name":"Type","type":"string","jsonPath":".spec.classification.pointType"}"#,
62    printcolumn = r#"{"name":"Substrate","type":"string","jsonPath":".spec.classification.substrate"}"#,
63    printcolumn = r#"{"name":"Gen","type":"integer","jsonPath":".status.attestation.generation"}"#,
64    printcolumn = r#"{"name":"Age","type":"date","jsonPath":".metadata.creationTimestamp"}"#
65)]
66#[serde(rename_all = "camelCase")]
67#[tatara(keyword = "defpoint")]
68pub struct ProcessSpec {
69    /// Identity (parent, name override).
70    #[serde(default)]
71    pub identity: IdentitySpec,
72
73    /// Lattice position (6 dimensions).
74    pub classification: Classification,
75
76    /// Where rendered artifacts come from. Exactly one variant must be set.
77    pub intent: Intent,
78
79    /// Boundary predicates (preconditions / postconditions).
80    #[serde(default)]
81    pub boundary: Boundary,
82
83    /// Compliance bindings + baseline.
84    #[serde(default)]
85    pub compliance: ComplianceSpec,
86
87    /// Lattice dependencies — must reach phase before we proceed.
88    #[serde(default)]
89    pub depends_on: Vec<DependsOn>,
90
91    /// Signal policy (grace, SIGHUP strategy, start-suspended).
92    #[serde(default)]
93    pub signals: SignalPolicy,
94
95    /// Lifetime — `Permanent` (default, re-converging) or `Ephemeral`
96    /// (auto-SIGTERM per `teardown_policy` + TTL clock).
97    #[serde(default, skip_serializing_if = "Lifetime::is_default")]
98    pub lifetime: Lifetime,
99
100    /// External edges — DNS + Ingress. When `None`, the Process is
101    /// internal-only (matches today's default). See
102    /// [`crate::routing`] for the full shape.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub routing: Option<RoutingSpec>,
105
106    /// Pre-existing in-cluster state this Process wraps. When `None`,
107    /// the Process is greenfield (Manage mode implicitly applied to
108    /// nothing pre-existing). See [`crate::encapsulates`] for the
109    /// three modes (Manage / Adopt / Observe).
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    pub encapsulates: Option<EncapsulatesSpec>,
112
113    /// Soft-suspend marker — reconciler treats as SIGSTOP.
114    /// Same effect as delivering SIGSTOP, but persistent across restarts.
115    #[serde(default)]
116    pub suspended: bool,
117}
118
119/// Process status — every field optional until the reconciler writes it.
120#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
121#[serde(rename_all = "camelCase")]
122pub struct ProcessStatus {
123    /// Hierarchical PID path — e.g., `"seph.1.7"`.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub pid: Option<String>,
126
127    /// Parent PID path (mirror of `spec.identity.parent`, resolved at fork).
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub parent: Option<String>,
130
131    /// Direct children's PID paths.
132    #[serde(default)]
133    pub children: Vec<String>,
134
135    /// Resolved identity (name + content hash).
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub identity: Option<Identity>,
138
139    /// Current phase.
140    #[serde(default)]
141    pub phase: ProcessPhase,
142
143    /// When the process entered the current phase.
144    #[serde(default, skip_serializing_if = "Option::is_none")]
145    pub phase_since: Option<DateTime<Utc>>,
146
147    /// Three-pillar attestation (written at end of every successful cycle).
148    #[serde(default, skip_serializing_if = "Option::is_none")]
149    pub attestation: Option<ProcessAttestation>,
150
151    /// FluxCD resources currently owned by this Process.
152    #[serde(default)]
153    pub flux_resources: Vec<FluxResourceRef>,
154
155    /// Boundary verification state.
156    #[serde(default)]
157    pub boundary: BoundaryStatus,
158
159    /// Compliance summary at the latest attestation.
160    #[serde(default)]
161    pub compliance: ComplianceStatus,
162
163    /// Pending signals (delivered, not yet handled).
164    #[serde(default)]
165    pub signal_queue: Vec<ProcessSignal>,
166
167    /// Standard K8s Conditions.
168    #[serde(default)]
169    pub conditions: Vec<ProcessCondition>,
170
171    /// Human-readable last status message.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub message: Option<String>,
174
175    /// Exit code (only set on Failed / Reaped).
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub exit_code: Option<i32>,
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::classification::{ConvergencePointType, SubstrateType};
184    use crate::intent::NixIntent;
185
186    #[test]
187    fn minimal_spec_serializes() {
188        let spec = ProcessSpec {
189            identity: IdentitySpec::default(),
190            classification: Classification {
191                point_type: ConvergencePointType::Gate,
192                substrate: SubstrateType::Observability,
193                horizon: Default::default(),
194                calm: Default::default(),
195                data_classification: Default::default(),
196            },
197            intent: Intent {
198                nix: Some(NixIntent {
199                    flake_ref: "github:pleme-io/k8s".into(),
200                    attribute: "obs".into(),
201                    system: None,
202                    attic_cache: None,
203                    extra_args: vec![],
204                    delegate_to_nix_build: false,
205                }),
206                ..Intent::default()
207            },
208            boundary: Default::default(),
209            compliance: Default::default(),
210            depends_on: vec![],
211            signals: Default::default(),
212            lifetime: Default::default(),
213            routing: None,
214            encapsulates: None,
215            suspended: false,
216        };
217        let yaml = serde_yaml::to_string(&spec).unwrap();
218        assert!(yaml.contains("pointType: Gate"));
219        assert!(yaml.contains("substrate: Observability"));
220        assert!(yaml.contains("flakeRef: github:pleme-io/k8s"));
221    }
222}