#![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 PODMAN_ENV: &str = "K1_PODMAN_EXECUTABLE";
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 = required_os(PODMAN_ENV)?;
let rust_config = rust_config(podman.clone())?;
let web_config = web_config(podman)?;
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 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"));
}
}