Skip to main content

kcode_k1_daemon_code_services/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2
3use kcode_k1_code_tool_image::CodeToolImage;
4use kcode_k1_groups::K1Groups;
5use kcode_k1_objects::K1Objects;
6use kcode_k1_peering::K1Peering;
7use kcode_k1_rust_code_ktool_service::RustCodeKtoolService;
8use kcode_k1_rust_coding::{RustCodingConfig, RustCodingConfigValues};
9use kcode_k1_rust_projection::K1RustProjection;
10use kcode_k1_txn_ordering::K1TxnOrdering;
11use kcode_k1_web_code_ktool_service::{K1WebCodeKtoolService, ServiceConfig, ServiceRevisions};
12use kcode_k1_web_code_workspace::K1WebCodeWorkspace;
13use kcode_k1_web_podman::{WebPodman, WebPodmanConfig};
14use kcode_k1_web_projection::K1WebProjection;
15use std::ffi::OsString;
16use std::fs;
17use std::path::{Path, PathBuf};
18use std::process::{Command, Output};
19use std::sync::Arc;
20use std::time::Duration;
21
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-v2";
25const RUST_COMMAND_POLICY: &str = "k1-rust-code-command-v2";
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_projection: Arc<K1RustProjection>,
35    rust: Arc<RustCodeKtoolService>,
36    web: K1WebCodeKtoolService,
37}
38
39impl CodeServices {
40    #[allow(clippy::too_many_arguments)]
41    pub fn open(
42        state_root: &Path,
43        ordering: Arc<K1TxnOrdering>,
44        peering: Arc<K1Peering>,
45        groups: Arc<K1Groups>,
46        objects: Arc<K1Objects>,
47        web_projection: Arc<K1WebProjection>,
48    ) -> Result<Self, String> {
49        let paths = CodePaths::open(state_root)?;
50        let tools = CodeToolImage::open(&paths.tool_build)?;
51        let checker = materialize_checker(tools.podman(), tools.image(), &paths.checker)?;
52        let (chromium, chromium_version) = discover_chromium(tools.podman(), tools.image())?;
53        let rust_config = rust_config(
54            tools.podman().as_os_str().to_owned(),
55            tools.image().to_owned(),
56        )?;
57        let web_config = WebPodmanConfig {
58            podman: tools.podman().to_path_buf(),
59            image: tools.image().to_owned(),
60            checker,
61            chromium,
62            chromium_version,
63            cpu_millis: 0,
64            memory_bytes: 0,
65            pids_limit: 0,
66            tmpfs_bytes: 0,
67            shm_bytes: 0,
68            checker_timeout: CHECK_DEADLINE,
69            wall_timeout: CHECK_DEADLINE,
70        };
71        WebPodman::new(web_config.clone())
72            .map_err(|error| format!("Web Podman configuration: {error}"))?;
73
74        let workspaces = Arc::new(
75            K1WebCodeWorkspace::open(Arc::clone(&ordering), Arc::clone(&peering))
76                .map_err(|error| format!("open Web code workspaces: {error}"))?,
77        );
78        let rust_projection =
79            K1RustProjection::open(paths.rust_projection, paths.rust_control, ordering, peering)
80                .map(Arc::new)
81                .map_err(|error| format!("open Rust projection: {error}"))?;
82        let rust = Arc::new(RustCodeKtoolService::new(
83            paths.rust_cache,
84            rust_config,
85            Arc::clone(&rust_projection),
86            Arc::clone(&objects),
87            Arc::clone(&groups),
88        ));
89        let web = K1WebCodeKtoolService::new(
90            ServiceConfig::new(
91                paths.web_cache,
92                paths.web_projection,
93                web_revisions(),
94                web_config,
95            ),
96            groups,
97            objects,
98            web_projection,
99            workspaces,
100        );
101        Ok(Self {
102            rust_projection,
103            rust,
104            web,
105        })
106    }
107
108    pub fn rust_projection(&self) -> Arc<K1RustProjection> {
109        Arc::clone(&self.rust_projection)
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 materialize_checker(podman: &Path, image: &str, destination: &Path) -> Result<PathBuf, String> {
158    let mut command = Command::new(podman);
159    command
160        .arg("run")
161        .arg("--rm")
162        .arg("--network=none")
163        .arg("--pull=never")
164        .arg("--entrypoint=/bin/cat")
165        .arg(image)
166        .arg("/usr/local/cargo/bin/kcode-k1-web-checker");
167    let output = run(command, "extract K1 Web checker")?;
168    if !output.status.success() {
169        return Err(command_failure("extract K1 Web checker", output));
170    }
171    if output.stdout.is_empty() {
172        return Err("extract K1 Web checker: image returned an empty executable".to_owned());
173    }
174    let staging = destination.with_extension("new");
175    fs::write(&staging, output.stdout)
176        .map_err(|error| format!("write Web checker {}: {error}", staging.display()))?;
177    set_executable(&staging)?;
178    fs::rename(&staging, destination)
179        .map_err(|error| format!("install Web checker {}: {error}", destination.display()))?;
180    fs::canonicalize(destination).map_err(|error| {
181        format!(
182            "canonicalize Web checker {}: {error}",
183            destination.display()
184        )
185    })
186}
187
188#[cfg(unix)]
189fn set_executable(path: &Path) -> Result<(), String> {
190    use std::os::unix::fs::PermissionsExt as _;
191    fs::set_permissions(path, fs::Permissions::from_mode(0o755))
192        .map_err(|error| format!("make Web checker executable {}: {error}", path.display()))
193}
194
195#[cfg(not(unix))]
196fn set_executable(_path: &Path) -> Result<(), String> {
197    Ok(())
198}
199
200fn discover_chromium(podman: &Path, image: &str) -> Result<(PathBuf, String), String> {
201    let mut command = Command::new(podman);
202    command
203        .arg("run")
204        .arg("--rm")
205        .arg("--network=none")
206        .arg("--pull=never")
207        .arg("--entrypoint=/bin/sh")
208        .arg(image)
209        .arg("-c")
210        .arg("command -v chromium; chromium --version");
211    let output = run(command, "discover Chromium in K1 code tool image")?;
212    if !output.status.success() {
213        return Err(command_failure(
214            "discover Chromium in K1 code tool image",
215            output,
216        ));
217    }
218    let text = String::from_utf8(output.stdout)
219        .map_err(|_| "discover Chromium: image output was not UTF-8".to_owned())?;
220    let mut lines = text.lines().filter(|line| !line.trim().is_empty());
221    let path = PathBuf::from(
222        lines
223            .next()
224            .ok_or_else(|| "discover Chromium: executable path was absent".to_owned())?,
225    );
226    let version = lines
227        .next()
228        .ok_or_else(|| "discover Chromium: version was absent".to_owned())?
229        .to_owned();
230    if !path.is_absolute() {
231        return Err("discover Chromium: executable path was not absolute".to_owned());
232    }
233    Ok((path, version))
234}
235
236fn rust_config(podman_program: OsString, image: String) -> Result<RustCodingConfig, String> {
237    RustCodingConfig::new(RustCodingConfigValues {
238        schema_id: RUST_SCHEMA.into(),
239        toolchain_policy: RUST_TOOLCHAIN_POLICY.into(),
240        image,
241        rust_toolchain: "1.97".into(),
242        check_policy: RUST_CHECK_POLICY.into(),
243        target_triple: rust_target()?.into(),
244        command_policy: RUST_COMMAND_POLICY.into(),
245        podman_program,
246    })
247    .map_err(|error| format!("Rust coding configuration: {error}"))
248}
249
250fn rust_target() -> Result<&'static str, String> {
251    match std::env::consts::ARCH {
252        "x86_64" => Ok("x86_64-unknown-linux-gnu"),
253        "aarch64" => Ok("aarch64-unknown-linux-gnu"),
254        architecture => Err(format!(
255            "unsupported Rust code host architecture: {architecture}"
256        )),
257    }
258}
259
260fn web_revisions() -> ServiceRevisions {
261    ServiceRevisions {
262        boot: WEB_BOOT.into(),
263        schema: WEB_SCHEMA.into(),
264        route: WEB_ROUTE.into(),
265        harness: WEB_HARNESS.into(),
266        check_policy: WEB_CHECK_POLICY.into(),
267    }
268}
269
270fn run(mut command: Command, label: &str) -> Result<Output, String> {
271    command
272        .output()
273        .map_err(|error| format!("{label}: could not start command: {error}"))
274}
275
276fn command_failure(label: &str, output: Output) -> String {
277    format!(
278        "{label} exited with {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
279        output
280            .status
281            .code()
282            .map_or_else(|| "signal".to_owned(), |code| code.to_string()),
283        String::from_utf8_lossy(&output.stdout),
284        String::from_utf8_lossy(&output.stderr)
285    )
286}
287
288fn ensure_directory(path: &Path) -> Result<(), String> {
289    match fs::symlink_metadata(path) {
290        Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => Ok(()),
291        Ok(_) => Err(format!(
292            "code-service path is not an ordinary directory: {}",
293            path.display()
294        )),
295        Err(error) if error.kind() == std::io::ErrorKind::NotFound => fs::create_dir(path)
296            .map_err(|error| format!("create code-service directory {}: {error}", path.display())),
297        Err(error) => Err(format!(
298            "inspect code-service directory {}: {error}",
299            path.display()
300        )),
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn fixed_paths_are_beneath_state_root() {
310        let root = tempfile::tempdir().unwrap();
311        let paths = CodePaths::open(root.path()).unwrap();
312        assert_eq!(paths.rust_projection, root.path().join("rust/projection"));
313        assert_eq!(paths.web_cache, root.path().join("web/code-cache"));
314        assert_eq!(
315            paths.checker,
316            root.path().join("code-tools/kcode-k1-web-checker")
317        );
318        assert_eq!(paths.tool_build, root.path().join("code-tools/image-build"));
319    }
320
321    #[test]
322    fn package_owns_only_service_policy() {
323        assert_eq!(CHECK_DEADLINE, Duration::from_secs(225));
324        assert_eq!(RUST_TOOLCHAIN_POLICY, "rust-1.97-k1-code-tools-v1");
325        assert_eq!(RUST_CHECK_POLICY, "k1-rust-code-check-v2");
326        assert_eq!(RUST_COMMAND_POLICY, "k1-rust-code-command-v2");
327    }
328}