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