fix(clawdie): rename service user to _clawdie + idempotent creation
Two changes to the clawdie deploy binary: 1. Service user renamed from 'clawdie' to '_clawdie' — follows FreeBSD daemon convention (underscore prefix). Avoids collision with the operator's interactive 'clawdie' user on existing hosts like OSA. 2. User creation is now idempotent — exit code 65 (pw: user already exists) is treated as success via the new allowed_exit_codes field on Action::Run. Deploy can safely re-run without failing. Full end-to-end test on OSA file-backed pool: all 7 steps (ZFS datasets, user, chown, rc.d write, sysrc enable) complete.
This commit is contained in:
parent
2dc6f12c3c
commit
46dcf7d7e7
3 changed files with 45 additions and 21 deletions
|
|
@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue