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