wasmcloud_host/
lib.rs

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