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 configuration_constraints: env_def.configuration_constraints().clone(),
230 })
231}
232
233async fn load_worlds(
234 world_refs: &[WorldRef],
235 relative_to_dir: &Option<PathBuf>,
236 cache: &spin_loader::cache::Cache,
237 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
238) -> anyhow::Result<CandidateWorlds> {
239 let mut worlds = vec![];
240
241 for world_ref in world_refs {
242 worlds.push(load_world(world_ref, relative_to_dir, cache, lockfile).await?);
243 }
244
245 Ok(CandidateWorlds { worlds })
246}
247
248async fn load_world(
249 world_ref: &WorldRef,
250 relative_to_dir: &Option<PathBuf>,
251 cache: &spin_loader::cache::Cache,
252 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
253) -> anyhow::Result<CandidateWorld> {
254 match world_ref {
255 WorldRef::DefaultRegistry(world) => {
256 load_world_from_registry(DEFAULT_PACKAGE_REGISTRY, world, cache, lockfile).await
257 }
258 WorldRef::Registry { registry, world } => {
259 load_world_from_registry(registry, world, cache, lockfile).await
260 }
261 WorldRef::OciRegistry { reference, world } => {
262 load_world_from_oci_ref(reference, world, cache, lockfile).await
263 }
264 WorldRef::WitDirectory { path, world } => {
265 let path = match relative_to_dir {
266 Some(dir) => dir.join(path),
267 None => path.to_owned(),
268 };
269 load_world_from_dir(&path, world)
270 }
271 }
272}
273
274fn load_world_from_dir(
275 path: impl AsRef<Path>,
276 world: &WorldName,
277) -> anyhow::Result<CandidateWorld> {
278 let path = path.as_ref();
279 let mut resolve = wit_parser::Resolve::default();
280 let (pkg_id, _) = resolve.push_dir(path)?;
281 let decoded = wit_parser::decoding::DecodedWasm::WitPackage(resolve, pkg_id);
282 CandidateWorld::from_decoded_wasm(world, path, decoded)
283}
284
285async fn load_world_from_registry(
290 registry: &str,
291 world_name: &WorldName,
292 cache: &spin_loader::cache::Cache,
293 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
294) -> anyhow::Result<CandidateWorld> {
295 use futures_util::TryStreamExt;
296
297 if let Some(digest) = lockfile
298 .read()
299 .await
300 .package_digest(registry, world_name.package())
301 && let Ok(cache_file) = cache.wasm_file(digest)
302 && let Ok(bytes) = tokio::fs::read(&cache_file).await
303 {
304 return CandidateWorld::from_package_bytes(world_name, bytes);
305 }
306
307 let pkg_name = world_name.package_namespaced_name();
308 let pkg_ref = world_name.package_ref()?;
309
310 let wkg_registry: wasm_pkg_client::Registry = registry
311 .parse()
312 .with_context(|| format!("Registry {registry} is not a valid registry name"))?;
313
314 let mut wkg_config = wasm_pkg_client::Config::global_defaults().await?;
315 wkg_config.set_package_registry_override(
316 pkg_ref,
317 wasm_pkg_client::RegistryMapping::Registry(wkg_registry),
318 );
319
320 let client = wasm_pkg_client::Client::new(wkg_config);
321
322 let package = pkg_name
323 .to_owned()
324 .try_into()
325 .with_context(|| format!("Failed to parse environment name {pkg_name} as package name"))?;
326 let version = world_name
327 .package_version() .ok_or_else(|| anyhow!("{world_name} is unversioned: this is not currently supported"))?;
329
330 let release = client
331 .get_release(&package, version)
332 .await
333 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?;
334 let stm = client
335 .stream_content(&package, &release)
336 .await
337 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?;
338 let bytes = stm
339 .try_collect::<bytes::BytesMut>()
340 .await
341 .with_context(|| format!("Failed to get {} from registry", world_name.package()))?
342 .to_vec();
343
344 let digest = release.content_digest.to_string();
345 _ = cache.write_wasm(&bytes, &digest).await; lockfile
347 .write()
348 .await
349 .set_package_digest(registry, world_name.package(), &digest);
350
351 CandidateWorld::from_package_bytes(world_name, bytes)
352}
353
354async fn load_world_from_oci_ref(
359 reference: &str,
360 world_name: &WorldName,
361 cache: &spin_loader::cache::Cache,
362 lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>,
363) -> anyhow::Result<CandidateWorld> {
364 if let Some(digest) = lockfile
365 .read()
366 .await
367 .package_digest(reference, world_name.package())
368 && let Ok(cache_file) = cache.wasm_file(digest)
369 && let Ok(bytes) = tokio::fs::read(&cache_file).await
370 {
371 return CandidateWorld::from_package_bytes(world_name, bytes);
372 }
373
374 let oci_client = oci_client::Client::new(oci_client::client::ClientConfig {
375 protocol: oci_client::client::ClientProtocol::Https,
376 ..Default::default()
377 });
378 let client = oci_wasm::WasmClient::new(oci_client);
379
380 let oci_reference = reference.parse().with_context(|| {
381 format!("Target environment contains invalid OCI reference {reference}")
382 })?;
383 let data = client
384 .pull(
385 &oci_reference,
386 &oci_client::secrets::RegistryAuth::Anonymous,
387 )
388 .await
389 .with_context(|| format!("Failed to get {reference} from registry"))?;
390
391 let bytes = data
392 .layers
393 .into_iter()
394 .next()
395 .ok_or_else(|| anyhow::anyhow!("No layers found in target environment {reference}"))?
396 .data
397 .to_vec();
398
399 if let Some(digest) = data.digest {
400 _ = cache.write_wasm(&bytes, &digest).await; lockfile
402 .write()
403 .await
404 .set_package_digest(reference, world_name.package(), &digest);
405 }
406
407 CandidateWorld::from_package_bytes(world_name, bytes)
408}