1#![warn(missing_docs)]
4#![forbid(clippy::unwrap_used)]
5
6pub mod wasmbus;
8
9pub mod oci;
11
12pub mod policy;
14
15pub mod registry;
17
18pub mod secrets;
20
21pub(crate) mod metrics;
23
24pub 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#[derive(PartialEq)]
49pub enum ResourceRef<'a> {
50 File(PathBuf),
52 Oci(&'a str),
54 Builtin(&'a str),
56}
57
58impl AsRef<str> for ResourceRef<'_> {
59 fn as_ref(&self) -> &str {
60 match self {
61 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 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 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#[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#[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 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 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 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 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 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 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}