TAKANA_X con caída a HAMMER_X (ADR 0016). No es un sed: estas variables son
contrato de usuario —knobs del instalador, el entorno del worker, mirror-env.sh—
y viven en perfiles de shell y units FUERA del repo, así que un renombre duro no
falla ruidosamente: la variable no aparece, se toma el default y el build se
comporta distinto sin que nada lo diga.
Rust: takana_core::env::{var,var_os} toma el nombre canónico y deriva el viejo
cambiando el prefijo — se le pasa TAKANA_* para que un grep del nombre nuevo
encuentre todas las lecturas. 5 tests, con DOS controles negativos: sin ninguna
de las dos no hay valor, y un nombre sin prefijo no inventa una caída.
Migrados los 17 sitios directos y los indirectos que el grep no mostraba
(bases_de_mirror, las constantes de kernel_cmd, ROOT_ENV de qorpa, env_path de
recover). takana-recover lleva la caída inline: es un mini-binario que se copia
a /usr/sbin y no vale arrastrarle una dep entera por dos líneas.
Scripts: 30 lecturas pasan a default, manteniendo el
nombre INTERNO de la variable para no tocar sus 190 usos.
Y donde el script EXPORTA en vez de leer, se ponen LAS DOS (mirror-env.sh y el
fragmento in-VM de bootstrap): ahí el lector puede ser un binario viejo —worker
sin recompilar, el /usr/bin/hammer pinado del baseline— que sólo conoce HAMMER_*.
La caída sirve para lectores nuevos; los viejos necesitan que la vieja siga puesta.
Verificado de punta a punta con el binario, no sólo con unit tests: HAMMER_LAB
sigue surtiendo efecto, TAKANA_LAB hace lo mismo, y con las dos gana TAKANA_LAB.
605 tests en verde y el hash de zlib sigue en b3:dc363f26… , intacto.
Cambio de comportamiento que va aparte y hay que decir: el hostname por defecto
de una instalación nueva pasa de 'hammer' a 'takana' (sólo si no se fija ninguna
de las dos variables).
NO se tocan: las rutas /var/lib/hammer de sistemas instalados, el volid
HAMMER_LIVE del ISO, ni el namespace HARKAQ_*, que es de otro subsistema.
164 lines
5.9 KiB
Rust
164 lines
5.9 KiB
Rust
//! Roundtrip e2e de Fase 4: construimos un `.swm` a mano (los tipos viven en hammer-core),
|
|
//! lo aplicamos a un prefix temporal, leemos el resultado y verificamos que los archivos
|
|
//! coinciden con lo esperado.
|
|
//!
|
|
//! NO ejercitamos `source_patch` porque arrastra todo el lab (red + sandbox); para esos
|
|
//! casos existen los tests gated en `TAKANA_NETWORK_TESTS`. Aquí cubrimos los caminos puros
|
|
//! de hammer-core: `verify_schema`, `verify_base`, `apply_config_edit`, `apply_file_drop`.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
|
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
|
use takana_core::apply::{apply_config_edit, apply_file_drop, rebase_path, verify_content_hash};
|
|
use takana_core::swm::{Base, BaseCompat, BaseRef, Mutation, Swm};
|
|
use takana_core::ArtifactHash;
|
|
|
|
fn write(p: &Path, content: &str) {
|
|
if let Some(parent) = p.parent() {
|
|
std::fs::create_dir_all(parent).unwrap();
|
|
}
|
|
std::fs::write(p, content).unwrap();
|
|
}
|
|
|
|
fn base(distro: &str, pins: &[(&str, &str)]) -> Base {
|
|
Base {
|
|
distro_version: distro.into(),
|
|
pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
|
|
}
|
|
}
|
|
|
|
fn baseref(distro: &str, pins: &[(&str, &str)]) -> BaseRef {
|
|
BaseRef {
|
|
distro_version: distro.into(),
|
|
pins: pins.iter().map(|(k, v)| ((*k).into(), (*v).into())).collect(),
|
|
}
|
|
}
|
|
|
|
fn mk_swm(mutations: Vec<Mutation>) -> Swm {
|
|
Swm {
|
|
swm_version: 1,
|
|
base: base("2026-06-06", &[("musl", "deadbeef")]),
|
|
mutations,
|
|
signature: None,
|
|
}
|
|
}
|
|
|
|
/// Verifica que un `.swm` pasa schema y base, y luego aplica cada mutación bajo un prefix.
|
|
/// Este es el camino que la CLI ejercita; aquí lo reproducimos como librería para que el
|
|
/// test se mantenga aunque la CLI cambie superficialmente.
|
|
fn apply_swm_under_prefix(swm: &Swm, prefix: &Path) {
|
|
swm.verify_schema().expect("schema válido");
|
|
// Base compatible (idéntica a la del .swm).
|
|
let local = BaseRef::from(&swm.base);
|
|
assert_eq!(swm.verify_base(&local), BaseCompat::Ok);
|
|
|
|
for m in &swm.mutations {
|
|
match m {
|
|
Mutation::ConfigEdit { file, inline_diff } => {
|
|
let target = rebase_path(file, Some(prefix));
|
|
apply_config_edit(&target, inline_diff).unwrap();
|
|
}
|
|
Mutation::FileDrop { path, content_hash, content_b64, .. } => {
|
|
let target = rebase_path(path, Some(prefix));
|
|
let b64 = content_b64.as_ref().expect("inline para tests");
|
|
apply_file_drop(&target, b64, content_hash).unwrap();
|
|
}
|
|
_ => panic!("este test sólo cubre config_edit/file_drop"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_yaml_apply_reproduces_files() {
|
|
let payload = b"hola hammer\n";
|
|
let payload_hash = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
|
|
let payload_b64 = STANDARD.encode(payload);
|
|
|
|
let swm = mk_swm(vec![
|
|
Mutation::ConfigEdit {
|
|
file: "/etc/network.conf".into(),
|
|
inline_diff: "- DHCP=yes\n+ IP=192.168.1.100\n".into(),
|
|
},
|
|
Mutation::FileDrop {
|
|
path: "/var/lib/hammer/saludo.txt".into(),
|
|
content_hash: payload_hash.clone(),
|
|
content_b64: Some(payload_b64),
|
|
content_url: None,
|
|
},
|
|
]);
|
|
|
|
// Roundtrip YAML completo: serializa, deserializa y reusa.
|
|
let yaml = swm.to_yaml().unwrap();
|
|
let swm2 = Swm::from_yaml(&yaml).unwrap();
|
|
assert_eq!(swm2.mutations.len(), 2);
|
|
|
|
let prefix_dir = tempfile::tempdir().unwrap();
|
|
let prefix = prefix_dir.path();
|
|
// El archivo objetivo del config_edit debe existir antes (config_edit no crea archivos).
|
|
write(
|
|
&prefix.join("etc/network.conf"),
|
|
"USE=eth0\nDHCP=yes\nMTU=1500\n",
|
|
);
|
|
|
|
apply_swm_under_prefix(&swm2, prefix);
|
|
|
|
let got_net = std::fs::read_to_string(prefix.join("etc/network.conf")).unwrap();
|
|
assert_eq!(got_net, "USE=eth0\nIP=192.168.1.100\nMTU=1500\n");
|
|
|
|
let got_drop = std::fs::read(prefix.join("var/lib/hammer/saludo.txt")).unwrap();
|
|
assert_eq!(got_drop, payload);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_base_distinguishes_compatible_and_not() {
|
|
let swm = mk_swm(vec![Mutation::ConfigEdit {
|
|
file: "/etc/x".into(),
|
|
inline_diff: "- a\n+ b\n".into(),
|
|
}]);
|
|
// distro distinta
|
|
let other = baseref("2026-09-09", &[("musl", "deadbeef")]);
|
|
assert!(matches!(
|
|
swm.verify_base(&other),
|
|
BaseCompat::DistroMismatch { .. }
|
|
));
|
|
// pin distinta
|
|
let bad_pin = baseref("2026-06-06", &[("musl", "ZZZZ")]);
|
|
assert!(matches!(swm.verify_base(&bad_pin), BaseCompat::PinMismatch(_)));
|
|
// pins extras locales no son problema
|
|
let mut more = BTreeMap::new();
|
|
more.insert("musl".into(), "deadbeef".into());
|
|
more.insert("kernel".into(), "abcdef".into());
|
|
let ok = BaseRef { distro_version: "2026-06-06".into(), pins: more };
|
|
assert_eq!(swm.verify_base(&ok), BaseCompat::Ok);
|
|
}
|
|
|
|
#[test]
|
|
fn verify_content_hash_catches_payload_tampering() {
|
|
let payload = b"datos integros\n";
|
|
let b64 = STANDARD.encode(payload);
|
|
let h = ArtifactHash::of_inputs(&[payload.as_slice()]).as_str().to_string();
|
|
verify_content_hash(&b64, &h).expect("hash ok");
|
|
|
|
// Mutar un byte del payload sin actualizar el hash ⇒ falla.
|
|
let mut bad = payload.to_vec();
|
|
bad[0] ^= 0x01;
|
|
let bad_b64 = STANDARD.encode(&bad);
|
|
let err = verify_content_hash(&bad_b64, &h).unwrap_err().to_string();
|
|
assert!(err.contains("no coincide"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn yaml_includes_only_inline_field_when_url_is_none() {
|
|
// Un .swm autocontenido (lo que produce export) no debe acabar emitiendo `content_url`.
|
|
let swm = mk_swm(vec![Mutation::FileDrop {
|
|
path: "/var/lib/x".into(),
|
|
content_hash: "b3:00".into(),
|
|
content_b64: Some("AA==".into()),
|
|
content_url: None,
|
|
}]);
|
|
let y = swm.to_yaml().unwrap();
|
|
assert!(y.contains("content_b64"));
|
|
assert!(!y.contains("content_url"), "skip_serializing_if=None debe omitir el campo");
|
|
}
|