1use anyhow::{anyhow, Context};
2
3mod environment;
4mod loader;
5
6use environment::{CandidateWorld, CandidateWorlds, TargetEnvironment, TriggerType};
7pub use loader::ApplicationToValidate;
8use loader::ComponentToValidate;
9use spin_manifest::schema::v2::TargetEnvironmentRef;
10
11#[derive(Default)]
17pub struct TargetEnvironmentValidation(Vec<anyhow::Error>);
18
19impl TargetEnvironmentValidation {
20 pub fn is_ok(&self) -> bool {
21 self.0.is_empty()
22 }
23
24 pub fn errors(&self) -> &[anyhow::Error] {
25 &self.0
26 }
27}
28
29pub async fn validate_application_against_environment_ids(
38 application: &ApplicationToValidate,
39 env_ids: &[TargetEnvironmentRef],
40 cache_root: Option<std::path::PathBuf>,
41 app_dir: &std::path::Path,
42) -> anyhow::Result<TargetEnvironmentValidation> {
43 if env_ids.is_empty() {
44 return Ok(Default::default());
45 }
46
47 let envs = TargetEnvironment::load_all(env_ids, cache_root, app_dir).await?;
48 validate_application_against_environments(application, &envs).await
49}
50
51async fn validate_application_against_environments(
56 application: &ApplicationToValidate,
57 envs: &[TargetEnvironment],
58) -> anyhow::Result<TargetEnvironmentValidation> {
59 for trigger_type in application.trigger_types() {
60 if let Some(env) = envs.iter().find(|e| !e.supports_trigger_type(trigger_type)) {
61 anyhow::bail!(
62 "Environment {} does not support trigger type {trigger_type}",
63 env.name()
64 );
65 }
66 }
67
68 let components_by_trigger_type = application.components_by_trigger_type().await?;
69
70 let mut errs = vec![];
71
72 for (trigger_type, component) in components_by_trigger_type {
73 for component in &component {
74 errs.extend(
75 validate_component_against_environments(envs, &trigger_type, component).await,
76 );
77 }
78 }
79
80 Ok(TargetEnvironmentValidation(errs))
81}
82
83async fn validate_component_against_environments(
91 envs: &[TargetEnvironment],
92 trigger_type: &TriggerType,
93 component: &ComponentToValidate<'_>,
94) -> Vec<anyhow::Error> {
95 let mut errs = vec![];
96
97 for env in envs {
98 let worlds = env.worlds(trigger_type);
99 if let Some(e) = validate_wasm_against_any_world(env, worlds, component)
100 .await
101 .err()
102 {
103 errs.push(e);
104 }
105
106 let host_caps = env.capabilities(trigger_type);
107 if let Some(e) = validate_host_reqs(env, host_caps, component).err() {
108 errs.push(e);
109 }
110 }
111
112 if errs.is_empty() {
113 tracing::info!(
114 "Validated component {} {} against all target worlds",
115 component.id(),
116 component.source_description()
117 );
118 }
119
120 errs
121}
122
123async fn validate_wasm_against_any_world(
127 env: &TargetEnvironment,
128 worlds: &CandidateWorlds,
129 component: &ComponentToValidate<'_>,
130) -> anyhow::Result<()> {
131 let mut result = Ok(());
132 for target_world in worlds {
133 tracing::debug!(
134 "Trying component {} {} against target world {target_world}",
135 component.id(),
136 component.source_description(),
137 );
138 match validate_wasm_against_world(env, target_world, component).await {
139 Ok(()) => {
140 tracing::info!(
141 "Validated component {} {} against target world {target_world}",
142 component.id(),
143 component.source_description(),
144 );
145 return Ok(());
146 }
147 Err(e) => {
148 tracing::info!(
150 "Rejecting component {} {} for target world {target_world} because {e:?}",
151 component.id(),
152 component.source_description(),
153 );
154 result = Err(e);
155 }
156 }
157 }
158 result
159}
160
161async fn validate_wasm_against_world(
162 env: &TargetEnvironment,
163 target_world: &CandidateWorld,
164 component: &ComponentToValidate<'_>,
165) -> anyhow::Result<()> {
166 use wac_types::{validate_target, ItemKind, Package as WacPackage, Types as WacTypes, WorldId};
167
168 fn get_wit_world(
171 types: &WacTypes,
172 top_level_world: WorldId,
173 world_name: &str,
174 ) -> anyhow::Result<WorldId> {
175 let top_level_world = &types[top_level_world];
176 let world = top_level_world
177 .exports
178 .get(world_name)
179 .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?;
180
181 let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
182 anyhow::bail!("wit package was not encoded properly")
184 };
185 let wit_world = &types[*world_id];
186 let world = wit_world.exports.values().next();
187 let Some(ItemKind::Component(w)) = world else {
188 anyhow::bail!("wit package was not encoded properly")
190 };
191 Ok(*w)
192 }
193
194 let mut types = WacTypes::default();
195
196 let target_world_package = WacPackage::from_bytes(
197 &target_world.package_namespaced_name(),
198 target_world.package_version(),
199 target_world.package_bytes(),
200 &mut types,
201 )?;
202
203 let target_world_id =
204 get_wit_world(&types, target_world_package.ty(), target_world.world_name())?;
205
206 let component_package =
207 WacPackage::from_bytes(component.id(), None, component.wasm_bytes(), &mut types)?;
208
209 let target_result = validate_target(&types, target_world_id, component_package.ty());
210
211 match target_result {
212 Ok(_) => Ok(()),
213 Err(report) => Err(format_target_result_error(
214 &types,
215 env.name(),
216 target_world.to_string(),
217 component.id(),
218 component.source_description(),
219 &report,
220 )),
221 }
222}
223
224fn validate_host_reqs(
225 env: &TargetEnvironment,
226 host_caps: &[String],
227 component: &ComponentToValidate,
228) -> anyhow::Result<()> {
229 let unsatisfied: Vec<_> = component
230 .host_requirements()
231 .iter()
232 .filter(|host_req| !satisfies(host_caps, host_req))
233 .cloned()
234 .collect();
235 if unsatisfied.is_empty() {
236 Ok(())
237 } else {
238 Err(anyhow!("Component {} can't run in environment {} because it requires the feature(s) '{}' which the environment does not support", component.id(), env.name(), unsatisfied.join(", ")))
239 }
240}
241
242fn satisfies(host_caps: &[String], host_req: &String) -> bool {
243 host_caps.contains(host_req)
244}
245
246fn format_target_result_error(
247 types: &wac_types::Types,
248 env_name: &str,
249 target_world_name: String,
250 component_id: &str,
251 source_description: &str,
252 report: &wac_types::TargetValidationReport,
253) -> anyhow::Error {
254 let mut error_string = format!(
255 "Component {} ({}) can't run in environment {} because world {} ...\n",
256 component_id, source_description, env_name, target_world_name
257 );
258
259 for (idx, import) in report.imports_not_in_target().enumerate() {
260 if idx == 0 {
261 error_string.push_str("... requires imports named\n - ");
262 } else {
263 error_string.push_str(" - ");
264 }
265 error_string.push_str(import);
266 error_string.push('\n');
267 }
268
269 for (idx, (export, export_kind)) in report.missing_exports().enumerate() {
270 if idx == 0 {
271 error_string.push_str("... requires exports named\n - ");
272 } else {
273 error_string.push_str(" - ");
274 }
275 error_string.push_str(export);
276 error_string.push_str(" (");
277 error_string.push_str(export_kind.desc(types));
278 error_string.push_str(")\n");
279 }
280
281 for (name, extern_kind, error) in report.mismatched_types() {
282 error_string.push_str("... found a type mismatch for ");
283 error_string.push_str(&format!("{extern_kind} {name}: {error}"));
284 }
285
286 anyhow!(error_string)
287}