use std::fmt::Write as _;
use anyhow::{bail, Result};
use workload_spec::sovereign::Participation;
use crate::config::{MachineConfig, SovereignGroupConfig};
pub const DROPIN_DIR: &str = "/etc/systemd/system/yubaba.service.d";
pub const DROPIN_NAME: &str = "90-sovereign.conf";
pub const ENV_GROUP: &str = "YUBABA_SOVEREIGN_GROUP";
pub const ENV_PARTICIPATION: &str = "YUBABA_SOVEREIGN_PARTICIPATION";
pub const ENV_VOTERS: &str = "YUBABA_SOVEREIGN_VOTERS";
pub const RETIRE_LITERALS_SCRIPT: &str = include_str!("sovereign_retire.sh");
pub fn dropin_path() -> String {
format!("{DROPIN_DIR}/{DROPIN_NAME}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SovereignFlags {
pub group: String,
pub participation: Participation,
pub voters: Vec<u64>,
}
pub fn flags_for(
groups: &[SovereignGroupConfig],
machine: &MachineConfig,
) -> Result<Option<SovereignFlags>> {
let Some(group) = machine.sovereign_group.as_deref() else {
return Ok(None);
};
let Some(decl) = groups.iter().find(|g| g.name == group) else {
bail!(
"{} declares sovereign_group = \"{group}\" but there is no \
.yah/infra/sovereign-groups/{group}.toml — refusing to render its drop-in",
machine.name
);
};
let mut voters: Vec<u64> = decl.voters.iter().map(|v| v.raft_node_id).collect();
voters.sort_unstable();
Ok(Some(SovereignFlags {
group: group.to_string(),
participation: machine.sovereign_participation,
voters,
}))
}
pub fn render_flags(machine: &str, flags: &SovereignFlags) -> String {
let mut s = String::new();
let _ = writeln!(
s,
"# R605-F37 — rendered by `yah cloud sovereign-dropin {machine}`. DO NOT EDIT ON THE NODE:"
);
s.push_str(
"# a roll refuses when this file disagrees with the render. Source of truth:\n\
# .yah/infra/machines/<name>.toml + .yah/infra/sovereign-groups/<group>.toml.\n\
# Environment= only; never re-declares the command line — see\n\
# oss/yubaba/crates/cloud/src/sovereign_unit.rs.\n\
[Service]\n",
);
let _ = writeln!(s, "Environment={ENV_GROUP}={}", flags.group);
let _ = writeln!(s, "Environment={ENV_PARTICIPATION}={}", flags.participation);
if !flags.voters.is_empty() {
let ids: Vec<String> = flags.voters.iter().map(u64::to_string).collect();
let _ = writeln!(s, "Environment={ENV_VOTERS}={}", ids.join(","));
}
s
}
pub fn render(groups: &[SovereignGroupConfig], machine: &MachineConfig) -> Result<Option<String>> {
Ok(flags_for(groups, machine)?.map(|f| render_flags(&machine.name, &f)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fragment {
pub path: String,
pub body: String,
}
pub fn parse_systemctl_cat(text: &str) -> Vec<Fragment> {
let mut out: Vec<Fragment> = Vec::new();
for line in text.lines() {
if let Some(path) = line.strip_prefix("# /") {
let path = path.trim();
if !path.contains(char::is_whitespace)
&& (path.ends_with(".service") || path.ends_with(".conf"))
{
out.push(Fragment {
path: format!("/{path}"),
body: String::new(),
});
continue;
}
}
if let Some(f) = out.last_mut() {
f.body.push_str(line);
f.body.push('\n');
}
}
out
}
fn assignments(body: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut pending = String::new();
for raw in body.lines() {
let line = raw.trim_end();
if pending.is_empty() && (line.trim_start().starts_with('#') || line.trim_start().starts_with(';')) {
continue;
}
if let Some(cont) = line.strip_suffix('\\') {
pending.push_str(cont);
pending.push(' ');
continue;
}
pending.push_str(line);
let full = std::mem::take(&mut pending);
if let Some((k, v)) = full.trim().split_once('=') {
out.push((k.trim().to_string(), v.trim().to_string()));
}
}
out
}
pub fn effective_exec_start(frags: &[Fragment]) -> Option<String> {
let mut cur: Option<String> = None;
for f in frags {
for (k, v) in assignments(&f.body) {
if k == "ExecStart" {
cur = if v.is_empty() { None } else { Some(v) };
}
}
}
cur
}
fn literal_sovereign_flags(exec: &str) -> Vec<(String, String)> {
let toks: Vec<&str> = exec.split_whitespace().collect();
let mut out = Vec::new();
let mut i = 0;
while i < toks.len() {
let t = toks[i];
if let Some(rest) = t.strip_prefix("--sovereign-") {
if let Some((name, val)) = rest.split_once('=') {
out.push((format!("--sovereign-{name}"), val.to_string()));
} else {
let val = toks.get(i + 1).copied().unwrap_or("").to_string();
out.push((t.to_string(), val));
i += 1;
}
}
i += 1;
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LiveFinding {
LegacyLiteral { flag: String, value: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
NoOp,
Install,
Remove,
Refuse { why: String, diff: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiveCheck {
pub verdict: Verdict,
pub findings: Vec<LiveFinding>,
}
fn line_diff(live: &str, rendered: &str) -> String {
let mut s = format!("--- live {}\n+++ rendered\n", dropin_path());
let live_lines: Vec<&str> = live.lines().collect();
let rend_lines: Vec<&str> = rendered.lines().collect();
for l in &live_lines {
if !rend_lines.contains(l) {
let _ = writeln!(s, "-{l}");
}
}
for l in &rend_lines {
if !live_lines.contains(l) {
let _ = writeln!(s, "+{l}");
}
}
s
}
fn flag_value_matches(flag: &str, value: &str, want: Option<&SovereignFlags>) -> bool {
let Some(want) = want else { return false };
match flag {
"--sovereign-group" => value == want.group,
"--sovereign-participation" => value == want.participation.as_str(),
"--sovereign-voters" => {
let mut got: Vec<u64> = value.split(',').filter_map(|v| v.trim().parse().ok()).collect();
got.sort_unstable();
got == want.voters && value.split(',').count() == got.len()
}
_ => false,
}
}
pub fn check_live(
machine: &str,
want: Option<&SovereignFlags>,
live_cat: &str,
accept_change: bool,
) -> LiveCheck {
let frags = parse_systemctl_cat(live_cat);
let ours = dropin_path();
let rendered = want.map(|f| render_flags(machine, f));
let mut findings = Vec::new();
for f in frags.iter().filter(|f| f.path != ours) {
for (k, v) in assignments(&f.body) {
if k == "Environment"
&& [ENV_GROUP, ENV_PARTICIPATION, ENV_VOTERS]
.iter()
.any(|e| v.contains(&format!("{e}=")))
{
return LiveCheck {
verdict: Verdict::Refuse {
why: format!(
"{} sets YUBABA_SOVEREIGN_* — only {ours} may; remove it by hand",
f.path
),
diff: format!("--- {}\n{}", f.path, f.body),
},
findings,
};
}
}
}
if let Some(exec) = effective_exec_start(&frags) {
for (flag, value) in literal_sovereign_flags(&exec) {
if flag_value_matches(&flag, &value, want) {
findings.push(LiveFinding::LegacyLiteral { flag, value });
} else {
let wanted = match (want, flag.as_str()) {
(None, _) => "nothing (the machine declares no sovereign_group)".to_string(),
(Some(w), "--sovereign-group") => w.group.clone(),
(Some(w), "--sovereign-participation") => w.participation.to_string(),
(Some(w), "--sovereign-voters") => w
.voters
.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(","),
(Some(_), _) => "no such flag".to_string(),
};
return LiveCheck {
verdict: Verdict::Refuse {
why: format!(
"the effective ExecStart= passes `{flag} {value}` but the declarations \
render {wanted}; argv beats the drop-in's env, so installing it would \
not take effect — fix the ExecStart= drop-in by hand (keep every other flag)"
),
diff: format!("-{flag} {value}\n+{flag} {wanted}\n"),
},
findings,
};
}
}
}
let live = frags.iter().find(|f| f.path == ours).map(|f| f.body.as_str());
let verdict = match (live, rendered.as_deref()) {
(None, None) => Verdict::NoOp,
(None, Some(_)) => Verdict::Install,
(Some(l), Some(r)) if l.trim_end() == r.trim_end() => Verdict::NoOp,
(Some(l), Some(r)) => {
if accept_change {
Verdict::Install
} else {
Verdict::Refuse {
why: format!(
"{ours} on the node differs from the render — a hand edit or an \
unrolled declaration change; re-run with --accept-sovereign-change \
if the declarations are what you mean"
),
diff: line_diff(l, r),
}
}
}
(Some(l), None) => {
if accept_change {
Verdict::Remove
} else {
Verdict::Refuse {
why: format!(
"{ours} exists on the node but {machine} declares no sovereign_group; \
re-run with --accept-sovereign-change to remove it"
),
diff: line_diff(l, ""),
}
}
}
};
LiveCheck { verdict, findings }
}
#[cfg(test)]
mod tests {
use super::*;
fn flags() -> SovereignFlags {
SovereignFlags {
group: "dev".into(),
participation: Participation::In,
voters: vec![11, 13, 14],
}
}
fn cat(extra: &str) -> String {
format!(
"# /etc/systemd/system/yubaba.service\n[Service]\nExecStart=/usr/local/bin/yubaba serve\n\
# /etc/systemd/system/yubaba.service.d/95-sovereign-group.conf\n[Service]\nExecStart=\n\
ExecStart=/usr/local/bin/yubaba serve --raft-node-id 11 --cluster-profile rig --sovereign-group dev\n{extra}"
)
}
#[test]
fn render_carries_only_environment_lines() {
let r = render_flags("us-west-011", &flags());
assert!(!r.contains("ExecStart"));
assert!(r.contains("Environment=YUBABA_SOVEREIGN_GROUP=dev\n"));
assert!(r.contains("Environment=YUBABA_SOVEREIGN_PARTICIPATION=in\n"));
assert!(r.contains("Environment=YUBABA_SOVEREIGN_VOTERS=11,13,14\n"));
}
#[test]
fn absent_dropin_with_agreeing_literal_installs_and_notes_the_legacy_flag() {
let c = check_live("us-west-011", Some(&flags()), &cat(""), false);
assert_eq!(c.verdict, Verdict::Install);
assert_eq!(
c.findings,
vec![LiveFinding::LegacyLiteral {
flag: "--sovereign-group".into(),
value: "dev".into()
}]
);
}
#[test]
fn identical_dropin_is_a_noop() {
let live = cat(&format!("# {}\n{}", dropin_path(), render_flags("us-west-011", &flags())));
assert_eq!(check_live("us-west-011", Some(&flags()), &live, false).verdict, Verdict::NoOp);
}
#[test]
fn hand_edited_dropin_refuses_with_a_diff_unless_accepted() {
let edited = render_flags("us-west-011", &flags()).replace("11,13,14", "11,13");
let live = cat(&format!("# {}\n{edited}", dropin_path()));
match check_live("us-west-011", Some(&flags()), &live, false).verdict {
Verdict::Refuse { diff, .. } => {
assert!(diff.contains("-Environment=YUBABA_SOVEREIGN_VOTERS=11,13\n"));
assert!(diff.contains("+Environment=YUBABA_SOVEREIGN_VOTERS=11,13,14\n"));
}
v => panic!("expected refuse, got {v:?}"),
}
assert_eq!(check_live("us-west-011", Some(&flags()), &live, true).verdict, Verdict::Install);
}
#[test]
fn a_disagreeing_literal_flag_refuses_even_when_accepted() {
let live = cat("").replace("--sovereign-group dev", "--sovereign-group prod");
assert!(matches!(
check_live("us-west-011", Some(&flags()), &live, true).verdict,
Verdict::Refuse { .. }
));
let live = cat("").replace("--sovereign-group dev", "--sovereign-group dev --sovereign-voters 11,13");
assert!(matches!(
check_live("us-west-011", Some(&flags()), &live, false).verdict,
Verdict::Refuse { .. }
));
}
#[test]
fn a_foreign_environment_owner_refuses() {
let live = cat("# /etc/systemd/system/yubaba.service.d/99-x.conf\n[Service]\nEnvironment=YUBABA_SOVEREIGN_VOTERS=1\n");
assert!(matches!(
check_live("us-west-011", Some(&flags()), &live, true).verdict,
Verdict::Refuse { .. }
));
}
#[test]
fn a_reset_execstart_drops_earlier_literals() {
let live = "# /etc/systemd/system/yubaba.service\n[Service]\nExecStart=/y serve --sovereign-group prod\n\
# /etc/systemd/system/yubaba.service.d/40-raft.conf\n[Service]\nExecStart=\nExecStart=/y serve \\\n --raft-node-id 11\n";
let c = check_live("us-west-011", Some(&flags()), live, false);
assert_eq!(c.verdict, Verdict::Install);
assert!(c.findings.is_empty());
}
#[test]
fn the_retire_script_is_gated_on_the_installed_binary_reading_the_env() {
let s = RETIRE_LITERALS_SCRIPT;
let gate = s.find("grep -q YUBABA_SOVEREIGN_VOTERS").expect("env gate");
let strip = s.find("sed -i").expect("strip step");
assert!(gate < strip, "the gate must run before any edit");
assert!(s.contains("--sovereign-(group|participation|voters)"));
assert!(!s.contains("systemctl restart"), "the caller owns the restart");
}
#[test]
fn no_group_and_no_dropin_is_a_noop() {
let live = "# /etc/systemd/system/yubaba.service\n[Service]\nExecStart=/y serve\n";
assert_eq!(check_live("us-west-002", None, live, false).verdict, Verdict::NoOp);
}
}