diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index e030d7b824..9bba932b27 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -207,6 +207,22 @@ impl<'a, L> App<'a, L> { .map(|locked| AppTrigger { app: self, locked }) } + /// Returns the trigger metadata for a specific trigger type. + pub fn get_trigger_metadata<'this, T: Deserialize<'this> + Default>( + &'this self, + trigger_type: &'a str, + ) -> Result { + Some(&self.locked.metadata["triggers"][trigger_type]) + .map(T::deserialize) + .transpose() + .map(|t| t.unwrap_or_default()) + .map_err(|err| { + Error::MetadataError(format!( + "invalid metadata value for {trigger_type:?}: {err:?}" + )) + }) + } + /// Returns an iterator of [`AppTrigger`]s defined for this app with /// the given `trigger_type`. pub fn triggers_with_type( diff --git a/crates/http/src/trigger.rs b/crates/http/src/trigger.rs index a8c713d00d..872546ef9c 100644 --- a/crates/http/src/trigger.rs +++ b/crates/http/src/trigger.rs @@ -2,13 +2,11 @@ use serde::{Deserialize, Serialize}; use spin_locked_app::MetadataKey; /// Http trigger metadata key -pub const METADATA_KEY: MetadataKey = MetadataKey::new("trigger"); +pub const METADATA_KEY: MetadataKey = MetadataKey::new("triggers.http"); #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Metadata { - // The type of trigger which should always been "http" in this case - pub r#type: String, // The based url #[serde(default = "default_base")] pub base: String, diff --git a/crates/redis/src/lib.rs b/crates/redis/src/lib.rs index e672ef5716..c3d034db61 100644 --- a/crates/redis/src/lib.rs +++ b/crates/redis/src/lib.rs @@ -8,14 +8,11 @@ use anyhow::{anyhow, Context, Result}; use futures::{future::join_all, StreamExt}; use redis::{Client, ConnectionLike}; use serde::{de::IgnoredAny, Deserialize, Serialize}; -use spin_app::MetadataKey; use spin_core::async_trait; use spin_trigger::{cli::NoArgs, TriggerAppEngine, TriggerExecutor}; use crate::spin::SpinRedisExecutor; -const TRIGGER_METADATA_KEY: MetadataKey = MetadataKey::new("trigger"); - pub(crate) type RuntimeData = (); pub(crate) type Store = spin_core::Store; @@ -44,7 +41,6 @@ pub struct RedisTriggerConfig { #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(deny_unknown_fields)] struct TriggerMetadata { - r#type: String, address: String, } @@ -56,7 +52,8 @@ impl TriggerExecutor for RedisTrigger { type RunConfig = NoArgs; async fn new(engine: TriggerAppEngine) -> Result { - let address = engine.app().require_metadata(TRIGGER_METADATA_KEY)?.address; + let meta = engine.trigger_metadata::()?; + let address = meta.address; let mut channel_components: HashMap> = HashMap::new(); diff --git a/crates/trigger-http/src/lib.rs b/crates/trigger-http/src/lib.rs index f898d40a84..d0fbcf1daa 100644 --- a/crates/trigger-http/src/lib.rs +++ b/crates/trigger-http/src/lib.rs @@ -93,10 +93,9 @@ impl TriggerExecutor for HttpTrigger { type RunConfig = CliArgs; async fn new(engine: TriggerAppEngine) -> Result { - let mut base = engine - .app() - .require_metadata(spin_http::trigger::METADATA_KEY)? - .base; + let meta = engine.trigger_metadata::()?; + + let mut base = meta.base; if !base.starts_with('/') { base = format!("/{base}"); } diff --git a/crates/trigger/src/lib.rs b/crates/trigger/src/lib.rs index 989e7541aa..694911bdca 100644 --- a/crates/trigger/src/lib.rs +++ b/crates/trigger/src/lib.rs @@ -281,6 +281,10 @@ impl TriggerAppEngine { self.app.borrowed() } + pub fn trigger_metadata(&self) -> spin_app::Result { + self.app().get_trigger_metadata(Executor::TRIGGER_TYPE) + } + /// Returns AppTriggers and typed TriggerConfigs for this executor type. pub fn trigger_configs(&self) -> impl Iterator { self.app() diff --git a/examples/spin-timer/src/lib.rs b/examples/spin-timer/src/lib.rs index 729a7c3df8..1177c36e27 100644 --- a/examples/spin-timer/src/lib.rs +++ b/examples/spin-timer/src/lib.rs @@ -45,7 +45,7 @@ pub struct TimerTriggerConfig { interval_secs: u64, } -const TRIGGER_METADATA_KEY: MetadataKey = MetadataKey::new("trigger"); +const TRIGGER_METADATA_KEY: MetadataKey = MetadataKey::new("triggers"); #[async_trait] impl TriggerExecutor for TimerTrigger { diff --git a/src/commands/up.rs b/src/commands/up.rs index 046ac06b27..be3f915ea2 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -16,6 +16,8 @@ use spin_oci::OciLoader; use spin_trigger::cli::{SPIN_LOCAL_APP_DIR, SPIN_LOCKED_URL, SPIN_WORKING_DIR}; use tempfile::TempDir; +use futures::future::join_all; + use crate::opts::*; use self::app_source::{AppSource, ResolvedAppSource}; @@ -154,7 +156,14 @@ impl UpCommand { .with_context(|| format!("Couldn't find trigger executor for {app_source}"))?; if self.help { - return self.run_trigger(trigger_cmd, None).await; + return join_all( + trigger_cmd + .iter() + .map(|cmd| async { self.run_trigger(cmd.clone(), None).await }), + ) + .await + .into_iter() + .collect(); } let mut locked_app = self @@ -165,13 +174,18 @@ impl UpCommand { let local_app_dir = app_source.local_app_dir().map(Into::into); - let run_opts = RunTriggerOpts { - locked_app, - working_dir, - local_app_dir, - }; - - self.run_trigger(trigger_cmd, Some(run_opts)).await + join_all(trigger_cmd.iter().map(|cmd| { + let run_opts = RunTriggerOpts { + locked_app: locked_app.clone(), + working_dir: working_dir.clone(), + local_app_dir: local_app_dir.clone(), + }; + + self.run_trigger(cmd.clone(), Some(run_opts)) + })) + .await + .into_iter() + .collect() } fn get_canonical_working_dir(&self) -> Result { @@ -191,7 +205,7 @@ impl UpCommand { } async fn run_trigger( - self, + &self, trigger_cmd: Vec, opts: Option, ) -> Result<(), anyhow::Error> { @@ -235,7 +249,7 @@ impl UpCommand { })?; } - let status = child.wait()?; + let status = tokio::task::spawn(async move { child.wait() }).await??; if status.success() { Ok(()) } else { @@ -424,16 +438,20 @@ fn trigger_command(trigger_type: &str) -> Vec { vec!["trigger".to_owned(), trigger_type.to_owned()] } -fn trigger_command_for_resolved_app_source(resolved: &ResolvedAppSource) -> Result> { +fn trigger_command_for_resolved_app_source( + resolved: &ResolvedAppSource, +) -> Result>> { let trigger_type = resolved.trigger_type()?; - - match trigger_type { - "http" | "redis" => Ok(trigger_command(trigger_type)), - _ => { - let cmd = resolve_trigger_plugin(trigger_type)?; - Ok(vec![cmd]) - } - } + trigger_type + .iter() + .map(|&t| match t { + "http" | "redis" => Ok(trigger_command(t)), + _ => { + let cmd = resolve_trigger_plugin(t)?; + Ok(vec![cmd]) + } + }) + .collect() } #[cfg(test)] diff --git a/src/commands/up/app_source.rs b/src/commands/up/app_source.rs index 3a6b7ae23f..77cb243442 100644 --- a/src/commands/up/app_source.rs +++ b/src/commands/up/app_source.rs @@ -83,7 +83,7 @@ pub enum ResolvedAppSource { } impl ResolvedAppSource { - pub fn trigger_type(&self) -> anyhow::Result<&str> { + pub fn trigger_type(&self) -> anyhow::Result> { let types = match self { ResolvedAppSource::File { manifest, .. } => { manifest.triggers.keys().collect::>() @@ -96,7 +96,6 @@ impl ResolvedAppSource { }; ensure!(!types.is_empty(), "no triggers in app"); - ensure!(types.len() == 1, "multiple trigger types not yet supported"); - Ok(types.into_iter().next().unwrap()) + Ok(types.into_iter().map(|t| t.as_str()).collect()) } }