kcode_k1_daemon_code_services/
lib.rs1#![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}