Skip to main content

spin_runtime_factors/
lib.rs

1mod build;
2
3pub use build::FactorsBuilder;
4
5use std::cell::OnceCell;
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9use anyhow::Context as _;
10use spin_common::arg_parser::parse_kv;
11use spin_factor_key_value::KeyValueFactor;
12use spin_factor_llm::LlmFactor;
13use spin_factor_otel::OtelFactor;
14use spin_factor_outbound_http::OutboundHttpFactor;
15use spin_factor_outbound_mqtt::{NetworkedMqttClient, OutboundMqttFactor};
16use spin_factor_outbound_mysql::OutboundMysqlFactor;
17use spin_factor_outbound_networking::OutboundNetworkingFactor;
18use spin_factor_outbound_pg::OutboundPgFactor;
19use spin_factor_outbound_redis::OutboundRedisFactor;
20use spin_factor_sqlite::SqliteFactor;
21use spin_factor_variables::VariablesFactor;
22use spin_factor_wasi::{spin::SpinFilesMounter, WasiFactor};
23use spin_factors::RuntimeFactors;
24use spin_runtime_config::{ResolvedRuntimeConfig, TomlRuntimeConfigSource};
25use spin_variables_static::VariableSource;
26
27#[derive(RuntimeFactors)]
28pub struct TriggerFactors {
29    pub otel: OtelFactor,
30    pub wasi: WasiFactor,
31    pub variables: VariablesFactor,
32    pub key_value: KeyValueFactor,
33    pub outbound_networking: OutboundNetworkingFactor,
34    pub outbound_http: OutboundHttpFactor,
35    pub sqlite: SqliteFactor,
36    pub redis: OutboundRedisFactor,
37    pub mqtt: OutboundMqttFactor,
38    pub pg: OutboundPgFactor,
39    pub mysql: OutboundMysqlFactor,
40    pub llm: LlmFactor,
41}
42
43impl TriggerFactors {
44    pub fn new(
45        state_dir: Option<PathBuf>,
46        working_dir: impl Into<PathBuf>,
47        allow_transient_writes: bool,
48        experimental_wasi_otel: bool,
49        spin_version: &str,
50    ) -> anyhow::Result<Self> {
51        Ok(Self {
52            otel: OtelFactor::new(spin_version, experimental_wasi_otel)?,
53            wasi: wasi_factor(working_dir, allow_transient_writes),
54            variables: VariablesFactor::default(),
55            key_value: KeyValueFactor::new(),
56            outbound_networking: outbound_networking_factor(),
57            outbound_http: OutboundHttpFactor::default(),
58            sqlite: SqliteFactor::new(),
59            redis: OutboundRedisFactor::new(),
60            mqtt: OutboundMqttFactor::new(NetworkedMqttClient::creator()),
61            pg: OutboundPgFactor::new(),
62            mysql: OutboundMysqlFactor::new(),
63            llm: LlmFactor::new(
64                spin_factor_llm::spin::default_engine_creator(state_dir)
65                    .context("failed to configure LLM factor")?,
66            ),
67        })
68    }
69}
70
71fn wasi_factor(working_dir: impl Into<PathBuf>, allow_transient_writes: bool) -> WasiFactor {
72    WasiFactor::new(SpinFilesMounter::new(working_dir, allow_transient_writes))
73}
74
75fn outbound_networking_factor() -> OutboundNetworkingFactor {
76    fn disallowed_host_handler(scheme: &str, authority: &str) {
77        let host_pattern = format!("{scheme}://{authority}");
78        tracing::error!("Outbound network destination not allowed: {host_pattern}");
79        if scheme.starts_with("http") && authority == "self" {
80            terminal::warn!("A component tried to make an HTTP request to its own app but it does not have permission.");
81        } else {
82            terminal::warn!(
83                "A component tried to make an outbound network connection to disallowed destination '{host_pattern}'."
84            );
85        };
86        eprintln!("To allow this request, add 'allowed_outbound_hosts = [\"{host_pattern}\"]' to the manifest component section.");
87    }
88
89    let mut factor = OutboundNetworkingFactor::new();
90    factor.set_disallowed_host_handler(disallowed_host_handler);
91    factor
92}
93
94/// Options for building a [`TriggerFactors`].
95#[derive(Default, clap::Args)]
96pub struct TriggerAppArgs {
97    /// Set the static assets of the components in the temporary directory as writable.
98    #[clap(long = "allow-transient-write")]
99    pub allow_transient_write: bool,
100
101    /// [Experimental] Enable experimental WASI OTel support. Backwards compatibility of the WIT is not guaranteed.
102    #[clap(long = "experimental-wasi-otel")]
103    pub experimental_wasi_otel: bool,
104
105    /// Set a key/value pair (key=value) in the application's
106    /// default store. Any existing value will be overwritten.
107    /// Can be used multiple times.
108    #[clap(long = "key-value", value_parser = parse_kv)]
109    pub key_values: Vec<(String, String)>,
110
111    /// Run a SQLite statement such as a migration against the default database.
112    /// To run from a file, prefix the filename with @ e.g. spin up --sqlite @migration.sql
113    #[clap(long = "sqlite")]
114    pub sqlite_statements: Vec<String>,
115
116    /// Sets the maxmimum memory allocation limit for an instance in bytes.
117    #[clap(long, env = "SPIN_MAX_INSTANCE_MEMORY")]
118    pub max_instance_memory: Option<usize>,
119
120    /// Variable(s) to be passed to the app
121    ///
122    /// A single key-value pair can be passed as `key=value`, or `key=@file` to
123    /// read the value from a text file. Alternatively, any number of key-value
124    /// pairs may be passed via a JSON or TOML file using the syntax `@file.json` or
125    /// `@file.toml`.
126    ///
127    /// This option may be repeated. If the same key is specified multiple times
128    /// the last value will be used.
129    #[clap(long, value_parser = clap::value_parser!(VariableSource),
130        value_name = "KEY=VALUE | KEY=@FILE | @FILE.json | @FILE.toml")]
131    pub variable: Vec<VariableSource>,
132
133    /// Cache variables to avoid reading files twice
134    #[clap(skip)]
135    variables_cache: OnceCell<HashMap<String, String>>,
136}
137
138impl From<ResolvedRuntimeConfig<TriggerFactorsRuntimeConfig>> for TriggerFactorsRuntimeConfig {
139    fn from(value: ResolvedRuntimeConfig<TriggerFactorsRuntimeConfig>) -> Self {
140        value.runtime_config
141    }
142}
143
144impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TriggerFactorsRuntimeConfig {
145    type Error = anyhow::Error;
146
147    fn try_from(value: TomlRuntimeConfigSource<'_, '_>) -> Result<Self, Self::Error> {
148        Self::from_source(value)
149    }
150}
151
152impl TriggerAppArgs {
153    /// Parse all variable sources into a single merged map.
154    pub fn get_variables(&self) -> anyhow::Result<&HashMap<String, String>> {
155        if self.variables_cache.get().is_none() {
156            let mut variables = HashMap::new();
157            for source in &self.variable {
158                variables.extend(source.get_variables()?);
159            }
160            self.variables_cache.set(variables).unwrap();
161        }
162        Ok(self.variables_cache.get().unwrap())
163    }
164}