Skip to main content

spin_environments/
lib.rs

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/// The result of validating an application against a list of target environments.
38/// If `is_ok` returns true (or equivalently if the `errors` collection is empty),
39/// the application passed validation, and can run in all the environments against
40/// which it was checked. Otherwise, at least one component cannot run in at least
41/// one target environment, and the `errors` collection contains the details.
42#[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
55/// Validates *all* application components against the list of referenced target enviroments. Each component must conform
56/// to *all* environments to pass.
57///
58/// If the return value is `Ok(...)`, this means only that we were able to perform the validation.
59/// The caller **MUST** still check the returned [TargetEnvironmentValidation] to determine the
60/// outcome of validation.
61///
62/// If the return value is `Err(...)`, then we weren't able even to attempt validation.
63pub 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
77/// Validates *all* application components against the list of (realised) target enviroments. Each component must conform
78/// to *all* environments to pass.
79///
80/// For the slightly funky return type, see [validate_application_against_environment_ids].
81async 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
110/// Validates the component against the list of target enviroments. The component must conform
111/// to *all* environments to pass.
112///
113/// The return value contains the list of validation errors. There may be up to one error per
114/// target environment, explaining why the component cannot run in that environment.
115/// An empty list means the component has passed validation and is compatible with
116/// all target environments.
117async fn validate_component_against_environments(
118    envs: &[Arc<TargetEnvironment>],
119    trigger_type: &TriggerType,
120    component: &ComponentToValidate<'_>,
121) -> Vec<anyhow::Error> {
122    let mut errs = vec![];
123
124    for env in envs {
125        let worlds = env.worlds(trigger_type);
126        if let Some(e) = validate_wasm_against_any_world(env, worlds, component)
127            .await
128            .err()
129        {
130            errs.push(e);
131        }
132
133        let host_caps = env.capabilities(trigger_type);
134        if let Some(e) = validate_host_reqs(env, host_caps, component).err() {
135            errs.push(e);
136        }
137    }
138
139    if errs.is_empty() {
140        tracing::info!(
141            "Validated component {} {} against all target worlds",
142            component.id(),
143            component.source_description()
144        );
145    }
146
147    errs
148}
149
150/// Validates the component against the list of candidate worlds. The component must conform
151/// to *at least one* candidate world to pass (since if it can run in one world provided by
152/// the target environment, it can run in the target environment).
153async fn validate_wasm_against_any_world(
154    env: &TargetEnvironment,
155    worlds: &CandidateWorlds,
156    component: &ComponentToValidate<'_>,
157) -> anyhow::Result<()> {
158    let mut result = Ok(());
159    for target_world in worlds {
160        tracing::debug!(
161            "Trying component {} {} against target world {target_world}",
162            component.id(),
163            component.source_description(),
164        );
165        match validate_wasm_against_world(env, target_world, component).await {
166            Ok(()) => {
167                tracing::info!(
168                    "Validated component {} {} against target world {target_world}",
169                    component.id(),
170                    component.source_description(),
171                );
172                return Ok(());
173            }
174            Err(e) => {
175                // Record the error, but continue in case a different world succeeds
176                tracing::info!(
177                    "Rejecting component {} {} for target world {target_world} because {e:?}",
178                    component.id(),
179                    component.source_description(),
180                );
181                result = Err(e);
182            }
183        }
184    }
185    result
186}
187
188async fn validate_wasm_against_world(
189    env: &TargetEnvironment,
190    target_world: &CandidateWorld,
191    component: &ComponentToValidate<'_>,
192) -> anyhow::Result<()> {
193    use wac_types::{ItemKind, Package as WacPackage, Types as WacTypes, WorldId, validate_target};
194
195    // Gets the selected world from the component encoded WIT package
196    // TODO: make this an export on `wac_types::Types`.
197    fn get_wit_world(
198        types: &WacTypes,
199        top_level_world: WorldId,
200        world_name: &str,
201    ) -> anyhow::Result<WorldId> {
202        let top_level_world = &types[top_level_world];
203        let world = top_level_world
204            .exports
205            .get(world_name)
206            .with_context(|| format!("wit package did not contain a world named '{world_name}'"))?;
207
208        let ItemKind::Type(wac_types::Type::World(world_id)) = world else {
209            // We expect the top-level world to export a world type
210            anyhow::bail!("wit package was not encoded properly")
211        };
212        let wit_world = &types[*world_id];
213        let world = wit_world.exports.values().next();
214        let Some(ItemKind::Component(w)) = world else {
215            // We expect the nested world type to export a component
216            anyhow::bail!("wit package was not encoded properly")
217        };
218        Ok(*w)
219    }
220
221    let mut types = WacTypes::default();
222
223    let target_world_package = WacPackage::from_bytes(
224        &target_world.package_namespaced_name(),
225        target_world.package_version(),
226        target_world.package_bytes(),
227        &mut types,
228    )?;
229
230    let target_world_id =
231        get_wit_world(&types, target_world_package.ty(), target_world.world_name())?;
232
233    let component_package =
234        WacPackage::from_bytes(component.id(), None, component.wasm_bytes(), &mut types)?;
235
236    let target_result = validate_target(&types, target_world_id, component_package.ty());
237
238    match target_result {
239        Ok(_) => Ok(()),
240        Err(report) => Err(format_target_result_error(
241            &types,
242            env.name(),
243            target_world.to_string(),
244            component.id(),
245            component.source_description(),
246            &report,
247        )),
248    }
249}
250
251fn validate_host_reqs(
252    env: &TargetEnvironment,
253    host_caps: &[String],
254    component: &ComponentToValidate,
255) -> anyhow::Result<()> {
256    let unsatisfied: Vec<_> = component
257        .host_requirements()
258        .iter()
259        .filter(|host_req| !satisfies(host_caps, host_req))
260        .cloned()
261        .collect();
262    if unsatisfied.is_empty() {
263        Ok(())
264    } else {
265        Err(anyhow!(
266            "Component {} can't run in environment {} because it requires the feature(s) '{}' which the environment does not support",
267            component.id(),
268            env.name(),
269            unsatisfied.join(", ")
270        ))
271    }
272}
273
274fn satisfies(host_caps: &[String], host_req: &String) -> bool {
275    host_caps.contains(host_req)
276}
277
278fn format_target_result_error(
279    types: &wac_types::Types,
280    env_name: &str,
281    target_world_name: String,
282    component_id: &str,
283    source_description: &str,
284    report: &wac_types::TargetValidationReport,
285) -> anyhow::Error {
286    let mut error_string = format!(
287        "Component {component_id} ({source_description}) can't run in environment {env_name} (world {target_world_name}).\n",
288    );
289
290    for (idx, import) in report.imports_not_in_target().enumerate() {
291        if idx == 0 {
292            error_string.push_str(
293                "The component requires the following imports, which the environment does not provide:\n  - ",
294            );
295        } else {
296            error_string.push_str("  - ");
297        }
298        error_string.push_str(import);
299        error_string.push('\n');
300    }
301
302    for (idx, (export, export_kind)) in report.missing_exports().enumerate() {
303        if idx == 0 {
304            error_string.push_str(
305                "The environment requires the following exports, which the component does not provide:\n  - ",
306            );
307        } else {
308            error_string.push_str("  - ");
309        }
310        error_string.push_str(export);
311        error_string.push_str(" (");
312        error_string.push_str(export_kind.desc(types));
313        error_string.push_str(")\n");
314    }
315
316    for (name, extern_kind, error) in report.mismatched_types() {
317        error_string.push_str("Found a type mismatch for ");
318        error_string.push_str(&format!("{extern_kind} {name}: {error}"));
319    }
320
321    anyhow!(error_string)
322}