Skip to main content

spin_environments/
loader.rs

1//! Loading an application for validation.
2
3use std::path::Path;
4
5use anyhow::{Context, anyhow};
6use futures::future::try_join_all;
7use spin_common::ui::quoted_path;
8
9pub(crate) struct ComponentToValidate<'a> {
10    id: &'a str,
11    source_description: String,
12    wasm: Vec<u8>,
13    host_requirements: Vec<String>,
14    manifest: spin_manifest::schema::v2::Component,
15}
16
17impl ComponentToValidate<'_> {
18    pub fn id(&self) -> &str {
19        self.id
20    }
21
22    pub fn source_description(&self) -> &str {
23        &self.source_description
24    }
25
26    pub fn wasm_bytes(&self) -> &[u8] {
27        &self.wasm
28    }
29
30    pub fn host_requirements(&self) -> &[String] {
31        &self.host_requirements
32    }
33
34    pub fn manifest(&self) -> &spin_manifest::schema::v2::Component {
35        &self.manifest
36    }
37
38    #[cfg(test)]
39    pub(crate) fn new(
40        id: &'static str,
41        description: &str,
42        wasm: Vec<u8>,
43        host_requirements: Vec<String>,
44        manifest: spin_manifest::schema::v2::Component,
45    ) -> Self {
46        Self {
47            id,
48            source_description: description.to_owned(),
49            wasm,
50            host_requirements,
51            manifest,
52        }
53    }
54}
55
56pub struct ApplicationToValidate {
57    manifest: spin_manifest::schema::v2::AppManifest,
58    component_ids: Vec<String>,
59    wasm_loader: spin_loader::WasmLoader,
60}
61
62impl ApplicationToValidate {
63    pub async fn new(
64        mut manifest: spin_manifest::schema::v2::AppManifest,
65        component_ids: &[String],
66        profile: Option<&str>,
67        base_dir: impl AsRef<Path>,
68    ) -> anyhow::Result<Self> {
69        spin_manifest::normalize::normalize_manifest(&mut manifest, profile)?;
70        let wasm_loader =
71            spin_loader::WasmLoader::new(base_dir.as_ref().to_owned(), None, None).await?;
72        Ok(Self {
73            manifest,
74            component_ids: component_ids.to_vec(),
75            wasm_loader,
76        })
77    }
78
79    fn component<'a>(
80        &'a self,
81        trigger: &'a spin_manifest::schema::v2::Trigger,
82    ) -> anyhow::Result<&'a spin_manifest::schema::v2::Component> {
83        let component_spec = trigger
84            .component
85            .as_ref()
86            .ok_or_else(|| anyhow!("No component specified for trigger {}", trigger.id))?;
87        match component_spec {
88            spin_manifest::schema::v2::ComponentSpec::Inline(c) => Ok(c.as_ref()),
89            spin_manifest::schema::v2::ComponentSpec::Reference(r) => {
90                let id = r.as_ref();
91                let Some(component) = self.manifest.components.get(r) else {
92                    anyhow::bail!(
93                        "Component {id} specified for trigger {} does not exist",
94                        trigger.id
95                    );
96                };
97                Ok(component)
98            }
99        }
100    }
101
102    fn component_source<'a>(
103        &'a self,
104        trigger: &'a spin_manifest::schema::v2::Trigger,
105    ) -> anyhow::Result<ComponentSource<'a>> {
106        let component_spec = trigger
107            .component
108            .as_ref()
109            .ok_or_else(|| anyhow!("No component specified for trigger {}", trigger.id))?;
110        let (id, source, dependencies, service_chaining) = match component_spec {
111            spin_manifest::schema::v2::ComponentSpec::Inline(c) => (
112                trigger.id.as_str(),
113                &c.source,
114                &c.dependencies,
115                spin_loader::requires_service_chaining(c),
116            ),
117            spin_manifest::schema::v2::ComponentSpec::Reference(r) => {
118                let id = r.as_ref();
119                let Some(component) = self.manifest.components.get(r) else {
120                    anyhow::bail!(
121                        "Component {id} specified for trigger {} does not exist",
122                        trigger.id
123                    );
124                };
125                (
126                    id,
127                    &component.source,
128                    &component.dependencies,
129                    spin_loader::requires_service_chaining(component),
130                )
131            }
132        };
133
134        Ok(ComponentSource {
135            id,
136            source,
137            dependencies: WrappedComponentDependencies::new(dependencies),
138            requires_service_chaining: service_chaining,
139        })
140    }
141
142    pub fn trigger_types(&self) -> impl Iterator<Item = &String> {
143        self.manifest.triggers.keys()
144    }
145
146    pub fn triggers(
147        &self,
148    ) -> impl Iterator<Item = (&String, &Vec<spin_manifest::schema::v2::Trigger>)> {
149        self.manifest.triggers.iter()
150    }
151
152    pub(crate) async fn components_by_trigger_type(
153        &self,
154    ) -> anyhow::Result<Vec<(String, Vec<ComponentToValidate<'_>>)>> {
155        use futures::FutureExt;
156
157        let components_by_trigger_type_futs = self.triggers().map(|(ty, ts)| {
158            self.components_for_trigger(ts)
159                .map(|css| css.map(|css| (ty.to_owned(), css)))
160        });
161        let components_by_trigger_type = try_join_all(components_by_trigger_type_futs)
162            .await
163            .context("Failed to prepare components for target environment checking")?;
164        Ok(components_by_trigger_type)
165    }
166
167    async fn components_for_trigger<'a>(
168        &'a self,
169        triggers: &'a [spin_manifest::schema::v2::Trigger],
170    ) -> anyhow::Result<Vec<ComponentToValidate<'a>>> {
171        let component_futures = triggers.iter().map(|t| self.load_and_resolve_trigger(t));
172        let components = try_join_all(component_futures).await?;
173        let components = components.into_iter().flatten().collect();
174        Ok(components)
175    }
176
177    async fn load_and_resolve_trigger<'a>(
178        &'a self,
179        trigger: &'a spin_manifest::schema::v2::Trigger,
180    ) -> anyhow::Result<Option<ComponentToValidate<'a>>> {
181        let component = self.component(trigger)?;
182        let component_source = self.component_source(trigger)?;
183        if !self.component_ids.is_empty()
184            && !self
185                .component_ids
186                .contains(&component_source.id.to_string())
187        {
188            return Ok(None);
189        }
190
191        let loader = ComponentSourceLoader::new(&self.wasm_loader);
192
193        let wasm = spin_compose::compose(&loader, &component_source, async |data| Ok(data)).await.with_context(|| format!("Spin needed to compose dependencies for {} as part of target checking, but composition failed", component_source.id))?;
194
195        let host_requirements = if component_source.requires_service_chaining {
196            vec!["local_service_chaining".to_string()]
197        } else {
198            vec![]
199        };
200
201        Ok(Some(ComponentToValidate {
202            id: component_source.id,
203            source_description: source_description(component_source.source),
204            wasm,
205            host_requirements,
206            manifest: component.clone(),
207        }))
208    }
209}
210
211struct ComponentSource<'a> {
212    id: &'a str,
213    source: &'a spin_manifest::schema::v2::ComponentSource,
214    dependencies: WrappedComponentDependencies,
215    requires_service_chaining: bool,
216}
217
218struct ComponentSourceLoader<'a> {
219    wasm_loader: &'a spin_loader::WasmLoader,
220}
221
222impl<'a> ComponentSourceLoader<'a> {
223    pub fn new(wasm_loader: &'a spin_loader::WasmLoader) -> Self {
224        Self { wasm_loader }
225    }
226}
227
228#[async_trait::async_trait]
229impl<'a> spin_compose::ComponentSourceLoader for ComponentSourceLoader<'a> {
230    type Component = ComponentSource<'a>;
231    type Dependency = WrappedComponentDependency;
232    type Source = spin_manifest::schema::v2::ComponentSource;
233    async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>> {
234        let path = self
235            .wasm_loader
236            .load_component_source(source.id, source.source)
237            .await?;
238        let bytes = tokio::fs::read(&path)
239            .await
240            .with_context(|| format!("reading {}", quoted_path(&path)))?;
241        let component = spin_componentize::componentize_if_necessary(&bytes)
242            .with_context(|| format!("componentizing {}", quoted_path(&path)))?;
243        Ok(component.into())
244    }
245
246    async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>> {
247        let (path, _) = self
248            .wasm_loader
249            .load_dependency_content(&source.name, &source.dependency)
250            .await?;
251        let bytes = tokio::fs::read(&path)
252            .await
253            .with_context(|| format!("reading {}", quoted_path(&path)))?;
254        let component = spin_componentize::componentize_if_necessary(&bytes)
255            .with_context(|| format!("componentizing {}", quoted_path(&path)))?;
256        Ok(component.into())
257    }
258
259    async fn load_source(&self, source: &Self::Source) -> anyhow::Result<Vec<u8>> {
260        let path = self
261            .wasm_loader
262            .load_component_source("in-memory-component", source)
263            .await?;
264        let bytes = tokio::fs::read(&path)
265            .await
266            .with_context(|| format!("reading {}", quoted_path(&path)))?;
267        let component = spin_componentize::componentize_if_necessary(&bytes)
268            .with_context(|| format!("componentizing {}", quoted_path(&path)))?;
269        Ok(component.into())
270    }
271}
272
273// This exists only to thwart the orphan rule
274struct WrappedComponentDependency {
275    name: spin_serde::DependencyName,
276    dependency: spin_manifest::schema::v2::ComponentDependency,
277}
278
279// To manage lifetimes around the thwarting of the orphan rule
280struct WrappedComponentDependencies {
281    dependencies: indexmap::IndexMap<spin_serde::DependencyName, WrappedComponentDependency>,
282}
283
284impl WrappedComponentDependencies {
285    fn new(deps: &spin_manifest::schema::v2::ComponentDependencies) -> Self {
286        let dependencies = deps
287            .inner
288            .clone()
289            .into_iter()
290            .map(|(k, v)| {
291                (
292                    k.clone(),
293                    WrappedComponentDependency {
294                        name: k,
295                        dependency: v,
296                    },
297                )
298            })
299            .collect();
300        Self { dependencies }
301    }
302}
303
304#[async_trait::async_trait]
305impl spin_compose::ComponentLike for ComponentSource<'_> {
306    type Dependency = WrappedComponentDependency;
307
308    fn dependencies(
309        &self,
310    ) -> impl std::iter::ExactSizeIterator<Item = (&spin_serde::DependencyName, &Self::Dependency)>
311    {
312        self.dependencies.dependencies.iter()
313    }
314
315    fn id(&self) -> &str {
316        self.id
317    }
318}
319
320#[async_trait::async_trait]
321impl spin_compose::DependencyLike for WrappedComponentDependency {
322    fn inherit(&self) -> spin_compose::InheritConfiguration {
323        // We don't care because this never runs - it is only used to
324        // verify import satisfaction. Choosing All avoids the compose
325        // algorithm meddling with it using the deny adapter.
326        spin_compose::InheritConfiguration::All
327    }
328
329    fn export(&self) -> &Option<String> {
330        match &self.dependency {
331            spin_manifest::schema::v2::ComponentDependency::Version(_) => &None,
332            spin_manifest::schema::v2::ComponentDependency::Package { export, .. } => export,
333            spin_manifest::schema::v2::ComponentDependency::Local { export, .. } => export,
334            spin_manifest::schema::v2::ComponentDependency::HTTP { export, .. } => export,
335            spin_manifest::schema::v2::ComponentDependency::AppComponent { export, .. } => export,
336        }
337    }
338}
339
340fn source_description(source: &spin_manifest::schema::v2::ComponentSource) -> String {
341    match source {
342        spin_manifest::schema::v2::ComponentSource::Local(path) => {
343            format!("file {}", quoted_path(path))
344        }
345        spin_manifest::schema::v2::ComponentSource::Remote { url, .. } => format!("URL {url}"),
346        spin_manifest::schema::v2::ComponentSource::Registry { package, .. } => {
347            format!("package {package}")
348        }
349    }
350}