Skip to content

Commit d106c6b

Browse files
committed
Less ghastly
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
1 parent a8271b0 commit d106c6b

File tree

9 files changed

+764
-919
lines changed

9 files changed

+764
-919
lines changed

crates/build/src/lib.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,21 @@ pub async fn build(
6464
manifest.clone(),
6565
manifest_file.parent().unwrap(),
6666
)
67-
.await?;
68-
let errors = spin_environments::validate_application_against_environment_ids(
67+
.await
68+
.context("unable to load application for checking against deployment targets")?;
69+
let target_validation = spin_environments::validate_application_against_environment_ids(
6970
&application,
7071
build_info.deployment_targets(),
7172
cache_root.clone(),
7273
&app_dir,
7374
)
74-
.await?;
75-
76-
for error in &errors {
77-
terminal::error!("{error}");
78-
}
75+
.await
76+
.context("unable to check if the application is compatible with deployment targets")?;
7977

80-
if !errors.is_empty() {
78+
if !target_validation.is_ok() {
79+
for error in target_validation.errors() {
80+
terminal::error!("{error}");
81+
}
8182
anyhow::bail!("All components built successfully, but one or more was incompatible with one or more of the deployment targets.");
8283
}
8384
}

crates/build/src/manifest.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use spin_manifest::{schema::v2, ManifestVersion};
88
pub enum ManifestBuildInfo {
99
Loadable {
1010
components: Vec<ComponentBuildInfo>,
11-
deployment_targets: Vec<spin_manifest::schema::v2::TargetEnvironmentRef2>,
11+
deployment_targets: Vec<spin_manifest::schema::v2::TargetEnvironmentRef>,
1212
manifest: spin_manifest::schema::v2::AppManifest,
1313
},
1414
Unloadable {
@@ -33,7 +33,7 @@ impl ManifestBuildInfo {
3333
}
3434
}
3535

36-
pub fn deployment_targets(&self) -> &[spin_manifest::schema::v2::TargetEnvironmentRef2] {
36+
pub fn deployment_targets(&self) -> &[spin_manifest::schema::v2::TargetEnvironmentRef] {
3737
match self {
3838
Self::Loadable {
3939
deployment_targets, ..
@@ -114,7 +114,7 @@ fn build_configs_from_manifest(
114114

115115
fn deployment_targets_from_manifest(
116116
manifest: &spin_manifest::schema::v2::AppManifest,
117-
) -> Vec<spin_manifest::schema::v2::TargetEnvironmentRef2> {
117+
) -> Vec<spin_manifest::schema::v2::TargetEnvironmentRef> {
118118
manifest.application.targets.clone()
119119
}
120120

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use std::{collections::HashMap, path::Path};
2+
3+
use anyhow::Context;
4+
use spin_common::ui::quoted_path;
5+
use spin_manifest::schema::v2::TargetEnvironmentRef;
6+
7+
mod definition;
8+
mod env_loader;
9+
mod lockfile;
10+
11+
use definition::WorldName;
12+
13+
/// A fully realised deployment environment, e.g. Spin 2.7,
14+
/// SpinKube 3.1, Fermyon Cloud. The `TargetEnvironment` provides a mapping
15+
/// from the Spin trigger types supported in the environment to the Component Model worlds
16+
/// supported by that trigger type. (A trigger type may support more than one world,
17+
/// for example when it supports multiple versions of the Spin or WASI interfaces.)
18+
pub struct TargetEnvironment {
19+
name: String,
20+
trigger_worlds: HashMap<TriggerType, CandidateWorlds>,
21+
unknown_trigger: UnknownTrigger,
22+
}
23+
24+
impl TargetEnvironment {
25+
/// Loads the specified list of environments. This fetches all required
26+
/// environment definitions from their references, and then chases packages
27+
/// references until the entire target environment is fully loaded.
28+
/// The function also caches registry references in the application directory,
29+
/// to avoid loading from the network when the app is validated again.
30+
pub async fn load_all(
31+
env_ids: &[TargetEnvironmentRef],
32+
cache_root: Option<std::path::PathBuf>,
33+
app_dir: &std::path::Path,
34+
) -> anyhow::Result<Vec<Self>> {
35+
env_loader::load_environments(env_ids, cache_root, app_dir).await
36+
}
37+
38+
/// The environment name for UI purposes
39+
pub fn name(&self) -> &str {
40+
&self.name
41+
}
42+
43+
/// Returns true if the given trigger type can run in this environment.
44+
pub fn supports_trigger_type(&self, trigger_type: &TriggerType) -> bool {
45+
self.unknown_trigger.allows(trigger_type) || self.trigger_worlds.contains_key(trigger_type)
46+
}
47+
48+
/// Lists all worlds supported for the given trigger type in this environment.
49+
pub fn worlds(&self, trigger_type: &TriggerType) -> &CandidateWorlds {
50+
self.trigger_worlds
51+
.get(trigger_type)
52+
.or_else(|| self.unknown_trigger.worlds())
53+
.unwrap_or(NO_WORLDS)
54+
}
55+
}
56+
57+
/// How a `TargetEnvironment` should validate components associated with trigger types
58+
/// not listed in the/ environment definition. This is used for best-effort validation in
59+
/// extensible environments.
60+
///
61+
/// For example, a "forgiving" definition of Spin CLI environment would
62+
/// validate that components associated with `cron` or `sqs` triggers adhere
63+
/// to the platform world, even though it cannot validate that the exports are correct
64+
/// or that the plugins are installed or up to date. This can result in failure at
65+
/// runtime, but that may be better than refusing to let cron jobs run!
66+
///
67+
/// On the other hand, the SpinKube environment rejects unknown triggers
68+
/// because SpinKube does not allow arbitrary triggers to be linked at
69+
/// runtime: the set of triggers is static for a given version.
70+
enum UnknownTrigger {
71+
/// Components for unknown trigger types fail validation.
72+
Deny,
73+
/// Components for unknown trigger types pass validation if they
74+
/// conform to (at least) one of the listed worlds.
75+
Allow(CandidateWorlds),
76+
}
77+
78+
impl UnknownTrigger {
79+
fn allows(&self, _trigger_type: &TriggerType) -> bool {
80+
matches!(self, Self::Allow(_))
81+
}
82+
83+
fn worlds(&self) -> Option<&CandidateWorlds> {
84+
match self {
85+
Self::Deny => None,
86+
Self::Allow(cw) => Some(cw),
87+
}
88+
}
89+
}
90+
91+
/// The set of worlds that a particular trigger type (in a given environment)
92+
/// can accept. For example, the Spin 3.2 CLI `http` trigger accepts various
93+
/// versions of the `spin:up/http-trigger` world.
94+
///
95+
/// A component will pass target validation if it conforms to
96+
/// at least one of these worlds.
97+
#[derive(Default)]
98+
pub struct CandidateWorlds {
99+
worlds: Vec<CandidateWorld>,
100+
}
101+
102+
impl<'a> IntoIterator for &'a CandidateWorlds {
103+
type Item = &'a CandidateWorld;
104+
105+
type IntoIter = std::slice::Iter<'a, CandidateWorld>;
106+
107+
fn into_iter(self) -> Self::IntoIter {
108+
self.worlds.iter()
109+
}
110+
}
111+
112+
const NO_WORLDS: &CandidateWorlds = &CandidateWorlds { worlds: vec![] };
113+
114+
/// A WIT world; specifically, a WIT world provided by a Spin host, against which
115+
/// a component can be validated.
116+
pub struct CandidateWorld {
117+
world: WorldName,
118+
package: wit_parser::Package,
119+
package_bytes: Vec<u8>,
120+
}
121+
122+
impl std::fmt::Display for CandidateWorld {
123+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124+
self.world.fmt(f)
125+
}
126+
}
127+
128+
impl CandidateWorld {
129+
/// Namespaced but unversioned package name (e.g. spin:up)
130+
pub fn package_namespaced_name(&self) -> String {
131+
format!("{}:{}", self.package.name.namespace, self.package.name.name)
132+
}
133+
134+
/// The package version for the environment package.
135+
pub fn package_version(&self) -> Option<&semver::Version> {
136+
self.package.name.version.as_ref()
137+
}
138+
139+
/// The Wasm-encoded bytes of the environment package.
140+
pub fn package_bytes(&self) -> &[u8] {
141+
&self.package_bytes
142+
}
143+
144+
fn from_package_bytes(world: &WorldName, bytes: Vec<u8>) -> anyhow::Result<Self> {
145+
let decoded = wit_component::decode(&bytes)
146+
.with_context(|| format!("Failed to decode package for environment {world}"))?;
147+
let package_id = decoded.package();
148+
let package = decoded
149+
.resolve()
150+
.packages
151+
.get(package_id)
152+
.with_context(|| {
153+
format!("The {world} package is invalid (no package for decoded package ID)")
154+
})?
155+
.clone();
156+
157+
Ok(Self {
158+
world: world.to_owned(),
159+
package,
160+
package_bytes: bytes,
161+
})
162+
}
163+
164+
fn from_decoded_wasm(
165+
world: &WorldName,
166+
source: &Path,
167+
decoded: wit_parser::decoding::DecodedWasm,
168+
) -> anyhow::Result<Self> {
169+
let package_id = decoded.package();
170+
let package = decoded
171+
.resolve()
172+
.packages
173+
.get(package_id)
174+
.with_context(|| {
175+
format!(
176+
"The {} environment is invalid (no package for decoded package ID)",
177+
quoted_path(source)
178+
)
179+
})?
180+
.clone();
181+
182+
let bytes = wit_component::encode(decoded.resolve(), package_id)?;
183+
184+
Ok(Self {
185+
world: world.to_owned(),
186+
package,
187+
package_bytes: bytes,
188+
})
189+
}
190+
}
191+
192+
pub type TriggerType = String;
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! Environment definition types and serialisation (TOML) formats
2+
//!
3+
//! This module does *not* cover loading those definitions from remote
4+
//! sources, or materialising WIT packages from files or registry references -
5+
//! only the types.
6+
7+
use std::collections::HashMap;
8+
9+
use anyhow::Context;
10+
11+
/// An environment definition, usually deserialised from a TOML document.
12+
/// Example:
13+
///
14+
/// ```
15+
/// # spin-up.3.2.toml
16+
/// [triggers]
17+
/// http = ["spin:up/http-trigger@3.2.0", "spin:up/http-trigger-rc20231018@3.2.0"]
18+
/// redis = ["spin:up/redis-trigger@3.2.0"]
19+
/// ```
20+
#[derive(Debug, serde::Deserialize)]
21+
#[serde(deny_unknown_fields)]
22+
pub struct EnvironmentDefinition {
23+
triggers: HashMap<String, Vec<WorldRef>>,
24+
default: Option<Vec<WorldRef>>,
25+
}
26+
27+
impl EnvironmentDefinition {
28+
pub fn triggers(&self) -> &HashMap<String, Vec<WorldRef>> {
29+
&self.triggers
30+
}
31+
32+
pub fn default(&self) -> Option<&Vec<WorldRef>> {
33+
self.default.as_ref()
34+
}
35+
}
36+
37+
/// A reference to a world in an [EnvironmentDefinition]. This is formed
38+
/// of a fully qualified (ns:pkg/id) world name, optionally with
39+
/// a location from which to get the package (a registry or WIT directory).
40+
#[derive(Clone, Debug, serde::Deserialize)]
41+
#[serde(untagged, deny_unknown_fields)]
42+
pub enum WorldRef {
43+
DefaultRegistry(WorldName),
44+
Registry {
45+
registry: String,
46+
world: WorldName,
47+
},
48+
WitDirectory {
49+
path: std::path::PathBuf,
50+
world: WorldName,
51+
},
52+
}
53+
54+
/// The qualified name of a world, e.g. spin:up/http-trigger@3.2.0.
55+
///
56+
/// (Internally it is represented as a PackageName plus unqualified
57+
/// world name, but it stringises to the standard WIT qualified name.)
58+
#[derive(Clone, Debug, serde::Deserialize)]
59+
#[serde(try_from = "String")]
60+
pub struct WorldName {
61+
package: wit_parser::PackageName,
62+
world: String,
63+
}
64+
65+
impl WorldName {
66+
pub fn package(&self) -> &wit_parser::PackageName {
67+
&self.package
68+
}
69+
70+
pub fn package_namespaced_name(&self) -> String {
71+
format!("{}:{}", self.package.namespace, self.package.name)
72+
}
73+
74+
pub fn package_ref(&self) -> anyhow::Result<wasm_pkg_client::PackageRef> {
75+
let pkg_name = self.package_namespaced_name();
76+
pkg_name
77+
.parse()
78+
.with_context(|| format!("Environment {pkg_name} is not a valid package name"))
79+
}
80+
81+
pub fn package_version(&self) -> Option<&semver::Version> {
82+
self.package.version.as_ref()
83+
}
84+
}
85+
86+
impl TryFrom<String> for WorldName {
87+
type Error = anyhow::Error;
88+
89+
fn try_from(value: String) -> Result<Self, Self::Error> {
90+
use wasmparser::names::{ComponentName, ComponentNameKind};
91+
92+
// World qnames have the same syntactic form as interface qnames
93+
let parsed = ComponentName::new(&value, 0)?;
94+
let ComponentNameKind::Interface(itf) = parsed.kind() else {
95+
anyhow::bail!("{value} is not a well-formed world name");
96+
};
97+
98+
let package = wit_parser::PackageName {
99+
namespace: itf.namespace().to_string(),
100+
name: itf.package().to_string(),
101+
version: itf.version(),
102+
};
103+
104+
let world = itf.interface().to_string();
105+
106+
Ok(Self { package, world })
107+
}
108+
}
109+
110+
impl std::fmt::Display for WorldName {
111+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112+
f.write_str(&self.package.namespace)?;
113+
f.write_str(":")?;
114+
f.write_str(&self.package.name)?;
115+
f.write_str("/")?;
116+
f.write_str(&self.world)?;
117+
118+
if let Some(v) = self.package.version.as_ref() {
119+
f.write_str("@")?;
120+
f.write_str(&v.to_string())?;
121+
}
122+
123+
Ok(())
124+
}
125+
}

0 commit comments

Comments
 (0)