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