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::fs;
15use std::path::{Path, PathBuf};
16use std::process::{Command, Output};
17use std::sync::Arc;
18use std::time::Duration;
19
20const CONTAINERFILE: &str = include_str!("assets/Containerfile");
21const RUST_SCHEMA: &str = "k1-rust-code-cache-v1";
22const RUST_TOOLCHAIN_POLICY: &str = "rust-1.97-k1-code-tools-v1";
23const RUST_CHECK_POLICY: &str = "k1-rust-code-check-v1";
24const RUST_COMMAND_POLICY: &str = "k1-rust-code-command-v1";
25const WEB_BOOT: &str = "k1-web-code-boot-v1";
26const WEB_SCHEMA: &str = "k1-web-code-cache-v1";
27const WEB_ROUTE: &str = "k1-web-code-routes-v1";
28const WEB_HARNESS: &str = "k1-web-code-harness-v1";
29const WEB_CHECK_POLICY: &str = "k1-web-code-check-v1";
30const CHECK_DEADLINE: Duration = Duration::from_secs(225);
31
32pub struct CodeServices {
33    rust: Arc<RustCodeKtoolService>,
34    web: K1WebCodeKtoolService,
35}
36
37impl CodeServices {
38    #[allow(clippy::too_many_arguments)]
39    pub fn open(
40        state_root: &Path,
41        ordering: Arc<K1TxnOrdering>,
42        peering: Arc<K1Peering>,
43        groups: Arc<K1Groups>,
44        objects: Arc<K1Objects>,
45        web_projection: Arc<K1WebProjection>,
46    ) -> Result<Self, String> {
47        let paths = CodePaths::open(state_root)?;
48        let podman = resolve_podman()?;
49        let image = tool_image();
50        ensure_tool_image(&podman, &image, &paths.tool_build)?;
51        let checker = materialize_checker(&podman, &image, &paths.checker)?;
52        let (chromium, chromium_version) = discover_chromium(&podman, &image)?;
53        let rust_config = rust_config(podman.clone().into_os_string(), image.clone())?;
54        let web_config = WebPodmanConfig {
55            podman,
56            image,
57            checker,
58            chromium,
59            chromium_version,
60            cpu_millis: 0,
61            memory_bytes: 0,
62            pids_limit: 0,
63            tmpfs_bytes: 0,
64            shm_bytes: 0,
65            checker_timeout: CHECK_DEADLINE,
66            wall_timeout: CHECK_DEADLINE,
67        };
68        WebPodman::new(web_config.clone())
69            .map_err(|error| format!("Web Podman configuration: {error}"))?;
70
71        let rust_projection =
72            K1RustProjection::open(paths.rust_projection, paths.rust_control, ordering, peering)
73                .map(Arc::new)
74                .map_err(|error| format!("open Rust projection: {error}"))?;
75        let rust = Arc::new(RustCodeKtoolService::new(
76            paths.rust_cache,
77            rust_config,
78            rust_projection,
79            Arc::clone(&objects),
80            Arc::clone(&groups),
81        ));
82        let web = K1WebCodeKtoolService::new(
83            ServiceConfig::new(
84                paths.web_cache,
85                paths.web_projection,
86                web_revisions(),
87                web_config,
88            ),
89            groups,
90            objects,
91            web_projection,
92        );
93        Ok(Self { rust, web })
94    }
95
96    pub fn into_parts(self) -> (Arc<RustCodeKtoolService>, K1WebCodeKtoolService) {
97        (self.rust, self.web)
98    }
99}
100
101struct CodePaths {
102    rust_projection: PathBuf,
103    rust_control: PathBuf,
104    rust_cache: PathBuf,
105    web_projection: PathBuf,
106    web_cache: PathBuf,
107    tool_build: PathBuf,
108    checker: PathBuf,
109}
110
111impl CodePaths {
112    fn open(state_root: &Path) -> Result<Self, String> {
113        let rust = state_root.join("rust");
114        let web = state_root.join("web");
115        let tools = state_root.join("code-tools");
116        for path in [&rust, &web, &tools] {
117            ensure_directory(path)?;
118        }
119        let value = Self {
120            rust_projection: rust.join("projection"),
121            rust_control: rust.join("control"),
122            rust_cache: rust.join("code-cache"),
123            web_projection: web.join("projection"),
124            web_cache: web.join("code-cache"),
125            tool_build: tools.join("image-build"),
126            checker: tools.join("kcode-k1-web-checker"),
127        };
128        for path in [
129            &value.rust_projection,
130            &value.rust_control,
131            &value.rust_cache,
132            &value.web_projection,
133            &value.web_cache,
134        ] {
135            ensure_directory(path)?;
136        }
137        Ok(value)
138    }
139}
140
141fn resolve_podman() -> Result<PathBuf, String> {
142    let path =
143        std::env::var_os("PATH").ok_or_else(|| "find podman: PATH is unavailable".to_owned())?;
144    resolve_executable("podman", std::env::split_paths(&path))
145        .ok_or_else(|| "find executable podman on PATH".to_owned())
146}
147
148fn resolve_executable(name: &str, paths: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
149    paths.into_iter().find_map(|directory| {
150        let candidate = directory.join(name);
151        executable(&candidate)
152            .then(|| fs::canonicalize(candidate).ok())
153            .flatten()
154            .filter(|path| path.is_absolute())
155    })
156}
157
158#[cfg(unix)]
159fn executable(path: &Path) -> bool {
160    use std::os::unix::fs::PermissionsExt as _;
161    fs::metadata(path)
162        .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
163}
164
165#[cfg(not(unix))]
166fn executable(path: &Path) -> bool {
167    fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
168}
169
170fn tool_image() -> String {
171    let mut hash = 0xcbf29ce484222325_u64;
172    for byte in CONTAINERFILE.bytes() {
173        hash ^= u64::from(byte);
174        hash = hash.wrapping_mul(0x100000001b3);
175    }
176    format!(
177        "localhost/kcode-k1-code-tools:{}-{hash:016x}",
178        env!("CARGO_PKG_VERSION")
179    )
180}
181
182fn ensure_tool_image(podman: &Path, image: &str, build_root: &Path) -> Result<(), String> {
183    let mut inspect = Command::new(podman);
184    inspect.arg("image").arg("exists").arg(image);
185    let inspected = run(inspect, "inspect K1 code tool image")?;
186    if inspected.status.success() {
187        return Ok(());
188    }
189    if inspected.status.code() != Some(1) {
190        return Err(command_failure("inspect K1 code tool image", inspected));
191    }
192
193    if build_root.exists() {
194        fs::remove_dir_all(build_root).map_err(|error| {
195            format!(
196                "replace code-tool image build directory {}: {error}",
197                build_root.display()
198            )
199        })?;
200    }
201    fs::create_dir(build_root).map_err(|error| {
202        format!(
203            "create code-tool image build directory {}: {error}",
204            build_root.display()
205        )
206    })?;
207    let containerfile = build_root.join("Containerfile");
208    fs::write(&containerfile, CONTAINERFILE).map_err(|error| {
209        format!(
210            "write embedded Containerfile {}: {error}",
211            containerfile.display()
212        )
213    })?;
214    let mut build = Command::new(podman);
215    build
216        .arg("build")
217        .arg("--tag")
218        .arg(image)
219        .arg("--file")
220        .arg(&containerfile)
221        .arg(build_root);
222    let built = run(build, "build K1 code tool image")?;
223    let _ = fs::remove_dir_all(build_root);
224    if built.status.success() {
225        Ok(())
226    } else {
227        Err(command_failure("build K1 code tool image", built))
228    }
229}
230
231fn materialize_checker(podman: &Path, image: &str, destination: &Path) -> Result<PathBuf, String> {
232    let mut command = Command::new(podman);
233    command
234        .arg("run")
235        .arg("--rm")
236        .arg("--network=none")
237        .arg("--pull=never")
238        .arg("--entrypoint=/bin/cat")
239        .arg(image)
240        .arg("/usr/local/cargo/bin/kcode-k1-web-checker");
241    let output = run(command, "extract K1 Web checker")?;
242    if !output.status.success() {
243        return Err(command_failure("extract K1 Web checker", output));
244    }
245    if output.stdout.is_empty() {
246        return Err("extract K1 Web checker: image returned an empty executable".to_owned());
247    }
248    let staging = destination.with_extension("new");
249    fs::write(&staging, output.stdout)
250        .map_err(|error| format!("write Web checker {}: {error}", staging.display()))?;
251    set_executable(&staging)?;
252    fs::rename(&staging, destination)
253        .map_err(|error| format!("install Web checker {}: {error}", destination.display()))?;
254    fs::canonicalize(destination).map_err(|error| {
255        format!(
256            "canonicalize Web checker {}: {error}",
257            destination.display()
258        )
259    })
260}
261
262#[cfg(unix)]
263fn set_executable(path: &Path) -> Result<(), String> {
264    use std::os::unix::fs::PermissionsExt as _;
265    fs::set_permissions(path, fs::Permissions::from_mode(0o755))
266        .map_err(|error| format!("make Web checker executable {}: {error}", path.display()))
267}
268
269#[cfg(not(unix))]
270fn set_executable(_path: &Path) -> Result<(), String> {
271    Ok(())
272}
273
274fn discover_chromium(podman: &Path, image: &str) -> Result<(PathBuf, String), String> {
275    let mut command = Command::new(podman);
276    command
277        .arg("run")
278        .arg("--rm")
279        .arg("--network=none")
280        .arg("--pull=never")
281        .arg("--entrypoint=/bin/sh")
282        .arg(image)
283        .arg("-c")
284        .arg("command -v chromium; chromium --version");
285    let output = run(command, "discover Chromium in K1 code tool image")?;
286    if !output.status.success() {
287        return Err(command_failure(
288            "discover Chromium in K1 code tool image",
289            output,
290        ));
291    }
292    let text = String::from_utf8(output.stdout)
293        .map_err(|_| "discover Chromium: image output was not UTF-8".to_owned())?;
294    let mut lines = text.lines().filter(|line| !line.trim().is_empty());
295    let path = PathBuf::from(
296        lines
297            .next()
298            .ok_or_else(|| "discover Chromium: executable path was absent".to_owned())?,
299    );
300    let version = lines
301        .next()
302        .ok_or_else(|| "discover Chromium: version was absent".to_owned())?
303        .to_owned();
304    if !path.is_absolute() {
305        return Err("discover Chromium: executable path was not absolute".to_owned());
306    }
307    Ok((path, version))
308}
309
310fn rust_config(podman_program: OsString, image: String) -> Result<RustCodingConfig, String> {
311    RustCodingConfig::new(RustCodingConfigValues {
312        schema_id: RUST_SCHEMA.into(),
313        toolchain_policy: RUST_TOOLCHAIN_POLICY.into(),
314        image,
315        rust_toolchain: "1.97".into(),
316        check_policy: RUST_CHECK_POLICY.into(),
317        target_triple: rust_target()?.into(),
318        command_policy: RUST_COMMAND_POLICY.into(),
319        podman_program,
320    })
321    .map_err(|error| format!("Rust coding configuration: {error}"))
322}
323
324fn rust_target() -> Result<&'static str, String> {
325    match std::env::consts::ARCH {
326        "x86_64" => Ok("x86_64-unknown-linux-gnu"),
327        "aarch64" => Ok("aarch64-unknown-linux-gnu"),
328        architecture => Err(format!(
329            "unsupported Rust code host architecture: {architecture}"
330        )),
331    }
332}
333
334fn web_revisions() -> ServiceRevisions {
335    ServiceRevisions {
336        boot: WEB_BOOT.into(),
337        schema: WEB_SCHEMA.into(),
338        route: WEB_ROUTE.into(),
339        harness: WEB_HARNESS.into(),
340        check_policy: WEB_CHECK_POLICY.into(),
341    }
342}
343
344fn run(mut command: Command, label: &str) -> Result<Output, String> {
345    command
346        .output()
347        .map_err(|error| format!("{label}: could not start command: {error}"))
348}
349
350fn command_failure(label: &str, output: Output) -> String {
351    format!(
352        "{label} exited with {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
353        output
354            .status
355            .code()
356            .map_or_else(|| "signal".to_owned(), |code| code.to_string()),
357        String::from_utf8_lossy(&output.stdout),
358        String::from_utf8_lossy(&output.stderr)
359    )
360}
361
362fn ensure_directory(path: &Path) -> Result<(), String> {
363    match fs::symlink_metadata(path) {
364        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
365        Ok(_) => Err(format!(
366            "code-service path is not an ordinary directory: {}",
367            path.display()
368        )),
369        Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(path)
370            .map_err(|error| format!("create code-service directory {}: {error}", path.display())),
371        Err(error) => Err(format!(
372            "inspect code-service directory {}: {error}",
373            path.display()
374        )),
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn fixed_paths_are_beneath_state_root() {
384        let root = tempfile::tempdir().unwrap();
385        let paths = CodePaths::open(root.path()).unwrap();
386        assert_eq!(paths.rust_projection, root.path().join("rust/projection"));
387        assert_eq!(paths.web_cache, root.path().join("web/code-cache"));
388        assert_eq!(
389            paths.checker,
390            root.path().join("code-tools/kcode-k1-web-checker")
391        );
392    }
393
394    #[cfg(unix)]
395    #[test]
396    fn podman_resolver_accepts_only_an_executable_file() {
397        use std::os::unix::fs::PermissionsExt as _;
398
399        let root = tempfile::tempdir().unwrap();
400        let podman = root.path().join("podman");
401        fs::write(&podman, b"#!/bin/sh\nexit 0\n").unwrap();
402        fs::set_permissions(&podman, fs::Permissions::from_mode(0o700)).unwrap();
403        assert_eq!(
404            resolve_executable("podman", [root.path().to_path_buf()]),
405            Some(fs::canonicalize(&podman).unwrap())
406        );
407        fs::set_permissions(&podman, fs::Permissions::from_mode(0o600)).unwrap();
408        assert!(resolve_executable("podman", [root.path().to_path_buf()]).is_none());
409    }
410
411    #[test]
412    fn tool_image_owns_the_full_runtime_without_environment_configuration() {
413        assert!(CONTAINERFILE.contains("rust:1.97-bookworm"));
414        assert!(CONTAINERFILE.contains("chromium"));
415        assert!(CONTAINERFILE.contains("kcode-k1-web-checker --version 0.1.1"));
416        assert!(tool_image().starts_with("localhost/kcode-k1-code-tools:0.1.3-"));
417        assert_eq!(CHECK_DEADLINE, Duration::from_secs(225));
418    }
419}