Skip to content

Commit 3e14103

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

File tree

19 files changed

+2612
-527
lines changed

19 files changed

+2612
-527
lines changed

Cargo.lock

+1,261-292
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ spin-app = { path = "crates/app" }
5555
spin-build = { path = "crates/build" }
5656
spin-common = { path = "crates/common" }
5757
spin-doctor = { path = "crates/doctor" }
58+
spin-environments = { path = "crates/environments" }
5859
spin-factor-outbound-networking = { path = "crates/factor-outbound-networking" }
5960
spin-http = { path = "crates/http" }
6061
spin-loader = { path = "crates/loader" }

crates/build/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edition = { workspace = true }
88
anyhow = { workspace = true }
99
serde = { workspace = true }
1010
spin-common = { path = "../common" }
11+
spin-environments = { path = "../environments" }
1112
spin-manifest = { path = "../manifest" }
1213
subprocess = "0.2"
1314
terminal = { path = "../terminal" }

crates/build/src/lib.rs

+64-16
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,79 @@ use subprocess::{Exec, Redirection};
1616
use crate::manifest::component_build_configs;
1717

1818
/// If present, run the build command of each component.
19-
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-
})?;
19+
pub async fn build(
20+
manifest_file: &Path,
21+
component_ids: &[String],
22+
skip_target_checks: bool,
23+
cache_root: Option<PathBuf>,
24+
) -> Result<()> {
25+
let build_info = component_build_configs(manifest_file)
26+
.await
27+
.with_context(|| {
28+
format!(
29+
"Cannot read manifest file from {}",
30+
quoted_path(manifest_file)
31+
)
32+
})?;
2933
let app_dir = parent_dir(manifest_file)?;
3034

31-
let build_result = build_components(component_ids, components, app_dir);
35+
let build_result = build_components(component_ids, build_info.components(), &app_dir);
3236

33-
if let Some(e) = manifest_err {
37+
// Emit any required warnings now, so that they don't bury any errors.
38+
if let Some(e) = build_info.load_error() {
39+
// The manifest had errors. We managed to attempt a build anyway, but we want to
40+
// let the user know about them.
3441
terminal::warn!("The manifest has errors not related to the Wasm component build. Error details:\n{e:#}");
42+
// Checking deployment targets requires a healthy manifest (because trigger types etc.),
43+
// if any of these were specified, warn they are being skipped.
44+
let should_have_checked_targets =
45+
!skip_target_checks && build_info.has_deployment_targets();
46+
if should_have_checked_targets {
47+
terminal::warn!(
48+
"The manifest error(s) prevented Spin from checking the deployment targets."
49+
);
50+
}
51+
}
52+
53+
// If the build failed, exit with an error at this point.
54+
build_result?;
55+
56+
let Some(manifest) = build_info.manifest() else {
57+
// We can't proceed to checking (because that needs a full healthy manifest), and we've
58+
// already emitted any necessary warning, so quit.
59+
return Ok(());
60+
};
61+
62+
if !skip_target_checks {
63+
let application = spin_environments::ApplicationToValidate::new(
64+
manifest.clone(),
65+
manifest_file.parent().unwrap(),
66+
)
67+
.await?;
68+
let errors = spin_environments::validate_application_against_environment_ids(
69+
&application,
70+
build_info.deployment_targets(),
71+
cache_root.clone(),
72+
&app_dir,
73+
)
74+
.await?;
75+
76+
for error in &errors {
77+
terminal::error!("{error}");
78+
}
79+
80+
if !errors.is_empty() {
81+
anyhow::bail!("All components built successfully, but one or more was incompatible with one or more of the deployment targets.");
82+
}
3583
}
3684

37-
build_result
85+
Ok(())
3886
}
3987

4088
fn build_components(
4189
component_ids: &[String],
4290
components: Vec<ComponentBuildInfo>,
43-
app_dir: PathBuf,
91+
app_dir: &Path,
4492
) -> Result<(), anyhow::Error> {
4593
let components_to_build = if component_ids.is_empty() {
4694
components
@@ -70,7 +118,7 @@ fn build_components(
70118

71119
components_to_build
72120
.into_iter()
73-
.map(|c| build_component(c, &app_dir))
121+
.map(|c| build_component(c, app_dir))
74122
.collect::<Result<Vec<_>, _>>()?;
75123

76124
terminal::step!("Finished", "building all Spin components");
@@ -171,6 +219,6 @@ mod tests {
171219
#[tokio::test]
172220
async fn can_load_even_if_trigger_invalid() {
173221
let bad_trigger_file = test_data_root().join("bad_trigger.toml");
174-
build(&bad_trigger_file, &[]).await.unwrap();
222+
build(&bad_trigger_file, &[], true, None).await.unwrap();
175223
}
176224
}

crates/build/src/manifest.rs

+111-13
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,119 @@ use std::{collections::BTreeMap, path::Path};
44

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

7+
pub enum ManifestBuildInfo {
8+
Loadable {
9+
components: Vec<ComponentBuildInfo>,
10+
deployment_targets: Vec<spin_manifest::schema::v2::TargetEnvironmentRef>,
11+
manifest: spin_manifest::schema::v2::AppManifest,
12+
},
13+
Unloadable {
14+
components: Vec<ComponentBuildInfo>,
15+
has_deployment_targets: bool,
16+
load_error: spin_manifest::Error,
17+
},
18+
}
19+
20+
impl ManifestBuildInfo {
21+
pub fn components(&self) -> Vec<ComponentBuildInfo> {
22+
match self {
23+
Self::Loadable { components, .. } => components.clone(),
24+
Self::Unloadable { components, .. } => components.clone(),
25+
}
26+
}
27+
28+
pub fn load_error(&self) -> Option<&spin_manifest::Error> {
29+
match self {
30+
Self::Loadable { .. } => None,
31+
Self::Unloadable { load_error, .. } => Some(load_error),
32+
}
33+
}
34+
35+
pub fn deployment_targets(&self) -> &[spin_manifest::schema::v2::TargetEnvironmentRef] {
36+
match self {
37+
Self::Loadable {
38+
deployment_targets, ..
39+
} => deployment_targets,
40+
Self::Unloadable { .. } => &[],
41+
}
42+
}
43+
44+
pub fn has_deployment_targets(&self) -> bool {
45+
match self {
46+
Self::Loadable {
47+
deployment_targets, ..
48+
} => !deployment_targets.is_empty(),
49+
Self::Unloadable {
50+
has_deployment_targets,
51+
..
52+
} => *has_deployment_targets,
53+
}
54+
}
55+
56+
pub fn manifest(&self) -> Option<&spin_manifest::schema::v2::AppManifest> {
57+
match self {
58+
Self::Loadable { manifest, .. } => Some(manifest),
59+
Self::Unloadable { .. } => None,
60+
}
61+
}
62+
}
63+
764
/// Returns a map of component IDs to [`v2::ComponentBuildConfig`]s for the
865
/// given (v1 or v2) manifest path. If the manifest cannot be loaded, the
966
/// function attempts fallback: if fallback succeeds, result is Ok but the load error
1067
/// is also returned via the second part of the return value tuple.
11-
pub async fn component_build_configs(
12-
manifest_file: impl AsRef<Path>,
13-
) -> Result<(Vec<ComponentBuildInfo>, Option<spin_manifest::Error>)> {
68+
pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<ManifestBuildInfo> {
1469
let manifest = spin_manifest::manifest_from_file(&manifest_file);
1570
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))),
71+
Ok(mut manifest) => {
72+
spin_manifest::normalize::normalize_manifest(&mut manifest);
73+
let components = build_configs_from_manifest(&manifest);
74+
let deployment_targets = deployment_targets_from_manifest(&manifest);
75+
Ok(ManifestBuildInfo::Loadable {
76+
components,
77+
deployment_targets,
78+
manifest,
79+
})
80+
}
81+
Err(load_error) => {
82+
// The manifest didn't load, but the problem might not be build-affecting.
83+
// Try to fall back by parsing out only the bits we need. And if something
84+
// goes wrong with the fallback, give up and return the original manifest load
85+
// error.
86+
let Ok(components) = fallback_load_build_configs(&manifest_file).await else {
87+
return Err(load_error.into());
88+
};
89+
let Ok(has_deployment_targets) = has_deployment_targets(&manifest_file).await else {
90+
return Err(load_error.into());
91+
};
92+
Ok(ManifestBuildInfo::Unloadable {
93+
components,
94+
has_deployment_targets,
95+
load_error,
96+
})
97+
}
2098
}
2199
}
22100

23101
fn build_configs_from_manifest(
24-
mut manifest: spin_manifest::schema::v2::AppManifest,
102+
manifest: &spin_manifest::schema::v2::AppManifest,
25103
) -> Vec<ComponentBuildInfo> {
26-
spin_manifest::normalize::normalize_manifest(&mut manifest);
27-
28104
manifest
29105
.components
30-
.into_iter()
106+
.iter()
31107
.map(|(id, c)| ComponentBuildInfo {
32108
id: id.to_string(),
33-
build: c.build,
109+
build: c.build.clone(),
34110
})
35111
.collect()
36112
}
37113

114+
fn deployment_targets_from_manifest(
115+
manifest: &spin_manifest::schema::v2::AppManifest,
116+
) -> Vec<spin_manifest::schema::v2::TargetEnvironmentRef> {
117+
manifest.application.targets.clone()
118+
}
119+
38120
async fn fallback_load_build_configs(
39121
manifest_file: impl AsRef<Path>,
40122
) -> Result<Vec<ComponentBuildInfo>> {
@@ -57,7 +139,23 @@ async fn fallback_load_build_configs(
57139
})
58140
}
59141

60-
#[derive(Deserialize)]
142+
async fn has_deployment_targets(manifest_file: impl AsRef<Path>) -> Result<bool> {
143+
let manifest_text = tokio::fs::read_to_string(manifest_file).await?;
144+
Ok(match ManifestVersion::detect(&manifest_text)? {
145+
ManifestVersion::V1 => false,
146+
ManifestVersion::V2 => {
147+
let table: toml::value::Table = toml::from_str(&manifest_text)?;
148+
table
149+
.get("application")
150+
.and_then(|a| a.as_table())
151+
.and_then(|t| t.get("targets"))
152+
.and_then(|arr| arr.as_array())
153+
.is_some_and(|arr| !arr.is_empty())
154+
}
155+
})
156+
}
157+
158+
#[derive(Clone, Deserialize)]
61159
pub struct ComponentBuildInfo {
62160
#[serde(default)]
63161
pub id: String,

0 commit comments

Comments
 (0)