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