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