Skip to main content

snix_build/buildservice/
oci.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::Stdio,
5};
6
7use anyhow::Context;
8use futures::stream::BoxStream;
9use snix_castore::{
10    blobservice::BlobService, directoryservice::DirectoryService, fs::fuse::FuseDaemon,
11};
12use tokio::process::{Child, Command};
13use tracing::{Span, debug, instrument};
14use uuid::Uuid;
15
16use super::{BuildFailure, BuildService, BuildUpdate};
17use crate::sandbox::event::{SandboxEvent, stream_process};
18use crate::{buildservice::BuildRequest, oci::OciOutputs};
19use crate::{
20    buildservice::streaming::run_build_streaming,
21    oci::{make_bundle, make_spec},
22};
23
24const SANDBOX_SHELL: &str = env!("SNIX_BUILD_SANDBOX_SHELL");
25const MAX_CONCURRENT_BUILDS: usize = 2; // TODO: make configurable
26
27pub struct OCIBuildService<BS, DS> {
28    /// Root path in which all bundles are created in
29    bundle_root: PathBuf,
30
31    /// Handle to a [BlobService], used by filesystems spawned during builds.
32    blob_service: BS,
33    /// Handle to a [DirectoryService], used by filesystems spawned during builds.
34    directory_service: DS,
35
36    // semaphore to track number of concurrently running builds.
37    // this is necessary, as otherwise we very quickly run out of open file handles.
38    concurrent_builds: tokio::sync::Semaphore,
39}
40
41impl<BS, DS> OCIBuildService<BS, DS> {
42    pub fn new(bundle_root: PathBuf, blob_service: BS, directory_service: DS) -> Self {
43        // We map root inside the container to the uid/gid this is running at,
44        // and allocate one for uid 1000 into the container from the range we
45        // got in /etc/sub{u,g}id.
46        // FUTUREWORK: use different uids?
47        Self {
48            bundle_root,
49            blob_service,
50            directory_service,
51            concurrent_builds: tokio::sync::Semaphore::new(MAX_CONCURRENT_BUILDS),
52        }
53    }
54}
55
56impl<BS, DS> OCIBuildService<BS, DS>
57where
58    BS: BlobService + Clone + 'static,
59    DS: DirectoryService + Clone + 'static,
60{
61    /// Assembles an OCI container environment and prepares its execution
62    ///
63    /// Returns a stream which emits events as the build progresses and a SandboxOutputs handle
64    /// which can be used by callers to access build output paths relative to the logical root of
65    /// the sandbox. SandboxOutputs.find_paths are only guaranteed to succeed after the build has
66    /// finished, in other words the stream has ended.
67    async fn spawn_bundle_process(
68        &self,
69        request: &BuildRequest,
70        bundle_path: &Path,
71        build_name: &str,
72    ) -> Result<(BoxStream<'static, SandboxEvent>, OciOutputs), BuildFailure> {
73        let outputs = OciOutputs::new(bundle_path, &request.scratch_paths)?;
74        let mut runtime_spec =
75            make_spec(request, true, SANDBOX_SHELL).context("failed to create spec")?;
76
77        let linux = runtime_spec.linux().clone().unwrap();
78        runtime_spec.set_linux(Some(linux));
79
80        make_bundle(request, &runtime_spec, bundle_path).context("failed to produce bundle")?;
81
82        let blob_service = self.blob_service.clone();
83        let directory_service = self.directory_service.clone();
84        let dest = bundle_path.join("inputs");
85        let root_nodes = Box::new(request.inputs.clone());
86
87        let fuse_daemon = tokio::task::spawn_blocking(move || {
88            let fs = snix_castore::fs::SnixStoreFs::new(
89                blob_service,
90                directory_service,
91                root_nodes,
92                snix_castore::fs::FSSettings {
93                    list_root: true,
94                    uid_gid_override: None,
95                    show_xattr: false,
96                },
97                tokio::runtime::Handle::current(),
98            );
99            FuseDaemon::new(fs, dest, 4, true).context("failed to start fuse daemon")
100        })
101        .await
102        .map_err(|e| BuildFailure::Other {
103            message: e.to_string(),
104        })??;
105
106        debug!(bundle.path=?bundle_path, "about to spawn bundle");
107
108        let child = spawn_bundle(bundle_path, build_name)?;
109        let stream = stream_process(child, fuse_daemon)?;
110        Ok((stream, outputs))
111    }
112}
113
114impl<BS, DS> BuildService for OCIBuildService<BS, DS>
115where
116    BS: BlobService + Clone + 'static,
117    DS: DirectoryService + Clone + 'static,
118{
119    #[instrument(skip_all, fields(build.name=tracing::field::Empty))]
120    fn do_build(&self, request: BuildRequest) -> BoxStream<'_, BuildUpdate> {
121        let span = Span::current();
122        let build_name = Uuid::new_v4();
123        let bundle_path = self.bundle_root.join(build_name.to_string());
124        span.record("build.name", build_name.to_string());
125
126        run_build_streaming(
127            &self.concurrent_builds,
128            self.blob_service.clone(),
129            self.directory_service.clone(),
130            request.outputs.clone(),
131            request.refscan_needles.clone(),
132            move || async move {
133                self.spawn_bundle_process(&request, &bundle_path, &build_name.to_string())
134                    .await
135            },
136        )
137    }
138}
139
140/// Spawns runc with the bundle at bundle_path.
141/// On success, returns the child.
142#[instrument(err)]
143fn spawn_bundle(
144    bundle_path: impl AsRef<OsStr> + std::fmt::Debug,
145    bundle_name: &str,
146) -> std::io::Result<Child> {
147    let mut command = Command::new("runc");
148
149    command
150        .args(&[
151            "run".into(),
152            "--bundle".into(),
153            bundle_path.as_ref().to_os_string(),
154            bundle_name.into(),
155        ])
156        .stderr(Stdio::piped())
157        .stdout(Stdio::piped())
158        .stdin(Stdio::null())
159        .kill_on_drop(true);
160
161    command.spawn()
162}