servicios de paquete: la receta ya sabe declarar su demonio — y el card sale IGUAL al hardcodeado
Un paquete con servicio no tenía dónde decirlo: los dos del producto (hammerd, sshd) vivían en constantes de Rust y los ocho de una sesión GNOME se lanzaban con `&` desde un script, sin supervisión ni backoff ni el CRASHED real — o sea sin nada de lo que arje es PID 1 para dar. `[[service]]` en la receta, fuera de `hash_inputs` como license/slots/evidence: declarar el servicio de openssh NO movió su hash, medido con el binario viejo (que ignora el bloque) contra el nuevo, b3:938835e6… en los dos. La prueba que autoriza el cambio no es que "parezca bien": el card generado desde recipes/openssh.toml se compara ENTERO contra SSHD_SERVICE_CARD, que es el que ya bootea en QEMU. Empatan ⇒ mover el servicio a la receta no cambiaría un byte de la seed ni del product_rootfs_hash. Por qué no alcanzaba `arje-absorb` (que existe y traduce systemd/openrc/runit/ dinit/sysvinit): sólo absorbe lo HABILITADO, los symlinks de <target>.wants/. Un rootfs nuestro no tiene ese estado — medido: 62 .service sellados en el store y CERO directorios .wants. Absorber devuelve vacío, y es la respuesta correcta a la pregunta que absorb contesta. El enable de una distro construida desde fuente no se lee del árbol: se declara.
This commit is contained in:
@@ -2083,6 +2083,31 @@ mod tests {
|
||||
assert!(sshd["supervision"]["Restart"]["initial"].is_number(), "sshd es Restart (lifecycle)");
|
||||
}
|
||||
|
||||
/// **La receta produce la MISMA Card que la constante hardcodeada.**
|
||||
///
|
||||
/// Esta es la prueba que autoriza a mover el servicio de un literal de Rust a `[[service]]` en
|
||||
/// `recipes/openssh.toml`: no compara «una card parecida», compara el `serde_json::Value`
|
||||
/// entero contra [`SSHD_SERVICE_CARD`], que es **la card que el producto ya bootea en QEMU**
|
||||
/// (verificada e2e por `scripts/ssh-e2e-test.sh`). Si empatan, declarar el servicio en la
|
||||
/// receta no cambia ni un byte de la seed — y por lo tanto tampoco el `product_rootfs_hash`.
|
||||
///
|
||||
/// Lee la receta REAL del repo a propósito: un fixture inline probaría el emisor contra sí
|
||||
/// mismo y no detectaría que alguien editó la receta y la sacó de sincronía con el producto.
|
||||
#[test]
|
||||
fn la_receta_de_openssh_reproduce_la_card_hardcodeada() {
|
||||
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let r = takana_core::Recipe::load_from_path(repo.join("recipes/openssh.toml")).unwrap();
|
||||
assert_eq!(r.services.len(), 1, "openssh declara exactamente un servicio");
|
||||
|
||||
let generada = r.services[0].card();
|
||||
let hardcodeada: serde_json::Value = serde_json::from_str(SSHD_SERVICE_CARD).unwrap();
|
||||
assert_eq!(
|
||||
generada, hardcodeada,
|
||||
"la card generada desde recipes/openssh.toml difiere de SSHD_SERVICE_CARD: \
|
||||
mover el servicio a la receta CAMBIARÍA la seed de producto"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn product_rootfs_hash_is_deterministic_and_sensitive() {
|
||||
let base = ArtifactHash::from_hex("ab");
|
||||
|
||||
@@ -17,6 +17,7 @@ pub mod proto;
|
||||
pub mod query;
|
||||
pub mod recipe;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
pub mod sign;
|
||||
pub mod store;
|
||||
pub mod swm;
|
||||
|
||||
@@ -61,6 +61,21 @@ pub struct Recipe {
|
||||
/// build. Añadirla no mueve un solo `ArtifactHash`.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub foreign: bool,
|
||||
/// **Los servicios que este paquete TRAE** (`[[service]]`), como Cards de arje.
|
||||
///
|
||||
/// Es la respuesta al hueco que la distro tenía abierto: un paquete con demonio no tenía dónde
|
||||
/// declararlo, así que los dos servicios del producto vivían hardcodeados en constantes de Rust
|
||||
/// y los ocho de una sesión GNOME se lanzaban con `&` desde un script, sin supervisión. Ver
|
||||
/// [`crate::service`] para el porqué completo (incluido por qué `arje-absorb` no lo cubre).
|
||||
///
|
||||
/// Declara **qué es** el servicio, no **si arranca**: lo segundo es del PERFIL, igual que
|
||||
/// systemd separa `[Service]` de `[Install]`.
|
||||
///
|
||||
/// **NO entra en `hash_inputs`**, como `license`, `slots` y `evidence`: describe cómo se
|
||||
/// supervisa el artefacto, no qué bytes tiene ⇒ se puebla en recetas ya selladas sin mover un
|
||||
/// solo `ArtifactHash`.
|
||||
#[serde(default, rename = "service", skip_serializing_if = "Vec::is_empty")]
|
||||
pub services: Vec<crate::service::Service>,
|
||||
/// Directorio base contra el que se resuelven rutas relativas de la receta
|
||||
/// (típicamente, `patches`). Lo fija `load_from_path`; al deserializar puro queda vacío.
|
||||
#[serde(skip, default)]
|
||||
@@ -502,7 +517,13 @@ impl Recipe {
|
||||
/// Parsea una receta desde su forma TOML. `base_dir` queda vacío: si la receta declara
|
||||
/// `patches`, el caller debe fijar `base_dir` antes de hashear, o usar `load_from_path`.
|
||||
pub fn from_toml(s: &str) -> crate::Result<Recipe> {
|
||||
toml::from_str(s).map_err(|e| crate::Error::Recipe(e.to_string()))
|
||||
let r: Recipe = toml::from_str(s).map_err(|e| crate::Error::Recipe(e.to_string()))?;
|
||||
// Los servicios se validan AL CARGAR, no al emitir la seed: un `id` mal puesto rompe la
|
||||
// reproducibilidad del rootfs de producto, y eso hay que verlo en la receta —no tres pasos
|
||||
// después, en un rootfs que ya no reproduce y no dice por qué. Recetas sin `[[service]]`
|
||||
// pasan por una lista vacía, así que esto no toca a las 979 existentes.
|
||||
crate::service::validate_all(&r.services)?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Serializa la receta a TOML. Útil para el sidecar de provenance que `takana-build`
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! `[[service]]` — **el servicio que un paquete TRAE**, traducido a una Card de arje.
|
||||
//!
|
||||
//! ## Por qué existe (el hueco, medido)
|
||||
//!
|
||||
//! Hasta hoy una receta no tenía dónde decir «yo traigo un demonio». La consecuencia no era
|
||||
//! teórica: los dos únicos servicios del producto (`hammerd` y `sshd`) vivían **hardcodeados en
|
||||
//! constantes de Rust** (`takana_bootstrap::SSHD_SERVICE_CARD`), y los ocho demonios de una sesión
|
||||
//! GNOME (dbus, logind-compat, accounts-daemon, upowerd, pipewire, pipewire-pulse, wireplumber,
|
||||
//! colord) se lanzaban a mano con `&` desde `scripts/gnome/gnome-start-qemu.sh` — **sin supervisión,
|
||||
//! sin backoff y sin el `CRASHED` real**, que es justo lo que arje sabe hacer y para lo que se lo
|
||||
//! adoptó como PID 1 (SDD 12).
|
||||
//!
|
||||
//! ## Por qué NO alcanza con `arje-absorb`
|
||||
//!
|
||||
//! arje ya trae el traductor de inits ajenos (`init/arje-absorb`, con lectores de systemd, OpenRC,
|
||||
//! runit, dinit y sysvinit). No sirve acá, y la razón es exacta: **`arje-absorb` sólo absorbe lo
|
||||
//! que está HABILITADO** — los symlinks de `<target>.wants/` — porque su trabajo es *retratar* un
|
||||
//! sistema vivo, no inventarle uno. Un rootfs de takana no tiene ese estado: medido el 2026-09-12
|
||||
//! sobre el store, hay **62 ficheros `.service` sellados (61 de usuario, 1 de sistema) y CERO
|
||||
//! directorios `.wants`**. Absorber un rootfs nuestro devuelve la lista vacía, y es la respuesta
|
||||
//! correcta a la pregunta que `arje-absorb` contesta.
|
||||
//!
|
||||
//! O sea que el «enable» de una distro construida desde fuente **no puede leerse del árbol: hay que
|
||||
//! declararlo**. Esa declaración se parte en dos, igual que systemd parte `[Service]` de
|
||||
//! `[Install]`:
|
||||
//!
|
||||
//! - **qué es el servicio** (exec, argv, supervisión) — propiedad del PAQUETE ⇒ este bloque;
|
||||
//! - **si arranca en esta imagen** — propiedad del PERFIL ⇒ `docs/state/targets.toml` (pendiente).
|
||||
//!
|
||||
//! ## Fuera de `hash_inputs`
|
||||
//!
|
||||
//! Igual que `license`, `slots` y `evidence`: describe **cómo se supervisa** el artefacto, no qué
|
||||
//! bytes tiene. `Recipe::hash_inputs` es una lista explícita de entradas, así que declarar servicios
|
||||
//! en recetas YA SELLADAS no mueve un solo `ArtifactHash` — que es lo único que hace la deuda
|
||||
//! pagable sin reconstruir el corpus.
|
||||
//!
|
||||
//! ## Límite conocido: no hay readiness
|
||||
//!
|
||||
//! arje ordena el arranque por **capacidades** (`resolve::plan_spawn`, orden topológico), pero eso
|
||||
//! ordena el *spawn*, no el *estar listo*: no existe primitiva «esperá a que aparezca este socket»
|
||||
//! (`DE-SYSTEMD-A-ARJE.md` §13 lo lista como hueco abierto — el `READY=1` de `Type=notify` se
|
||||
//! registra y no ordena). Mientras no exista, un servicio que necesita esperar a otro lo hace
|
||||
//! DENTRO de su propio card, con un `sh -c '…; exec …'` — que es exactamente lo que ya hace el card
|
||||
//! de `sshd` (levanta la red y genera las host keys antes del `exec`). Este módulo no lo esconde:
|
||||
//! lo deja explícito en `argv`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// Un servicio declarado por una receta. Se traduce 1:1 a una Card de arje ([`Service::card`]).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Service {
|
||||
/// Nombre de la unidad (`sshd`). Es el `label` de la Card y la clave con la que `arjectl` la
|
||||
/// nombra. Sin `/`: también es nombre de fichero en `cards.d/`.
|
||||
pub label: String,
|
||||
/// ULID de la Card. **Se declara, no se genera**: un id aleatorio haría que la seed —y con ella
|
||||
/// el `product_rootfs_hash`— cambiara en cada corrida, rompiendo la reproducibilidad del
|
||||
/// producto. Que sea estable es un requisito, no un detalle.
|
||||
pub id: String,
|
||||
/// El ELF a encarnar (`Payload::Native.exec`). Absoluto: lo ejecuta PID 1, que no tiene `PATH`.
|
||||
pub exec: String,
|
||||
#[serde(default)]
|
||||
pub argv: Vec<String>,
|
||||
/// Entorno explícito. arje inyecta además `ENTE_BUS_SOCK` y `ENTE_ID` por su cuenta.
|
||||
#[serde(default)]
|
||||
pub envp: Vec<(String, String)>,
|
||||
/// `daemon` (default) u `oneshot` — el `lifecycle` de la Card.
|
||||
#[serde(default)]
|
||||
pub kind: Kind,
|
||||
/// Política de supervisión. **Ausente ⇒ `OneShot`** (no revive). Presente ⇒ `Restart` con
|
||||
/// backoff exponencial entre `initial_ms` y `max_ms`, y su muerte es el `CRASHED` real.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub restart: Option<Restart>,
|
||||
/// Permiso de red del Ente. Default `none`: un demonio que no lo declara no toca la red.
|
||||
#[serde(default)]
|
||||
pub networking: Networking,
|
||||
/// Slice de cgroup v2. Default `arje.slice/<label>`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cgroup: Option<String>,
|
||||
}
|
||||
|
||||
/// `lifecycle` de la Card.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Kind {
|
||||
#[default]
|
||||
Daemon,
|
||||
Oneshot,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Kind::Daemon => "daemon",
|
||||
Kind::Oneshot => "oneshot",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `permissions.networking` de la Card.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Networking {
|
||||
#[default]
|
||||
None,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl Networking {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Networking::None => "none",
|
||||
Networking::Full => "full",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Backoff exponencial ×2 desde `initial_ms`, saturado en `max_ms`; si el Ente sobrevive `max_ms`
|
||||
/// se considera estable y el backoff resetea (`sandokan-lifecycle`, fuente única de la política).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Restart {
|
||||
pub initial_ms: u64,
|
||||
pub max_ms: u64,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
/// El slice de cgroup efectivo.
|
||||
pub fn cgroup_path(&self) -> String {
|
||||
self.cgroup.clone().unwrap_or_else(|| format!("arje.slice/{}", self.label))
|
||||
}
|
||||
|
||||
/// La Card de arje, como `serde_json::Value`.
|
||||
///
|
||||
/// La forma es la del esquema validado de `card_core::Card` (`schema_version` 1), verificada
|
||||
/// contra `seeds/arje-qemu.card.json` y contra las dos cards que el producto ya bootea. Los
|
||||
/// campos que este bloque no expone salen con el valor inerte que ya usaban esas cards
|
||||
/// (`lineage: null`, `provides`/`requires` vacíos, namespaces todos `false`, rlimits `null`):
|
||||
/// **no se inventan defaults nuevos**, se reproducen los que están en producción.
|
||||
pub fn card(&self) -> Value {
|
||||
let supervision = match &self.restart {
|
||||
Some(r) => json!({ "Restart": { "initial": r.initial_ms, "max": r.max_ms } }),
|
||||
None => json!("OneShot"),
|
||||
};
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"id": self.id,
|
||||
"lineage": null,
|
||||
"label": self.label,
|
||||
"provides": [],
|
||||
"requires": [],
|
||||
"permissions": {
|
||||
"networking": self.networking.as_str(),
|
||||
"filesystem": "read-write",
|
||||
"ipc": { "allow": [] },
|
||||
"processes": true
|
||||
},
|
||||
"soma": {
|
||||
"namespaces": {
|
||||
"mount": false, "pid": false, "net": false, "uts": false,
|
||||
"ipc": false, "user": false, "cgroup": false
|
||||
},
|
||||
"rlimits": { "mem_bytes": null, "nproc": null, "nofile": null },
|
||||
"cgroup": { "path": self.cgroup_path(), "cpu_weight": null, "io_weight": null },
|
||||
"cpu_affinity": null
|
||||
},
|
||||
"payload": { "Native": {
|
||||
"exec": self.exec,
|
||||
"argv": self.argv,
|
||||
"envp": self.envp
|
||||
}},
|
||||
"supervision": supervision,
|
||||
"lifecycle": self.kind.as_str(),
|
||||
"priority": "normal",
|
||||
"flow": { "input": [], "output": [] },
|
||||
"genesis": []
|
||||
})
|
||||
}
|
||||
|
||||
/// Valida la FORMA (no la existencia del binario, que es cosa del artefacto).
|
||||
pub fn validate(&self) -> crate::Result<()> {
|
||||
let bad = |m: String| Err(crate::Error::Recipe(m));
|
||||
if self.label.trim().is_empty() || self.label.contains('/') || self.label.contains("..") {
|
||||
return bad(format!("service.label inválido: {:?} (sin `/`, no vacío)", self.label));
|
||||
}
|
||||
// El id es el ancla de la reproducibilidad de la seed: si falta o es basura, la Card cambia
|
||||
// de identidad entre corridas y el `product_rootfs_hash` deja de ser estable.
|
||||
if self.id.len() != 26 || !self.id.bytes().all(|b| b.is_ascii_alphanumeric()) {
|
||||
return bad(format!(
|
||||
"service[{}].id debe ser un ULID de 26 caracteres alfanuméricos, es {:?}",
|
||||
self.label, self.id
|
||||
));
|
||||
}
|
||||
if !self.exec.starts_with('/') {
|
||||
return bad(format!(
|
||||
"service[{}].exec debe ser ruta absoluta (PID 1 no tiene PATH), es {:?}",
|
||||
self.label, self.exec
|
||||
));
|
||||
}
|
||||
if let Some(r) = &self.restart {
|
||||
if r.initial_ms == 0 || r.max_ms < r.initial_ms {
|
||||
return bad(format!(
|
||||
"service[{}].restart: se espera 0 < initial_ms ≤ max_ms, es {}..{}",
|
||||
self.label, r.initial_ms, r.max_ms
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Valida una lista de servicios y rechaza labels e ids repetidos: dos Cards con el mismo `id` son
|
||||
/// la misma Card para el grafo de arje, y el segundo servicio se perdería en silencio.
|
||||
pub fn validate_all(services: &[Service]) -> crate::Result<()> {
|
||||
let mut labels = std::collections::BTreeSet::new();
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
for s in services {
|
||||
s.validate()?;
|
||||
if !labels.insert(&s.label) {
|
||||
return Err(crate::Error::Recipe(format!("service.label duplicado: {:?}", s.label)));
|
||||
}
|
||||
if !ids.insert(&s.id) {
|
||||
return Err(crate::Error::Recipe(format!(
|
||||
"service.id duplicado: {:?} (lo usan {:?} y otro)",
|
||||
s.id, s.label
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sshd() -> Service {
|
||||
Service {
|
||||
label: "sshd".into(),
|
||||
id: "01HQAR53D4M2NBV8KZTYXFQA12".into(),
|
||||
exec: "/bin/busybox".into(),
|
||||
argv: ["sh", "-c", "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
envp: vec![("HOME".into(), "/root".into())],
|
||||
kind: Kind::Daemon,
|
||||
restart: Some(Restart { initial_ms: 500, max_ms: 20000 }),
|
||||
networking: Networking::Full,
|
||||
cgroup: Some("arje.slice/sshd".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn card_tiene_la_forma_del_esquema() {
|
||||
let c = sshd().card();
|
||||
assert_eq!(c["label"], "sshd");
|
||||
assert_eq!(c["payload"]["Native"]["exec"], "/bin/busybox");
|
||||
assert_eq!(c["payload"]["Native"]["envp"][0][0], "HOME");
|
||||
assert_eq!(c["supervision"]["Restart"]["initial"], 500);
|
||||
assert_eq!(c["permissions"]["networking"], "full");
|
||||
assert_eq!(c["soma"]["cgroup"]["path"], "arje.slice/sshd");
|
||||
assert_eq!(c["lifecycle"], "daemon");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sin_restart_es_oneshot() {
|
||||
let mut s = sshd();
|
||||
s.restart = None;
|
||||
assert_eq!(s.card()["supervision"], json!("OneShot"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cgroup_por_defecto_deriva_del_label() {
|
||||
let mut s = sshd();
|
||||
s.cgroup = None;
|
||||
assert_eq!(s.cgroup_path(), "arje.slice/sshd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn se_deserializa_de_toml() {
|
||||
let t = r#"
|
||||
label = "sshd"
|
||||
id = "01HQAR53D4M2NBV8KZTYXFQA12"
|
||||
exec = "/bin/busybox"
|
||||
argv = ["sh", "-c", "exec /usr/sbin/sshd -D -e"]
|
||||
envp = [["HOME", "/root"]]
|
||||
networking = "full"
|
||||
restart = { initial_ms = 500, max_ms = 20000 }
|
||||
"#;
|
||||
let s: Service = toml::from_str(t).unwrap();
|
||||
s.validate().unwrap();
|
||||
assert_eq!(s.kind, Kind::Daemon); // default
|
||||
assert_eq!(s.restart.unwrap().max_ms, 20000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rechaza_lo_que_rompe_la_seed() {
|
||||
let mut s = sshd();
|
||||
s.id = "corto".into();
|
||||
assert!(s.validate().unwrap_err().to_string().contains("ULID"));
|
||||
|
||||
let mut s = sshd();
|
||||
s.exec = "sshd".into();
|
||||
assert!(s.validate().unwrap_err().to_string().contains("absoluta"));
|
||||
|
||||
let mut s = sshd();
|
||||
s.label = "a/b".into();
|
||||
assert!(s.validate().unwrap_err().to_string().contains("label"));
|
||||
|
||||
let mut s = sshd();
|
||||
s.restart = Some(Restart { initial_ms: 0, max_ms: 10 });
|
||||
assert!(s.validate().unwrap_err().to_string().contains("initial_ms"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_all_rechaza_duplicados() {
|
||||
let a = sshd();
|
||||
let mut b = sshd();
|
||||
b.label = "otro".into();
|
||||
// mismo id, distinto label ⇒ colisión de identidad en el grafo
|
||||
assert!(validate_all(&[a.clone(), b]).unwrap_err().to_string().contains("id duplicado"));
|
||||
assert!(validate_all(&[a.clone(), a]).unwrap_err().to_string().contains("label duplicado"));
|
||||
}
|
||||
}
|
||||
@@ -77,3 +77,25 @@ build = ["zlib", "openssl", "perl"]
|
||||
configure = "./configure --prefix=/usr --sysconfdir=/etc/ssh --with-ssl-dir=/usr --with-zlib=/usr --with-privsep-path=/var/empty --without-pam --without-selinux --without-hardening CC='zig cc -mcpu=baseline -target x86_64-linux-musl' LDFLAGS=-static && sed -i '/^CHANNELLIBS=/{s/-lcrypto/-l:libcrypto.a/g;s/-lz/-l:libz.a/g}' Makefile"
|
||||
compile = "make -j\"$(nproc)\""
|
||||
install = "make install-nokeys DESTDIR=/out STRIP_OPT="
|
||||
|
||||
# ── EL SERVICIO QUE ESTE PAQUETE TRAE ───────────────────────────────────────────────────────────
|
||||
# Hasta hoy esta Card vivía HARDCODEADA en `takana_bootstrap::SSHD_SERVICE_CARD`, una constante de
|
||||
# Rust: el paquete que trae el demonio no tenía dónde decirlo, así que lo decía el binario de takana.
|
||||
# El `id` es el ULID que esa constante ya usaba — **cambiarlo movería la seed de producto y con ella
|
||||
# el `product_rootfs_hash`**, así que se preserva tal cual, no se regenera.
|
||||
#
|
||||
# El `argv` hace tres cosas antes del `exec` porque arje NO TIENE readiness por unidad (ver
|
||||
# `takana_core::service`): levanta la red, genera las host keys al primer boot, y recién ahí encarna
|
||||
# sshd. Eso no es una maña de esta receta — es el patrón obligado mientras el hueco siga abierto, y
|
||||
# por eso está a la vista en el argv en vez de escondido en un wrapper.
|
||||
#
|
||||
# Fuera de `hash_inputs`: declarar esto NO re-hashea openssh.
|
||||
[[service]]
|
||||
label = "sshd"
|
||||
id = "01HQAR53D4M2NBV8KZTYXFQA12"
|
||||
exec = "/bin/busybox"
|
||||
argv = ["sh", "-c", "/usr/bin/netup; /usr/bin/ssh-keygen -A; exec /usr/sbin/sshd -D -e"]
|
||||
envp = [["HOME", "/root"]]
|
||||
networking = "full"
|
||||
cgroup = "arje.slice/sshd"
|
||||
restart = { initial_ms = 500, max_ms = 20000 }
|
||||
|
||||
Reference in New Issue
Block a user