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