Skip to content

Commit 42f9d82

Browse files
committed
Validate against target environments during build
Signed-off-by: itowlson <[email protected]>
1 parent a22119c commit 42f9d82

File tree

18 files changed

+1586
-583
lines changed

18 files changed

+1586
-583
lines changed

Cargo.lock

Lines changed: 338 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ spin-app = { path = "crates/app" }
5959
spin-build = { path = "crates/build" }
6060
spin-common = { path = "crates/common" }
6161
spin-doctor = { path = "crates/doctor" }
62+
spin-environments = { path = "crates/environments" }
6263
spin-expressions = { path = "crates/expressions" }
6364
spin-factor-outbound-networking = { path = "crates/factor-outbound-networking" }
6465
spin-http = { path = "crates/http" }

crates/build/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ anyhow = { workspace = true }
99
futures = { workspace = true }
1010
serde = { workspace = true }
1111
spin-common = { path = "../common" }
12+
spin-environments = { path = "../environments" }
1213
spin-manifest = { path = "../manifest" }
1314
subprocess = "0.2"
1415
terminal = { path = "../terminal" }

crates/build/src/deployment.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#[derive(Default)]
2+
pub struct DeploymentTargets {
3+
target_environments: Vec<DeploymentTarget>,
4+
}
5+
pub type DeploymentTarget = String;
6+
7+
impl DeploymentTargets {
8+
pub fn new(envs: Vec<String>) -> Self {
9+
Self {
10+
target_environments: envs,
11+
}
12+
}
13+
14+
pub fn iter(&self) -> impl Iterator<Item = &str> {
15+
self.target_environments.iter().map(|s| s.as_str())
16+
}
17+
18+
pub fn is_empty(&self) -> bool {
19+
// TODO: it would be nice to let "no-op" behaviour fall out organically,
20+
// but currently we do some stuff eagerly, so...
21+
self.target_environments.is_empty()
22+
}
23+
}

crates/build/src/lib.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
//! A library for building Spin components.
44
5+
mod deployment;
56
mod manifest;
67

78
use anyhow::{anyhow, bail, Context, Result};
@@ -17,24 +18,38 @@ use crate::manifest::component_build_configs;
1718

1819
/// If present, run the build command of each component.
1920
pub async fn build(manifest_file: &Path, component_ids: &[String]) -> Result<()> {
20-
let (components, manifest_err) =
21-
component_build_configs(manifest_file)
22-
.await
23-
.with_context(|| {
24-
format!(
25-
"Cannot read manifest file from {}",
26-
quoted_path(manifest_file)
27-
)
28-
})?;
21+
let (components, deployment_targets, manifest) = component_build_configs(manifest_file)
22+
.await
23+
.with_context(|| {
24+
format!(
25+
"Cannot read manifest file from {}",
26+
quoted_path(manifest_file)
27+
)
28+
})?;
2929
let app_dir = parent_dir(manifest_file)?;
3030

3131
let build_result = build_components(component_ids, components, app_dir);
3232

33-
if let Some(e) = manifest_err {
33+
if let Err(e) = &manifest {
3434
terminal::warn!("The manifest has errors not related to the Wasm component build. Error details:\n{e:#}");
3535
}
3636

37-
build_result
37+
build_result?;
38+
39+
if let Ok(manifest) = &manifest {
40+
if !deployment_targets.is_empty() {
41+
let resolution_context =
42+
spin_environments::ResolutionContext::new(manifest_file.parent().unwrap()).await?;
43+
spin_environments::validate_application_against_environment_ids(
44+
deployment_targets.iter(),
45+
manifest,
46+
&resolution_context,
47+
)
48+
.await?;
49+
}
50+
}
51+
52+
Ok(())
3853
}
3954

4055
fn build_components(

crates/build/src/manifest.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,60 @@ use std::{collections::BTreeMap, path::Path};
44

55
use spin_manifest::{schema::v2, ManifestVersion};
66

7+
use crate::deployment::DeploymentTargets;
8+
79
/// Returns a map of component IDs to [`v2::ComponentBuildConfig`]s for the
810
/// given (v1 or v2) manifest path. If the manifest cannot be loaded, the
911
/// function attempts fallback: if fallback succeeds, result is Ok but the load error
1012
/// is also returned via the second part of the return value tuple.
1113
pub async fn component_build_configs(
1214
manifest_file: impl AsRef<Path>,
13-
) -> Result<(Vec<ComponentBuildInfo>, Option<spin_manifest::Error>)> {
15+
) -> Result<(
16+
Vec<ComponentBuildInfo>,
17+
DeploymentTargets,
18+
Result<spin_manifest::schema::v2::AppManifest, spin_manifest::Error>,
19+
)> {
1420
let manifest = spin_manifest::manifest_from_file(&manifest_file);
1521
match manifest {
16-
Ok(manifest) => Ok((build_configs_from_manifest(manifest), None)),
17-
Err(e) => fallback_load_build_configs(&manifest_file)
18-
.await
19-
.map(|bc| (bc, Some(e))),
22+
Ok(mut manifest) => {
23+
spin_manifest::normalize::normalize_manifest(&mut manifest);
24+
let bc = build_configs_from_manifest(&manifest);
25+
let dt = deployment_targets_from_manifest(&manifest);
26+
Ok((bc, dt, Ok(manifest)))
27+
}
28+
Err(e) => {
29+
let bc = fallback_load_build_configs(&manifest_file).await?;
30+
let dt = fallback_load_deployment_targets(&manifest_file).await?;
31+
Ok((bc, dt, Err(e)))
32+
}
2033
}
2134
}
2235

2336
fn build_configs_from_manifest(
24-
mut manifest: spin_manifest::schema::v2::AppManifest,
37+
manifest: &spin_manifest::schema::v2::AppManifest,
2538
) -> Vec<ComponentBuildInfo> {
26-
spin_manifest::normalize::normalize_manifest(&mut manifest);
27-
2839
manifest
2940
.components
30-
.into_iter()
41+
.iter()
3142
.map(|(id, c)| ComponentBuildInfo {
3243
id: id.to_string(),
33-
build: c.build,
44+
build: c.build.clone(),
3445
})
3546
.collect()
3647
}
3748

49+
fn deployment_targets_from_manifest(
50+
manifest: &spin_manifest::schema::v2::AppManifest,
51+
) -> DeploymentTargets {
52+
let target_environments = manifest.application.targets.clone();
53+
// let components = manifest
54+
// .components
55+
// .iter()
56+
// .map(|(id, c)| (id.to_string(), c.source.clone()))
57+
// .collect();
58+
DeploymentTargets::new(target_environments)
59+
}
60+
3861
async fn fallback_load_build_configs(
3962
manifest_file: impl AsRef<Path>,
4063
) -> Result<Vec<ComponentBuildInfo>> {
@@ -57,6 +80,42 @@ async fn fallback_load_build_configs(
5780
})
5881
}
5982

83+
async fn fallback_load_deployment_targets(
84+
manifest_file: impl AsRef<Path>,
85+
) -> Result<DeploymentTargets> {
86+
// fn try_parse_component_source(c: (&String, &toml::Value)) -> Option<(String, spin_manifest::schema::v2::ComponentSource)> {
87+
// let (id, ctab) = c;
88+
// let cs = ctab.as_table()
89+
// .and_then(|c| c.get("source"))
90+
// .and_then(|cs| spin_manifest::schema::v2::ComponentSource::deserialize(cs.clone()).ok());
91+
// cs.map(|cs| (id.to_string(), cs))
92+
// }
93+
let manifest_text = tokio::fs::read_to_string(manifest_file).await?;
94+
Ok(match ManifestVersion::detect(&manifest_text)? {
95+
ManifestVersion::V1 => Default::default(),
96+
ManifestVersion::V2 => {
97+
let table: toml::value::Table = toml::from_str(&manifest_text)?;
98+
let target_environments = table
99+
.get("application")
100+
.and_then(|a| a.as_table())
101+
.and_then(|t| t.get("targets"))
102+
.and_then(|arr| arr.as_array())
103+
.map(|v| v.as_slice())
104+
.unwrap_or_default()
105+
.iter()
106+
.filter_map(|t| t.as_str())
107+
.map(|s| s.to_owned())
108+
.collect();
109+
// let components = table
110+
// .get("component")
111+
// .and_then(|cs| cs.as_table())
112+
// .map(|table| table.iter().filter_map(try_parse_component_source).collect())
113+
// .unwrap_or_default();
114+
DeploymentTargets::new(target_environments)
115+
}
116+
})
117+
}
118+
60119
#[derive(Deserialize)]
61120
pub struct ComponentBuildInfo {
62121
#[serde(default)]

crates/compose/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ async-trait = { workspace = true }
1414
indexmap = "2"
1515
semver = "1"
1616
spin-app = { path = "../app" }
17+
spin-common = { path = "../common" }
1718
spin-componentize = { workspace = true }
1819
spin-serde = { path = "../serde" }
1920
thiserror = { workspace = true }
21+
tokio = { version = "1.23", features = ["fs"] }
2022
wac-graph = "0.6"
2123

2224
[lints]

crates/compose/src/lib.rs

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use anyhow::Context;
22
use indexmap::IndexMap;
33
use semver::Version;
4-
use spin_app::locked::{self, InheritConfiguration, LockedComponent, LockedComponentDependency};
4+
use spin_app::locked::InheritConfiguration as LockedInheritConfiguration;
55
use spin_serde::{DependencyName, KebabId};
66
use std::collections::BTreeMap;
77
use thiserror::Error;
@@ -28,18 +28,68 @@ use wac_graph::{CompositionGraph, NodeId};
2828
/// composition graph into a byte array and return it.
2929
pub async fn compose<'a, L: ComponentSourceLoader>(
3030
loader: &'a L,
31-
component: &LockedComponent,
31+
component: &L::Component,
3232
) -> Result<Vec<u8>, ComposeError> {
3333
Composer::new(loader).compose(component).await
3434
}
3535

36+
#[async_trait::async_trait]
37+
pub trait DependencyLike {
38+
fn inherit(&self) -> InheritConfiguration;
39+
fn export(&self) -> &Option<String>;
40+
}
41+
42+
pub enum InheritConfiguration {
43+
All,
44+
Some(Vec<String>),
45+
}
46+
47+
#[async_trait::async_trait]
48+
pub trait ComponentLike {
49+
type Dependency: DependencyLike;
50+
51+
fn dependencies(
52+
&self,
53+
) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)>;
54+
fn id(&self) -> &str;
55+
}
56+
57+
#[async_trait::async_trait]
58+
impl ComponentLike for spin_app::locked::LockedComponent {
59+
type Dependency = spin_app::locked::LockedComponentDependency;
60+
61+
fn dependencies(
62+
&self,
63+
) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)> {
64+
self.dependencies.iter()
65+
}
66+
67+
fn id(&self) -> &str {
68+
&self.id
69+
}
70+
}
71+
72+
#[async_trait::async_trait]
73+
impl DependencyLike for spin_app::locked::LockedComponentDependency {
74+
fn inherit(&self) -> InheritConfiguration {
75+
match &self.inherit {
76+
LockedInheritConfiguration::All => InheritConfiguration::All,
77+
LockedInheritConfiguration::Some(cfgs) => InheritConfiguration::Some(cfgs.clone()),
78+
}
79+
}
80+
81+
fn export(&self) -> &Option<String> {
82+
&self.export
83+
}
84+
}
85+
3686
/// This trait is used to load component source code from a locked component source across various embdeddings.
3787
#[async_trait::async_trait]
3888
pub trait ComponentSourceLoader {
39-
async fn load_component_source(
40-
&self,
41-
source: &locked::LockedComponentSource,
42-
) -> anyhow::Result<Vec<u8>>;
89+
type Component: ComponentLike<Dependency = Self::Dependency>;
90+
type Dependency: DependencyLike;
91+
async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>>;
92+
async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>>;
4393
}
4494

4595
/// Represents an error that can occur when composing dependencies.
@@ -98,19 +148,19 @@ struct Composer<'a, L> {
98148
}
99149

100150
impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
101-
async fn compose(mut self, component: &LockedComponent) -> Result<Vec<u8>, ComposeError> {
151+
async fn compose(mut self, component: &L::Component) -> Result<Vec<u8>, ComposeError> {
102152
let source = self
103153
.loader
104-
.load_component_source(&component.source)
154+
.load_component_source(component)
105155
.await
106156
.map_err(ComposeError::PrepareError)?;
107157

108-
if component.dependencies.is_empty() {
158+
if component.dependencies().len() == 0 {
109159
return Ok(source);
110160
}
111161

112162
let (world_id, instantiation_id) = self
113-
.register_package(&component.id, None, source)
163+
.register_package(component.id(), None, source)
114164
.map_err(ComposeError::PrepareError)?;
115165

116166
let prepared = self.prepare_dependencies(world_id, component).await?;
@@ -150,15 +200,15 @@ impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
150200
async fn prepare_dependencies(
151201
&mut self,
152202
world_id: WorldId,
153-
component: &LockedComponent,
203+
component: &L::Component,
154204
) -> Result<IndexMap<String, DependencyInfo>, ComposeError> {
155205
let imports = self.graph.types()[world_id].imports.clone();
156206

157207
let import_keys = imports.keys().cloned().collect::<Vec<_>>();
158208

159209
let mut mappings: BTreeMap<String, Vec<DependencyInfo>> = BTreeMap::new();
160210

161-
for (dependency_name, dependency) in &component.dependencies {
211+
for (dependency_name, dependency) in component.dependencies() {
162212
let mut matched = Vec::new();
163213

164214
for import_name in &import_keys {
@@ -171,7 +221,7 @@ impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
171221

172222
if matched.is_empty() {
173223
return Err(ComposeError::UnmatchedDependencyName {
174-
component_id: component.id.clone(),
224+
component_id: component.id().to_owned(),
175225
dependency_name: dependency_name.clone(),
176226
});
177227
}
@@ -195,7 +245,7 @@ impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
195245

196246
if !conflicts.is_empty() {
197247
return Err(ComposeError::DependencyConflicts {
198-
component_id: component.id.clone(),
248+
component_id: component.id().to_owned(),
199249
conflicts: conflicts
200250
.into_iter()
201251
.map(|(import_name, infos)| {
@@ -300,19 +350,16 @@ impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
300350
async fn register_dependency(
301351
&mut self,
302352
dependency_name: DependencyName,
303-
dependency: &LockedComponentDependency,
353+
dependency: &L::Dependency,
304354
) -> anyhow::Result<DependencyInfo> {
305-
let mut dependency_source = self
306-
.loader
307-
.load_component_source(&dependency.source)
308-
.await?;
355+
let mut dependency_source = self.loader.load_dependency_source(dependency).await?;
309356

310357
let package_name = match &dependency_name {
311358
DependencyName::Package(name) => name.package.to_string(),
312359
DependencyName::Plain(name) => name.to_string(),
313360
};
314361

315-
match &dependency.inherit {
362+
match dependency.inherit() {
316363
InheritConfiguration::Some(configurations) => {
317364
if configurations.is_empty() {
318365
// Configuration inheritance is disabled, apply deny_all adapter
@@ -333,7 +380,7 @@ impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
333380
manifest_name: dependency_name,
334381
instantiation_id,
335382
world_id,
336-
export_name: dependency.export.clone(),
383+
export_name: dependency.export().clone(),
337384
})
338385
}
339386

0 commit comments

Comments
 (0)