diff --git a/crates/loader/src/local/mod.rs b/crates/loader/src/local/mod.rs index 433f660f96..41be72717a 100644 --- a/crates/loader/src/local/mod.rs +++ b/crates/loader/src/local/mod.rs @@ -40,12 +40,20 @@ pub async fn from_file( app: impl AsRef, base_dst: Option>, bindle_connection: &Option, + include_components: Vec, ) -> Result { let app = absolutize(app)?; let manifest = raw_manifest_from_file(&app).await?; validate_raw_app_manifest(&manifest)?; - prepare_any_version(manifest, app, base_dst, bindle_connection).await + prepare_any_version( + manifest, + app, + base_dst, + bindle_connection, + include_components, + ) + .await } /// Reads the spin.toml file as a raw manifest. @@ -93,9 +101,12 @@ async fn prepare_any_version( src: impl AsRef, base_dst: Option>, bindle_connection: &Option, + include_components: Vec, ) -> Result { match raw { - RawAppManifestAnyVersion::V1(raw) => prepare(raw, src, base_dst, bindle_connection).await, + RawAppManifestAnyVersion::V1(raw) => { + prepare(raw, src, base_dst, bindle_connection, include_components).await + } } } @@ -125,12 +136,21 @@ pub fn validate_raw_app_manifest(raw: &RawAppManifestAnyVersion) -> Result<()> { Ok(()) } +fn include_component(components: &Vec, component_id: &String) -> bool { + if components.is_empty() { + true + } else { + components.contains(component_id) + } +} + /// Converts a raw application manifest into Spin configuration. async fn prepare( raw: RawAppManifest, src: impl AsRef, base_dst: Option>, bindle_connection: &Option, + include_components: Vec, ) -> Result { let info = info(raw.info, &src); @@ -138,13 +158,16 @@ async fn prepare( let component_triggers = raw .components - .iter() - .map(|c| (c.id.clone(), c.trigger.clone())) + .clone() + .into_iter() + .filter(|c| include_component(&include_components, &c.id)) + .map(|c| (c.id.clone(), c.trigger)) .collect(); let components = future::join_all( raw.components .into_iter() + .filter(|c| include_component(&include_components, &c.id)) .map(|c| async { core(c, &src, base_dst.as_ref(), bindle_connection).await }) .collect::>(), ) @@ -153,6 +176,16 @@ async fn prepare( .collect::>>() .context("Failed to prepare configuration")?; + if components.is_empty() { + match include_components.is_empty() { + true => bail!("No components found in manifest"), + false => bail!( + "No components found in manifest matching '{}'", + include_components.join(", "), + ), + } + } + let variables = raw .variables .into_iter() diff --git a/crates/loader/src/local/tests.rs b/crates/loader/src/local/tests.rs index 3aae011dc4..cb02a4753b 100644 --- a/crates/loader/src/local/tests.rs +++ b/crates/loader/src/local/tests.rs @@ -11,7 +11,7 @@ async fn test_from_local_source() -> Result<()> { let temp_dir = tempfile::tempdir()?; let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir), &None).await?; + let app = from_file(MANIFEST, Some(dir), &None, vec![]).await?; assert_eq!(app.info.name, "spin-local-source-test"); assert_eq!(app.info.version, "1.0.0"); @@ -157,7 +157,7 @@ async fn test_invalid_manifest() -> Result<()> { let temp_dir = tempfile::tempdir()?; let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir), &None).await; + let app = from_file(MANIFEST, Some(dir), &None, vec![]).await; let e = app.unwrap_err().to_string(); assert!( @@ -214,7 +214,7 @@ async fn test_duplicate_component_id_is_rejected() -> Result<()> { let temp_dir = tempfile::tempdir()?; let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir), &None).await; + let app = from_file(MANIFEST, Some(dir), &None, vec![]).await; assert!( app.is_err(), @@ -236,7 +236,7 @@ async fn test_insecure_allow_all_with_invalid_url() -> Result<()> { let temp_dir = tempfile::tempdir()?; let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir), &None).await; + let app = from_file(MANIFEST, Some(dir), &None, vec![]).await; assert!( app.is_ok(), @@ -252,7 +252,7 @@ async fn test_invalid_url_in_allowed_http_hosts_is_rejected() -> Result<()> { let temp_dir = tempfile::tempdir()?; let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir), &None).await; + let app = from_file(MANIFEST, Some(dir), &None, vec![]).await; assert!(app.is_err(), "Expected allowed_http_hosts parsing error"); @@ -268,3 +268,48 @@ async fn test_invalid_url_in_allowed_http_hosts_is_rejected() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_can_include_specific_components() -> Result<()> { + const MANIFEST: &str = "tests/valid-with-files/spin.toml"; + + let temp_dir = tempfile::tempdir()?; + let dir = temp_dir.path(); + let app = from_file(MANIFEST, Some(dir), &None, vec!["fs2".to_string()]).await; + + let _ = app.unwrap().components.len() == 1; + + let app = from_file( + MANIFEST, + Some(dir), + &None, + vec!["fs".to_string(), "fs2".to_string()], + ) + .await; + + let _ = app.unwrap().components.len() == 2; + + Ok(()) +} + +#[tokio::test] +async fn test_handles_unknown_include_component() -> Result<()> { + const MANIFEST: &str = "tests/valid-with-files/spin.toml"; + + let temp_dir = tempfile::tempdir()?; + let dir = temp_dir.path(); + let app = from_file( + MANIFEST, + Some(dir), + &None, + vec!["random_fake_component".to_string()], + ) + .await; + + assert!( + app.is_err(), + "No components found in manifest matching 'random_fake_component'" + ); + + Ok(()) +} diff --git a/crates/loader/tests/valid-manifest.toml b/crates/loader/tests/valid-manifest.toml index fb26c55073..1d7d4973cc 100644 --- a/crates/loader/tests/valid-manifest.toml +++ b/crates/loader/tests/valid-manifest.toml @@ -2,32 +2,42 @@ spin_version = "1" authors = ["Gul Madred", "Edward Jellico", "JL"] description = "A simple application that returns the number of lights" name = "chain-of-command" -trigger = {type = "http", base = "/"} +trigger = { type = "http", base = "/" } version = "6.11.2" [[component]] -files = ["file.txt", { source = "valid-with-files", destination = "/vwf" }, "subdir/another.txt"] +files = [ + "file.txt", + { source = "valid-with-files", destination = "/vwf" }, + "subdir/another.txt" +] id = "four-lights" source = "path/to/wasm/file.wasm" + [component.trigger] -executor = {type = "spin"} +executor = { type = "spin" } route = "/lights" + [component.environment] env1 = "first" env2 = "second" [[component]] id = "abc" + [component.source] parcel = "parcel" reference = "bindle reference" + [component.trigger] route = "/test" [[component]] id = "web" + [component.source] url = "https://example.com/wasm.wasm.wasm" digest = "sha256:12345" + [component.trigger] route = "/dont/test" diff --git a/crates/loader/tests/valid-with-files/spin.toml b/crates/loader/tests/valid-with-files/spin.toml index 5524ff0ae2..c5d3004caa 100644 --- a/crates/loader/tests/valid-with-files/spin.toml +++ b/crates/loader/tests/valid-with-files/spin.toml @@ -1,7 +1,7 @@ spin_version = "1" authors = ["Fermyon Engineering "] name = "spin-local-source-test" -trigger = {type = "http", base = "/"} +trigger = { type = "http", base = "/" } version = "1.0.0" [[component]] @@ -10,5 +10,14 @@ id = "fs" source = "spin-fs.wasm" [component.trigger] -executor = {type = "spin"} +executor = { type = "spin" } route = "/..." + +[[component]] +files = ["**/*"] +id = "fs2" +source = "spin-fs.wasm" + +[component.trigger] +executor = { type = "spin" } +route = "/fs2" diff --git a/crates/trigger/src/locked.rs b/crates/trigger/src/locked.rs index e485531cf7..fe0bd67a67 100644 --- a/crates/trigger/src/locked.rs +++ b/crates/trigger/src/locked.rs @@ -235,7 +235,7 @@ mod tests { std::fs::write("spin.toml", TEST_MANIFEST).expect("write manifest"); std::fs::write("test-source.wasm", "not actual wasm").expect("write source"); std::fs::write("static.txt", "content").expect("write static"); - let app = spin_loader::local::from_file("spin.toml", Some(&tempdir), &None) + let app = spin_loader::local::from_file("spin.toml", Some(&tempdir), &None, vec![]) .await .expect("load app"); (app, tempdir) diff --git a/src/commands/up.rs b/src/commands/up.rs index a9c4f8550f..c8cb7bece7 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -98,6 +98,14 @@ pub struct UpCommand { /// All other args, to be passed through to the trigger #[clap(hide = true)] pub trigger_args: Vec, + + /// Only run a subset of the components + #[clap( + long = "include_components", + short = 'c', + conflicts_with = "BINDLE_ID_OPT" + )] + pub include_components: Vec, } impl UpCommand { @@ -135,12 +143,20 @@ impl UpCommand { .as_deref() .unwrap_or_else(|| DEFAULT_MANIFEST_FILE.as_ref()); let bindle_connection = self.bindle_connection(); + let asset_dst = if self.direct_mounts { None } else { Some(&working_dir) }; - spin_loader::from_file(manifest_file, asset_dst, &bindle_connection).await? + + spin_loader::from_file( + manifest_file, + asset_dst, + &bindle_connection, + self.include_components.clone(), + ) + .await? } (None, Some(bindle)) => match &self.server { Some(server) => {