Skip to main content

spin_environments/
lib.rs

1use std::{collections::HashMap, sync::Arc};
2
3use anyhow::{anyhow, Context};
4
5mod environment;
6mod loader;
7
8use environment::{CandidateWorld, CandidateWorlds, TargetEnvironment, TriggerType};
9pub use loader::ApplicationToValidate;
10use loader::ComponentToValidate;
11use spin_manifest::schema::v2::TargetEnvironmentRef;
12
13use crate::environment::RealisedTargets;
14
15#[derive(Default)]
16pub struct Targets<'a> {
17    pub default: &'a [TargetEnvironmentRef],
18    pub overrides: HashMap<String, &'a [TargetEnvironmentRef]>,
19}
20
21impl<'a> Targets<'a> {
22    fn is_empty(&self) -> bool {
23        self.default.is_empty() && self.overrides.is_empty()
24    }
25
26    fn all_refs(&self) -> Vec<&TargetEnvironmentRef> {
27        let mut set = std::collections::HashSet::new();
28        for env_id in self.default {
29            set.insert(env_id);
30        }
31        for list in self.overrides.values() {
32            for env_id in *list {
33                set.insert(env_id);
34            }
35        }
36        set.into_iter().collect()
37    }
38}
39
40/// The result of validating an application against a list of target environments.
41/// If `is_ok` returns true (or equivalently if the `errors` collection is empty),
42/// the application passed validation, and can run in all the environments against
43/// which it was checked. Otherwise, at least one component cannot run in at least
44/// one target environment, and the `errors` collection contains the details.
45#[derive(Default)]
46pub struct TargetEnvironmentValidation(Vec<anyhow::Error>);
47
48impl TargetEnvironmentValidation {
49    pub fn is_ok(&self) -> bool {
50        self.0.is_empty()
51    }
52
53    pub fn errors(&self) -> &[anyhow::Error] {
54        &self.0
55    }
56}
57
58/// Validates *all* application components against the list of referenced target enviroments. Each component must conform
59/// to *all* environments to pass.
60///
61/// If the return value is `Ok(...)`, this means only that we were able to perform the validation.
62/// The caller **MUST** still check the returned [TargetEnvironmentValidation] to determine the
63/// outcome of validation.
64///
65/// If the return value is `Err(...)`, then we weren't able even to attempt validation.
66pub async fn validate_application_against_environment_ids<'a>(
67    application: &ApplicationToValidate,
68    targets: Targets<'a>,
69    cache_root: Option<std::path::PathBuf>,
70    app_dir: &std::path::Path,
71) -> anyhow::Result<TargetEnvironmentValidation> {
72    if targets.is_empty() {
73        return Ok(Default::default());
74    }
75
76    let envs = TargetEnvironment::load_all(targets, cache_root, app_dir).await?;
77    validate_application_against_environments(application, &envs).await
78}
79
80/// Validates *all* application components against the list of (realised) target enviroments. Each component must conform
81/// to *all* environments to pass.
82///
83/// For the slightly funky return type, see [validate_application_against_environment_ids].
84async fn validate_application_against_environments(
85    application: &ApplicationToValidate,
86    envs: &RealisedTargets,
87) -> anyhow::Result<TargetEnvironmentValidation> {
88    for trigger_type in application.trigger_types() {
89        if let Some(env) = envs.iter().find(|e| !e.supports_trigger_type(trigger_type)) {
90            anyhow::bail!(
91                "Environment {} does not support trigger type {trigger_type}",
92                env.name()
93            );
94        }
95    }
96
97    let components_by_trigger_type = application.components_by_trigger_type().await?;
98
99    let mut errs = vec![];
100
101    for (trigger_type, component) in components_by_trigger_type {
102        for component in &component {
103            let envs = envs.get(component.id());
104            errs.extend(
105                validate_component_against_environments(envs, &trigger_type, component).await,
106            );
107        }
108    }
109
110    Ok(TargetEnvironmentValidation(errs))
111}
112
113/// Validates the component against the list of target enviroments. The component must conform
114/// to *all* environments to pass.
115///
116/// The return value contains the list of validation errors. There may be up to one error per
117/// target environment, explaining why the component cannot run in that environment.
118/// An empty list means the component has passed validation and is compatible with
119/// all target environments.
120async fn validate_component_against_environments(
121    envs: &[Arc<TargetEnvironment>],
122    trigger_type: &TriggerType,
123    component: &ComponentToValidate<'_>,
124) -> Vec<anyhow::Error> {
125    let mut errs = vec![];
126
127    for env in envs {
128        let worlds = env.worlds(trigger_type);
129        if let Some(e) = validate_wasm_against_any_world(env, worlds, component)
130            .await
131            .err()
132        {
133            errs.push(e);
134        }
135
136        let host_caps = env.capabilities(trigger_type);
137        if let Some(e) = validate_host_reqs(env, host_caps, component).err() {
138            errs.push(e);
139        }
140    }
141
142    if errs.is_empty() {
143        tracing::info!(
144            "Validated component {} {} against all target worlds",
145            component.id(),
146            component.source_description()
147        );
148    }
149
150    errs
151}
152
153/// Validates the component against the list of candidate worlds. The component must conform
154/// to *at least one* candidate world to pass (since if it can run in one world provided by
155/// the target environment, it can run in the target environment).
156async fn validate_wasm_against_any_world(
157    env: &TargetEnvironment,
158    worlds: &CandidateWorlds,
159    component: &ComponentToValidate<'_>,
160) -> anyhow::Result<()> {
161    let mut result = Ok(());
162    for target_world in worlds {
163        tracing::debug!(
164            "Trying component {} {} against target world {target_world}",
165            component.id(),
166            component.source_description(),
167        );
168        match validate_wasm_against_world(env, target_world, component).await {
169            Ok(()) => {
170                tracing::info!(
171                    "Validated component {} {} against target world {target_world}",
172                    component.id(),
173                    component.source_description(),
174                );
175                return Ok(());
176            }
177            Err(e) => {
178                // Record the error, but continue in case a different world succeeds
179                tracing::info!(
180                    "Rejecting component {} {} for target world {target_world} because {e:?}",
181                    component.id(),
182                    component.source_description(),
183                );
184                result = Err(e);
185            }
186        }
187    }
188    result
189}
190
191async fn validate_wasm_against_world(
192    env: &TargetEnvironment,
193    target_world: &CandidateWorld,
194    component: &ComponentToValidate<'_>,
195) -> anyhow::Result<()> {
196    use wac_types::{validate_target, ItemKind, Package as WacPackage, Types as WacTypes, WorldId};
197
198    // Gets the selected world from the component encoded WIT package
199    // TODO: make this an export on `wac_types::Types`.
200    fn get_wit_world(
201        types: &WacTypes,
202        top_level_world: WorldId,
203        world_name: &str,
204    ) -> anyhow::Result<WorldId> {
205        let top_level_world = &types[top_level_world];
206        let world = top_level_world
207            .exports
208            .get(world_name)
209            .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?;
210
211        let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
212            // We expect the top-level world to export a world type
213            anyhow::bail!("wit package was not encoded properly")
214        };
215        let wit_world = &types[*world_id];
216        let world = wit_world.exports.values().next();
217        let Some(ItemKind::Component(w)) = world else {
218            // We expect the nested world type to export a component
219            anyhow::bail!("wit package was not encoded properly")
220        };
221        Ok(*w)
222    }
223
224    let mut types = WacTypes::default();
225
226    let target_world_package = WacPackage::from_bytes(
227        &target_world.package_namespaced_name(),
228        target_world.package_version(),
229        target_world.package_bytes(),
230        &mut types,
231    )?;
232
233    let target_world_id =
234        get_wit_world(&types, target_world_package.ty(), target_world.world_name())?;
235
236    let component_package =
237        WacPackage::from_bytes(component.id(), None, component.wasm_bytes(), &mut types)?;
238
239    let target_result = validate_target(&types, target_world_id, component_package.ty());
240
241    match target_result {
242        Ok(_) => Ok(()),
243        Err(report) => Err(format_target_result_error(
244            &types,
245            env.name(),
246            target_world.to_string(),
247            component.id(),
248            component.source_description(),
249            &report,
250        )),
251    }
252}
253
254fn validate_host_reqs(
255    env: &TargetEnvironment,
256    host_caps: &[String],
257    component: &ComponentToValidate,
258) -> anyhow::Result<()> {
259    let unsatisfied: Vec<_> = component
260        .host_requirements()
261        .iter()
262        .filter(|host_req| !satisfies(host_caps, host_req))
263        .cloned()
264        .collect();
265    if unsatisfied.is_empty() {
266        Ok(())
267    } else {
268        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(", ")))
269    }
270}
271
272fn satisfies(host_caps: &[String], host_req: &String) -> bool {
273    host_caps.contains(host_req)
274}
275
276fn format_target_result_error(
277    types: &wac_types::Types,
278    env_name: &str,
279    target_world_name: String,
280    component_id: &str,
281    source_description: &str,
282    report: &wac_types::TargetValidationReport,
283) -> anyhow::Error {
284    let mut error_string = format!(
285        "Component {} ({}) can't run in environment {} because world {} ...\n",
286        component_id, source_description, env_name, target_world_name
287    );
288
289    for (idx, import) in report.imports_not_in_target().enumerate() {
290        if idx == 0 {
291            error_string.push_str("... requires imports named\n  - ");
292        } else {
293            error_string.push_str("  - ");
294        }
295        error_string.push_str(import);
296        error_string.push('\n');
297    }
298
299    for (idx, (export, export_kind)) in report.missing_exports().enumerate() {
300        if idx == 0 {
301            error_string.push_str("... requires exports named\n  - ");
302        } else {
303            error_string.push_str("  - ");
304        }
305        error_string.push_str(export);
306        error_string.push_str(" (");
307        error_string.push_str(export_kind.desc(types));
308        error_string.push_str(")\n");
309    }
310
311    for (name, extern_kind, error) in report.mismatched_types() {
312        error_string.push_str("... found a type mismatch for ");
313        error_string.push_str(&format!("{extern_kind} {name}: {error}"));
314    }
315
316    anyhow!(error_string)
317}