1pub mod allocation;
8pub mod attestation;
9pub mod boundary;
10pub mod classification;
11pub mod compliance;
12pub mod crd;
13pub mod encapsulates;
14pub mod env;
15pub mod ephemeral;
16pub mod export;
17pub mod hostname;
18pub mod identity;
19pub mod intent;
20pub mod lifetime;
21pub mod lifetime_clock;
22pub mod matrix;
23pub mod phase;
24pub mod pool;
25pub mod receipt;
26pub mod routing;
27pub mod signal;
28pub mod spec;
29pub mod status;
30pub mod table;
31pub mod tagged_union;
32
33pub mod prelude {
34 pub use crate::allocation::{
35 AllocationCondition, AllocationPhase, AllocationSpec, AllocationStatus,
36 EphemeralAllocation, Requestor,
37 };
38 pub use crate::attestation::ProcessAttestation;
39 pub use crate::boundary::{Boundary, Condition, ConditionKind, UnknownConditionKind};
40 pub use crate::classification::{
41 Arity, CalmClassification, Classification, ConvergencePointType, DataClassification,
42 Horizon, HorizonKind, OptimizationDirection, SubstrateType, UnknownCalmClassification,
43 UnknownConvergencePointType, UnknownDataClassification, UnknownHorizonKind,
44 UnknownOptimizationDirection, UnknownSubstrateType,
45 };
46 pub use crate::compliance::{
47 ComplianceBinding, ComplianceSpec, UnknownVerificationPhase, VerificationPhase,
48 };
49 pub use crate::crd::{Process, ProcessSpec, ProcessStatus};
50 pub use crate::encapsulates::{
51 BareWorkload, EncapsulatesSpec, EncapsulationKind, EncapsulationKindError,
52 EncapsulationKindVariant, EncapsulationMode, EncapsulationTarget, ExistingHelmRelease,
53 ExistingKustomization, UnknownEncapsulationMode, UnknownEncapsulationTarget,
54 };
55 pub use crate::ephemeral::{compile_ephemeral_source, EphemeralSpec};
56 pub use crate::export::{
57 ArtifactError, ArtifactKind, ArtifactSource, ArtifactVariant, ChannelError, ChannelKind,
58 ChannelVariant, ExportSpec, ExportTrigger, HttpEventChannel, NatsSubjectChannel,
59 ProcessSnapshotSource, ReceiptsSource, ReportFormat, ReportPayloadShape, RunMarkerSource,
60 StdoutChannel, TestReportSource, UnknownArtifactKind, UnknownChannelKind,
61 UnknownExportTrigger, UnknownReportFormat, VectorChannel, DEFAULT_NATS_URL,
62 DEFAULT_VECTOR_INGEST,
63 };
64 pub use crate::hostname::{
65 ephemeral_id_from_spec, fmt_fqdn, fmt_fqdn_stable, resolve_ephemeral_id, HostnameError,
66 EPHEMERAL_ID_HASH_LEN,
67 };
68 pub use crate::identity::{content_hash, derive_identity, format_process_address, Identity};
69 pub use crate::intent::{
70 AplicacaoIntent, ContainerIntent, FluxIntent, GuestIntent, HelmLifecyclePolicy,
71 HelmRemediationPolicy, Intent, IntentError, IntentKind, IntentVariant, LispIntent,
72 NixIntent, UnknownWorkloadKind, WorkloadKind, FLUX_HELM_DEFAULT_INTERVAL,
73 HELM_LIFECYCLE_DEFAULT_RETRIES, HELM_LIFECYCLE_DEFAULT_TIMEOUT,
74 };
75 pub use crate::lifetime::{
76 EphemeralLifetime, Lifetime, LifetimeError, LifetimeKind, LifetimeVariant,
77 PermanentLifetime, TeardownPolicy, UnknownTeardownPolicy,
78 };
79 pub use crate::lifetime_clock::{
80 evaluate as lifetime_clock_evaluate, AutoTerminate, AutoTerminateKind, TerminateReason,
81 TerminateReasonKind, UnknownAutoTerminateKind, UnknownTerminateReasonKind,
82 };
83 pub use crate::matrix::{
84 compile_env_matrix_source, EnvMatrixSpec, MatrixAxis, MatrixBudget, NamedEphemeral,
85 SelectStrategy, SelectStrategyKind, UnknownSelectStrategyKind,
86 };
87 pub use crate::phase::{ProcessPhase, UnknownPhase};
88 pub use crate::pool::{
89 AllocationRef, EphemeralPool, MatchKey, MemberState, PoolCondition, PoolMember, PoolPhase,
90 PoolSelector, PoolSpec, PoolStatus, ReplacementPolicy, ReturnPolicy, UnknownMemberState,
91 UnknownPoolPhase, UnknownReplacementPolicy,
92 };
93 pub use crate::receipt::{
94 default_receipt_config_map_name, ReceiptEnvelope, ReceiptError, ReceiptKind,
95 RECEIPT_CM_SUFFIX, RECEIPT_VERSION,
96 };
97 pub use crate::routing::{RoutingBackend, RoutingForm, RoutingHostname, RoutingSpec};
98 pub use crate::signal::{ProcessSignal, SighupStrategy, UnknownSighupStrategy};
99 pub use crate::spec::{
100 DependsOn, IdentitySpec, MustReachPhase, SignalPolicy, UnknownMustReachPhase,
101 };
102 pub use crate::status::{
103 BoundaryStatus, CheckedCondition, ComplianceStatus, FluxResourceRef, ProcessCondition,
104 RenderedResourceCoords,
105 };
106 pub use crate::table::{
107 ClaimRecord, ProcessEntry, ProcessTable, ProcessTableSpec, ProcessTableStatus,
108 };
109}
110
111pub const GROUP: &str = "tatara.pleme.io";
113pub const VERSION: &str = "v1alpha1";
115pub const PROCESS_KIND: &str = "Process";
123
124pub fn api_version() -> String {
133 format!("{GROUP}/{VERSION}")
134}
135
136pub fn owner_reference_json(name: &str, uid: &str) -> serde_json::Value {
157 serde_json::json!({
158 "apiVersion": api_version(),
159 "kind": PROCESS_KIND,
160 "name": name,
161 "uid": uid,
162 "controller": true,
163 "blockOwnerDeletion": true,
164 })
165}
166
167pub mod annotations {
169 pub const MANAGED_BY: &str = "tatara.pleme.io/managed-by";
170 pub const PROCESS: &str = "tatara.pleme.io/process";
171 pub const PID: &str = "tatara.pleme.io/pid";
172 pub const CONTENT_HASH: &str = "tatara.pleme.io/content-hash";
173 pub const ATTESTATION_ROOT: &str = "tatara.pleme.io/attestation-root";
174 pub const GENERATION: &str = "tatara.pleme.io/generation";
175 pub const SIGNAL: &str = "tatara.pleme.io/signal";
176 pub const RELEASED_FROM: &str = "tatara.pleme.io/released-from";
182 pub const ROLE: &str = "tatara.pleme.io/role";
185 pub const EXPORT_INDEX: &str = "tatara.pleme.io/export-index";
190 pub const APP: &str = "tatara.pleme.io/app";
197 pub const ROUTING_FORM: &str = "tatara.pleme.io/routing-form";
204}
205
206pub const PROCESS_FINALIZER: &str = "tatara.pleme.io/process-finalizer";
208
209pub mod schema_helpers {
216 use schemars::{gen::SchemaGenerator, schema::Schema};
217 pub fn preserve_unknown_object(_g: &mut SchemaGenerator) -> Schema {
222 serde_json::from_value(serde_json::json!({
223 "type": "object",
224 "x-kubernetes-preserve-unknown-fields": true
225 }))
226 .expect("static JSON literal parses as Schema")
227 }
228}
229
230#[cfg(test)]
231mod owner_reference_tests {
232 use super::{api_version, owner_reference_json, GROUP, PROCESS_KIND, VERSION};
243 use serde_json::json;
244
245 #[test]
246 fn api_version_composes_group_and_version() {
247 assert_eq!(api_version(), format!("{GROUP}/{VERSION}"));
249 }
250
251 #[test]
252 fn api_version_byte_matches_wire_form_pre_lift() {
253 assert_eq!(api_version(), "tatara.pleme.io/v1alpha1");
260 }
261
262 #[test]
263 fn process_kind_is_process_literal() {
264 assert_eq!(PROCESS_KIND, "Process");
267 }
268
269 #[test]
270 fn owner_reference_json_has_all_six_slots_present() {
271 let v = owner_reference_json("my-process", "abc-uid");
272 let obj = v.as_object().expect("owner reference is a JSON object");
273 for k in [
274 "apiVersion",
275 "kind",
276 "name",
277 "uid",
278 "controller",
279 "blockOwnerDeletion",
280 ] {
281 assert!(obj.contains_key(k), "missing owner-reference slot: {k}");
282 }
283 assert_eq!(obj.len(), 6, "owner reference must have exactly 6 slots");
284 }
285
286 #[test]
287 fn owner_reference_json_apiversion_routes_through_api_version_owner() {
288 let v = owner_reference_json("x", "y");
289 assert_eq!(v["apiVersion"], api_version());
290 }
291
292 #[test]
293 fn owner_reference_json_kind_routes_through_process_kind_const() {
294 let v = owner_reference_json("x", "y");
295 assert_eq!(v["kind"], PROCESS_KIND);
296 }
297
298 #[test]
299 fn owner_reference_json_stamps_supplied_name_and_uid() {
300 let v = owner_reference_json("some-name", "some-uid");
301 assert_eq!(v["name"], "some-name");
302 assert_eq!(v["uid"], "some-uid");
303 }
304
305 #[test]
306 fn owner_reference_json_controller_and_block_owner_deletion_are_true() {
307 let v = owner_reference_json("x", "y");
312 assert_eq!(v["controller"], true);
313 assert_eq!(v["blockOwnerDeletion"], true);
314 }
315
316 #[test]
317 fn owner_reference_json_matches_hand_authored_shape_pre_lift() {
318 let via_owner = owner_reference_json("p", "u");
325 let hand_authored = json!({
326 "apiVersion": "tatara.pleme.io/v1alpha1",
327 "kind": "Process",
328 "name": "p",
329 "uid": "u",
330 "controller": true,
331 "blockOwnerDeletion": true,
332 });
333 assert_eq!(via_owner, hand_authored);
334 }
335
336 #[test]
337 fn owner_reference_json_preserves_empty_name_and_uid_bytewise() {
338 let v = owner_reference_json("", "");
348 assert_eq!(v["name"], "");
349 assert_eq!(v["uid"], "");
350 }
351}
352
353pub type Definition = tatara_lisp::NamedDefinition<crate::crd::ProcessSpec>;
361
362pub fn compile_source(src: &str) -> tatara_lisp::Result<Vec<Definition>> {
365 tatara_lisp::compile_named::<crate::crd::ProcessSpec>(src)
366}
367
368pub fn register_all() {
375 tatara_lisp::domain::register::<crate::crd::ProcessSpec>();
376 tatara_lisp::domain::register::<crate::ephemeral::EphemeralSpec>();
377}
378
379#[cfg(test)]
380mod compile_tests {
381 use super::compile_source;
382 use crate::classification::{ConvergencePointType, SubstrateType};
383 use crate::compliance::VerificationPhase;
384 use crate::spec::MustReachPhase;
385
386 #[test]
389 fn full_processspec_round_trip_via_derive() {
390 let src = r#"
391 (defpoint observability-stack
392 :identity (:parent "seph.1")
393 :classification (:point-type Gate
394 :substrate Observability
395 :horizon (:kind Bounded)
396 :calm Monotone
397 :data-classification Internal)
398 :intent (:nix (:flake-ref "github:pleme-io/k8s"
399 :attribute "observability"
400 :attic-cache "main"))
401 :boundary (:postconditions
402 ((:kind KustomizationHealthy
403 :params (:name "observability-stack"
404 :namespace "flux-system"))
405 (:kind PromQL
406 :params (:query "up == 1")))
407 :timeout "15m")
408 :compliance (:baseline "fedramp-moderate"
409 :bindings ((:framework "nist-800-53"
410 :control-id "SC-7"
411 :phase AtBoundary)))
412 :depends-on ((:name "secret-injection" :must-reach Attested))
413 :signals (:sigterm-grace-seconds 480
414 :sighup-strategy Reconverge))
415 "#;
416 let defs = compile_source(src).expect("compile");
417 assert_eq!(defs.len(), 1);
418 let d = &defs[0];
419 assert_eq!(d.name, "observability-stack");
420
421 assert_eq!(d.spec.identity.parent.as_deref(), Some("seph.1"));
423
424 assert_eq!(d.spec.classification.point_type, ConvergencePointType::Gate);
426 assert_eq!(
427 d.spec.classification.substrate,
428 SubstrateType::Observability
429 );
430
431 let nix = d.spec.intent.nix.as_ref().expect("nix intent");
433 assert_eq!(nix.flake_ref, "github:pleme-io/k8s");
434 assert_eq!(nix.attribute, "observability");
435 assert_eq!(nix.attic_cache.as_deref(), Some("main"));
436
437 assert_eq!(d.spec.boundary.postconditions.len(), 2);
439 assert_eq!(d.spec.boundary.timeout.as_deref(), Some("15m"));
440
441 assert_eq!(
443 d.spec.compliance.baseline.as_deref(),
444 Some("fedramp-moderate")
445 );
446 assert_eq!(d.spec.compliance.bindings.len(), 1);
447 assert_eq!(
448 d.spec.compliance.bindings[0].phase,
449 VerificationPhase::AtBoundary
450 );
451
452 assert_eq!(d.spec.depends_on.len(), 1);
454 assert_eq!(d.spec.depends_on[0].must_reach, MustReachPhase::Attested);
455
456 assert_eq!(d.spec.signals.sigterm_grace_seconds, 480);
458 }
459
460 #[test]
461 fn missing_required_field_errors() {
462 let src = r#"(defpoint x :intent (:nix (:flake-ref "f" :attribute "a")))"#;
464 assert!(compile_source(src).is_err());
465 }
466
467 #[test]
468 fn serde_default_fields_are_optional() {
469 let src = r#"
472 (defpoint x
473 :classification (:point-type Transform :substrate Compute)
474 :intent (:flux (:git-repository "g" :path ".")))
475 "#;
476 let defs = compile_source(src).expect("compile");
477 assert_eq!(defs.len(), 1);
478 let d = &defs[0];
479 assert!(d.spec.depends_on.is_empty());
480 assert!(d.spec.boundary.postconditions.is_empty());
481 assert!(d.spec.compliance.bindings.is_empty());
482 assert!(!d.spec.suspended);
483 assert!(d.spec.lifetime.is_default());
485 assert!(!d.spec.lifetime.is_ephemeral());
486 }
487
488 #[test]
491 fn register_all_resolves_defpoint_and_defephemeral() {
492 use tatara_lisp::domain::lookup;
493 super::register_all();
494 super::register_all(); assert!(lookup("defpoint").is_some(), "defpoint must resolve");
496 assert!(
497 lookup("defephemeral").is_some(),
498 "defephemeral must resolve"
499 );
500 }
501
502 #[test]
506 fn defpoint_with_aplicacao_intent_and_ephemeral_lifetime() {
507 use crate::intent::IntentVariant;
508 use crate::lifetime::{LifetimeVariant, TeardownPolicy};
509 let src = r#"
510 (defpoint closed-loop-attest
511 :classification (:point-type Gate :substrate Compute)
512 :intent (:aplicacao
513 (:chart-ref "oci://ghcr.io/pleme-io/charts/lareira-demo-app"
514 :version "0.5.5"
515 :profile "all-in-one"
516 :values-overlay (:cluster (:name "ephemeral-test-01"))
517 :target-namespace "demo-test"))
518 :boundary (:postconditions
519 ((:kind HelmReleaseReleased
520 :params (:name "demo-app-consolidated"
521 :namespace "demo-test"))
522 (:kind ClosedLoopAuth
523 :params (:issuer (:service "demo-app-issuer" :port 8080)
524 :consumer (:service "demo-app-gateway" :port 8000)
525 :probeImage "ghcr.io/pleme-io/closed-loop-probe:0.1.0"))))
526 :lifetime (:ephemeral (:ttl "1h"
527 :teardown-policy OnAttested
528 :max-concurrent 1)))
529 "#;
530 let defs = compile_source(src).expect("compile");
531 assert_eq!(defs.len(), 1);
532 let d = &defs[0];
533
534 match d.spec.intent.variant().unwrap() {
536 IntentVariant::Aplicacao(a) => {
537 assert_eq!(a.profile, "all-in-one");
538 assert_eq!(a.version, "0.5.5");
539 assert_eq!(a.target_namespace.as_deref(), Some("demo-test"));
540 assert_eq!(a.values_overlay["cluster"]["name"], "ephemeral-test-01");
541 }
542 other => panic!("expected Aplicacao, got {other:?}"),
543 }
544
545 match d.spec.lifetime.variant().unwrap() {
547 LifetimeVariant::Ephemeral(e) => {
548 assert_eq!(e.ttl, "1h");
549 assert_eq!(e.teardown_policy, TeardownPolicy::OnAttested);
550 assert_eq!(e.max_concurrent, 1);
551 }
552 other => panic!("expected ephemeral, got {other:?}"),
553 }
554
555 assert_eq!(d.spec.boundary.postconditions.len(), 2);
557 assert_eq!(
558 d.spec.boundary.postconditions[1].kind,
559 crate::boundary::ConditionKind::ClosedLoopAuth
560 );
561 }
562}