1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![forbid(clippy::unwrap_used)]
4
5pub mod config;
9
10pub mod event;
12
13pub mod nats;
16
17pub mod metrics;
19
20pub mod oci;
22
23pub mod policy;
26
27pub mod registry;
30
31pub mod secrets;
33
34pub mod store;
36
37pub mod wasmbus;
39
40pub 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#[derive(PartialEq)]
59pub enum ResourceRef<'a> {
60 File(PathBuf),
62 Oci(&'a str),
64 Builtin(&'a str),
66}
67
68impl AsRef<str> for ResourceRef<'_> {
69 fn as_ref(&self) -> &str {
70 match self {
71 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 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 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#[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#[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 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 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 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 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 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 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}