1use std::{collections::HashMap, sync::Arc};
2
3use anyhow::{Context, anyhow};
4use itertools::Itertools;
5
6mod environment;
7mod loader;
8
9use environment::{CandidateWorld, CandidateWorlds, TargetEnvironment, TriggerType};
10pub use environment::{Catalogue, EnvironmentDefinition, load_environment_def};
11pub use loader::ApplicationToValidate;
12use loader::ComponentToValidate;
13use spin_manifest::schema::v2::TargetEnvironmentRef;
14
15use crate::environment::RealisedTargets;
16
17#[derive(Default)]
18pub struct Targets<'a> {
19 pub default: &'a [TargetEnvironmentRef],
20 pub overrides: HashMap<String, &'a [TargetEnvironmentRef]>,
21}
22
23impl<'a> Targets<'a> {
24 fn is_empty(&self) -> bool {
25 self.default.is_empty() && self.overrides.is_empty()
26 }
27
28 fn all_refs(&self) -> Vec<&TargetEnvironmentRef> {
29 self.default
30 .iter()
31 .chain(self.overrides.values().flat_map(|list| list.iter()))
32 .unique()
33 .collect()
34 }
35}
36
37#[derive(Default)]
43pub struct TargetEnvironmentValidation(Vec<anyhow::Error>);
44
45impl TargetEnvironmentValidation {
46 pub fn is_ok(&self) -> bool {
47 self.0.is_empty()
48 }
49
50 pub fn errors(&self) -> &[anyhow::Error] {
51 &self.0
52 }
53}
54
55pub async fn validate_application_against_environment_ids<'a>(
64 application: &ApplicationToValidate,
65 targets: Targets<'a>,
66 cache_root: Option<std::path::PathBuf>,
67 app_dir: &std::path::Path,
68) -> anyhow::Result<TargetEnvironmentValidation> {
69 if targets.is_empty() {
70 return Ok(Default::default());
71 }
72
73 let envs = TargetEnvironment::load_all(targets, cache_root, app_dir).await?;
74 validate_application_against_environments(application, &envs).await
75}
76
77async fn validate_application_against_environments(
82 application: &ApplicationToValidate,
83 envs: &RealisedTargets,
84) -> anyhow::Result<TargetEnvironmentValidation> {
85 for trigger_type in application.trigger_types() {
86 if let Some(env) = envs.iter().find(|e| !e.supports_trigger_type(trigger_type)) {
87 anyhow::bail!(
88 "Environment {} does not support trigger type {trigger_type}",
89 env.name()
90 );
91 }
92 }
93
94 let components_by_trigger_type = application.components_by_trigger_type().await?;
95
96 let mut errs = vec![];
97
98 for (trigger_type, component) in components_by_trigger_type {
99 for component in &component {
100 let envs = envs.get(component.id());
101 errs.extend(
102 validate_component_against_environments(envs, &trigger_type, component).await,
103 );
104 }
105 }
106
107 Ok(TargetEnvironmentValidation(errs))
108}
109
110async fn validate_component_against_environments(
117 envs: &[Arc<TargetEnvironment>],
118 trigger_type: &TriggerType,
119 component: &ComponentToValidate<'_>,
120) -> Vec<anyhow::Error> {
121 let mut errs = vec![];
122
123 for env in envs {
124 let worlds = env.worlds(trigger_type);
125 if let Some(e) = validate_wasm_against_any_world(env, worlds, component)
126 .await
127 .err()
128 {
129 errs.push(e);
130 }
131
132 let host_caps = env.capabilities(trigger_type);
133 if let Some(e) = validate_host_reqs(env, host_caps, component).err() {
134 errs.push(e);
135 }
136
137 let configuration_constraints = env.configuration_constraints();
138 validate_configuration_constraints(env, configuration_constraints, component, &mut errs);
139 }
140
141 if errs.is_empty() {
142 tracing::info!(
143 "Validated component {} {} against all target worlds",
144 component.id(),
145 component.source_description()
146 );
147 }
148
149 errs
150}
151
152async fn validate_wasm_against_any_world(
156 env: &TargetEnvironment,
157 worlds: &CandidateWorlds,
158 component: &ComponentToValidate<'_>,
159) -> anyhow::Result<()> {
160 let mut result = Ok(());
161 for target_world in worlds {
162 tracing::debug!(
163 "Trying component {} {} against target world {target_world}",
164 component.id(),
165 component.source_description(),
166 );
167 match validate_wasm_against_world(env, target_world, component).await {
168 Ok(()) => {
169 tracing::info!(
170 "Validated component {} {} against target world {target_world}",
171 component.id(),
172 component.source_description(),
173 );
174 return Ok(());
175 }
176 Err(e) => {
177 tracing::info!(
179 "Rejecting component {} {} for target world {target_world} because {e:?}",
180 component.id(),
181 component.source_description(),
182 );
183 result = Err(e);
184 }
185 }
186 }
187 result
188}
189
190async fn validate_wasm_against_world(
191 env: &TargetEnvironment,
192 target_world: &CandidateWorld,
193 component: &ComponentToValidate<'_>,
194) -> anyhow::Result<()> {
195 use wac_types::{ItemKind, Package as WacPackage, Types as WacTypes, WorldId, validate_target};
196
197 fn get_wit_world(
200 types: &WacTypes,
201 top_level_world: WorldId,
202 world_name: &str,
203 ) -> anyhow::Result<WorldId> {
204 let top_level_world = &types[top_level_world];
205 let world = top_level_world
206 .exports
207 .get(world_name)
208 .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?;
209
210 let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
211 anyhow::bail!("wit package was not encoded properly")
213 };
214 let wit_world = &types[*world_id];
215 let world = wit_world.exports.values().next();
216 let Some(ItemKind::Component(w)) = world else {
217 anyhow::bail!("wit package was not encoded properly")
219 };
220 Ok(*w)
221 }
222
223 let mut types = WacTypes::default();
224
225 let target_world_package = WacPackage::from_bytes(
226 &target_world.package_namespaced_name(),
227 target_world.package_version(),
228 target_world.package_bytes(),
229 &mut types,
230 )?;
231
232 let target_world_id =
233 get_wit_world(&types, target_world_package.ty(), target_world.world_name())?;
234
235 let component_package =
236 WacPackage::from_bytes(component.id(), None, component.wasm_bytes(), &mut types)?;
237
238 let target_result = validate_target(&types, target_world_id, component_package.ty());
239
240 match target_result {
241 Ok(_) => Ok(()),
242 Err(report) => Err(format_target_result_error(
243 &types,
244 env.name(),
245 target_world.to_string(),
246 component.id(),
247 component.source_description(),
248 &report,
249 )),
250 }
251}
252
253fn validate_host_reqs(
254 env: &TargetEnvironment,
255 host_caps: &[String],
256 component: &ComponentToValidate,
257) -> anyhow::Result<()> {
258 let unsatisfied: Vec<_> = component
259 .host_requirements()
260 .iter()
261 .filter(|host_req| !satisfies(host_caps, host_req))
262 .cloned()
263 .collect();
264 if unsatisfied.is_empty() {
265 Ok(())
266 } else {
267 Err(anyhow!(
268 "Component {} can't run in environment {} because it requires the feature(s) '{}' which the environment does not support",
269 component.id(),
270 env.name(),
271 unsatisfied.join(", ")
272 ))
273 }
274}
275
276fn validate_configuration_constraints(
277 env: &TargetEnvironment,
278 configuration_constraints: &environment::ConfigurationConstraints,
279 component: &ComponentToValidate,
280 errors: &mut Vec<anyhow::Error>,
281) {
282 let mut validate_only_permitted = |descriptor,
283 permitted_values: &Option<Vec<String>>,
284 required_values| {
285 if let Some(permitted_values) = permitted_values.as_ref() {
286 for required_value in required_values {
287 if !permitted_values.contains(required_value) {
288 let error = anyhow!(
289 "Component {} can't run in environment {} because it requires the '{required_value}' {descriptor} which the environment does not support",
290 component.id(),
291 env.name()
292 );
293 errors.push(error);
294 }
295 }
296 }
297 };
298
299 validate_only_permitted(
300 "key-value store",
301 &configuration_constraints.key_value_stores,
302 &component.manifest().key_value_stores,
303 );
304 validate_only_permitted(
305 "SQLite database",
306 &configuration_constraints.sqlite_databases,
307 &component.manifest().sqlite_databases,
308 );
309 validate_only_permitted(
310 "AI model",
311 &configuration_constraints.ai_models,
312 &component.manifest().ai_models,
313 );
314}
315
316fn satisfies(host_caps: &[String], host_req: &String) -> bool {
317 host_caps.contains(host_req)
318}
319
320fn format_target_result_error(
321 types: &wac_types::Types,
322 env_name: &str,
323 target_world_name: String,
324 component_id: &str,
325 source_description: &str,
326 report: &wac_types::TargetValidationReport,
327) -> anyhow::Error {
328 let mut error_string = format!(
329 "Component {component_id} ({source_description}) can't run in environment {env_name} (world {target_world_name}).\n",
330 );
331
332 for (idx, import) in report.imports_not_in_target().enumerate() {
333 if idx == 0 {
334 error_string.push_str(
335 "The component requires the following imports, which the environment does not provide:\n - ",
336 );
337 } else {
338 error_string.push_str(" - ");
339 }
340 error_string.push_str(import);
341 error_string.push('\n');
342 }
343
344 for (idx, (export, export_kind)) in report.missing_exports().enumerate() {
345 if idx == 0 {
346 error_string.push_str(
347 "The environment requires the following exports, which the component does not provide:\n - ",
348 );
349 } else {
350 error_string.push_str(" - ");
351 }
352 error_string.push_str(export);
353 error_string.push_str(" (");
354 error_string.push_str(export_kind.desc(types));
355 error_string.push_str(")\n");
356 }
357
358 for (name, extern_kind, error) in report.mismatched_types() {
359 error_string.push_str("Found a type mismatch for ");
360 error_string.push_str(&format!("{extern_kind} {name}: {error}"));
361 }
362
363 anyhow!(error_string)
364}