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