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