kcode-k1-daemon-code-services 0.1.1

Open the daemon's process-owned RustCode and WebCode services
Documentation
#![doc = include_str!("../Documentation.md")]

use kcode_k1_groups::K1Groups;
use kcode_k1_objects::K1Objects;
use kcode_k1_peering::K1Peering;
use kcode_k1_rust_code_ktool_service::RustCodeKtoolService;
use kcode_k1_rust_coding::{RustCodingConfig, RustCodingConfigValues};
use kcode_k1_rust_projection::K1RustProjection;
use kcode_k1_txn_ordering::K1TxnOrdering;
use kcode_k1_web_code_ktool_service::{K1WebCodeKtoolService, ServiceConfig, ServiceRevisions};
use kcode_k1_web_podman::{WebPodman, WebPodmanConfig};
use kcode_k1_web_projection::K1WebProjection;
use std::ffi::OsString;
use std::fmt::Display;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

const RUST_SCHEMA_ENV: &str = "K1_RUST_CODE_SCHEMA_REVISION";
const RUST_TOOLCHAIN_POLICY_ENV: &str = "K1_RUST_CODE_TOOLCHAIN_POLICY_REVISION";
const RUST_IMAGE_ENV: &str = "K1_RUST_CODE_IMAGE";
const RUST_CHECK_POLICY_ENV: &str = "K1_RUST_CODE_CHECK_POLICY_REVISION";
const RUST_TARGET_ENV: &str = "K1_RUST_TARGET_TRIPLE";
const RUST_COMMAND_POLICY_ENV: &str = "K1_RUST_CODE_COMMAND_POLICY_REVISION";
const WEB_IMAGE_ENV: &str = "K1_WEB_CODE_IMAGE";
const WEB_CHECKER_ENV: &str = "K1_WEB_CHECKER_EXECUTABLE";
const WEB_CHROMIUM_ENV: &str = "K1_WEB_CHROMIUM";
const WEB_CHROMIUM_VERSION_ENV: &str = "K1_WEB_CHROMIUM_VERSION";
const WEB_CPU_ENV: &str = "K1_WEB_CPU_MILLIS";
const WEB_MEMORY_ENV: &str = "K1_WEB_MEMORY_BYTES";
const WEB_PIDS_ENV: &str = "K1_WEB_PIDS_LIMIT";
const WEB_TMPFS_ENV: &str = "K1_WEB_TMPFS_BYTES";
const WEB_SHM_ENV: &str = "K1_WEB_SHM_BYTES";
const WEB_CHECKER_TIMEOUT_ENV: &str = "K1_WEB_CHECKER_TIMEOUT_MS";
const WEB_WALL_TIMEOUT_ENV: &str = "K1_WEB_WALL_TIMEOUT_MS";
const WEB_BOOT_ENV: &str = "K1_WEB_CODE_BOOT_REVISION";
const WEB_SCHEMA_ENV: &str = "K1_WEB_CODE_SCHEMA_REVISION";
const WEB_ROUTE_ENV: &str = "K1_WEB_CODE_ROUTE_REVISION";
const WEB_HARNESS_ENV: &str = "K1_WEB_CODE_HARNESS_REVISION";
const WEB_CHECK_POLICY_ENV: &str = "K1_WEB_CODE_CHECK_POLICY_REVISION";

pub struct CodeServices {
    rust: Arc<RustCodeKtoolService>,
    web: K1WebCodeKtoolService,
}

impl CodeServices {
    #[allow(clippy::too_many_arguments)]
    pub fn open(
        state_root: &Path,
        ordering: Arc<K1TxnOrdering>,
        peering: Arc<K1Peering>,
        groups: Arc<K1Groups>,
        objects: Arc<K1Objects>,
        web_projection: Arc<K1WebProjection>,
    ) -> Result<Self, String> {
        let paths = CodePaths::open(state_root)?;
        let podman = resolve_podman()?;
        let rust_config = rust_config(podman.clone().into_os_string())?;
        let web_config = web_config(podman.into_os_string())?;
        WebPodman::new(web_config.clone())
            .map_err(|error| format!("Web Podman configuration: {error}"))?;

        let rust_projection =
            K1RustProjection::open(paths.rust_projection, paths.rust_control, ordering, peering)
                .map(Arc::new)
                .map_err(|error| format!("open Rust projection: {error}"))?;
        let rust = Arc::new(RustCodeKtoolService::new(
            paths.rust_cache,
            rust_config,
            rust_projection,
            Arc::clone(&objects),
            Arc::clone(&groups),
        ));
        let web = K1WebCodeKtoolService::new(
            ServiceConfig::new(
                paths.web_cache,
                paths.web_projection,
                web_revisions()?,
                web_config,
            ),
            groups,
            objects,
            web_projection,
        );
        Ok(Self { rust, web })
    }

    pub fn into_parts(self) -> (Arc<RustCodeKtoolService>, K1WebCodeKtoolService) {
        (self.rust, self.web)
    }
}

struct CodePaths {
    rust_projection: PathBuf,
    rust_control: PathBuf,
    rust_cache: PathBuf,
    web_projection: PathBuf,
    web_cache: PathBuf,
}

impl CodePaths {
    fn open(state_root: &Path) -> Result<Self, String> {
        let rust = state_root.join("rust");
        let web = state_root.join("web");
        ensure_directory(&rust)?;
        ensure_directory(&web)?;
        let value = Self {
            rust_projection: rust.join("projection"),
            rust_control: rust.join("control"),
            rust_cache: rust.join("code-cache"),
            web_projection: web.join("projection"),
            web_cache: web.join("code-cache"),
        };
        for path in [
            &value.rust_projection,
            &value.rust_control,
            &value.rust_cache,
            &value.web_projection,
            &value.web_cache,
        ] {
            ensure_directory(path)?;
        }
        Ok(value)
    }
}

fn resolve_podman() -> Result<PathBuf, String> {
    let path =
        std::env::var_os("PATH").ok_or_else(|| "find podman: PATH is unavailable".to_owned())?;
    resolve_executable("podman", std::env::split_paths(&path))
        .ok_or_else(|| "find executable podman on PATH".to_owned())
}

fn resolve_executable(name: &str, paths: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
    paths.into_iter().find_map(|directory| {
        let candidate = directory.join(name);
        executable(&candidate)
            .then(|| fs::canonicalize(candidate).ok())
            .flatten()
            .filter(|path| path.is_absolute())
    })
}

#[cfg(unix)]
fn executable(path: &Path) -> bool {
    use std::os::unix::fs::PermissionsExt as _;
    fs::metadata(path)
        .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
}

#[cfg(not(unix))]
fn executable(path: &Path) -> bool {
    fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
}

fn rust_config(podman_program: OsString) -> Result<RustCodingConfig, String> {
    RustCodingConfig::new(RustCodingConfigValues {
        schema_id: required_string(RUST_SCHEMA_ENV)?,
        toolchain_policy: required_string(RUST_TOOLCHAIN_POLICY_ENV)?,
        image: required_string(RUST_IMAGE_ENV)?,
        rust_toolchain: "1.97".into(),
        check_policy: required_string(RUST_CHECK_POLICY_ENV)?,
        target_triple: required_string(RUST_TARGET_ENV)?,
        command_policy: required_string(RUST_COMMAND_POLICY_ENV)?,
        podman_program,
    })
    .map_err(|error| format!("Rust coding configuration: {error}"))
}

fn web_config(podman: OsString) -> Result<WebPodmanConfig, String> {
    let checker_timeout = Duration::from_millis(positive(WEB_CHECKER_TIMEOUT_ENV)?);
    let wall_timeout = Duration::from_millis(positive(WEB_WALL_TIMEOUT_ENV)?);
    if wall_timeout <= checker_timeout {
        return Err(format!(
            "{WEB_WALL_TIMEOUT_ENV} must exceed {WEB_CHECKER_TIMEOUT_ENV}"
        ));
    }
    Ok(WebPodmanConfig {
        podman: PathBuf::from(podman),
        image: required_string(WEB_IMAGE_ENV)?,
        checker: PathBuf::from(required_os(WEB_CHECKER_ENV)?),
        chromium: PathBuf::from(required_os(WEB_CHROMIUM_ENV)?),
        chromium_version: required_string(WEB_CHROMIUM_VERSION_ENV)?,
        cpu_millis: positive(WEB_CPU_ENV)?,
        memory_bytes: positive(WEB_MEMORY_ENV)?,
        pids_limit: positive(WEB_PIDS_ENV)?,
        tmpfs_bytes: positive(WEB_TMPFS_ENV)?,
        shm_bytes: positive(WEB_SHM_ENV)?,
        checker_timeout,
        wall_timeout,
    })
}

fn web_revisions() -> Result<ServiceRevisions, String> {
    Ok(ServiceRevisions {
        boot: required_string(WEB_BOOT_ENV)?,
        schema: required_string(WEB_SCHEMA_ENV)?,
        route: required_string(WEB_ROUTE_ENV)?,
        harness: required_string(WEB_HARNESS_ENV)?,
        check_policy: required_string(WEB_CHECK_POLICY_ENV)?,
    })
}

fn required_os(name: &str) -> Result<OsString, String> {
    let value = std::env::var_os(name).ok_or_else(|| format!("{name} is required"))?;
    if value.is_empty() {
        Err(format!("{name} must be nonempty"))
    } else {
        Ok(value)
    }
}

fn required_string(name: &str) -> Result<String, String> {
    required_os(name)?
        .into_string()
        .map_err(|_| format!("{name} must be UTF-8"))
}

fn positive<T>(name: &str) -> Result<T, String>
where
    T: FromStr + Default + PartialEq,
    T::Err: Display,
{
    let value = required_string(name)?
        .parse::<T>()
        .map_err(|error| format!("{name} is invalid: {error}"))?;
    if value == T::default() {
        Err(format!("{name} must be nonzero"))
    } else {
        Ok(value)
    }
}

fn ensure_directory(path: &Path) -> Result<(), String> {
    match fs::symlink_metadata(path) {
        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
        Ok(_) => Err(format!(
            "code-service path is not an ordinary directory: {}",
            path.display()
        )),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(path)
            .map_err(|error| format!("create code-service directory {}: {error}", path.display())),
        Err(error) => Err(format!(
            "inspect code-service directory {}: {error}",
            path.display()
        )),
    }
}

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

    #[test]
    fn fixed_paths_are_beneath_state_root() {
        let root = tempfile::tempdir().unwrap();
        fs::create_dir(root.path().join("web")).unwrap();
        fs::create_dir(root.path().join("web/projection")).unwrap();
        let paths = CodePaths::open(root.path()).unwrap();
        assert_eq!(paths.rust_projection, root.path().join("rust/projection"));
        assert_eq!(paths.rust_control, root.path().join("rust/control"));
        assert_eq!(paths.rust_cache, root.path().join("rust/code-cache"));
        assert_eq!(paths.web_projection, root.path().join("web/projection"));
        assert_eq!(paths.web_cache, root.path().join("web/code-cache"));
    }

    #[cfg(unix)]
    #[test]
    fn podman_resolver_accepts_only_an_executable_file() {
        use std::os::unix::fs::PermissionsExt as _;

        let root = tempfile::tempdir().unwrap();
        let podman = root.path().join("podman");
        fs::write(&podman, b"#!/bin/sh\nexit 0\n").unwrap();
        fs::set_permissions(&podman, fs::Permissions::from_mode(0o700)).unwrap();
        assert_eq!(
            resolve_executable("podman", [root.path().to_path_buf()]),
            Some(fs::canonicalize(&podman).unwrap())
        );

        fs::set_permissions(&podman, fs::Permissions::from_mode(0o600)).unwrap();
        assert!(resolve_executable("podman", [root.path().to_path_buf()]).is_none());
    }
}