Skip to content

Commit 4576c80

Browse files
committed
Dependencies by component ref but ugh
Signed-off-by: itowlson <[email protected]>
1 parent e4a1600 commit 4576c80

File tree

6 files changed

+131
-5
lines changed

6 files changed

+131
-5
lines changed

crates/build/src/manifest.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub async fn component_build_configs(manifest_file: impl AsRef<Path>) -> Result<
7070
let manifest = spin_manifest::manifest_from_file(&manifest_file);
7171
match manifest {
7272
Ok(mut manifest) => {
73-
spin_manifest::normalize::normalize_manifest(&mut manifest);
73+
spin_manifest::normalize::normalize_manifest(&mut manifest)?;
7474
let components = build_configs_from_manifest(&manifest);
7575
let deployment_targets = deployment_targets_from_manifest(&manifest);
7676
Ok(ManifestBuildInfo::Loadable {

crates/environments/src/loader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ impl spin_compose::DependencyLike for WrappedComponentDependency {
272272
spin_manifest::schema::v2::ComponentDependency::Package { export, .. } => export,
273273
spin_manifest::schema::v2::ComponentDependency::Local { export, .. } => export,
274274
spin_manifest::schema::v2::ComponentDependency::HTTP { export, .. } => export,
275+
spin_manifest::schema::v2::ComponentDependency::AppComponent { export, .. } => export,
275276
}
276277
}
277278
}

crates/loader/src/local.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl LocalLoader {
7373

7474
// Load the given manifest into a LockedApp, ready for execution.
7575
pub(crate) async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
76-
spin_manifest::normalize::normalize_manifest(&mut manifest);
76+
spin_manifest::normalize::normalize_manifest(&mut manifest)?;
7777

7878
manifest.validate_dependencies()?;
7979

@@ -867,6 +867,9 @@ impl WasmLoader {
867867
let content = self.load_http_source(&url, &digest).await?;
868868
Ok((content, export))
869869
}
870+
v2::ComponentDependency::AppComponent { .. } => {
871+
panic!("should have been normalised away by this point");
872+
}
870873
}
871874
}
872875
}

crates/manifest/src/normalize.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
//! Manifest normalization functions.
22
3-
use std::collections::HashSet;
3+
use std::{collections::HashSet, path::PathBuf};
44

55
use crate::schema::v2::{AppManifest, ComponentSpec, KebabId};
6+
use anyhow::Context;
67

78
/// Normalizes some optional [`AppManifest`] features into a canonical form:
89
/// - Inline components in trigger configs are moved into top-level
910
/// components and replaced with a reference.
1011
/// - Any triggers without an ID are assigned a generated ID.
11-
pub fn normalize_manifest(manifest: &mut AppManifest) {
12+
pub fn normalize_manifest(manifest: &mut AppManifest) -> anyhow::Result<()> {
1213
normalize_trigger_ids(manifest);
1314
normalize_inline_components(manifest);
15+
normalize_dependency_component_refs(manifest)?;
16+
Ok(())
1417
}
1518

1619
fn normalize_inline_components(manifest: &mut AppManifest) {
@@ -103,3 +106,99 @@ fn normalize_trigger_ids(manifest: &mut AppManifest) {
103106
}
104107
}
105108
}
109+
110+
use crate::schema::v2::{Component, ComponentDependency, ComponentSource};
111+
112+
fn normalize_dependency_component_refs(manifest: &mut AppManifest) -> anyhow::Result<()> {
113+
// `clone` a snapshot, because we are about to mutate collection elements,
114+
// and the borrow checker gets mad at us if we try to index into the collection
115+
// while that's happening.
116+
let components = manifest.components.clone();
117+
118+
for (depender_id, component) in &mut manifest.components {
119+
for dependency in component.dependencies.inner.values_mut() {
120+
if let ComponentDependency::AppComponent {
121+
component: depended_on_id,
122+
export,
123+
} = dependency
124+
{
125+
let depended_on = components
126+
.get(depended_on_id)
127+
.with_context(|| format!("dependency ID {depended_on_id} does not exist"))?;
128+
ensure_is_acceptable_dependency(depended_on, depended_on_id, depender_id)?;
129+
*dependency = component_source_to_dependency(&depended_on.source, export.clone());
130+
}
131+
}
132+
}
133+
134+
Ok(())
135+
}
136+
137+
fn component_source_to_dependency(
138+
source: &ComponentSource,
139+
export: Option<String>,
140+
) -> ComponentDependency {
141+
match source {
142+
ComponentSource::Local(path) => ComponentDependency::Local {
143+
path: PathBuf::from(path),
144+
export,
145+
},
146+
ComponentSource::Remote { url, digest } => ComponentDependency::HTTP {
147+
url: url.clone(),
148+
digest: digest.clone(),
149+
export,
150+
},
151+
ComponentSource::Registry {
152+
registry,
153+
package,
154+
version,
155+
} => ComponentDependency::Package {
156+
version: version.clone(),
157+
registry: registry.as_ref().map(|r| r.to_string()),
158+
package: Some(package.to_string()),
159+
export,
160+
},
161+
}
162+
}
163+
164+
/// If a dependency has things like files or KV stores or network access...
165+
/// those won't apply when it's composed, and that's likely to be surprising,
166+
/// and developers hate surprises.
167+
fn ensure_is_acceptable_dependency(
168+
component: &Component,
169+
depended_on_id: &KebabId,
170+
depender_id: &KebabId,
171+
) -> anyhow::Result<()> {
172+
let mut surprises = vec![];
173+
174+
if !component.ai_models.is_empty() {
175+
surprises.push("ai_models");
176+
}
177+
if !component.allowed_outbound_hosts.is_empty() {
178+
surprises.push("allowed_outbound_hosts");
179+
}
180+
if !component.dependencies.inner.is_empty() {
181+
surprises.push("dependencies");
182+
}
183+
if !component.environment.is_empty() {
184+
surprises.push("environment");
185+
}
186+
if !component.files.is_empty() {
187+
surprises.push("files");
188+
}
189+
if !component.key_value_stores.is_empty() {
190+
surprises.push("key_value_stores");
191+
}
192+
if !component.sqlite_databases.is_empty() {
193+
surprises.push("sqlite_databases");
194+
}
195+
if !component.variables.is_empty() {
196+
surprises.push("variables");
197+
}
198+
199+
if surprises.is_empty() {
200+
Ok(())
201+
} else {
202+
anyhow::bail!("Dependencies may not have their own resources or permissions. Component {depended_on_id} cannot be used as a dependency of {depender_id} because it specifies: {}", surprises.join(", "));
203+
}
204+
}

crates/manifest/src/schema/v2.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ impl TryFrom<toml::Value> for ComponentSpec {
187187
///
188188
/// Example: `"my:dependency" = { path = "path/to/component.wasm", export = "my-export" }`
189189
///
190+
/// - A component in the application. The referenced component binary is composed: additional
191+
/// configuration such as files, networking, storage, etc. are ignored. This is intended
192+
/// primarily as a convenience for including dependencies in the manifest so that they
193+
/// can be built using `spin build`.
194+
///
195+
/// Example: `"my:dependency" = { component_id = "my-dependency", export = "my-export" }`
196+
///
190197
/// - A package from an HTTP URL.
191198
///
192199
/// Example: `"my:import" = { url = "https://example.com/component.wasm", sha256 = "sha256:..." }`
@@ -268,6 +275,22 @@ pub enum ComponentDependency {
268275
/// Learn more: https://spinframework.dev/writing-apps#dependencies-from-a-url
269276
export: Option<String>,
270277
},
278+
/// `... = { component_id = "my-dependency" }`
279+
#[schemars(description = "")] // schema docs are on the parent
280+
AppComponent {
281+
/// The ID of the component which implements the dependency.
282+
///
283+
/// Example: `"my:dep/import" = { component = "my-dependency" }`
284+
///
285+
/// Learn more: https://spinframework.dev/writing-apps#using-component-dependencies
286+
component: KebabId,
287+
/// The name of the export in the package. If omitted, this defaults to the name of the import.
288+
///
289+
/// Example: `"my:dep/import" = { export = "your:impl/export", component = "my-dependency" }`
290+
///
291+
/// Learn more: https://spinframework.dev/writing-apps#using-component-dependencies
292+
export: Option<String>,
293+
},
271294
}
272295

273296
/// A Spin component.

crates/manifest/tests/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@ fn run_v1_to_v2_test(input: &Path) -> Result<String, Failed> {
4545

4646
fn run_normalization_test(input: impl AsRef<Path>) -> Result<String, Failed> {
4747
let mut manifest = spin_manifest::manifest_from_file(input)?;
48-
normalize_manifest(&mut manifest);
48+
normalize_manifest(&mut manifest)?;
4949
Ok(toml::to_string(&manifest).expect("serialization should work"))
5050
}

0 commit comments

Comments
 (0)