Skip to main content

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" => {
40            // dummy wants no authority, path etc.
41            if url.has_authority() {
42                Err(std::io::Error::other("dummy must not have authority"))?
43            }
44            if !url.path().is_empty() {
45                Err(std::io::Error::other("dummy must not have path"))?
46            }
47            Box::<DummyBuildService>::default()
48        }
49        #[cfg(target_os = "linux")]
50        "oci" => {
51            // oci wants no authority component in the URI
52            if url.has_authority() {
53                Err(std::io::Error::other("oci must not have authority"))?
54            }
55            // oci wants a path in which it creates bundles.
56            if url.path().is_empty() {
57                Err(std::io::Error::other("oci needs a bundle dir as path"))?
58            }
59
60            // TODO: make sandbox shell and rootless_uid_gid
61
62            Box::new(OCIBuildService::new(
63                url.path().into(),
64                blob_service,
65                directory_service,
66            ))
67        }
68        #[cfg(target_os = "linux")]
69        "bwrap" => {
70            // bwrap wants no authority component in the URI
71            if url.has_authority() {
72                Err(std::io::Error::other("bwrap must not have authority"))?
73            }
74            // bwrap wants a path in which it creates bundles.
75            if url.path().is_empty() {
76                Err(std::io::Error::other("bwap needs a bundle dir as path"))?
77            }
78
79            Box::new(BubblewrapBuildService::new(
80                url.path().into(),
81                blob_service,
82                directory_service,
83            ))
84        }
85        scheme => {
86            if scheme.starts_with("grpc+") {
87                let client =
88                    crate::proto::build_service_client::BuildServiceClient::with_interceptor(
89                        snix_castore::tonic::channel_from_url(&url)
90                            .await
91                            .map_err(std::io::Error::other)?,
92                        snix_tracing::propagate::tonic::send_trace,
93                    );
94                // FUTUREWORK: also allow responding to {blob,directory}_service
95                // requests from the remote BuildService?
96                Box::new(GRPCBuildService::from_client(client))
97            } else {
98                Err(std::io::Error::other(format!(
99                    "unknown scheme: {}",
100                    url.scheme()
101                )))?
102            }
103        }
104    })
105}
106
107#[cfg(test)]
108mod tests {
109    use super::from_addr;
110    use rstest::rstest;
111    use snix_castore::blobservice::{BlobService, MemoryBlobService};
112    use std::sync::Arc;
113    #[cfg(target_os = "linux")]
114    use std::sync::LazyLock;
115    #[cfg(target_os = "linux")]
116    use tempfile::TempDir;
117
118    #[cfg(target_os = "linux")]
119    static TMPDIR_OCI_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
120    #[cfg(target_os = "linux")]
121    static TMPDIR_OCI_2: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
122    #[cfg(target_os = "linux")]
123    static TMPDIR_BWRAP_1: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
124    #[cfg(target_os = "linux")]
125    static TMPDIR_BWRAP_2: LazyLock<TempDir> = LazyLock::new(|| TempDir::new().unwrap());
126
127    #[rstest]
128    /// This uses an unsupported scheme.
129    #[case::unsupported_scheme("http://foo.example/test", false)]
130    /// This configures dummy
131    #[case::valid_dummy("dummy:", true)]
132    /// This configures dummy, but with authority, which is wrong.
133    #[case::invalid_dummy_authority("dummy://", false)]
134    /// Correct scheme to connect to a unix socket.
135    #[case::grpc_valid_unix_socket("grpc+unix:/path/to/somewhere", true)]
136    /// unix socket, but with authority.
137    #[case::grpc_invalid_unix_socket_authority("grpc+unix:///path/to/somewhere", false)]
138    /// Correct scheme for unix socket, but setting a host too, which is invalid.
139    #[case::grpc_invalid_unix_socket_and_host("grpc+unix://host.example/path/to/somewhere", false)]
140    /// Correct scheme to connect to localhost, with port 12345
141    #[case::grpc_valid_ipv6_localhost_port_12345("grpc+http://[::1]:12345", true)]
142    /// Correct scheme to connect to localhost over http, without specifying a port.
143    #[case::grpc_valid_http_host_without_port("grpc+http://localhost", true)]
144    /// Correct scheme to connect to localhost over http, without specifying a port.
145    #[case::grpc_valid_https_host_without_port("grpc+https://localhost", true)]
146    /// Correct scheme to connect to localhost over http, but with additional path, which is invalid.
147    #[case::grpc_invalid_host_and_path("grpc+http://localhost/some-path", false)]
148    /// This configures OCI, but doesn't specify the bundle path
149    #[cfg_attr(target_os = "linux", case::oci_missing_bundle_dir("oci:", false))]
150    /// This configures OCI, specifying the bundle path
151    #[cfg_attr(target_os = "linux", case::oci_bundle_path(&format!("oci:{}", TMPDIR_OCI_1.path().to_str().unwrap()), true))]
152    /// oci, but with authority.
153    #[cfg_attr(
154        target_os = "linux",
155        case::oci_bundle_path_authority(&format!("oci://{}", TMPDIR_OCI_2.path().to_str().unwrap()), false)
156    )]
157    /// This configures bwrap, but doesn't specify the bundle path
158    #[cfg_attr(target_os = "linux", case::bwrap_missing_bundle_dir("bwrap:", false))]
159    /// This configures bwrap, specifying the bundle path
160    #[cfg_attr(target_os = "linux", case::bwrap_bundle_path(&format!("bwrap:{}", TMPDIR_BWRAP_1.path().to_str().unwrap()), true))]
161    /// bwrap, but with authority.
162    #[cfg_attr(
163        target_os = "linux",
164        case::bwrap_bundle_path_authority(&format!("bwrap://{}", TMPDIR_BWRAP_2.path().to_str().unwrap()), false)
165    )]
166    #[tokio::test]
167    async fn test_from_addr(#[case] uri_str: &str, #[case] exp_succeed: bool) {
168        let blob_service: Arc<dyn BlobService> = Arc::from(MemoryBlobService::default());
169        let directory_service = snix_castore::utils::gen_test_directory_service();
170
171        let resp = from_addr(uri_str, blob_service, directory_service).await;
172
173        if exp_succeed {
174            resp.expect("should succeed");
175        } else {
176            assert!(resp.is_err(), "should fail");
177        }
178    }
179}