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 environments. 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 environments. 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 environments. The component must conform
111/// to *all* environments to pass.
112///
113/// The return value contains the list of validation errors.
114/// An empty list means the component has passed validation and is compatible with
115/// all target environments.
116async 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
152/// Validates the component against the list of candidate worlds. The component must conform
153/// to *at least one* candidate world to pass (since if it can run in one world provided by
154/// the target environment, it can run in the target environment).
155async 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                // Record the error, but continue in case a different world succeeds
178                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    // Gets the selected world from the component encoded WIT package
198    // TODO: make this an export on `wac_types::Types`.
199    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            // We expect the top-level world to export a world type
212            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            // We expect the nested world type to export a component
218            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}