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