Skip to main content

spin_environments/
loader.rs

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