Skip to main content

spin_trigger/
cli.rs

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