Merge pull request 'fix(clawdie): rename service user to _clawdie + idempotent creation' (#128) from fix/clawdie-idempotent-user into main
Some checks are pending
CI / rust (push) Waiting to run
CI / markdown (push) Waiting to run
CI / port (push) Waiting to run
CI / agent-jail-pkgs (push) Waiting to run

Reviewed-on: #128
This commit is contained in:
clawdie 2026-06-21 15:19:49 +02:00
commit a7fc408bd2
3 changed files with 45 additions and 21 deletions

View file

@ -15,18 +15,24 @@ pub fn apply(steps: &[Step]) -> Result<(), String> {
for (i, step) in steps.iter().enumerate() {
eprintln!("[{}/{}] {}", i + 1, total, step.describe);
match &step.action {
Action::Run(argv) => {
Action::Run {
argv,
allowed_exit_codes,
} => {
let (cmd, rest) = argv.split_first().ok_or("empty command in step")?;
let status = Command::new(cmd)
let output = Command::new(cmd)
.args(rest)
.status()
.output()
.map_err(|e| format!("failed to run `{}`: {e}", argv.join(" ")))?;
if !status.success() {
let code = output.status.code().unwrap_or(-1);
if !output.status.success() && !allowed_exit_codes.contains(&code) {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"step {} failed ({}): `{}`",
"step {} failed (exit {}): `{}`\n{}",
i + 1,
status,
argv.join(" ")
code,
argv.join(" "),
stderr.trim()
));
}
}

View file

@ -146,7 +146,7 @@ pub fn render(
out.push_str("\nSteps:\n");
for (i, step) in steps.iter().enumerate() {
let detail = match &step.action {
Action::Run(argv) => format!("$ {}", argv.join(" ")),
Action::Run { argv, .. } => format!("$ {}", argv.join(" ")),
Action::WriteFile { path, mode, .. } => format!("write {path} (mode {mode:o})"),
};
out.push_str(&format!(
@ -224,7 +224,7 @@ mod tests {
// plain dirs: 2 dir steps + 5 linux service steps
let s = build(&Linux, &Storage::PlainDirs, &svc);
assert_eq!(s.len(), 7);
assert!(matches!(&s[0].action, Action::Run(a) if a[0] == "mkdir"));
assert!(matches!(&s[0].action, Action::Run { argv, .. } if argv[0] == "mkdir"));
// create pool: 1 zpool + 3 dataset steps + 4 freebsd service steps
let c = build(
&FreeBsd,
@ -234,10 +234,10 @@ mod tests {
},
&svc,
);
assert!(
matches!(&c[0].action, Action::Run(a) if a[0] == "zpool" && a.contains(&"/dev/sdb".to_string()))
);
assert_eq!(c.len(), 8);
assert!(
matches!(&c[0].action, Action::Run { argv, .. } if argv[0] == "zpool" && argv.contains(&"/dev/sdb".to_string()))
);
}
#[test]

View file

@ -37,7 +37,7 @@ impl Default for ServiceSpec {
Self {
name: "clawdie".into(),
exec: "/usr/local/bin/colibri-daemon".into(),
user: "clawdie".into(),
user: "_clawdie".into(),
data_dir: "/var/db/clawdie".into(),
}
}
@ -46,7 +46,11 @@ impl Default for ServiceSpec {
/// One provisioning action — either a command to run or a file to write.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Run(Vec<String>),
/// Run argv[0] with argv[1..]; non-zero exit fails unless listed in allowed_exit_codes.
Run {
argv: Vec<String>,
allowed_exit_codes: Vec<i32>,
},
WriteFile {
path: String,
mode: u32,
@ -64,7 +68,20 @@ impl Step {
pub fn run(describe: impl Into<String>, argv: &[&str]) -> Self {
Step {
describe: describe.into(),
action: Action::Run(argv.iter().map(|s| s.to_string()).collect()),
action: Action::Run {
argv: argv.iter().map(|s| s.to_string()).collect(),
allowed_exit_codes: vec![],
},
}
}
/// Like `run`, but treats the given exit codes as success (for idempotent steps).
pub fn run_idem(describe: impl Into<String>, argv: &[&str], allowed: &[i32]) -> Self {
Step {
describe: describe.into(),
action: Action::Run {
argv: argv.iter().map(|s| s.to_string()).collect(),
allowed_exit_codes: allowed.to_vec(),
},
}
}
pub fn write(
@ -107,7 +124,7 @@ impl Platform for FreeBsd {
);
let owner = format!("{}:{}", s.user, s.user);
vec![
Step::run(
Step::run_idem(
format!("create service user {}", s.user),
&[
"pw",
@ -120,6 +137,7 @@ impl Platform for FreeBsd {
"-c",
"Clawdie host service",
],
&[65], // already exists → skip
),
Step::run(
"own clawdie state directories",
@ -210,20 +228,20 @@ mod tests {
assert_eq!(FreeBsd.service_kind(), "rc.d");
// user create, ownership, rc.d write, sysrc enable
assert_eq!(steps.len(), 4);
if let Action::Run(argv) = &steps[1].action {
if let Action::Run { argv, .. } = &steps[1].action {
assert_eq!(argv[0], "chown");
assert!(argv.contains(&"clawdie:clawdie".to_string()));
assert!(argv.contains(&"_clawdie:_clawdie".to_string()));
} else {
panic!("expected chown run step");
}
assert!(matches!(steps[2].action, Action::WriteFile { .. }));
if let Action::WriteFile { contents, .. } = &steps[2].action {
assert!(contents.contains("daemon\""));
assert!(contents.contains("-u clawdie"));
assert!(contents.contains("-u _clawdie"));
} else {
panic!("expected rc.d write step");
}
if let Action::Run(argv) = &steps[3].action {
if let Action::Run { argv, .. } = &steps[3].action {
assert_eq!(argv[0], "sysrc");
assert_eq!(argv[1], "clawdie_enable=YES");
} else {
@ -236,7 +254,7 @@ mod tests {
let steps = Linux.install_service_steps(&ServiceSpec::default());
assert_eq!(Linux.service_kind(), "systemd");
assert_eq!(steps.len(), 5);
if let Action::Run(argv) = &steps[1].action {
if let Action::Run { argv, .. } = &steps[1].action {
assert_eq!(argv[0], "chown");
} else {
panic!("expected chown run step");