snix_build/buildservice/
from_addr.rs

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