Skip to main content

spin_environments/
lib.rs

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