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