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 RUST_SCHEMA_ENV: &str = "K1_RUST_CODE_SCHEMA_REVISION";
22const RUST_TOOLCHAIN_POLICY_ENV: &str = "K1_RUST_CODE_TOOLCHAIN_POLICY_REVISION";
23const RUST_IMAGE_ENV: &str = "K1_RUST_CODE_IMAGE";
24const RUST_CHECK_POLICY_ENV: &str = "K1_RUST_CODE_CHECK_POLICY_REVISION";
25const RUST_TARGET_ENV: &str = "K1_RUST_TARGET_TRIPLE";
26const RUST_COMMAND_POLICY_ENV: &str = "K1_RUST_CODE_COMMAND_POLICY_REVISION";
27const WEB_IMAGE_ENV: &str = "K1_WEB_CODE_IMAGE";
28const WEB_CHECKER_ENV: &str = "K1_WEB_CHECKER_EXECUTABLE";
29const WEB_CHROMIUM_ENV: &str = "K1_WEB_CHROMIUM";
30const WEB_CHROMIUM_VERSION_ENV: &str = "K1_WEB_CHROMIUM_VERSION";
31const WEB_CPU_ENV: &str = "K1_WEB_CPU_MILLIS";
32const WEB_MEMORY_ENV: &str = "K1_WEB_MEMORY_BYTES";
33const WEB_PIDS_ENV: &str = "K1_WEB_PIDS_LIMIT";
34const WEB_TMPFS_ENV: &str = "K1_WEB_TMPFS_BYTES";
35const WEB_SHM_ENV: &str = "K1_WEB_SHM_BYTES";
36const WEB_CHECKER_TIMEOUT_ENV: &str = "K1_WEB_CHECKER_TIMEOUT_MS";
37const WEB_WALL_TIMEOUT_ENV: &str = "K1_WEB_WALL_TIMEOUT_MS";
38const WEB_BOOT_ENV: &str = "K1_WEB_CODE_BOOT_REVISION";
39const WEB_SCHEMA_ENV: &str = "K1_WEB_CODE_SCHEMA_REVISION";
40const WEB_ROUTE_ENV: &str = "K1_WEB_CODE_ROUTE_REVISION";
41const WEB_HARNESS_ENV: &str = "K1_WEB_CODE_HARNESS_REVISION";
42const WEB_CHECK_POLICY_ENV: &str = "K1_WEB_CODE_CHECK_POLICY_REVISION";
43
44pub struct CodeServices {
45    rust: Arc<RustCodeKtoolService>,
46    web: K1WebCodeKtoolService,
47}
48
49impl CodeServices {
50    #[allow(clippy::too_many_arguments)]
51    pub fn open(
52        state_root: &Path,
53        ordering: Arc<K1TxnOrdering>,
54        peering: Arc<K1Peering>,
55        groups: Arc<K1Groups>,
56        objects: Arc<K1Objects>,
57        web_projection: Arc<K1WebProjection>,
58    ) -> Result<Self, String> {
59        let paths = CodePaths::open(state_root)?;
60        let podman = resolve_podman()?;
61        let rust_config = rust_config(podman.clone().into_os_string())?;
62        let web_config = web_config(podman.into_os_string())?;
63        WebPodman::new(web_config.clone())
64            .map_err(|error| format!("Web Podman configuration: {error}"))?;
65
66        let rust_projection =
67            K1RustProjection::open(paths.rust_projection, paths.rust_control, ordering, peering)
68                .map(Arc::new)
69                .map_err(|error| format!("open Rust projection: {error}"))?;
70        let rust = Arc::new(RustCodeKtoolService::new(
71            paths.rust_cache,
72            rust_config,
73            rust_projection,
74            Arc::clone(&objects),
75            Arc::clone(&groups),
76        ));
77        let web = K1WebCodeKtoolService::new(
78            ServiceConfig::new(
79                paths.web_cache,
80                paths.web_projection,
81                web_revisions()?,
82                web_config,
83            ),
84            groups,
85            objects,
86            web_projection,
87        );
88        Ok(Self { rust, web })
89    }
90
91    pub fn into_parts(self) -> (Arc<RustCodeKtoolService>, K1WebCodeKtoolService) {
92        (self.rust, self.web)
93    }
94}
95
96struct CodePaths {
97    rust_projection: PathBuf,
98    rust_control: PathBuf,
99    rust_cache: PathBuf,
100    web_projection: PathBuf,
101    web_cache: PathBuf,
102}
103
104impl CodePaths {
105    fn open(state_root: &Path) -> Result<Self, String> {
106        let rust = state_root.join("rust");
107        let web = state_root.join("web");
108        ensure_directory(&rust)?;
109        ensure_directory(&web)?;
110        let value = Self {
111            rust_projection: rust.join("projection"),
112            rust_control: rust.join("control"),
113            rust_cache: rust.join("code-cache"),
114            web_projection: web.join("projection"),
115            web_cache: web.join("code-cache"),
116        };
117        for path in [
118            &value.rust_projection,
119            &value.rust_control,
120            &value.rust_cache,
121            &value.web_projection,
122            &value.web_cache,
123        ] {
124            ensure_directory(path)?;
125        }
126        Ok(value)
127    }
128}
129
130fn resolve_podman() -> Result<PathBuf, String> {
131    let path =
132        std::env::var_os("PATH").ok_or_else(|| "find podman: PATH is unavailable".to_owned())?;
133    resolve_executable("podman", std::env::split_paths(&path))
134        .ok_or_else(|| "find executable podman on PATH".to_owned())
135}
136
137fn resolve_executable(name: &str, paths: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
138    paths.into_iter().find_map(|directory| {
139        let candidate = directory.join(name);
140        executable(&candidate)
141            .then(|| fs::canonicalize(candidate).ok())
142            .flatten()
143            .filter(|path| path.is_absolute())
144    })
145}
146
147#[cfg(unix)]
148fn executable(path: &Path) -> bool {
149    use std::os::unix::fs::PermissionsExt as _;
150    fs::metadata(path)
151        .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
152}
153
154#[cfg(not(unix))]
155fn executable(path: &Path) -> bool {
156    fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
157}
158
159fn rust_config(podman_program: OsString) -> Result<RustCodingConfig, String> {
160    RustCodingConfig::new(RustCodingConfigValues {
161        schema_id: required_string(RUST_SCHEMA_ENV)?,
162        toolchain_policy: required_string(RUST_TOOLCHAIN_POLICY_ENV)?,
163        image: required_string(RUST_IMAGE_ENV)?,
164        rust_toolchain: "1.97".into(),
165        check_policy: required_string(RUST_CHECK_POLICY_ENV)?,
166        target_triple: required_string(RUST_TARGET_ENV)?,
167        command_policy: required_string(RUST_COMMAND_POLICY_ENV)?,
168        podman_program,
169    })
170    .map_err(|error| format!("Rust coding configuration: {error}"))
171}
172
173fn web_config(podman: OsString) -> Result<WebPodmanConfig, String> {
174    let checker_timeout = Duration::from_millis(positive(WEB_CHECKER_TIMEOUT_ENV)?);
175    let wall_timeout = Duration::from_millis(positive(WEB_WALL_TIMEOUT_ENV)?);
176    if wall_timeout <= checker_timeout {
177        return Err(format!(
178            "{WEB_WALL_TIMEOUT_ENV} must exceed {WEB_CHECKER_TIMEOUT_ENV}"
179        ));
180    }
181    Ok(WebPodmanConfig {
182        podman: PathBuf::from(podman),
183        image: required_string(WEB_IMAGE_ENV)?,
184        checker: PathBuf::from(required_os(WEB_CHECKER_ENV)?),
185        chromium: PathBuf::from(required_os(WEB_CHROMIUM_ENV)?),
186        chromium_version: required_string(WEB_CHROMIUM_VERSION_ENV)?,
187        cpu_millis: positive(WEB_CPU_ENV)?,
188        memory_bytes: positive(WEB_MEMORY_ENV)?,
189        pids_limit: positive(WEB_PIDS_ENV)?,
190        tmpfs_bytes: positive(WEB_TMPFS_ENV)?,
191        shm_bytes: positive(WEB_SHM_ENV)?,
192        checker_timeout,
193        wall_timeout,
194    })
195}
196
197fn web_revisions() -> Result<ServiceRevisions, String> {
198    Ok(ServiceRevisions {
199        boot: required_string(WEB_BOOT_ENV)?,
200        schema: required_string(WEB_SCHEMA_ENV)?,
201        route: required_string(WEB_ROUTE_ENV)?,
202        harness: required_string(WEB_HARNESS_ENV)?,
203        check_policy: required_string(WEB_CHECK_POLICY_ENV)?,
204    })
205}
206
207fn required_os(name: &str) -> Result<OsString, String> {
208    let value = std::env::var_os(name).ok_or_else(|| format!("{name} is required"))?;
209    if value.is_empty() {
210        Err(format!("{name} must be nonempty"))
211    } else {
212        Ok(value)
213    }
214}
215
216fn required_string(name: &str) -> Result<String, String> {
217    required_os(name)?
218        .into_string()
219        .map_err(|_| format!("{name} must be UTF-8"))
220}
221
222fn positive<T>(name: &str) -> Result<T, String>
223where
224    T: FromStr + Default + PartialEq,
225    T::Err: Display,
226{
227    let value = required_string(name)?
228        .parse::<T>()
229        .map_err(|error| format!("{name} is invalid: {error}"))?;
230    if value == T::default() {
231        Err(format!("{name} must be nonzero"))
232    } else {
233        Ok(value)
234    }
235}
236
237fn ensure_directory(path: &Path) -> Result<(), String> {
238    match fs::symlink_metadata(path) {
239        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
240        Ok(_) => Err(format!(
241            "code-service path is not an ordinary directory: {}",
242            path.display()
243        )),
244        Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(path)
245            .map_err(|error| format!("create code-service directory {}: {error}", path.display())),
246        Err(error) => Err(format!(
247            "inspect code-service directory {}: {error}",
248            path.display()
249        )),
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn fixed_paths_are_beneath_state_root() {
259        let root = tempfile::tempdir().unwrap();
260        fs::create_dir(root.path().join("web")).unwrap();
261        fs::create_dir(root.path().join("web/projection")).unwrap();
262        let paths = CodePaths::open(root.path()).unwrap();
263        assert_eq!(paths.rust_projection, root.path().join("rust/projection"));
264        assert_eq!(paths.rust_control, root.path().join("rust/control"));
265        assert_eq!(paths.rust_cache, root.path().join("rust/code-cache"));
266        assert_eq!(paths.web_projection, root.path().join("web/projection"));
267        assert_eq!(paths.web_cache, root.path().join("web/code-cache"));
268    }
269
270    #[cfg(unix)]
271    #[test]
272    fn podman_resolver_accepts_only_an_executable_file() {
273        use std::os::unix::fs::PermissionsExt as _;
274
275        let root = tempfile::tempdir().unwrap();
276        let podman = root.path().join("podman");
277        fs::write(&podman, b"#!/bin/sh\nexit 0\n").unwrap();
278        fs::set_permissions(&podman, fs::Permissions::from_mode(0o700)).unwrap();
279        assert_eq!(
280            resolve_executable("podman", [root.path().to_path_buf()]),
281            Some(fs::canonicalize(&podman).unwrap())
282        );
283
284        fs::set_permissions(&podman, fs::Permissions::from_mode(0o600)).unwrap();
285        assert!(resolve_executable("podman", [root.path().to_path_buf()]).is_none());
286    }
287}