knut-bifrost 0.3.0

Bifröst — watch lakehouse tables (Iceberg via Spark Connect) and materialise them as a FalkorDB property graph
Documentation
//! Where an installed bifrost service lives — shared by `bifrost doris …`
//! (`service`) and `bifrost falkordb …` (`falkordb-service`), so a FalkorDB-only
//! build carries none of the Doris sink.

use std::path::PathBuf;

/// Where the binary lives inside the Kubernetes image.
pub const IMAGE_EXEC: &str = "/usr/local/bin/bifrost";
/// The cursor volume's mount point inside the Kubernetes pod.
pub const K8S_STATE_DIR: &str = "/var/lib/bifrost";

fn home() -> PathBuf {
    std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("/"))
}

/// Where an installed service keeps its cursor.
pub fn installed_state_dir(name: &str, system: bool) -> PathBuf {
    if system {
        PathBuf::from("/var/lib").join(name)
    } else {
        home().join(".local/state").join(name)
    }
}

/// Where a unit's secrets live: a 0600 `EnvironmentFile=`. System scope keeps it
/// under `/etc/<name>/`; user scope beside the unit, which is where skidbladnir
/// writes it, so the path the unit names is the path that exists.
pub fn env_file_path(name: &str, system: bool) -> PathBuf {
    if system {
        PathBuf::from("/etc").join(name).join(format!("{name}.env"))
    } else {
        home().join(".config/systemd/user").join(format!("{name}.env"))
    }
}

/// The systemd spec a lifecycle installs, with its secrets routed to
/// [`env_file_path`]. skidbladnir refuses a spec that carries secrets and no
/// env file, and the neutral `ServiceSpec` cannot name one.
pub fn systemd_spec(lc: &skidbladnir::service::Lifecycle, name: &str, system: bool) -> skidbladnir::systemd::SystemdServiceSpec {
    let spec = lc.systemd_spec();
    if spec.has_secrets() {
        spec.with_env_file(env_file_path(name, system).to_string_lossy())
    } else {
        spec
    }
}

fn scope(system: bool) -> skidbladnir::systemd::Scope {
    if system { skidbladnir::systemd::Scope::System } else { skidbladnir::systemd::Scope::User }
}

/// The unit text `install` writes, `EnvironmentFile=` included.
pub fn unit_text(lc: &skidbladnir::service::Lifecycle, name: &str, system: bool) -> String {
    skidbladnir::systemd::systemd_unit_scoped(&systemd_spec(lc, name, system), scope(system))
}

/// Install the unit and its 0600 env file (no enable/start).
pub fn install(lc: &skidbladnir::service::Lifecycle, name: &str, system: bool) -> anyhow::Result<()> {
    skidbladnir::systemd::install_systemd_scoped(&systemd_spec(lc, name, system), scope(system))
}

/// Remove the env file `install` wrote; skidbladnir's uninstall does not know it.
pub fn remove_env_file(name: &str, system: bool) -> std::io::Result<bool> {
    match std::fs::remove_file(env_file_path(name, system)) {
        Ok(()) => Ok(true),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(e) => Err(e),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn secrets_go_to_an_env_file_the_unit_names_and_never_inline() {
        let spec = skidbladnir::service::ServiceSpec::new("bf", "d", "/usr/local/bin/bifrost")
            .scope(skidbladnir::systemd::Scope::System)
            .secret_env("MINIO_SECRET_KEY", "hunter2");
        let lc = skidbladnir::service::Lifecycle::new(spec);
        let sys = systemd_spec(&lc, "bf", true);
        sys.validate().expect("secrets + env file is a valid spec");
        let unit = unit_text(&lc, "bf", true);
        assert!(unit.contains("EnvironmentFile=/etc/bf/bf.env"), "{unit}");
        assert!(!unit.contains("hunter2"));
        assert!(skidbladnir::systemd::env_file(&sys).contains("hunter2"));
    }

    #[test]
    fn no_secrets_no_env_file() {
        let spec = skidbladnir::service::ServiceSpec::new("bf", "d", "/x");
        let lc = skidbladnir::service::Lifecycle::new(spec);
        assert!(!unit_text(&lc, "bf", false).contains("EnvironmentFile="));
    }

    #[test]
    fn the_user_env_file_sits_beside_the_user_unit() {
        assert!(env_file_path("bf", false).ends_with(".config/systemd/user/bf.env"));
    }
}