Skip to main content

bupha_systemd/
lib.rs

1//! bupha-deploy — install-time helpers so users never copy files:
2//! `bupha deploy service` writes + enables the systemd unit,
3//! `bupha deploy webhook` registers the GitHub hook, and
4//! `bupha deploy env` seeds the secrets env file.
5
6use std::io;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10fn home() -> PathBuf {
11    std::env::var("HOME")
12        .map(PathBuf::from)
13        .unwrap_or_else(|_| ".".into())
14}
15
16/// Render the user unit. `unit` is a systemd user service running
17/// `bupha watch`; the watcher spawns the configured tunnel itself.
18pub fn write_service_unit(
19    bin: &Path,
20    env_file: &Path,
21    config: Option<&Path>,
22) -> io::Result<PathBuf> {
23    let dir = home().join(".config/systemd/user");
24    std::fs::create_dir_all(&dir)?;
25    let path = dir.join("bupha-watch.service");
26    std::fs::write(
27        &path,
28        format!(
29            "[Unit]
30Description=bupha — headless CI watcher (webhook -> queue -> runner)
31After=network-online.target
32Wants=network-online.target
33
34[Service]
35EnvironmentFile={}
36ExecStart={} watch{}
37Restart=always
38RestartSec=5
39
40[Install]
41WantedBy=default.target
42",
43            env_file.display(),
44            bin.display(),
45            config
46                .map(|c| format!(" --config {}", c.display()))
47                .unwrap_or_default()
48        ),
49    )?;
50    Ok(path)
51}
52
53/// systemctl --user daemon-reload + enable --now. Needs `loginctl
54/// enable-linger` (once) to survive logout/boot.
55pub fn enable_service(unit: &str) -> io::Result<()> {
56    run("systemctl", &["--user", "daemon-reload"])?;
57    run("systemctl", &["--user", "enable", "--now", unit])
58}
59
60/// Seed ~/.config/bupha/env with the secret names (empty values —
61/// never invent secrets).
62pub fn write_env_skeleton(dir: &Path) -> io::Result<PathBuf> {
63    std::fs::create_dir_all(dir)?;
64    let path = dir.join("env");
65    if !path.exists() {
66        std::fs::write(
67            &path,
68            "# Secrets only — everything else lives in bupha.toml\n\
69             BUPHA_WEBHOOK_SECRET=\nBUPHA_TOKEN=\n# CODECOV_TOKEN=\n",
70        )?;
71    }
72    #[cfg(unix)]
73    {
74        use std::os::unix::fs::PermissionsExt;
75        // token-bearing file — owner-only regardless of umask.
76        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
77    }
78    Ok(path)
79}
80
81/// Disable + remove the user unit. Best-effort stop first — an absent
82/// or never-enabled unit still gets its file removed.
83pub fn disable_service(unit: &str) -> io::Result<PathBuf> {
84    let _ = run("systemctl", &["--user", "disable", "--now", unit]);
85    let path = home().join(".config/systemd/user").join(unit);
86    if path.exists() {
87        std::fs::remove_file(&path)?;
88    }
89    run("systemctl", &["--user", "daemon-reload"])?;
90    Ok(path)
91}
92
93fn run(bin: &str, args: &[&str]) -> io::Result<()> {
94    let st = Command::new(bin).args(args).status()?;
95    if st.success() {
96        Ok(())
97    } else {
98        Err(io::Error::other(format!("{bin} {args:?} -> {st}")))
99    }
100}
101
102/// systemd impl of the `Service` port.
103pub struct SystemdService {
104    /// Unit name, e.g. "bupha-watch.service".
105    pub unit: String,
106    /// Binary to ExecStart.
107    pub bin: PathBuf,
108    /// EnvironmentFile (secrets), e.g. ~/.config/bupha/env.
109    pub env: PathBuf,
110    /// Optional --config arg.
111    pub config: Option<PathBuf>,
112}
113
114impl bupha_service::Service for SystemdService {
115    fn write(&self) -> io::Result<PathBuf> {
116        write_service_unit(&self.bin, &self.env, self.config.as_deref())
117    }
118    fn enable(&self) -> io::Result<()> {
119        enable_service(&self.unit)
120    }
121    fn disable(&self) -> io::Result<PathBuf> {
122        disable_service(&self.unit)
123    }
124}