use std::path::Path;
use std::process::Command;
use ferrodock_core::builtin::LINUX_SOCKET;
use super::{Paths, Resources, download};
const SERVICE: &str = "ferrodock-engine.service";
const SETUP: &str = r#"set -eu
programs=$1 group=$2
install -d -m 0755 /opt/ferrodock/engine/bin
install -m 0755 "$programs"/* /opt/ferrodock/engine/bin/
cat >/etc/systemd/system/ferrodock-engine.service <<EOF
[Unit]
Description=Ferrodock's Docker engine
After=network-online.target firewalld.service
Wants=network-online.target
[Service]
Type=notify
Environment=PATH=/opt/ferrodock/engine/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/opt/ferrodock/engine/bin/dockerd --host unix:///run/ferrodock/docker.sock --group $group --data-root /var/lib/ferrodock --exec-root /run/ferrodock/exec --pidfile /run/ferrodock/dockerd.pid
RuntimeDirectory=ferrodock
Delegate=yes
KillMode=process
LimitNOFILE=infinity
LimitNPROC=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now ferrodock-engine.service
"#;
fn as_root(program: &str, args: &[&str]) -> Result<(), String> {
let status = Command::new("pkexec")
.arg(program)
.args(args)
.status()
.map_err(|_| {
"Ferrodock needs your admin password to set up its engine, but this system \
has no pkexec (polkit) to ask for it."
.to_owned()
})?;
match status.code() {
Some(0) => Ok(()),
Some(126) | Some(127) => Err(
"Ferrodock's engine needs your admin password once, to run the Docker engine. \
Click Set up again to enter it."
.into(),
),
_ => Err(format!(
"Setting up the engine failed ({status}). `journalctl -u {SERVICE}` has the details."
)),
}
}
pub fn install(paths: &Paths, archive: &[u8]) -> Result<(), String> {
download::unpack(archive, &paths.image, &["bin/dockerd"])?;
let group = Command::new("id")
.arg("-g")
.output()
.map_err(|e| e.to_string())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())?;
let script = paths.dir.join("setup-engine.sh");
std::fs::write(&script, SETUP).map_err(|e| format!("{}: {e}", script.display()))?;
let programs = paths.image.join("bin");
let result = as_root(
"/bin/sh",
&[
&script.to_string_lossy(),
&programs.to_string_lossy(),
&group,
],
);
let _ = std::fs::remove_file(&script);
let _ = std::fs::remove_dir_all(&paths.image);
result
}
pub struct Running;
impl Running {
pub fn start(_paths: &Paths, _resources: &Resources) -> Result<Self, String> {
let active = Command::new("systemctl")
.args(["is-active", "--quiet", SERVICE])
.status()
.is_ok_and(|s| s.success());
if !active {
as_root("systemctl", &["start", SERVICE])?;
}
for _ in 0..300 {
if Path::new(LINUX_SOCKET).exists() {
return Ok(Self);
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(format!(
"The engine didn't start. `journalctl -u {SERVICE}` has the details."
))
}
pub fn is_running(&self) -> bool {
Path::new(LINUX_SOCKET).exists()
}
pub fn stop(&self) {}
}
#[cfg(test)]
mod tests {
#[test]
fn installs_where_the_app_looks() {
let setup = super::SETUP;
assert!(setup.contains(&format!("unix://{}", super::LINUX_SOCKET)));
assert!(setup.contains(ferrodock_core::builtin::LINUX_PROGRAMS));
assert!(setup.contains(ferrodock_core::builtin::LINUX_SERVICE));
}
}