wasmcloud_host/
lib.rs

1//! wasmCloud host library
2
3#![warn(missing_docs)]
4#![forbid(clippy::unwrap_used)]
5
6/// wasmbus host
7pub mod wasmbus;
8
9/// OCI artifact fetching
10pub mod oci;
11
12/// wasmCloud policy service
13pub mod policy;
14
15/// Common registry types
16pub mod registry;
17
18/// Secret management
19pub mod secrets;
20
21/// wasmCloud host metrics
22pub(crate) mod metrics;
23
24/// experimental workload identity
25pub mod workload_identity;
26
27pub use metrics::HostMetrics;
28pub use oci::Config as OciConfig;
29pub use policy::{
30    HostInfo as PolicyHostInfo, Manager as PolicyManager, Response as PolicyResponse,
31};
32pub use secrets::Manager as SecretsManager;
33pub use wasmbus::{Host as WasmbusHost, HostConfig as WasmbusHostConfig};
34pub use wasmcloud_core::{OciFetcher, RegistryAuth, RegistryConfig, RegistryType};
35
36pub use url;
37
38use std::collections::HashMap;
39use std::path::PathBuf;
40
41use anyhow::{anyhow, bail, ensure, Context as _};
42use tokio::fs;
43use tracing::{debug, instrument, warn};
44use url::Url;
45use wascap::jwt;
46
47/// A reference to a resource, either a file, an OCI image, or a builtin provider
48#[derive(PartialEq)]
49pub enum ResourceRef<'a> {
50    /// A file reference
51    File(PathBuf),
52    /// An OCI reference
53    Oci(&'a str),
54    /// A builtin provider reference
55    Builtin(&'a str),
56}
57
58impl AsRef<str> for ResourceRef<'_> {
59    fn as_ref(&self) -> &str {
60        match self {
61            // Resource ref must have originated from a URL, which can only be constructed from a
62            // valid string
63            ResourceRef::File(path) => path.to_str().expect("invalid file reference URL"),
64            ResourceRef::Oci(s) => s,
65            ResourceRef::Builtin(s) => s,
66        }
67    }
68}
69
70impl<'a> TryFrom<&'a str> for ResourceRef<'a> {
71    type Error = anyhow::Error;
72
73    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
74        match Url::parse(s) {
75            Ok(url) => {
76                match url.scheme() {
77                    "file" => url
78                        .to_file_path()
79                        .map(Self::File)
80                        .map_err(|()| anyhow!("failed to convert `{url}` to a file path")),
81                    "oci" => {
82                        // Note: oci is not a scheme, but using this as a prefix takes out the guesswork
83                        s.strip_prefix("oci://")
84                            .map(Self::Oci)
85                            .context("invalid OCI reference")
86                    }
87                    "wasmcloud+builtin" => s
88                        .strip_prefix("wasmcloud+builtin://")
89                        .map(Self::Builtin)
90                        .context("invalid builtin reference"),
91                    scheme @ ("http" | "https") => {
92                        debug!(%url, "interpreting reference as OCI");
93                        s.strip_prefix(&format!("{scheme}://"))
94                            .map(Self::Oci)
95                            .context("invalid OCI reference")
96                    }
97                    _ => {
98                        // handle strings like `registry:5000/v2/foo:0.1.0`
99                        debug!(%url, "unknown scheme in reference, assuming OCI");
100                        Ok(Self::Oci(s))
101                    }
102                }
103            }
104            Err(url::ParseError::RelativeUrlWithoutBase) => {
105                match Url::parse(&format!("oci://{s}")) {
106                    Ok(_url) => Ok(Self::Oci(s)),
107                    Err(e) => Err(anyhow!(e).context("failed to parse reference as OCI reference")),
108                }
109            }
110            Err(e) => {
111                bail!(anyhow!(e).context(format!("failed to parse reference `{s}`")))
112            }
113        }
114    }
115}
116
117impl ResourceRef<'_> {
118    fn authority(&self) -> Option<&str> {
119        match self {
120            ResourceRef::File(_) => None,
121            ResourceRef::Oci(s) => {
122                let (l, _) = s.split_once('/')?;
123                Some(l)
124            }
125            ResourceRef::Builtin(_) => None,
126        }
127    }
128}
129
130/// Fetch an component from a reference.
131#[instrument(level = "debug", skip(default_config, registry_config))]
132pub async fn fetch_component(
133    component_ref: &str,
134    allow_file_load: bool,
135    default_config: &oci::Config,
136    registry_config: &HashMap<String, RegistryConfig>,
137) -> anyhow::Result<Vec<u8>> {
138    match ResourceRef::try_from(component_ref)? {
139        ResourceRef::File(component_ref) => {
140            ensure!(
141                allow_file_load,
142                "unable to start component from file, file loading is disabled"
143            );
144            fs::read(component_ref)
145                .await
146                .context("failed to read component")
147        }
148        ref oci_ref @ ResourceRef::Oci(component_ref) => oci_ref
149            .authority()
150            .and_then(|authority| registry_config.get(authority))
151            .map(OciFetcher::from)
152            .unwrap_or_else(|| {
153                OciFetcher::from(
154                    RegistryConfig::builder()
155                        .reg_type(RegistryType::Oci)
156                        .additional_ca_paths(default_config.additional_ca_paths.clone())
157                        .allow_latest(default_config.allow_latest)
158                        .allow_insecure(
159                            oci_ref
160                                .authority()
161                                .map(|authority| {
162                                    default_config
163                                        .allowed_insecure
164                                        .contains(&authority.to_string())
165                                })
166                                .unwrap_or(false),
167                        )
168                        .auth(RegistryAuth::Anonymous)
169                        .build()
170                        .unwrap_or_default(),
171                )
172            })
173            .with_additional_ca_paths(&default_config.additional_ca_paths)
174            .fetch_component(component_ref)
175            .await
176            .with_context(|| {
177                format!("failed to fetch component under OCI reference `{component_ref}`")
178            }),
179        ResourceRef::Builtin(..) => bail!("nothing to fetch for a builtin"),
180    }
181}
182
183/// Fetch a provider from a reference.
184#[instrument(skip(registry_config, host_id), fields(provider_ref = %provider_ref.as_ref()))]
185pub(crate) async fn fetch_provider(
186    provider_ref: &ResourceRef<'_>,
187    host_id: impl AsRef<str>,
188    allow_file_load: bool,
189    default_config: &oci::Config,
190    registry_config: &HashMap<String, RegistryConfig>,
191) -> anyhow::Result<(PathBuf, Option<jwt::Token<jwt::CapabilityProvider>>)> {
192    match provider_ref {
193        ResourceRef::File(provider_path) => {
194            ensure!(
195                allow_file_load,
196                "unable to start provider from file, file loading is disabled"
197            );
198            wasmcloud_core::par::read(
199                provider_path,
200                host_id,
201                provider_ref,
202                wasmcloud_core::par::UseParFileCache::Ignore,
203            )
204            .await
205            .context("failed to read provider")
206        }
207        oci_ref @ ResourceRef::Oci(provider_ref) => oci_ref
208            .authority()
209            .and_then(|authority| registry_config.get(authority))
210            .map(OciFetcher::from)
211            .unwrap_or_else(|| {
212                OciFetcher::from(
213                    RegistryConfig::builder()
214                        .reg_type(RegistryType::Oci)
215                        .additional_ca_paths(default_config.additional_ca_paths.clone())
216                        .allow_latest(default_config.allow_latest)
217                        .allow_insecure(
218                            oci_ref
219                                .authority()
220                                .map(|authority| {
221                                    default_config
222                                        .allowed_insecure
223                                        .contains(&authority.to_string())
224                                })
225                                .unwrap_or(false),
226                        )
227                        .auth(RegistryAuth::Anonymous)
228                        .build()
229                        .unwrap_or_default(),
230                )
231            })
232            .with_additional_ca_paths(&default_config.additional_ca_paths)
233            .fetch_provider(provider_ref, host_id)
234            .await
235            .with_context(|| {
236                format!("failed to fetch provider under OCI reference `{provider_ref}`")
237            }),
238        ResourceRef::Builtin(..) => bail!("nothing to fetch for a builtin"),
239    }
240}
241
242#[test]
243fn parse_references() -> anyhow::Result<()> {
244    // file:// URL
245    let file_url = "file:///tmp/foo_s.wasm";
246    ensure!(
247        ResourceRef::try_from(file_url).expect("failed to parse")
248            == ResourceRef::File("/tmp/foo_s.wasm".into()),
249        "file reference should be parsed as file and converted to path"
250    );
251
252    // oci:// "scheme" URL
253    ensure!(
254        ResourceRef::try_from("oci://some-registry/foo:0.1.0").expect("failed to parse")
255            == ResourceRef::Oci("some-registry/foo:0.1.0"),
256        "OCI reference should be parsed as OCI and stripped of scheme"
257    );
258
259    // http URL
260    ensure!(
261        ResourceRef::try_from("http://127.0.0.1:5000/v2/foo:0.1.0").expect("failed to parse")
262            == ResourceRef::Oci("127.0.0.1:5000/v2/foo:0.1.0"),
263        "http reference should be parsed as OCI and stripped of scheme"
264    );
265
266    // https URL
267    ensure!(
268        ResourceRef::try_from("https://some-registry.sh/foo:0.1.0").expect("failed to parse")
269            == ResourceRef::Oci("some-registry.sh/foo:0.1.0"),
270        "https reference should be parsed as OCI and stripped of scheme"
271    );
272
273    // localhost URL
274    ensure!(
275        ResourceRef::try_from("localhost:5000/v2/foo:0.1.0").expect("failed to parse")
276            == ResourceRef::Oci("localhost:5000/v2/foo:0.1.0"),
277        "localhost reference should be parsed as OCI and left intact"
278    );
279
280    // container name URL
281    ensure!(
282        ResourceRef::try_from("registry:5000/v2/foo:0.1.0").expect("failed to parse")
283            == ResourceRef::Oci("registry:5000/v2/foo:0.1.0"),
284        "container reference should be parsed as OCI and left intact"
285    );
286
287    Ok(())
288}