spin_environments/environment/
env_loader.rs1use std::path::PathBuf;
6use std::sync::Arc;
7use std::{collections::HashMap, path::Path};
8
9use anyhow::{Context, anyhow};
10use futures::future::try_join_all;
11use spin_common::ui::quoted_path;
12use spin_manifest::schema::v2::TargetEnvironmentRef;
13
14use crate::environment::catalogue::Catalogue;
15
16use super::definition::{EnvironmentDefinition, WorldName, WorldRef};
17use super::lockfile::TargetEnvironmentLockfile;
18use super::{CandidateWorld, CandidateWorlds, TargetEnvironment, UnknownTrigger};
19
20const DEFAULT_PACKAGE_REGISTRY: &str = "spinframework.dev";
21
22pub struct LoadedEnvironmentDefinition {
23 pub name: String,
24 pub env_def: EnvironmentDefinition,
25 pub relative_path_base: Option<PathBuf>,
26}
27
28impl LoadedEnvironmentDefinition {
29 fn new(
30 name: impl Into<String>,
31 env_def: EnvironmentDefinition,
32 relative_path_base: Option<PathBuf>,
33 ) -> Self {
34 Self {
35 name: name.into(),
36 env_def,
37 relative_path_base,
38 }
39 }
40}
41
42pub async fn load_environments<'a>(
47 env_ids: &[&'a TargetEnvironmentRef],
48 cache_root: Option<std::path::PathBuf>,
49 app_dir: &std::path::Path,
50) -> anyhow::Result<HashMap<&'a TargetEnvironmentRef, Arc<TargetEnvironment>>> {
51 if env_ids.is_empty() {
52 return Ok(Default::default());
53 }
54
55 let cache = spin_loader::cache::Cache::new(cache_root)
56 .await
57 .context("Unable to create cache")?;
58 let lockfile_dir = app_dir.join(".spin");
59 let lockfile_path = lockfile_dir.join("target-environments.lock");
60
61 let orig_lockfile: TargetEnvironmentLockfile = tokio::fs::read_to_string(&lockfile_path)
62 .await
63 .ok()
64 .and_then(|s| serde_json::from_str(&s).ok())
65 .unwrap_or_default();
66 let lockfile = std::sync::Arc::new(tokio::sync::RwLock::new(orig_lockfile.clone()));
67
68 let envs = try_join_all(
69 env_ids
70 .iter()
71 .map(|e| load_environment(e, app_dir, &cache, &lockfile)),
72 )
73 .await?
74 .into_iter()
75 .map(|(k, v)| (k, Arc::new(v)))
76 .collect();
77
78 let final_lockfile = &*lockfile.read().await;
79 if *final_lockfile != orig_lockfile
80 && let Ok(lockfile_json) = serde_json::to_string_pretty(&final_lockfile)
81 {
82 _ = tokio::fs::create_dir_all(lockfile_dir).await;
83 _ = tokio::fs::write(&lockfile_path, lockfile_json).await; }
85
86 Ok(envs)
87}
88
89async fn load_environment<'a>(
91 env_id: &'a TargetEnvironmentRef,
92 app_dir: &Path,
93 cache: &spin_loader::cache::Cache,
94 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
95) -> anyhow::Result<(&'a TargetEnvironmentRef, TargetEnvironment)> {
96 let loaded_env_def = load_environment_def(env_id, app_dir).await?;
97 let env = load_environment_from_env_def(loaded_env_def, cache, lockfile).await?;
98 Ok((env_id, env))
99}
100
101pub async fn load_environment_def(
102 env_id: &TargetEnvironmentRef,
103 app_dir: &Path,
104) -> Result<LoadedEnvironmentDefinition, anyhow::Error> {
105 match env_id {
106 TargetEnvironmentRef::Catalogue(id) => load_environment_def_from_catalogue(id).await,
107 TargetEnvironmentRef::Http { url } => load_environment_def_from_http(url).await,
108 TargetEnvironmentRef::File { path } => {
109 load_environment_def_from_file(app_dir.join(path)).await
110 }
111 }
112}
113
114async fn load_environment_def_from_catalogue(
119 env_id: &str,
120) -> anyhow::Result<LoadedEnvironmentDefinition> {
121 let catalogue = Catalogue::try_default()?;
122 let env_id = env_id.replace(':', "@");
123 let env_def = match catalogue.get(&env_id).await? {
124 Some(env_def) => env_def,
125 None => {
126 catalogue.update().await?;
127 catalogue
128 .get(&env_id)
129 .await?
130 .with_context(|| anyhow!("Cannot load target environment '{env_id}'"))?
131 }
132 };
133 Ok(LoadedEnvironmentDefinition::new(env_id, env_def, None))
134}
135
136async fn load_environment_def_from_http(url: &str) -> anyhow::Result<LoadedEnvironmentDefinition> {
141 let toml_text = reqwest::get(url).await?.text().await?;
142 let env_def: EnvironmentDefinition = toml::from_str(&toml_text)?;
143 let url = url::Url::parse(url)?;
144 let env_id = url
145 .path_segments()
146 .with_context(|| format!("environment URL {url} does not have a path"))?
147 .next_back()
148 .with_context(|| format!("environment URL {url} does not have a path"))?;
149 let env_id = env_id
150 .rsplit_once('.')
151 .map(|(stem, _)| stem)
152 .unwrap_or(env_id);
153 Ok(LoadedEnvironmentDefinition::new(env_id, env_def, None))
154}
155
156async fn load_environment_def_from_file(
160 path: impl AsRef<Path>,
161) -> anyhow::Result<LoadedEnvironmentDefinition> {
162 let path = path.as_ref();
163 let env_def_dir = path.parent().map(|p| p.to_owned());
164 let name = path
165 .file_stem()
166 .and_then(|s| s.to_str())
167 .map(|s| s.to_owned())
168 .unwrap();
169 let toml_text = tokio::fs::read_to_string(path).await.with_context(|| {
170 format!(
171 "unable to read target environment from {}",
172 quoted_path(path)
173 )
174 })?;
175 let env_def: EnvironmentDefinition = toml::from_str(&toml_text)?;
176 Ok(LoadedEnvironmentDefinition::new(name, env_def, env_def_dir))
177}
178
179async fn load_environment_from_env_def(
183 loaded_env_def: LoadedEnvironmentDefinition,
184 cache: &spin_loader::cache::Cache,
185 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
186) -> anyhow::Result<TargetEnvironment> {
187 let mut trigger_worlds = HashMap::new();
188 let mut trigger_capabilities = HashMap::new();
189
190 let LoadedEnvironmentDefinition {
191 name,
192 env_def,
193 relative_path_base,
194 } = loaded_env_def;
195
196 for (trigger_type, trigger_env) in env_def.triggers() {
199 trigger_worlds.insert(
200 trigger_type.to_owned(),
201 load_worlds(
202 trigger_env.world_refs(),
203 &relative_path_base,
204 cache,
205 lockfile,
206 )
207 .await?,
208 );
209 trigger_capabilities.insert(trigger_type.to_owned(), trigger_env.capabilities());
210 }
211
212 let unknown_trigger = match env_def.default() {
213 None => UnknownTrigger::Deny,
214 Some(env) => UnknownTrigger::Allow(
215 load_worlds(env.world_refs(), &relative_path_base, cache, lockfile).await?,
216 ),
217 };
218 let unknown_capabilities = match env_def.default() {
219 None => vec![],
220 Some(env) => env.capabilities(),
221 };
222
223 Ok(TargetEnvironment {
224 name: name.to_owned(),
225 trigger_worlds,
226 trigger_capabilities,
227 unknown_trigger,
228 unknown_capabilities,
229 })
230}
231
232async fn load_worlds(
233 world_refs: &[WorldRef],
234 relative_to_dir: &Option<PathBuf>,
235 cache: &spin_loader::cache::Cache,
236 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
237) -> anyhow::Result<CandidateWorlds> {
238 let mut worlds = vec![];
239
240 for world_ref in world_refs {
241 worlds.push(load_world(world_ref, relative_to_dir, cache, lockfile).await?);
242 }
243
244 Ok(CandidateWorlds { worlds })
245}
246
247async fn load_world(
248 world_ref: &WorldRef,
249 relative_to_dir: &Option<PathBuf>,
250 cache: &spin_loader::cache::Cache,
251 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
252) -> anyhow::Result<CandidateWorld> {
253 match world_ref {
254 WorldRef::DefaultRegistry(world) => {
255 load_world_from_registry(DEFAULT_PACKAGE_REGISTRY, world, cache, lockfile).await
256 }
257 WorldRef::Registry { registry, world } => {
258 load_world_from_registry(registry, world, cache, lockfile).await
259 }
260 WorldRef::OciRegistry { reference, world } => {
261 load_world_from_oci_ref(reference, world, cache, lockfile).await
262 }
263 WorldRef::WitDirectory { path, world } => {
264 let path = match relative_to_dir {
265 Some(dir) => dir.join(path),
266 None => path.to_owned(),
267 };
268 load_world_from_dir(&path, world)
269 }
270 }
271}
272
273fn load_world_from_dir(
274 path: impl AsRef<Path>,
275 world: &WorldName,
276) -> anyhow::Result<CandidateWorld> {
277 let path = path.as_ref();
278 let mut resolve = wit_parser::Resolve::default();
279 let (pkg_id, _) = resolve.push_dir(path)?;
280 let decoded = wit_parser::decoding::DecodedWasm::WitPackage(resolve, pkg_id);
281 CandidateWorld::from_decoded_wasm(world, path, decoded)
282}
283
284async fn load_world_from_registry(
289 registry: &str,
290 world_name: &WorldName,
291 cache: &spin_loader::cache::Cache,
292 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
293) -> anyhow::Result<CandidateWorld> {
294 use futures_util::TryStreamExt;
295
296 if let Some(digest) = lockfile
297 .read()
298 .await
299 .package_digest(registry, world_name.package())
300 && let Ok(cache_file) = cache.wasm_file(digest)
301 && let Ok(bytes) = tokio::fs::read(&cache_file).await
302 {
303 return CandidateWorld::from_package_bytes(world_name, bytes);
304 }
305
306 let pkg_name = world_name.package_namespaced_name();
307 let pkg_ref = world_name.package_ref()?;
308
309 let wkg_registry: wasm_pkg_client::Registry = registry
310 .parse()
311 .with_context(|| format!("Registry {registry} is not a valid registry name"))?;
312
313 let mut wkg_config = wasm_pkg_client::Config::global_defaults().await?;
314 wkg_config.set_package_registry_override(
315 pkg_ref,
316 wasm_pkg_client::RegistryMapping::Registry(wkg_registry),
317 );
318
319 let client = wasm_pkg_client::Client::new(wkg_config);
320
321 let package = pkg_name
322 .to_owned()
323 .try_into()
324 .with_context(|| format!("Failed to parse environment name {pkg_name} as package name"))?;
325 let version = world_name
326 .package_version() .ok_or_else(|| anyhow!("{world_name} is unversioned: this is not currently supported"))?;
328
329 let release = client
330 .get_release(&package, version)
331 .await
332 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?;
333 let stm = client
334 .stream_content(&package, &release)
335 .await
336 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?;
337 let bytes = stm
338 .try_collect::<bytes::BytesMut>()
339 .await
340 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?
341 .to_vec();
342
343 let digest = release.content_digest.to_string();
344 _ = cache.write_wasm(&bytes, &digest).await; lockfile
346 .write()
347 .await
348 .set_package_digest(registry, world_name.package(), &digest);
349
350 CandidateWorld::from_package_bytes(world_name, bytes)
351}
352
353async fn load_world_from_oci_ref(
358 reference: &str,
359 world_name: &WorldName,
360 cache: &spin_loader::cache::Cache,
361 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
362) -> anyhow::Result<CandidateWorld> {
363 if let Some(digest) = lockfile
364 .read()
365 .await
366 .package_digest(reference, world_name.package())
367 && let Ok(cache_file) = cache.wasm_file(digest)
368 && let Ok(bytes) = tokio::fs::read(&cache_file).await
369 {
370 return CandidateWorld::from_package_bytes(world_name, bytes);
371 }
372
373 let oci_client = oci_client::Client::new(oci_client::client::ClientConfig {
374 protocol: oci_client::client::ClientProtocol::Https,
375 ..Default::default()
376 });
377 let client = oci_wasm::WasmClient::new(oci_client);
378
379 let oci_reference = reference.parse().with_context(|| {
380 format!("Target environment contains invalid OCI reference {reference}")
381 })?;
382 let data = client
383 .pull(
384 &oci_reference,
385 &oci_client::secrets::RegistryAuth::Anonymous,
386 )
387 .await
388 .with_context(|| format!("Failed to get {reference} from registry"))?;
389
390 let bytes = data
391 .layers
392 .into_iter()
393 .next()
394 .ok_or_else(|| anyhow::anyhow!("No layers found in target environment {reference}"))?
395 .data
396 .to_vec();
397
398 if let Some(digest) = data.digest {
399 _ = cache.write_wasm(&bytes, &digest).await; lockfile
401 .write()
402 .await
403 .set_package_digest(reference, world_name.package(), &digest);
404 }
405
406 CandidateWorld::from_package_bytes(world_name, bytes)
407}