snix_build/buildservice/
oci.rs1use anyhow::Context;
2use bstr::BStr;
3use snix_castore::{
4 blobservice::BlobService,
5 directoryservice::DirectoryService,
6 fs::fuse::FuseDaemon,
7 import::fs::ingest_path,
8 refscan::{ReferencePattern, ReferenceScanner},
9};
10use tokio::process::{Child, Command};
11use tonic::async_trait;
12use tracing::{Span, debug, instrument, warn};
13use uuid::Uuid;
14
15use crate::buildservice::{BuildOutput, BuildRequest, BuildResult};
16use crate::oci::{get_host_output_paths, make_bundle, make_spec};
17use std::{ffi::OsStr, path::PathBuf, process::Stdio};
18
19use super::BuildService;
20
21const SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");
22const MAX_CONCURRENT_BUILDS: usize = 2; pub struct OCIBuildService<BS, DS> {
25 bundle_root: PathBuf,
27
28 blob_service: BS,
30 directory_service: DS,
32
33 concurrent_builds: tokio::sync::Semaphore,
36}
37
38impl<BS, DS> OCIBuildService<BS, DS> {
39 pub fn new(bundle_root: PathBuf, blob_service: BS, directory_service: DS) -> Self {
40 Self {
45 bundle_root,
46 blob_service,
47 directory_service,
48 concurrent_builds: tokio::sync::Semaphore::new(MAX_CONCURRENT_BUILDS),
49 }
50 }
51}
52
53#[async_trait]
54impl<BS, DS> BuildService for OCIBuildService<BS, DS>
55where
56 BS: BlobService + Clone + 'static,
57 DS: DirectoryService + Clone + 'static,
58{
59 #[instrument(skip_all, err)]
60 async fn do_build(&self, request: BuildRequest) -> std::io::Result<BuildResult> {
61 let _permit = self.concurrent_builds.acquire().await.unwrap();
62
63 let bundle_name = Uuid::new_v4();
64 let bundle_path = self.bundle_root.join(bundle_name.to_string());
65
66 let span = Span::current();
67 span.record("bundle_name", bundle_name.to_string());
68
69 let mut runtime_spec = make_spec(&request, true, SANDBOX_SHELL)
70 .context("failed to create spec")
71 .map_err(std::io::Error::other)?;
72
73 let linux = runtime_spec.linux().clone().unwrap();
74
75 runtime_spec.set_linux(Some(linux));
76
77 make_bundle(&request, &runtime_spec, &bundle_path)
78 .context("failed to produce bundle")
79 .map_err(std::io::Error::other)?;
80
81 let host_output_paths = get_host_output_paths(&request, &bundle_path)
85 .context("failed to calculate host output paths")
86 .map_err(std::io::Error::other)?;
87
88 let patterns = ReferencePattern::new(request.refscan_needles);
90 let _fuse_daemon = tokio::task::spawn_blocking({
92 let blob_service = self.blob_service.clone();
93 let directory_service = self.directory_service.clone();
94
95 let dest = bundle_path.join("inputs");
96
97 let root_nodes = Box::new(request.inputs);
98 move || {
99 let fs = snix_castore::fs::SnixStoreFs::new(
100 blob_service,
101 directory_service,
102 root_nodes,
103 snix_castore::fs::FSSettings {
104 list_root: true,
105 uid_gid_override: None,
106 show_xattr: false,
107 },
108 tokio::runtime::Handle::current(),
109 );
110 FuseDaemon::new(fs, dest, 4, true).context("failed to start fuse daemon")
113 }
114 })
115 .await?
116 .context("mounting")
117 .map_err(std::io::Error::other)?;
118
119 debug!(bundle.path=?bundle_path, bundle.name=%bundle_name, "about to spawn bundle");
120
121 let child = spawn_bundle(bundle_path, &bundle_name.to_string())?;
123
124 let child_output = child
127 .wait_with_output()
128 .await
129 .context("failed to run process")
130 .map_err(std::io::Error::other)?;
131
132 if !child_output.status.success() {
134 let stdout = BStr::new(&child_output.stdout);
135 let stderr = BStr::new(&child_output.stderr);
136
137 warn!(stdout=%stdout, stderr=%stderr, exit_code=%child_output.status, "build failed");
138
139 return Err(std::io::Error::other("nonzero exit code".to_string()));
140 }
141
142 let outputs = futures::future::try_join_all(host_output_paths.into_iter().enumerate().map(
146 |(i, host_output_path)| {
147 let output_path = &request.outputs[i];
148 let patterns = patterns.clone();
149 async move {
150 debug!(host.path=?host_output_path, output.path=?output_path, "ingesting path");
151
152 let scanner = ReferenceScanner::new(patterns);
153
154 Ok::<_, std::io::Error>(BuildOutput {
155 node: ingest_path(
156 self.blob_service.clone(),
157 &self.directory_service,
158 host_output_path,
159 Some(&scanner),
160 )
161 .await
162 .map_err(|e| {
163 std::io::Error::new(
164 std::io::ErrorKind::InvalidData,
165 format!("Unable to ingest output: {e}"),
166 )
167 })?,
168
169 output_needles: scanner
170 .matches()
171 .into_iter()
172 .enumerate()
173 .filter(|(_, val)| *val)
174 .map(|(idx, _)| idx as u64)
175 .collect(),
176 })
177 }
178 },
179 ))
180 .await?;
181
182 Ok(BuildResult { outputs })
183 }
184}
185
186#[instrument(err)]
189fn spawn_bundle(
190 bundle_path: impl AsRef<OsStr> + std::fmt::Debug,
191 bundle_name: &str,
192) -> std::io::Result<Child> {
193 let mut command = Command::new("runc");
194
195 command
196 .args(&[
197 "run".into(),
198 "--bundle".into(),
199 bundle_path.as_ref().to_os_string(),
200 bundle_name.into(),
201 ])
202 .stderr(Stdio::piped())
203 .stdout(Stdio::piped())
204 .stdin(Stdio::null());
205
206 command.spawn()
207}