1use 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
213struct WrappedComponentDependency {
215 name: spin_serde::DependencyName,
216 dependency: spin_manifest::schema::v2::ComponentDependency,
217}
218
219struct 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 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}