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
147#[cfg(feature = "experimental-wasm-features")]
148#[derive(Clone, Debug, ValueEnum)]
149pub enum ExperimentalWasmFeature {
150    Gc,
151    ReferenceTypes,
152    Exceptions,
153    FunctionReferences,
154}
155
156/// Configuration options that are common to all triggers.
157#[derive(Debug, Default)]
158pub struct FactorsConfig {
159    /// The Spin working directory.
160    pub working_dir: PathBuf,
161    /// Path to the runtime config file.
162    pub runtime_config_file: Option<PathBuf>,
163    /// Path to the state directory.
164    pub state_dir: UserProvidedPath,
165    /// Path to the local app directory.
166    pub local_app_dir: Option<String>,
167    /// Which components should have their logs followed.
168    pub follow_components: FollowComponents,
169    /// Log directory for component stdout/stderr.
170    pub log_dir: UserProvidedPath,
171    /// If set, Spin truncates the log files before starting the application.
172    pub truncate_logs: bool,
173}
174
175/// An empty implementation of clap::Args to be used as TriggerExecutor::RunConfig
176/// for executors that do not need additional CLI args.
177#[derive(Args)]
178pub struct NoCliArgs;
179
180impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> FactorsTriggerCommand<T, B> {
181    /// Create a new TriggerExecutorBuilder from this TriggerExecutorCommand.
182    pub async fn run(self) -> Result<()> {
183        // Handle --help-args-only
184        if self.help_args_only {
185            Self::command()
186                .disable_help_flag(true)
187                .help_template("{all-args}")
188                .print_long_help()?;
189            return Ok(());
190        }
191
192        // Handle --launch-metadata-only
193        if self.launch_metadata_only {
194            let lm = LaunchMetadata::infer::<T, B>();
195            let json = serde_json::to_string_pretty(&lm)?;
196            eprintln!("{json}");
197            return Ok(());
198        }
199
200        // Required env vars
201        let working_dir = std::env::var(SPIN_WORKING_DIR).context(SPIN_WORKING_DIR)?;
202        let locked_url = std::env::var(SPIN_LOCKED_URL).context(SPIN_LOCKED_URL)?;
203        let local_app_dir = std::env::var(SPIN_LOCAL_APP_DIR).ok();
204
205        let follow_components = self.follow_components();
206
207        // Load App
208        let app = {
209            let path = parse_file_url(&locked_url)?;
210            let contents = std::fs::read(&path)
211                .with_context(|| format!("failed to read manifest at {}", quoted_path(&path)))?;
212            let locked =
213                serde_json::from_slice(&contents).context("failed to parse app lock file JSON")?;
214            App::new(locked_url, locked)
215        };
216
217        // Validate required host features
218        if let Err(unmet) = app.ensure_needs_only(T::TYPE, &T::supported_host_requirements()) {
219            anyhow::bail!(
220                "This application requires the following features that are not available in this version of the '{}' trigger: {unmet}",
221                T::TYPE
222            );
223        }
224
225        let trigger = T::new(self.trigger_args, &app)?;
226        let mut builder: TriggerAppBuilder<T, B> = TriggerAppBuilder::new(trigger);
227        let config = builder.engine_config();
228
229        // Apply --cache / --disable-cache
230        if !self.disable_cache {
231            config.enable_cache(&self.cache)?;
232        }
233
234        if self.disable_pooling {
235            config.disable_pooling();
236        }
237
238        if self.debug_info {
239            config.enable_debug_info();
240        }
241
242        #[cfg(feature = "experimental-wasm-features")]
243        {
244            let wasmtime_config = config.wasmtime_config();
245            for wasm_feature in self.experimental_wasm_feature {
246                match wasm_feature {
247                    ExperimentalWasmFeature::Gc => wasmtime_config.wasm_gc(true),
248                    ExperimentalWasmFeature::ReferenceTypes => {
249                        wasmtime_config.wasm_reference_types(true)
250                    }
251                    ExperimentalWasmFeature::Exceptions => wasmtime_config.wasm_exceptions(true),
252                    ExperimentalWasmFeature::FunctionReferences => {
253                        wasmtime_config.wasm_function_references(true)
254                    }
255                };
256            }
257        }
258
259        let state_dir = match &self.state_dir {
260            // Make sure `--state-dir=""` unsets the state dir
261            Some(s) if s.is_empty() => UserProvidedPath::Unset,
262            Some(s) => UserProvidedPath::Provided(PathBuf::from(s)),
263            None => UserProvidedPath::Default,
264        };
265        let log_dir = match &self.log {
266            // Make sure `--log-dir=""` unsets the log dir
267            Some(p) if p.as_os_str().is_empty() => UserProvidedPath::Unset,
268            Some(p) => UserProvidedPath::Provided(p.clone()),
269            None => UserProvidedPath::Default,
270        };
271        let common_options = FactorsConfig {
272            working_dir: PathBuf::from(working_dir),
273            runtime_config_file: self.runtime_config_file.clone(),
274            state_dir,
275            local_app_dir: local_app_dir.clone(),
276            follow_components,
277            log_dir,
278            truncate_logs: self.truncate_logs,
279        };
280
281        let loader = ComponentLoaderImpl::new();
282        let run_fut = builder
283            .run(app, common_options, self.builder_args, &loader)
284            .await?;
285
286        let (abortable, abort_handle) = futures::future::abortable(run_fut);
287        ctrlc::set_handler(move || abort_handle.abort())?;
288        match abortable.await {
289            Ok(Ok(())) => {
290                tracing::info!("Trigger executor shut down: exiting");
291                Ok(())
292            }
293            Ok(Err(err)) => {
294                tracing::error!("Trigger executor failed");
295                Err(err)
296            }
297            Err(_aborted) => {
298                tracing::info!("User requested shutdown: exiting");
299                Ok(())
300            }
301        }
302    }
303
304    fn follow_components(&self) -> FollowComponents {
305        if self.silence_component_logs {
306            FollowComponents::None
307        } else if self.follow_components.is_empty() {
308            FollowComponents::All
309        } else {
310            let followed = self.follow_components.clone().into_iter().collect();
311            FollowComponents::Named(followed)
312        }
313    }
314}
315
316const SLOTH_WARNING_DELAY_MILLIS: u64 = 1250;
317
318fn warn_if_wasm_build_slothful() -> sloth::SlothGuard {
319    #[cfg(debug_assertions)]
320    let message = "\
321        This is a debug build - preparing Wasm modules might take a few seconds\n\
322        If you're experiencing long startup times please switch to the release build";
323
324    #[cfg(not(debug_assertions))]
325    let message = "Preparing Wasm modules is taking a few seconds...";
326
327    sloth::warn_if_slothful(SLOTH_WARNING_DELAY_MILLIS, format!("{message}\n"))
328}
329
330fn help_heading<T: Trigger<F>, F: RuntimeFactors>() -> Option<&'static str> {
331    if T::TYPE == <help::HelpArgsOnlyTrigger as Trigger<F>>::TYPE {
332        Some("Trigger Options")
333    } else {
334        let heading = format!("{} Trigger Options", T::display_name());
335        let as_str = Box::new(heading).leak();
336        Some(as_str)
337    }
338}
339
340/// A builder for a [`TriggerApp`].
341pub struct TriggerAppBuilder<T, B> {
342    engine_config: spin_core::Config,
343    pub trigger: T,
344    _factors_builder: std::marker::PhantomData<B>,
345}
346
347impl<T: Trigger<B::Factors>, B: RuntimeFactorsBuilder> TriggerAppBuilder<T, B> {
348    pub fn new(trigger: T) -> Self {
349        Self {
350            engine_config: spin_core::Config::default(),
351            trigger,
352            _factors_builder: Default::default(),
353        }
354    }
355
356    pub fn engine_config(&mut self) -> &mut spin_core::Config {
357        &mut self.engine_config
358    }
359
360    /// Build a [`TriggerApp`] from the given [`App`] and options.
361    pub async fn build(
362        &mut self,
363        app: App,
364        common_options: FactorsConfig,
365        options: B::CliArgs,
366        loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
367    ) -> anyhow::Result<TriggerApp<T, B::Factors>> {
368        let mut core_engine_builder = {
369            self.trigger.update_core_config(&mut self.engine_config)?;
370
371            spin_core::Engine::builder(&self.engine_config)?
372        };
373        self.trigger.add_to_linker(core_engine_builder.linker())?;
374
375        let (factors, runtime_config) = B::build(&common_options, &options)?;
376
377        let mut executor = FactorsExecutor::new(core_engine_builder, factors)?;
378        B::configure_app(&mut executor, &runtime_config, &common_options, &options)?;
379        let executor = Arc::new(executor);
380
381        let configured_app = {
382            let _sloth_guard = warn_if_wasm_build_slothful();
383            executor
384                .load_app(app, runtime_config.into(), loader, Some(T::TYPE))
385                .await?
386        };
387
388        Ok(configured_app)
389    }
390
391    /// Run the [`TriggerApp`] with the given [`App`] and options.
392    pub async fn run(
393        mut self,
394        app: App,
395        common_options: FactorsConfig,
396        options: B::CliArgs,
397        loader: &impl ComponentLoader<B::Factors, T::InstanceState>,
398    ) -> anyhow::Result<impl Future<Output = anyhow::Result<()>>> {
399        let configured_app = self.build(app, common_options, options, loader).await?;
400        Ok(self.trigger.run(configured_app))
401    }
402}
403
404/// A builder for runtime factors.
405pub trait RuntimeFactorsBuilder {
406    /// The factors type to build.
407    type Factors: RuntimeFactors;
408    /// CLI arguments not included in [`FactorsConfig`] needed  to build the [`RuntimeFactors`].
409    type CliArgs: clap::Args;
410    /// The wrapped runtime config type.
411    type RuntimeConfig: Into<<Self::Factors as RuntimeFactors>::RuntimeConfig>;
412
413    /// Build the factors and runtime config from the given options.
414    fn build(
415        config: &FactorsConfig,
416        args: &Self::CliArgs,
417    ) -> anyhow::Result<(Self::Factors, Self::RuntimeConfig)>;
418
419    /// Configure the factors in the executor.
420    fn configure_app<U: Send + 'static>(
421        executor: &mut FactorsExecutor<Self::Factors, U>,
422        runtime_config: &Self::RuntimeConfig,
423        config: &FactorsConfig,
424        args: &Self::CliArgs,
425    ) -> anyhow::Result<()> {
426        let _ = (executor, runtime_config, config, args);
427        Ok(())
428    }
429}
430
431pub mod help {
432    use super::*;
433
434    /// Null object to support --help-args-only in the absence of
435    /// a `spin.toml` file.
436    pub struct HelpArgsOnlyTrigger;
437
438    impl<F: RuntimeFactors> Trigger<F> for HelpArgsOnlyTrigger {
439        const TYPE: &'static str = "help-args-only";
440        type CliArgs = NoCliArgs;
441        type InstanceState = ();
442
443        fn new(_cli_args: Self::CliArgs, _app: &App) -> anyhow::Result<Self> {
444            Ok(Self)
445        }
446
447        async fn run(self, _configured_app: TriggerApp<Self, F>) -> anyhow::Result<()> {
448            Ok(())
449        }
450    }
451}
452
453/// A user provided option which be either be provided, default, or explicitly none.
454#[derive(Clone, Debug, Default)]
455pub enum UserProvidedPath {
456    /// Use the explicitly provided directory.
457    Provided(PathBuf),
458    /// Use the default.
459    #[default]
460    Default,
461    /// Explicitly unset.
462    Unset,
463}