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 user=$2
shift 2
getent group ferrodock >/dev/null || groupadd --system ferrodock
usermod -a -G ferrodock "$user"
install -d -m 0755 /opt/ferrodock /opt/ferrodock/engine/bin
stage=$(mktemp -d /opt/ferrodock/.setup.XXXXXX)
trap 'rm -rf "$stage"' EXIT
for entry in "$@"; do
name=${entry%%=*} sum=${entry#*=}
case $name in ''|.*|*/*) echo "Unexpected program name: $name" >&2; exit 1;; esac
cp -- "$programs/$name" "$stage/$name"
if ! echo "$sum $stage/$name" | sha256sum -c --quiet --status -; then
echo "$name changed after it was downloaded; not installing it." >&2
exit 1
fi
done
install -m 0755 "$stage"/* /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 ferrodock --data-root /var/lib/ferrodock --exec-root /run/ferrodock/exec --pidfile /run/ferrodock/dockerd.pid
# Joining a group only counts from the next login; owning the socket lets
# the user in straight away (it's root:ferrodock, 0660, otherwise).
ExecStartPost=/bin/chown $user /run/ferrodock/docker.sock
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 ferrodock-engine.service
# Restarted, not just started: an engine set up before ran with the
# user's primary group, and so would keep running until the next boot.
systemctl restart ferrodock-engine.service
"#;
const RESET: &str = "set -eu
systemctl stop ferrodock-engine.service
rm -rf --one-file-system /var/lib/ferrodock
systemctl start ferrodock-engine.service
";
fn as_root(doing: &str, program: &str, args: &[&str]) -> Result<(), String> {
let status = Command::new("pkexec")
.arg(program)
.args(args)
.status()
.map_err(|_| {
format!(
"{doing} needs your admin password, but this system has no pkexec (polkit) \
to ask for it."
)
})?;
match status.code() {
Some(0) => Ok(()),
Some(126) | Some(127) => Err(format!(
"{doing} needs your admin password: the Docker engine runs as root. \
Try again to enter it."
)),
_ => Err(format!(
"{doing} 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 user = Command::new("id")
.arg("-un")
.output()
.map_err(|e| e.to_string())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_owned())?;
let programs = paths.image.join("bin");
let result = checksums(&programs).and_then(|sums| {
let mut args = vec![
"-c".to_owned(),
SETUP.to_owned(),
"ferrodock-setup".to_owned(),
programs.to_string_lossy().into_owned(),
user,
];
args.extend(sums);
let args: Vec<&str> = args.iter().map(String::as_str).collect();
as_root("Setting up the engine", "/bin/sh", &args)
});
let _ = std::fs::remove_dir_all(&paths.image);
result
}
fn checksums(dir: &Path) -> Result<Vec<String>, String> {
use sha2::{Digest, Sha256};
let mut sums = Vec::new();
for entry in std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))? {
let entry = entry.map_err(|e| e.to_string())?;
let name = entry.file_name().to_string_lossy().into_owned();
let bytes = std::fs::read(entry.path()).map_err(|e| format!("{name}: {e}"))?;
sums.push(format!(
"{name}={}",
ferrodock_core::hex(&Sha256::digest(&bytes))
));
}
sums.sort();
Ok(sums)
}
pub fn reset(_paths: &Paths) -> Result<(), String> {
as_root("Resetting the engine", "/bin/sh", &["-c", RESET])
}
pub fn update(paths: &Paths, archive: &[u8]) -> Result<(), String> {
install(paths, archive)
}
pub struct Running;
impl Running {
pub fn find() -> Option<Self> {
Path::new(LINUX_SOCKET).exists().then_some(Self)
}
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("Starting the engine", "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) {}
pub fn shut_down(&self) -> Result<(), String> {
as_root("Stopping the engine", "systemctl", &["stop", SERVICE])
}
}
#[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));
}
#[test]
fn only_its_own_group_may_use_the_engine() {
let setup = super::SETUP;
assert!(setup.contains("--group ferrodock "));
assert!(setup.contains("groupadd --system ferrodock"));
assert!(setup.contains("usermod -a -G ferrodock \"$user\""));
assert!(!setup.contains("$group"));
assert!(setup.contains("ExecStartPost=/bin/chown $user /run/ferrodock/docker.sock"));
}
#[test]
fn checksums_every_program() {
let dir = std::env::temp_dir().join(format!("ferrodock-sums-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("dockerd"), b"abc").unwrap();
std::fs::write(dir.join("containerd"), b"").unwrap();
let sums = super::checksums(&dir).unwrap();
std::fs::remove_dir_all(&dir).unwrap();
assert_eq!(
sums,
[
"containerd=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"dockerd=ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
]
);
}
#[test]
fn resets_the_data_the_service_keeps() {
assert!(super::SETUP.contains("--data-root /var/lib/ferrodock "));
assert!(super::RESET.contains("rm -rf --one-file-system /var/lib/ferrodock\n"));
assert!(super::RESET.contains("start ferrodock-engine.service"));
}
}