Skip to main content

snix_build/buildservice/
from_addr.rs

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