spin_environments/
lib.rs

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/// The result of validating an application against a list of target environments.
12/// If `is_ok` returns true (or equivalently if the `errors` collection is empty),
13/// the application passed validation, and can run in all the environments against
14/// which it was checked. Otherwise, at least one component cannot run in at least
15/// one target environment, and the `errors` collection contains the details.
16#[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
29/// Validates *all* application components against the list of referenced target enviroments. Each component must conform
30/// to *all* environments to pass.
31///
32/// If the return value is `Ok(...)`, this means only that we were able to perform the validation.
33/// The caller **MUST** still check the returned [TargetEnvironmentValidation] to determine the
34/// outcome of validation.
35///
36/// If the return value is `Err(...)`, then we weren't able even to attempt validation.
37pub 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
51/// Validates *all* application components against the list of (realised) target enviroments. Each component must conform
52/// to *all* environments to pass.
53///
54/// For the slightly funky return type, see [validate_application_against_environment_ids].
55async 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
83/// Validates the component against the list of target enviroments. The component must conform
84/// to *all* environments to pass.
85///
86/// The return value contains the list of validation errors. There may be up to one error per
87/// target environment, explaining why the component cannot run in that environment.
88/// An empty list means the component has passed validation and is compatible with
89/// all target environments.
90async 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
123/// Validates the component against the list of candidate worlds. The component must conform
124/// to *at least one* candidate world to pass (since if it can run in one world provided by
125/// the target environment, it can run in the target environment).
126async 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                // Record the error, but continue in case a different world succeeds
149                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    // Gets the selected world from the component encoded WIT package
169    // TODO: make this an export on `wac_types::Types`.
170    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            // We expect the top-level world to export a world type
183            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            // We expect the nested world type to export a component
189            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}