Skip to main content

kcode_k1_daemon_code_services/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2
3use kcode_k1_groups::K1Groups;
4use kcode_k1_objects::K1Objects;
5use kcode_k1_peering::K1Peering;
6use kcode_k1_rust_code_ktool_service::RustCodeKtoolService;
7use kcode_k1_rust_coding::{RustCodingConfig, RustCodingConfigValues};
8use kcode_k1_rust_projection::K1RustProjection;
9use kcode_k1_txn_ordering::K1TxnOrdering;
10use kcode_k1_web_code_ktool_service::{K1WebCodeKtoolService, ServiceConfig, ServiceRevisions};
11use kcode_k1_web_podman::{WebPodman, WebPodmanConfig};
12use kcode_k1_web_projection::K1WebProjection;
13use std::ffi::OsString;
14use std::fmt::Display;
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::str::FromStr;
18use std::sync::Arc;
19use std::time::Duration;
20
21const PODMAN_ENV: &str = "K1_PODMAN_EXECUTABLE";
22const RUST_SCHEMA_ENV: &str = "K1_RUST_CODE_SCHEMA_REVISION";
23const RUST_TOOLCHAIN_POLICY_ENV: &str = "K1_RUST_CODE_TOOLCHAIN_POLICY_REVISION";
24const RUST_IMAGE_ENV: &str = "K1_RUST_CODE_IMAGE";
25const RUST_CHECK_POLICY_ENV: &str = "K1_RUST_CODE_CHECK_POLICY_REVISION";
26const RUST_TARGET_ENV: &str = "K1_RUST_TARGET_TRIPLE";
27const RUST_COMMAND_POLICY_ENV: &str = "K1_RUST_CODE_COMMAND_POLICY_REVISION";
28const WEB_IMAGE_ENV: &str = "K1_WEB_CODE_IMAGE";
29const WEB_CHECKER_ENV: &str = "K1_WEB_CHECKER_EXECUTABLE";
30const WEB_CHROMIUM_ENV: &str = "K1_WEB_CHROMIUM";
31const WEB_CHROMIUM_VERSION_ENV: &str = "K1_WEB_CHROMIUM_VERSION";
32const WEB_CPU_ENV: &str = "K1_WEB_CPU_MILLIS";
33const WEB_MEMORY_ENV: &str = "K1_WEB_MEMORY_BYTES";
34const WEB_PIDS_ENV: &str = "K1_WEB_PIDS_LIMIT";
35const WEB_TMPFS_ENV: &str = "K1_WEB_TMPFS_BYTES";
36const WEB_SHM_ENV: &str = "K1_WEB_SHM_BYTES";
37const WEB_CHECKER_TIMEOUT_ENV: &str = "K1_WEB_CHECKER_TIMEOUT_MS";
38const WEB_WALL_TIMEOUT_ENV: &str = "K1_WEB_WALL_TIMEOUT_MS";
39const WEB_BOOT_ENV: &str = "K1_WEB_CODE_BOOT_REVISION";
40const WEB_SCHEMA_ENV: &str = "K1_WEB_CODE_SCHEMA_REVISION";
41const WEB_ROUTE_ENV: &str = "K1_WEB_CODE_ROUTE_REVISION";
42const WEB_HARNESS_ENV: &str = "K1_WEB_CODE_HARNESS_REVISION";
43const WEB_CHECK_POLICY_ENV: &str = "K1_WEB_CODE_CHECK_POLICY_REVISION";
44
45pub struct CodeServices {
46    rust: Arc<RustCodeKtoolService>,
47    web: K1WebCodeKtoolService,
48}
49
50impl CodeServices {
51    #[allow(clippy::too_many_arguments)]
52    pub fn open(
53        state_root: &Path,
54        ordering: Arc<K1TxnOrdering>,
55        peering: Arc<K1Peering>,
56        groups: Arc<K1Groups>,
57        objects: Arc<K1Objects>,
58        web_projection: Arc<K1WebProjection>,
59    ) -> Result<Self, String> {
60        let paths = CodePaths::open(state_root)?;
61        let podman = required_os(PODMAN_ENV)?;
62        let rust_config = rust_config(podman.clone())?;
63        let web_config = web_config(podman)?;
64        WebPodman::new(web_config.clone())
65            .map_err(|error| format!("Web Podman configuration: {error}"))?;
66
67        let rust_projection =
68            K1RustProjection::open(paths.rust_projection, paths.rust_control, ordering, peering)
69                .map(Arc::new)
70                .map_err(|error| format!("open Rust projection: {error}"))?;
71        let rust = Arc::new(RustCodeKtoolService::new(
72            paths.rust_cache,
73            rust_config,
74            rust_projection,
75            Arc::clone(&objects),
76            Arc::clone(&groups),
77        ));
78        let web = K1WebCodeKtoolService::new(
79            ServiceConfig::new(
80                paths.web_cache,
81                paths.web_projection,
82                web_revisions()?,
83                web_config,
84            ),
85            groups,
86            objects,
87            web_projection,
88        );
89        Ok(Self { rust, web })
90    }
91
92    pub fn into_parts(self) -> (Arc<RustCodeKtoolService>, K1WebCodeKtoolService) {
93        (self.rust, self.web)
94    }
95}
96
97struct CodePaths {
98    rust_projection: PathBuf,
99    rust_control: PathBuf,
100    rust_cache: PathBuf,
101    web_projection: PathBuf,
102    web_cache: PathBuf,
103}
104
105impl CodePaths {
106    fn open(state_root: &Path) -> Result<Self, String> {
107        let rust = state_root.join("rust");
108        let web = state_root.join("web");
109        ensure_directory(&rust)?;
110        ensure_directory(&web)?;
111        let value = Self {
112            rust_projection: rust.join("projection"),
113            rust_control: rust.join("control"),
114            rust_cache: rust.join("code-cache"),
115            web_projection: web.join("projection"),
116            web_cache: web.join("code-cache"),
117        };
118        for path in [
119            &value.rust_projection,
120            &value.rust_control,
121            &value.rust_cache,
122            &value.web_projection,
123            &value.web_cache,
124        ] {
125            ensure_directory(path)?;
126        }
127        Ok(value)
128    }
129}
130
131fn rust_config(podman_program: OsString) -> Result<RustCodingConfig, String> {
132    RustCodingConfig::new(RustCodingConfigValues {
133        schema_id: required_string(RUST_SCHEMA_ENV)?,
134        toolchain_policy: required_string(RUST_TOOLCHAIN_POLICY_ENV)?,
135        image: required_string(RUST_IMAGE_ENV)?,
136        rust_toolchain: "1.97".into(),
137        check_policy: required_string(RUST_CHECK_POLICY_ENV)?,
138        target_triple: required_string(RUST_TARGET_ENV)?,
139        command_policy: required_string(RUST_COMMAND_POLICY_ENV)?,
140        podman_program,
141    })
142    .map_err(|error| format!("Rust coding configuration: {error}"))
143}
144
145fn web_config(podman: OsString) -> Result<WebPodmanConfig, String> {
146    let checker_timeout = Duration::from_millis(positive(WEB_CHECKER_TIMEOUT_ENV)?);
147    let wall_timeout = Duration::from_millis(positive(WEB_WALL_TIMEOUT_ENV)?);
148    if wall_timeout <= checker_timeout {
149        return Err(format!(
150            "{WEB_WALL_TIMEOUT_ENV} must exceed {WEB_CHECKER_TIMEOUT_ENV}"
151        ));
152    }
153    Ok(WebPodmanConfig {
154        podman: PathBuf::from(podman),
155        image: required_string(WEB_IMAGE_ENV)?,
156        checker: PathBuf::from(required_os(WEB_CHECKER_ENV)?),
157        chromium: PathBuf::from(required_os(WEB_CHROMIUM_ENV)?),
158        chromium_version: required_string(WEB_CHROMIUM_VERSION_ENV)?,
159        cpu_millis: positive(WEB_CPU_ENV)?,
160        memory_bytes: positive(WEB_MEMORY_ENV)?,
161        pids_limit: positive(WEB_PIDS_ENV)?,
162        tmpfs_bytes: positive(WEB_TMPFS_ENV)?,
163        shm_bytes: positive(WEB_SHM_ENV)?,
164        checker_timeout,
165        wall_timeout,
166    })
167}
168
169fn web_revisions() -> Result<ServiceRevisions, String> {
170    Ok(ServiceRevisions {
171        boot: required_string(WEB_BOOT_ENV)?,
172        schema: required_string(WEB_SCHEMA_ENV)?,
173        route: required_string(WEB_ROUTE_ENV)?,
174        harness: required_string(WEB_HARNESS_ENV)?,
175        check_policy: required_string(WEB_CHECK_POLICY_ENV)?,
176    })
177}
178
179fn required_os(name: &str) -> Result<OsString, String> {
180    let value = std::env::var_os(name).ok_or_else(|| format!("{name} is required"))?;
181    if value.is_empty() {
182        Err(format!("{name} must be nonempty"))
183    } else {
184        Ok(value)
185    }
186}
187
188fn required_string(name: &str) -> Result<String, String> {
189    required_os(name)?
190        .into_string()
191        .map_err(|_| format!("{name} must be UTF-8"))
192}
193
194fn positive<T>(name: &str) -> Result<T, String>
195where
196    T: FromStr + Default + PartialEq,
197    T::Err: Display,
198{
199    let value = required_string(name)?
200        .parse::<T>()
201        .map_err(|error| format!("{name} is invalid: {error}"))?;
202    if value == T::default() {
203        Err(format!("{name} must be nonzero"))
204    } else {
205        Ok(value)
206    }
207}
208
209fn ensure_directory(path: &Path) -> Result<(), String> {
210    match fs::symlink_metadata(path) {
211        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
212        Ok(_) => Err(format!(
213            "code-service path is not an ordinary directory: {}",
214            path.display()
215        )),
216        Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(path)
217            .map_err(|error| format!("create code-service directory {}: {error}", path.display())),
218        Err(error) => Err(format!(
219            "inspect code-service directory {}: {error}",
220            path.display()
221        )),
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn fixed_paths_are_beneath_state_root() {
231        let root = tempfile::tempdir().unwrap();
232        fs::create_dir(root.path().join("web")).unwrap();
233        fs::create_dir(root.path().join("web/projection")).unwrap();
234        let paths = CodePaths::open(root.path()).unwrap();
235        assert_eq!(paths.rust_projection, root.path().join("rust/projection"));
236        assert_eq!(paths.rust_control, root.path().join("rust/control"));
237        assert_eq!(paths.rust_cache, root.path().join("rust/code-cache"));
238        assert_eq!(paths.web_projection, root.path().join("web/projection"));
239        assert_eq!(paths.web_cache, root.path().join("web/code-cache"));
240    }
241}