spin_trigger/
cli.rs

1mod initial_kv_setter;
2mod launch_metadata;
3mod max_instance_memory;
4mod sqlite_statements;
5mod stdio;
6mod summary;
7
8use std::path::PathBuf;
9use std::{future::Future, sync::Arc};
10
11use anyhow::{Context, Result};
12use clap::{Args, IntoApp, Parser};
13use spin_app::App;
14use spin_common::sloth;
15use spin_common::ui::quoted_path;
16use spin_common::url::parse_file_url;
17use spin_factors::RuntimeFactors;
18use spin_factors_executor::{ComponentLoader, FactorsExecutor};
19
20use crate::{loader::ComponentLoader as ComponentLoaderImpl, Trigger, TriggerApp};
21pub use initial_kv_setter::InitialKvSetterHook;
22pub use launch_metadata::LaunchMetadata;
23pub use max_instance_memory::MaxInstanceMemoryHook;
24pub use sqlite_statements::SqlStatementExecutorHook;
25use stdio::FollowComponents;
26pub use stdio::StdioLoggingExecutorHooks;
27pub use summary::{KeyValueDefaultStoreSummaryHook, SqliteDefaultStoreSummaryHook};
28
29pub const APP_LOG_DIR: &str = "APP_LOG_DIR";
30pub const DISABLE_WASMTIME_CACHE: &str = "DISABLE_WASMTIME_CACHE";
31pub const FOLLOW_LOG_OPT: &str = "FOLLOW_ID";
32pub const WASMTIME_CACHE_FILE: &str = "WASMTIME_CACHE_FILE";
33pub const RUNTIME_CONFIG_FILE: &str = "RUNTIME_CONFIG_FILE";
34
35// Set by `spin up`
36pub const SPIN_LOCKED_URL: &str = "SPIN_LOCKED_URL";
37pub const SPIN_LOCAL_APP_DIR: &str = "SPIN_LOCAL_APP_DIR";
38pub const SPIN_WORKING_DIR: &str = "SPIN_WORKING_DIR";
39
40/// A command that runs a TriggerExecutor.
41#[derive(Parser, Debug)]
42#[clap(
43    usage = "spin [COMMAND] [OPTIONS]",
44    next_help_heading = help_heading::<T, B::Factors>()
45)]
46pub struct FactorsTriggerCommand<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> {
47    /// Log directory for the stdout and stderr of components. Setting to
48    /// the empty string disables logging to disk.
49    #[clap(
50        name = APP_LOG_DIR,
51        short = 'L',
52        long = "log-dir",
53        env = "SPIN_LOG_DIR",
54    )]
55    pub log: Option<PathBuf>,
56
57    /// Disable Wasmtime cache.
58    #[clap(
59        name = DISABLE_WASMTIME_CACHE,
60        long = "disable-cache",
61        env = DISABLE_WASMTIME_CACHE,
62        conflicts_with = WASMTIME_CACHE_FILE,
63        takes_value = false,
64    )]
65    pub disable_cache: bool,
66
67    /// Wasmtime cache configuration file.
68    #[clap(
69        name = WASMTIME_CACHE_FILE,
70        long = "cache",
71        env = WASMTIME_CACHE_FILE,
72        conflicts_with = DISABLE_WASMTIME_CACHE,
73    )]
74    pub cache: Option<PathBuf>,
75
76    /// Disable Wasmtime's pooling instance allocator.
77    #[clap(long = "disable-pooling")]
78    pub disable_pooling: bool,
79
80    /// Print output to stdout/stderr only for given component(s)
81    #[clap(
82        name = FOLLOW_LOG_OPT,
83        long = "follow",
84        multiple_occurrences = true,
85    )]
86    pub follow_components: Vec<String>,
87
88    /// Silence all component output to stdout/stderr
89    #[clap(
90        long = "quiet",
91        short = 'q',
92        aliases = &["sh", "shush"],
93        conflicts_with = FOLLOW_LOG_OPT,
94        )]
95    pub silence_component_logs: bool,
96
97    /// Configuration file for config providers and wasmtime config.
98    #[clap(
99        name = RUNTIME_CONFIG_FILE,
100        long = "runtime-config-file",
101        env = RUNTIME_CONFIG_FILE,
102    )]
103    pub runtime_config_file: Option<PathBuf>,
104
105    /// Set the application state directory path. This is used in the default
106    /// locations for logs, key value stores, etc.
107    ///
108    /// For local apps, this defaults to `.spin/` relative to the `spin.toml` file.
109    /// For remote apps, this has no default (unset).
110    /// Passing an empty value forces the value to be unset.
111    #[clap(long)]
112    pub state_dir: Option<String>,
113
114    #[clap(flatten)]
115    pub trigger_args: T::CliArgs,
116
117    #[clap(flatten)]
118    pub builder_args: B::CliArgs,
119
120    #[clap(long = "help-args-only", hide = true)]
121    pub help_args_only: bool,
122
123    #[clap(long = "launch-metadata-only", hide = true)]
124    pub launch_metadata_only: bool,
125}
126
127/// Configuration options that are common to all triggers.
128#[derive(Debug, Default)]
129pub struct FactorsConfig {
130    /// The Spin working directory.
131    pub working_dir: PathBuf,
132    /// Path to the runtime config file.
133    pub runtime_config_file: Option<PathBuf>,
134    /// Path to the state directory.
135    pub state_dir: UserProvidedPath,
136    /// Path to the local app directory.
137    pub local_app_dir: Option<String>,
138    /// Which components should have their logs followed.
139    pub follow_components: FollowComponents,
140    /// Log directory for component stdout/stderr.
141    pub log_dir: UserProvidedPath,
142}
143
144/// An empty implementation of clap::Args to be used as TriggerExecutor::RunConfig
145/// for executors that do not need additional CLI args.
146#[derive(Args)]
147pub struct NoCliArgs;
148
149impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> FactorsTriggerCommand<T, B> {
150    /// Create a new TriggerExecutorBuilder from this TriggerExecutorCommand.
151    pub async fn run(self) -> Result<()> {
152        // Handle --help-args-only
153        if self.help_args_only {
154            Self::command()
155                .disable_help_flag(true)
156                .help_template("{all-args}")
157                .print_long_help()?;
158            return Ok(());
159        }
160
161        // Handle --launch-metadata-only
162        if self.launch_metadata_only {
163            let lm = LaunchMetadata::infer::<T, B>();
164            let json = serde_json::to_string_pretty(&lm)?;
165            eprintln!("{json}");
166            return Ok(());
167        }
168
169        // Required env vars
170        let working_dir = std::env::var(SPIN_WORKING_DIR).context(SPIN_WORKING_DIR)?;
171        let locked_url = std::env::var(SPIN_LOCKED_URL).context(SPIN_LOCKED_URL)?;
172        let local_app_dir = std::env::var(SPIN_LOCAL_APP_DIR).ok();
173
174        let follow_components = self.follow_components();
175
176        // Load App
177        let app = {
178            let path = parse_file_url(&locked_url)?;
179            let contents = std::fs::read(&path)
180                .with_context(|| format!("failed to read manifest at {}", quoted_path(&path)))?;
181            let locked =
182                serde_json::from_slice(&contents).context("failed to parse app lock file JSON")?;
183            App::new(locked_url, locked)
184        };
185
186        // Validate required host features
187        if let Err(unmet) = app.ensure_needs_only(&T::supported_host_requirements()) {
188            anyhow::bail!("This application requires the following features that are not available in this version of the '{}' trigger: {unmet}", T::TYPE);
189        }
190
191        let trigger = T::new(self.trigger_args, &app)?;
192        let mut builder: TriggerAppBuilder<T, B> = TriggerAppBuilder::new(trigger);
193        let config = builder.engine_config();
194
195        // Apply --cache / --disable-cache
196        if !self.disable_cache {
197            config.enable_cache(&self.cache)?;
198        }
199
200        if self.disable_pooling {
201            config.disable_pooling();
202        }
203
204        let state_dir = match &self.state_dir {
205            // Make sure `--state-dir=""` unsets the state dir
206            Some(s) if s.is_empty() => UserProvidedPath::Unset,
207            Some(s) => UserProvidedPath::Provided(PathBuf::from(s)),
208            None => UserProvidedPath::Default,
209        };
210        let log_dir = match &self.log {
211            // Make sure `--log-dir=""` unsets the log dir
212            Some(p) if p.as_os_str().is_empty() => UserProvidedPath::Unset,
213            Some(p) => UserProvidedPath::Provided(p.clone()),
214            None => UserProvidedPath::Default,
215        };
216        let common_options = FactorsConfig {
217            working_dir: PathBuf::from(working_dir),
218            runtime_config_file: self.runtime_config_file.clone(),
219            state_dir,
220            local_app_dir: local_app_dir.clone(),
221            follow_components,
222            log_dir,
223        };
224
225        let run_fut = builder
226            .run(
227                app,
228                common_options,
229                self.builder_args,
230                &ComponentLoaderImpl::new(),
231            )
232            .await?;
233
234        let (abortable, abort_handle) = futures::future::abortable(run_fut);
235        ctrlc::set_handler(move || abort_handle.abort())?;
236        match abortable.await {
237            Ok(Ok(())) => {
238                tracing::info!("Trigger executor shut down: exiting");
239                Ok(())
240            }
241            Ok(Err(err)) => {
242                tracing::error!("Trigger executor failed");
243                Err(err)
244            }
245            Err(_aborted) => {
246                tracing::info!("User requested shutdown: exiting");
247                Ok(())
248            }
249        }
250    }
251
252    fn follow_components(&self) -> FollowComponents {
253        if self.silence_component_logs {
254            FollowComponents::None
255        } else if self.follow_components.is_empty() {
256            FollowComponents::All
257        } else {
258            let followed = self.follow_components.clone().into_iter().collect();
259            FollowComponents::Named(followed)
260        }
261    }
262}
263
264const SLOTH_WARNING_DELAY_MILLIS: u64 = 1250;
265
266fn warn_if_wasm_build_slothful() -> sloth::SlothGuard {
267    #[cfg(debug_assertions)]
268    let message = "\
269        This is a debug build - preparing Wasm modules might take a few seconds\n\
270        If you're experiencing long startup times please switch to the release build";
271
272    #[cfg(not(debug_assertions))]
273    let message = "Preparing Wasm modules is taking a few seconds...";
274
275    sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
276}
277
278fn help_heading<T: Trigger<F>, F: RuntimeFactors>() -> Option<&'static str> {
279    if T::TYPE == <help::HelpArgsOnlyTrigger as Trigger<F>>::TYPE {
280        Some("TRIGGER OPTIONS")
281    } else {
282        let heading = format!("{} TRIGGER OPTIONS", T::TYPE.to_uppercase());
283        let as_str = Box::new(heading).leak();
284        Some(as_str)
285    }
286}
287
288/// A builder for a [`TriggerApp`].
289pub struct TriggerAppBuilder<T, B> {
290    engine_config: spin_core::Config,
291    pub trigger: T,
292    _factors_builder: std::marker::PhantomData<B>,
293}
294
295impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> TriggerAppBuilder<T, B> {
296    pub fn new(trigger: T) -> Self {
297        Self {
298            engine_config: spin_core::Config::default(),
299            trigger,
300            _factors_builder: Default::default(),
301        }
302    }
303
304    pub fn engine_config(&mut self) -> &mut spin_core::Config {
305        &mut self.engine_config
306    }
307
308    /// Build a [`TriggerApp`] from the given [`App`] and options.
309    pub async fn build(
310        &mut self,
311        app: App,
312        common_options: FactorsConfig,
313        options: B::CliArgs,
314        loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
315    ) -> anyhow::Result<TriggerApp<T, B::Factors>> {
316        let mut core_engine_builder = {
317            self.trigger.update_core_config(&mut self.engine_config)?;
318
319            spin_core::Engine::builder(&self.engine_config)?
320        };
321        self.trigger.add_to_linker(core_engine_builder.linker())?;
322
323        let (factors, runtime_config) = B::build(&common_options, &options)?;
324
325        let mut executor = FactorsExecutor::new(core_engine_builder, factors)?;
326        B::configure_app(&mut executor, &runtime_config, &common_options, &options)?;
327        let executor = Arc::new(executor);
328
329        let configured_app = {
330            let _sloth_guard = warn_if_wasm_build_slothful();
331            executor
332                .load_app(app, runtime_config.into(), loader)
333                .await?
334        };
335
336        Ok(configured_app)
337    }
338
339    /// Run the [`TriggerApp`] with the given [`App`] and options.
340    pub async fn run(
341        mut self,
342        app: App,
343        common_options: FactorsConfig,
344        options: B::CliArgs,
345        loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
346    ) -> anyhow::Result<impl Future<Output = anyhow::Result<()>>> {
347        let configured_app = self.build(app, common_options, options, loader).await?;
348        Ok(self.trigger.run(configured_app))
349    }
350}
351
352/// A builder for runtime factors.
353pub trait RuntimeFactorsBuilder {
354    /// The factors type to build.
355    type Factors: RuntimeFactors;
356    /// CLI arguments not included in [`FactorsConfig`] needed  to build the [`RuntimeFactors`].
357    type CliArgs: clap::Args;
358    /// The wrapped runtime config type.
359    type RuntimeConfig: Into<<Self::Factors as RuntimeFactors>::RuntimeConfig>;
360
361    /// Build the factors and runtime config from the given options.
362    fn build(
363        config: &FactorsConfig,
364        args: &Self::CliArgs,
365    ) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)>;
366
367    /// Configure the factors in the executor.
368    fn configure_app<U: Send + 'static>(
369        executor: &mut FactorsExecutor<Self::Factors, U>,
370        runtime_config: &Self::RuntimeConfig,
371        config: &FactorsConfig,
372        args: &Self::CliArgs,
373    ) -> anyhow::Result<()> {
374        let _ = (executor, runtime_config, config, args);
375        Ok(())
376    }
377}
378
379pub mod help {
380    use super::*;
381
382    /// Null object to support --help-args-only in the absence of
383    /// a `spin.toml` file.
384    pub struct HelpArgsOnlyTrigger;
385
386    impl<F: RuntimeFactors> Trigger<F> for HelpArgsOnlyTrigger {
387        const TYPE: &'static str = "help-args-only";
388        type CliArgs = NoCliArgs;
389        type InstanceState = ();
390
391        fn new(_cli_args: Self::CliArgs, _app: &App) -> anyhow::Result<Self> {
392            Ok(Self)
393        }
394
395        async fn run(self, _configured_app: TriggerApp<Self, F>) -> anyhow::Result<()> {
396            Ok(())
397        }
398    }
399}
400
401/// A user provided option which be either be provided, default, or explicitly none.
402#[derive(Clone, Debug, Default)]
403pub enum UserProvidedPath {
404    /// Use the explicitly provided directory.
405    Provided(PathBuf),
406    /// Use the default.
407    #[default]
408    Default,
409    /// Explicitly unset.
410    Unset,
411}