Skip to content

Commit

Permalink
Tidying and splitting up
Browse files Browse the repository at this point in the history
Signed-off-by: itowlson <[email protected]>
  • Loading branch information
itowlson committed Sep 9, 2024
1 parent d840496 commit 2900e6e
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 242 deletions.
5 changes: 2 additions & 3 deletions crates/build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions crates/environments/src/environment_definition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use wasm_pkg_loader::PackageRef;

#[derive(Debug, serde::Deserialize)]
pub struct TargetEnvironment {
pub name: String,
pub environments: std::collections::HashMap<TriggerType, TargetWorld>,
}

#[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<String>),
}

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<String> {
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;
255 changes: 16 additions & 239 deletions crates/environments/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String>),
}

impl TargetWorld {
fn versioned_name(&self, world_name: &str) -> String {
format!("{}/{}@{}", self.wit_package, world_name, self.package_ver)
}

fn versioned_names(&self) -> Vec<String> {
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<TriggerType, TargetWorld>,
}

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<ComponentToValidate<'a>> {
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<Item = &str>,
Expand Down Expand Up @@ -202,157 +114,22 @@ async fn validate_component_against_environments(
Ok(())
}

impl ResolutionContext {
async fn wasm_loader(&self) -> anyhow::Result<spin_loader::WasmLoader> {
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<Vec<u8>> {
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<Vec<u8>> {
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<spin_serde::DependencyName, WrappedComponentDependency>,
}

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<Item = (&spin_serde::DependencyName, &Self::Dependency)>
{
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<Vec<u8>> { 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<String> {
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<Vec<u8>> {
// todo!()
// }
// async fn load_dependency_source(
// &self,
// source: &Self::Dependency,
// ) -> anyhow::Result<Vec<u8>> {
// todo!()
// }
// }

async fn validate_component_against_worlds(
target_worlds: impl Iterator<Item = (&str, &TargetWorld)>,
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(())
Expand All @@ -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(());
Expand All @@ -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);
Expand Down Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 2900e6e

Please sign in to comment.