snix_build/buildservice/
from_addr.rs

1#[cfg(target_os = "linux")]
2use crate::buildservice::bwrap::BubblewrapBuildService;
3
4use super::{BuildService, DummyBuildService, grpc::GRPCBuildService};
5use snix_castore::{blobservice::BlobService, directoryservice::DirectoryService};
6use url::Url;
7
8#[cfg(target_os = "linux")]
9use super::oci::OCIBuildService;
10#[cfg(all(not(target_os = "linux"), doc))]
11struct OCIBuildService;
12#[cfg(all(not(target_os = "linux"), doc))]
13struct BubblewrapBuildService;
14
15/// Constructs a new instance of a [BuildService] from an URI.
16///
17/// The following schemes are supported by the following services:
18/// - `dummy://` ([DummyBuildService])
19/// - `oci://` ([OCIBuildService])
20/// - `grpc+*://` ([GRPCBuildService])
21/// - `bwrap://` ([BubblewrapBuildService])
22///
23/// As some of these [BuildService] need to talk to a [BlobService] and
24/// [DirectoryService], these also need to be passed in.
25#[cfg_attr(target_os = "macos", allow(unused_variables))]
26pub async fn from_addr<BS, DS>(
27    uri: &str,
28    blob_service: BS,
29    directory_service: DS,
30) -> std::io::Result<Box<dyn BuildService>>
31where
32    BS: BlobService + Send + Sync + Clone + 'static,
33    DS: DirectoryService + Send + Sync + Clone + 'static,
34{
35    let url =
36        Url::parse(uri).map_err(|e| std::io::Error::other(format!("unable to parse url: {e}")))?;
37
38    Ok(match url.scheme() {
39        // dummy doesn't care about parameters.
40        "dummy" => Box::<DummyBuildService>::default(),
41        #[cfg(target_os = "linux")]
42        "oci" => {
43            // oci wants a path in which it creates bundles.
44            if url.path().is_empty() {
45                Err(std::io::Error::other("oci needs a bundle dir as path"))?
46            }
47
48            // TODO: make sandbox shell and rootless_uid_gid
49
50            Box::new(OCIBuildService::new(
51                url.path().into(),
52                blob_service,
53                directory_service,
54            ))
55        }
56        #[cfg(target_os = "linux")]
57        "bwrap" => {
58            // bwrap wants a path in which it creates bundles.
59            if url.path().is_empty() {
60                Err(std::io::Error::other("bwap needs a bundle dir as path"))?
61            }
62
63            Box::new(BubblewrapBuildService::new(
64                url.path().into(),
65                blob_service,
66                directory_service,
67            ))
68        }
69        scheme => {
70            if scheme.starts_with("grpc+") {
71                let client = crate::proto::build_service_client::BuildServiceClient::new(
72                    snix_castore::tonic::channel_from_url(&url)
73                        .await
74                        .map_err(std::io::Error::other)?,
75                );
76                // FUTUREWORK: also allow responding to {blob,directory}_service
77                // requests from the remote BuildService?
78                Box::new(GRPCBuildService::from_client(client))
79            } else {
80                Err(std::io::Error::other(format!(
81                    "unknown scheme: {}",
82                    url.scheme()
83                )))?
84            }
85        }
86    })
87}
88
89#[cfg(test)]
90mod tests {
91    use super::from_addr;
92    use rstest::rstest;
93    use snix_castore::blobservice::{BlobService, MemoryBlobService};
94    use std::sync::Arc;
95    #[cfg(target_os = "linux")]
96    use std::sync::LazyLock;
97    #[cfg(target_os = "linux")]
98    use tempfile::TempDir;
99
100    #[cfg(target_os = "linux")]
101    static TMPDIR_OCI_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
102
103    #[rstest]
104    /// This uses an unsupported scheme.
105    #[case::unsupported_scheme("http://foo.example/test", false)]
106    /// This configures dummy
107    #[case::valid_dummy("dummy://", true)]
108    /// Correct scheme to connect to a unix socket.
109    #[case::grpc_valid_unix_socket("grpc+unix:/path/to/somewhere", true)]
110    /// Correct scheme for unix socket, but setting a host too, which is invalid.
111    #[case::grpc_invalid_unix_socket_and_host("grpc+unix://host.example/path/to/somewhere", false)]
112    /// Correct scheme to connect to localhost, with port 12345
113    #[case::grpc_valid_ipv6_localhost_port_12345("grpc+http://[::1]:12345", true)]
114    /// Correct scheme to connect to localhost over http, without specifying a port.
115    #[case::grpc_valid_http_host_without_port("grpc+http://localhost", true)]
116    /// Correct scheme to connect to localhost over http, without specifying a port.
117    #[case::grpc_valid_https_host_without_port("grpc+https://localhost", true)]
118    /// Correct scheme to connect to localhost over http, but with additional path, which is invalid.
119    #[case::grpc_invalid_host_and_path("grpc+http://localhost/some-path", false)]
120    /// This configures OCI, but doesn't specify the bundle path
121    #[cfg_attr(target_os = "linux", case::oci_missing_bundle_dir("oci://", false))]
122    /// This configures OCI, specifying the bundle path
123    #[cfg_attr(target_os = "linux", case::oci_bundle_path(&format!("oci://{}", TMPDIR_OCI_1.path().to_str().unwrap()), true))]
124    #[tokio::test]
125    async fn test_from_addr(#[case] uri_str: &str, #[case] exp_succeed: bool) {
126        let blob_service: Arc<dyn BlobService> = Arc::from(MemoryBlobService::default());
127        let directory_service = snix_castore::utils::gen_test_directory_service();
128
129        let resp = from_addr(uri_str, blob_service, directory_service).await;
130
131        if exp_succeed {
132            resp.expect("should succeed");
133        } else {
134            assert!(resp.is_err(), "should fail");
135        }
136    }
137}