diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index daa0ead993..845bebb441 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -38,9 +38,8 @@ pub async fn build(manifest_file: &Path, component_ids: &[String]) -> Result<()> if let Ok(manifest) = &manifest { if !deployment_targets.is_empty() { - let resolution_context = spin_environments::ResolutionContext { - base_dir: manifest_file.parent().unwrap().to_owned(), - }; + let resolution_context = + spin_environments::ResolutionContext::new(manifest_file.parent().unwrap()).await?; spin_environments::validate_application_against_environment_ids( deployment_targets.iter(), manifest, diff --git a/crates/environments/src/environment_definition.rs b/crates/environments/src/environment_definition.rs new file mode 100644 index 0000000000..79813ecac3 --- /dev/null +++ b/crates/environments/src/environment_definition.rs @@ -0,0 +1,38 @@ +use wasm_pkg_loader::PackageRef; + +#[derive(Debug, serde::Deserialize)] +pub struct TargetEnvironment { + pub name: String, + pub environments: std::collections::HashMap, +} + +#[derive(Debug, Eq, Hash, PartialEq, serde::Deserialize)] +pub struct TargetWorld { + wit_package: PackageRef, + package_ver: String, // TODO: tidy to semver::Version + world_name: WorldNames, +} + +#[derive(Debug, Eq, Hash, PartialEq, serde::Deserialize)] +#[serde(untagged)] +enum WorldNames { + Exactly(String), + AnyOf(Vec), +} + +impl TargetWorld { + fn versioned_name(&self, world_name: &str) -> String { + format!("{}/{}@{}", self.wit_package, world_name, self.package_ver) + } + + pub fn versioned_names(&self) -> Vec { + match &self.world_name { + WorldNames::Exactly(name) => vec![self.versioned_name(name)], + WorldNames::AnyOf(names) => { + names.iter().map(|name| self.versioned_name(name)).collect() + } + } + } +} + +pub type TriggerType = String; diff --git a/crates/environments/src/lib.rs b/crates/environments/src/lib.rs index e5df3aadd9..2578c17583 100644 --- a/crates/environments/src/lib.rs +++ b/crates/environments/src/lib.rs @@ -1,99 +1,11 @@ -use std::path::PathBuf; - use anyhow::{anyhow, Context}; -use spin_common::ui::quoted_path; -use wasm_pkg_loader::PackageRef; - -#[derive(Debug, Eq, Hash, PartialEq, serde::Deserialize)] -struct TargetWorld { - wit_package: PackageRef, - package_ver: String, // TODO: tidy to semver::Version - world_name: WorldNames, -} - -#[derive(Debug, Eq, Hash, PartialEq, serde::Deserialize)] -#[serde(untagged)] -enum WorldNames { - Exactly(String), - AnyOf(Vec), -} - -impl TargetWorld { - fn versioned_name(&self, world_name: &str) -> String { - format!("{}/{}@{}", self.wit_package, world_name, self.package_ver) - } - - fn versioned_names(&self) -> Vec { - match &self.world_name { - WorldNames::Exactly(name) => vec![self.versioned_name(name)], - WorldNames::AnyOf(names) => { - names.iter().map(|name| self.versioned_name(name)).collect() - } - } - } -} -type TriggerType = String; +mod environment_definition; +mod loader; -struct ComponentToValidate<'a> { - id: &'a str, - source: &'a spin_manifest::schema::v2::ComponentSource, - dependencies: WrappedComponentDependencies, -} - -impl<'a> ComponentToValidate<'a> { - fn source_description(&self) -> String { - match self.source { - spin_manifest::schema::v2::ComponentSource::Local(path) => { - format!("file {}", quoted_path(path)) - } - spin_manifest::schema::v2::ComponentSource::Remote { url, .. } => format!("URL {url}"), - spin_manifest::schema::v2::ComponentSource::Registry { package, .. } => { - format!("package {package}") - } - } - } -} - -#[derive(Debug, serde::Deserialize)] -pub struct TargetEnvironment { - name: String, - environments: std::collections::HashMap, -} - -pub struct ResolutionContext { - pub base_dir: PathBuf, -} - -fn component_source<'a>( - app: &'a spin_manifest::schema::v2::AppManifest, - trigger: &'a spin_manifest::schema::v2::Trigger, -) -> anyhow::Result> { - let component_spec = trigger - .component - .as_ref() - .ok_or_else(|| anyhow!("No component specified for trigger {}", trigger.id))?; - let (id, source, dependencies) = match component_spec { - spin_manifest::schema::v2::ComponentSpec::Inline(c) => { - (trigger.id.as_str(), &c.source, &c.dependencies) - } - spin_manifest::schema::v2::ComponentSpec::Reference(r) => { - let id = r.as_ref(); - let Some(component) = app.components.get(r) else { - anyhow::bail!( - "Component {id} specified for trigger {} does not exist", - trigger.id - ); - }; - (id, &component.source, &component.dependencies) - } - }; - Ok(ComponentToValidate { - id, - source, - dependencies: WrappedComponentDependencies::new(dependencies), - }) -} +use environment_definition::{TargetEnvironment, TargetWorld, TriggerType}; +pub use loader::ResolutionContext; +use loader::{component_source, ComponentSourceLoader, ComponentToValidate}; pub async fn validate_application_against_environment_ids( env_ids: impl Iterator, @@ -202,157 +114,22 @@ async fn validate_component_against_environments( Ok(()) } -impl ResolutionContext { - async fn wasm_loader(&self) -> anyhow::Result { - spin_loader::WasmLoader::new(self.base_dir.clone(), None, None).await - } -} - -struct ComponentSourceLoader<'a> { - wasm_loader: spin_loader::WasmLoader, - _phantom: std::marker::PhantomData<&'a usize>, -} - -#[async_trait::async_trait] -impl<'a> spin_compose::ComponentSourceLoader for ComponentSourceLoader<'a> { - type Component = ComponentToValidate<'a>; - type Dependency = WrappedComponentDependency; - async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result> { - use spin_compose::ComponentLike; - let path = self - .wasm_loader - .load_component_source(source.id(), source.source) - .await?; - let bytes = tokio::fs::read(&path).await?; - let component = spin_componentize::componentize_if_necessary(&bytes)?; - Ok(component.into()) - } - - async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result> { - let (path, _) = self - .wasm_loader - .load_component_dependency(&source.name, &source.dependency) - .await?; - let bytes = tokio::fs::read(&path).await?; - let component = spin_componentize::componentize_if_necessary(&bytes)?; - Ok(component.into()) - } -} - -// This exists only to thwart the orphan rule -struct WrappedComponentDependency { - name: spin_serde::DependencyName, - dependency: spin_manifest::schema::v2::ComponentDependency, -} - -// To manage lifetimes around the thwarting of the orphan rule -struct WrappedComponentDependencies { - dependencies: indexmap::IndexMap, -} - -impl WrappedComponentDependencies { - fn new(deps: &spin_manifest::schema::v2::ComponentDependencies) -> Self { - let dependencies = deps - .inner - .clone() - .into_iter() - .map(|(k, v)| { - ( - k.clone(), - WrappedComponentDependency { - name: k, - dependency: v, - }, - ) - }) - .collect(); - Self { dependencies } - } -} - -#[async_trait::async_trait] -impl<'a> spin_compose::ComponentLike for ComponentToValidate<'a> { - type Dependency = WrappedComponentDependency; - - fn dependencies( - &self, - ) -> impl std::iter::ExactSizeIterator - { - self.dependencies.dependencies.iter() - } - - fn id(&self) -> &str { - self.id - } -} - -#[async_trait::async_trait] -impl spin_compose::DependencyLike for WrappedComponentDependency { - // async fn load_bytes(&self) -> anyhow::Result> { todo!() } - fn inherit(&self) -> spin_compose::InheritConfiguration { - // We don't care because this never runs - it is only used to - // verify import satisfaction - spin_compose::InheritConfiguration::All - } - - fn export(&self) -> &Option { - match &self.dependency { - spin_manifest::schema::v2::ComponentDependency::Version(_) => &None, - spin_manifest::schema::v2::ComponentDependency::Package { export, .. } => export, - spin_manifest::schema::v2::ComponentDependency::Local { export, .. } => export, - } - } -} - -// #[async_trait::async_trait] -// impl<'a> spin_compose::ComponentSourceLoader for CSL<'a> { -// type Component = ComponentToValidate<'a>; -// type Dependency = DEPPY; -// async fn load_component_source( -// &self, -// source: &Self::Component, -// ) -> anyhow::Result> { -// todo!() -// } -// async fn load_dependency_source( -// &self, -// source: &Self::Dependency, -// ) -> anyhow::Result> { -// todo!() -// } -// } - async fn validate_component_against_worlds( target_worlds: impl Iterator, component: &ComponentToValidate<'_>, resolution_context: &ResolutionContext, ) -> anyhow::Result<()> { - // let raw_wasm = resolution_context - // .load_wasm(component) - // .await - // .with_context(|| format!("Couldn't read Wasm {}", component.source_description()))?; - let loader = ComponentSourceLoader { - wasm_loader: resolution_context.wasm_loader().await?, - _phantom: std::marker::PhantomData, - }; - let cooked_wasm = spin_compose::compose(&loader, component).await?; - // FUTURE: take in manifest composition as well - // let cooked_wasm = - // spin_componentize::componentize_if_necessary(&raw_wasm).with_context(|| { - // format!( - // "Couldn't componentize Wasm {}", - // component.source_description() - // ) - // })?; + let loader = ComponentSourceLoader::new(resolution_context.wasm_loader()); + let wasm_bytes = spin_compose::compose(&loader, component).await?; for (env_name, target_world) in target_worlds { - validate_wasm_against_any_world(env_name, target_world, component, cooked_wasm.as_ref()) + validate_wasm_against_any_world(env_name, target_world, component, wasm_bytes.as_ref()) .await?; } tracing::info!( "Validated component {} {} against all target worlds", - component.id, + component.id(), component.source_description() ); Ok(()) @@ -368,14 +145,14 @@ async fn validate_wasm_against_any_world( for target_str in target_world.versioned_names() { tracing::info!( "Trying component {} {} against target world {target_str}", - component.id, + component.id(), component.source_description(), ); match validate_wasm_against_world(env_name, &target_str, component, wasm).await { Ok(()) => { tracing::info!( "Validated component {} {} against target world {target_str}", - component.id, + component.id(), component.source_description(), ); return Ok(()); @@ -384,7 +161,7 @@ async fn validate_wasm_against_any_world( // Record the error, but continue in case a different world succeeds tracing::info!( "Rejecting component {} {} for target world {target_str} because {e:?}", - component.id, + component.id(), component.source_description(), ); result = Err(e); @@ -429,17 +206,17 @@ async fn validate_wasm_against_world( Ok(_) => Ok(()), Err(wac_parser::resolution::Error::TargetMismatch { kind, name, world, .. }) => { // This one doesn't seem to get hit at the moment - we get MissingTargetExport or ImportNotInTarget instead - Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} expects an {} named {name}", component.id, component.source_description(), kind.to_string().to_lowercase())) + Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} expects an {} named {name}", component.id(), component.source_description(), kind.to_string().to_lowercase())) } Err(wac_parser::resolution::Error::MissingTargetExport { name, world, .. }) => { - Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} requires an export named {name}, which the component does not provide", component.id, component.source_description())) + Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} requires an export named {name}, which the component does not provide", component.id(), component.source_description())) } Err(wac_parser::resolution::Error::PackageMissingExport { export, .. }) => { // TODO: The export here seems wrong - it seems to contain the world name rather than the interface name - Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {target_str} requires an export named {export}, which the component does not provide", component.id, component.source_description())) + Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {target_str} requires an export named {export}, which the component does not provide", component.id(), component.source_description())) } Err(wac_parser::resolution::Error::ImportNotInTarget { name, world, .. }) => { - Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} does not provide an import named {name}, which the component requires", component.id, component.source_description())) + Err(anyhow!("Component {} ({}) can't run in environment {env_name} because world {world} does not provide an import named {name}, which the component requires", component.id(), component.source_description())) } Err(e) => { Err(anyhow!(e)) diff --git a/crates/environments/src/loader.rs b/crates/environments/src/loader.rs new file mode 100644 index 0000000000..a33a6dc0ea --- /dev/null +++ b/crates/environments/src/loader.rs @@ -0,0 +1,178 @@ +use std::path::Path; + +use anyhow::anyhow; +use spin_common::ui::quoted_path; + +pub(crate) struct ComponentToValidate<'a> { + id: &'a str, + source: &'a spin_manifest::schema::v2::ComponentSource, + dependencies: WrappedComponentDependencies, +} + +impl<'a> ComponentToValidate<'a> { + pub fn id(&self) -> &str { + self.id + } + + pub fn source_description(&self) -> String { + match self.source { + spin_manifest::schema::v2::ComponentSource::Local(path) => { + format!("file {}", quoted_path(path)) + } + spin_manifest::schema::v2::ComponentSource::Remote { url, .. } => format!("URL {url}"), + spin_manifest::schema::v2::ComponentSource::Registry { package, .. } => { + format!("package {package}") + } + } + } +} + +pub fn component_source<'a>( + app: &'a spin_manifest::schema::v2::AppManifest, + trigger: &'a spin_manifest::schema::v2::Trigger, +) -> anyhow::Result> { + let component_spec = trigger + .component + .as_ref() + .ok_or_else(|| anyhow!("No component specified for trigger {}", trigger.id))?; + let (id, source, dependencies) = match component_spec { + spin_manifest::schema::v2::ComponentSpec::Inline(c) => { + (trigger.id.as_str(), &c.source, &c.dependencies) + } + spin_manifest::schema::v2::ComponentSpec::Reference(r) => { + let id = r.as_ref(); + let Some(component) = app.components.get(r) else { + anyhow::bail!( + "Component {id} specified for trigger {} does not exist", + trigger.id + ); + }; + (id, &component.source, &component.dependencies) + } + }; + Ok(ComponentToValidate { + id, + source, + dependencies: WrappedComponentDependencies::new(dependencies), + }) +} + +pub struct ResolutionContext { + wasm_loader: spin_loader::WasmLoader, +} + +impl ResolutionContext { + pub async fn new(base_dir: impl AsRef) -> anyhow::Result { + let wasm_loader = + spin_loader::WasmLoader::new(base_dir.as_ref().to_owned(), None, None).await?; + Ok(Self { wasm_loader }) + } + + pub(crate) fn wasm_loader(&self) -> &spin_loader::WasmLoader { + &self.wasm_loader + } +} + +pub(crate) struct ComponentSourceLoader<'a> { + wasm_loader: &'a spin_loader::WasmLoader, + _phantom: std::marker::PhantomData<&'a usize>, +} + +impl<'a> ComponentSourceLoader<'a> { + pub fn new(wasm_loader: &'a spin_loader::WasmLoader) -> Self { + Self { + wasm_loader, + _phantom: std::marker::PhantomData, + } + } +} + +#[async_trait::async_trait] +impl<'a> spin_compose::ComponentSourceLoader for ComponentSourceLoader<'a> { + type Component = ComponentToValidate<'a>; + type Dependency = WrappedComponentDependency; + async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result> { + let path = self + .wasm_loader + .load_component_source(source.id(), source.source) + .await?; + let bytes = tokio::fs::read(&path).await?; + let component = spin_componentize::componentize_if_necessary(&bytes)?; + Ok(component.into()) + } + + async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result> { + let (path, _) = self + .wasm_loader + .load_component_dependency(&source.name, &source.dependency) + .await?; + let bytes = tokio::fs::read(&path).await?; + let component = spin_componentize::componentize_if_necessary(&bytes)?; + Ok(component.into()) + } +} + +// This exists only to thwart the orphan rule +pub(crate) struct WrappedComponentDependency { + name: spin_serde::DependencyName, + dependency: spin_manifest::schema::v2::ComponentDependency, +} + +// To manage lifetimes around the thwarting of the orphan rule +struct WrappedComponentDependencies { + dependencies: indexmap::IndexMap, +} + +impl WrappedComponentDependencies { + fn new(deps: &spin_manifest::schema::v2::ComponentDependencies) -> Self { + let dependencies = deps + .inner + .clone() + .into_iter() + .map(|(k, v)| { + ( + k.clone(), + WrappedComponentDependency { + name: k, + dependency: v, + }, + ) + }) + .collect(); + Self { dependencies } + } +} + +#[async_trait::async_trait] +impl<'a> spin_compose::ComponentLike for ComponentToValidate<'a> { + type Dependency = WrappedComponentDependency; + + fn dependencies( + &self, + ) -> impl std::iter::ExactSizeIterator + { + self.dependencies.dependencies.iter() + } + + fn id(&self) -> &str { + self.id + } +} + +#[async_trait::async_trait] +impl spin_compose::DependencyLike for WrappedComponentDependency { + fn inherit(&self) -> spin_compose::InheritConfiguration { + // We don't care because this never runs - it is only used to + // verify import satisfaction. Choosing All avoids the compose + // algorithm meddling with it using the deny adapter. + spin_compose::InheritConfiguration::All + } + + fn export(&self) -> &Option { + match &self.dependency { + spin_manifest::schema::v2::ComponentDependency::Version(_) => &None, + spin_manifest::schema::v2::ComponentDependency::Package { export, .. } => export, + spin_manifest::schema::v2::ComponentDependency::Local { export, .. } => export, + } + } +}