workload_spec/
control_plane_install.rs1fn sh_squote(s: &str) -> String {
33 format!("'{}'", s.replace('\'', r"'\''"))
34}
35
36pub fn build_install_script(version: &str, url: &str, sha256: &str, sudo: bool) -> String {
50 let sudo_kw = if sudo { "sudo" } else { "" };
51 format!(
52 r#"set -euo pipefail
53SUDO={sudo_kw}
54URL={url}
55SHA={sha}
56VER={ver}
57WORK="$(mktemp -d /tmp/yah-roll.XXXXXX)"
58trap 'rm -rf "$WORK"' EXIT
59cd "$WORK"
60echo "== fetch + verify (sha256 from signed manifest) =="
61curl -fsSL -o pair.tar.gz "$URL"
62printf '%s pair.tar.gz\n' "$SHA" | sha256sum -c -
63mkdir x && tar -xzf pair.tar.gz -C x
64D="$(find x -maxdepth 1 -type d -name 'yubaba-*' | head -1)"
65[ -n "$D" ] || {{ echo 'tarball layout unexpected: no yubaba-* dir' >&2; exit 1; }}
66# Atomic install: stage next to the target on the SAME filesystem, then rename.
67# A rename within a filesystem is atomic, so a half-written binary/unit is
68# never observable. This script touches /usr/local/bin bytes + unit files ONLY
69# (durable host state is deliberately out of scope — see the module docs).
70install_atomic() {{ # src mode dest
71 $SUDO install -m"$2" "$1" "$3.roll-new.$$"
72 $SUDO mv -f "$3.roll-new.$$" "$3"
73}}
74install_atomic "$D/yubaba" 0755 /usr/local/bin/yubaba
75install_atomic "$D/kamaji" 0755 /usr/local/bin/kamaji
76install_atomic "$D/yubaba.slice" 0644 /etc/systemd/system/yubaba.slice
77install_atomic "$D/kamaji.service" 0644 /etc/systemd/system/kamaji.service
78install_atomic "$D/yubaba.service" 0644 /etc/systemd/system/yubaba.service
79echo "== restart supervision tree (kamaji then yubaba, W154 order) =="
80$SUDO systemctl daemon-reload
81$SUDO systemctl restart kamaji.service
82$SUDO systemctl restart yubaba.service
83echo "installed target=$VER yubaba=$(/usr/local/bin/yubaba --version 2>/dev/null) kamaji=$(/usr/local/bin/kamaji --version 2>/dev/null)"
84"#,
85 sudo_kw = sudo_kw,
86 url = sh_squote(url),
87 sha = sh_squote(sha256),
88 ver = sh_squote(version),
89 )
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn script_embeds_the_signed_digest_and_url() {
98 let url = "https://cdn.yah.dev/yubaba/0.8.19/yubaba-0.8.19-x86_64-unknown-linux-musl.tar.gz";
99 let sha = "abc123def456";
100 let s = build_install_script("0.8.19", url, sha, false);
101 assert!(s.contains(sha), "script must carry the manifest sha256");
102 assert!(s.contains(url), "script must carry the manifest url");
103 assert!(s.contains("sha256sum -c -"), "script must verify the digest");
104 }
105
106 #[test]
107 fn script_is_atomic_and_installs_the_whole_pair() {
108 let s = build_install_script("0.8.19", "u", "d", false);
109 assert!(s.contains("mv -f"), "install must be an atomic rename");
111 assert!(s.contains(".roll-new.$$"), "install must stage to a temp name");
112 for target in [
114 "/usr/local/bin/yubaba",
115 "/usr/local/bin/kamaji",
116 "/etc/systemd/system/yubaba.slice",
117 "/etc/systemd/system/kamaji.service",
118 "/etc/systemd/system/yubaba.service",
119 ] {
120 assert!(s.contains(target), "script must install {target}");
121 }
122 let k = s.find("restart kamaji.service").unwrap();
124 let y = s.find("restart yubaba.service").unwrap();
125 assert!(k < y, "kamaji must restart before yubaba");
126 assert!(s.contains("daemon-reload"));
127 }
128
129 #[test]
130 fn script_never_touches_durable_state() {
131 let s = build_install_script("0.8.19", "u", "d", true);
135 assert!(!s.contains("identity.json"), "must not touch host identity");
136 assert!(!s.contains("/var/lib/yah-cloud"), "must not touch state dir");
137 assert!(!s.contains("raft"), "must not touch the raft log dir");
138 assert!(!s.contains("rm -rf /"), "must not wipe system paths");
139 }
140
141 #[test]
142 fn sudo_prefix_tracks_the_caller() {
143 let s = build_install_script("0.8.19", "u", "d", true);
144 assert!(s.contains("SUDO=sudo"));
145 let s = build_install_script("0.8.19", "u", "d", false);
146 assert!(s.contains("SUDO=\n") || s.contains("SUDO="));
147 }
148}