1use 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
238struct WrappedComponentDependency {
240 name: spin_serde::DependencyName,
241 dependency: spin_manifest::schema::v2::ComponentDependency,
242}
243
244struct 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 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}