⎇ 762bf95dfd770cc8bf51f21605bc2cc6cc88ea76
crates/init/arje-absorb/src/card.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! Traducción del modelo intermedio a Tarjetas Semilla brahman.
2//!
3//! Cada [`ForeignService`] se vuelve una Card hija; el conjunto cuelga
4//! como `genesis` de una Card raíz que `arje-zero` encarna al arrancar.
5
6use std::time::Duration;
7
8use brahman_card::{
9 Capability, Card, CgroupSpec, FsPolicy, Lifecycle, NetworkingPolicy, Payload, Permissions,
10 Priority, SomaSpec, Supervision,
11};
12
13use crate::model::{ForeignService, ServiceKind};
14
15/// Convierte un servicio absorbido en una Card hija (genesis child).
16fn service_to_card(svc: &ForeignService) -> Card {
17 let (lifecycle, supervision) = match svc.kind {
18 ServiceKind::Daemon => (
19 Lifecycle::Daemon,
20 Supervision::Restart {
21 initial: Duration::from_millis(1_000),
22 max: Duration::from_millis(60_000),
23 },
24 ),
25 ServiceKind::OneShot => (Lifecycle::Oneshot, Supervision::OneShot),
26 };
27 Card {
28 payload: Payload::Native {
29 exec: svc.exec.clone(),
30 argv: svc.argv.clone(),
31 envp: svc.env.clone(),
32 },
33 supervision,
34 lifecycle,
35 priority: Priority::Normal,
36 // Servicio de sistema absorbido: sin aislar (namespaces en
37 // `false` por defecto), con FS de escritura, red y subprocesos.
38 // La migración conserva el comportamiento; endurecer el sandbox
39 // queda como trabajo posterior, Card por Card.
40 permissions: Permissions {
41 filesystem: FsPolicy::ReadWrite,
42 networking: NetworkingPolicy::Full,
43 processes: true,
44 ..Permissions::default()
45 },
46 soma: SomaSpec {
47 cgroup: CgroupSpec {
48 path: "ente.slice/absorbed".to_string(),
49 ..CgroupSpec::default()
50 },
51 ..SomaSpec::default()
52 },
53 ..Card::new(svc.name.clone())
54 }
55}
56
57/// La Card de `carmen-dm`: el compositor `mirada` en modo greeter,
58/// como gestor de login gráfico. Para agregar a una Semilla absorbida
59/// al migrar un escritorio (flag `--with-carmen`). Idéntica a la
60/// entrada `carmen-dm` de `seeds/arje-host.card.json`.
61pub fn carmen_dm_card() -> Card {
62 Card {
63 payload: Payload::Native {
64 exec: "/usr/bin/mirada-compositor".to_string(),
65 argv: vec!["--greeter".to_string(), "--drm".to_string()],
66 envp: vec![
67 (
68 "PATH".to_string(),
69 "/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin".to_string(),
70 ),
71 ("XDG_RUNTIME_DIR".to_string(), "/run".to_string()),
72 (
73 "MIRADA_GREETER_BIN".to_string(),
74 "/usr/bin/mirada-greeter".to_string(),
75 ),
76 ],
77 },
78 supervision: Supervision::Restart {
79 initial: Duration::from_millis(2_000),
80 max: Duration::from_millis(60_000),
81 },
82 lifecycle: Lifecycle::Daemon,
83 priority: Priority::High,
84 permissions: Permissions {
85 filesystem: FsPolicy::ReadWrite,
86 networking: NetworkingPolicy::None,
87 processes: true,
88 ..Permissions::default()
89 },
90 soma: SomaSpec {
91 cgroup: CgroupSpec {
92 path: "ente.slice/carmen".to_string(),
93 ..CgroupSpec::default()
94 },
95 ..SomaSpec::default()
96 },
97 ..Card::new("carmen-dm")
98 }
99}
100
101/// Arma una Tarjeta Semilla raíz que encarna todos los servicios
102/// absorbidos como hijas `genesis` de `arje-zero`.
103pub fn build_seed(label: &str, services: &[ForeignService]) -> Card {
104 let genesis: Vec<Card> = services.iter().map(service_to_card).collect();
105 Card {
106 provides: [Capability::Spawn, Capability::Journal]
107 .into_iter()
108 .collect(),
109 permissions: Permissions {
110 filesystem: FsPolicy::ReadWrite,
111 networking: NetworkingPolicy::Full,
112 processes: true,
113 ..Permissions::default()
114 },
115 genesis,
116 ..Card::new(label)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn svc(name: &str, kind: ServiceKind) -> ForeignService {
125 ForeignService {
126 name: name.to_string(),
127 exec: "/usr/bin/foo".to_string(),
128 argv: vec!["-x".to_string()],
129 env: Vec::new(),
130 kind,
131 }
132 }
133
134 #[test]
135 fn seed_validates() {
136 let seed = build_seed(
137 "arje.seed.absorbed",
138 &[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::OneShot)],
139 );
140 seed.validate().expect("la Semilla absorbida debe validar");
141 assert_eq!(seed.genesis.len(), 2);
142 }
143
144 #[test]
145 fn daemon_maps_to_restart() {
146 let c = service_to_card(&svc("d", ServiceKind::Daemon));
147 assert!(matches!(c.supervision, Supervision::Restart { .. }));
148 assert_eq!(c.lifecycle, Lifecycle::Daemon);
149 }
150
151 #[test]
152 fn oneshot_maps_to_oneshot() {
153 let c = service_to_card(&svc("o", ServiceKind::OneShot));
154 assert!(matches!(c.supervision, Supervision::OneShot));
155 assert_eq!(c.lifecycle, Lifecycle::Oneshot);
156 }
157
158 #[test]
159 fn children_get_distinct_ids() {
160 let seed = build_seed(
161 "x",
162 &[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::Daemon)],
163 );
164 assert_ne!(seed.genesis[0].id, seed.genesis[1].id);
165 }
166
167 #[test]
168 fn empty_service_list_still_validates() {
169 let seed = build_seed("arje.seed.absorbed", &[]);
170 seed.validate().expect("una Semilla sin hijas es válida");
171 }
172
173 #[test]
174 fn carmen_card_is_valid_greeter() {
175 let c = carmen_dm_card();
176 c.validate().expect("la Card de carmen-dm debe validar");
177 match &c.payload {
178 brahman_card::Payload::Native { exec, argv, .. } => {
179 assert_eq!(exec, "/usr/bin/mirada-compositor");
180 assert!(argv.contains(&"--greeter".to_string()));
181 }
182 _ => panic!("carmen-dm debe ser un payload Native"),
183 }
184 }
185
186 #[test]
187 fn carmen_can_be_appended_to_absorbed_seed() {
188 let mut seed = build_seed("arje.seed.absorbed", &[svc("a", ServiceKind::Daemon)]);
189 seed.genesis.push(carmen_dm_card());
190 seed.validate().expect("la Semilla con carmen debe validar");
191 assert_eq!(seed.genesis.len(), 2);
192 }
193}