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 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
273struct WrappedComponentDependency {
275 name: spin_serde::DependencyName,
276 dependency: spin_manifest::schema::v2::ComponentDependency,
277}
278
279struct 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 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}