1use std::path::{Path, PathBuf};
2
3use anyhow::Context as _;
4use spin_common::ui::quoted_path;
5use spin_factor_key_value::runtime_config::spin::{self as key_value};
6use spin_factor_key_value::KeyValueFactor;
7use spin_factor_llm::{spin as llm, LlmFactor};
8use spin_factor_outbound_http::OutboundHttpFactor;
9use spin_factor_outbound_mqtt::OutboundMqttFactor;
10use spin_factor_outbound_mysql::OutboundMysqlFactor;
11use spin_factor_outbound_networking::runtime_config::spin::SpinRuntimeConfig as OutboundNetworkingSpinRuntimeConfig;
12use spin_factor_outbound_networking::OutboundNetworkingFactor;
13use spin_factor_outbound_pg::OutboundPgFactor;
14use spin_factor_outbound_redis::OutboundRedisFactor;
15use spin_factor_sqlite::SqliteFactor;
16use spin_factor_variables::VariablesFactor;
17use spin_factor_wasi::WasiFactor;
18use spin_factors::runtime_config::toml::GetTomlValue as _;
19use spin_factors::{
20 runtime_config::toml::TomlKeyTracker, FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer,
21};
22use spin_key_value_spin::{SpinKeyValueRuntimeConfig, SpinKeyValueStore};
23use spin_sqlite as sqlite;
24use spin_trigger::cli::UserProvidedPath;
25use toml::Value;
26
27pub mod variables;
28
29pub const DEFAULT_STATE_DIR: &str = ".spin";
31
32pub struct ResolvedRuntimeConfig<T> {
36 pub runtime_config: T,
38 pub key_value_resolver: key_value::RuntimeConfigResolver,
40 pub sqlite_resolver: sqlite::RuntimeConfigResolver,
42 pub state_dir: Option<PathBuf>,
46 pub log_dir: Option<PathBuf>,
50 pub max_instance_memory: Option<usize>,
52 pub toml: toml::Table,
54}
55
56impl<T> ResolvedRuntimeConfig<T> {
57 pub fn summarize(&self, runtime_config_path: Option<&Path>) {
58 let summarize_labeled_typed_tables = |key| {
59 let mut summaries = vec![];
60 if let Some(tables) = self.toml.get(key).and_then(Value::as_table) {
61 for (label, config) in tables {
62 if let Some(ty) = config.get("type").and_then(Value::as_str) {
63 summaries.push(format!("[{key}.{label}: {ty}]"))
64 }
65 }
66 }
67 summaries
68 };
69
70 let mut summaries = vec![];
71 summaries.extend(summarize_labeled_typed_tables("key_value_store"));
73 summaries.extend(summarize_labeled_typed_tables("sqlite_database"));
75 if let Some(table) = self.toml.get("llm_compute").and_then(Value::as_table) {
77 if let Some(ty) = table.get("type").and_then(Value::as_str) {
78 summaries.push(format!("[llm_compute: {ty}"));
79 }
80 }
81 if !summaries.is_empty() {
82 let summaries = summaries.join(", ");
83 let from_path = runtime_config_path
84 .map(|path| format!("from {}", quoted_path(path)))
85 .unwrap_or_default();
86 println!("Using runtime config {summaries} {from_path}");
87 }
88 }
89}
90
91impl<T> ResolvedRuntimeConfig<T>
92where
93 T: for<'a, 'b> TryFrom<TomlRuntimeConfigSource<'a, 'b>>,
94 for<'a, 'b> <T as TryFrom<TomlRuntimeConfigSource<'a, 'b>>>::Error: Into<anyhow::Error>,
95{
96 pub fn from_file(
100 runtime_config_path: Option<&Path>,
101 local_app_dir: Option<PathBuf>,
102 provided_state_dir: UserProvidedPath,
103 provided_log_dir: UserProvidedPath,
104 ) -> anyhow::Result<Self> {
105 let toml = match runtime_config_path {
106 Some(runtime_config_path) => {
107 let file = std::fs::read_to_string(runtime_config_path).with_context(|| {
108 format!(
109 "failed to read runtime config file '{}'",
110 runtime_config_path.display()
111 )
112 })?;
113 toml::from_str(&file).with_context(|| {
114 format!(
115 "failed to parse runtime config file '{}' as toml",
116 runtime_config_path.display()
117 )
118 })?
119 }
120 None => Default::default(),
121 };
122 let toml_resolver =
123 TomlResolver::new(&toml, local_app_dir, provided_state_dir, provided_log_dir);
124
125 Self::new(toml_resolver, runtime_config_path)
126 }
127
128 pub fn new(
130 toml_resolver: TomlResolver<'_>,
131 runtime_config_path: Option<&Path>,
132 ) -> anyhow::Result<Self> {
133 let runtime_config_dir = runtime_config_path
134 .and_then(Path::parent)
135 .map(ToOwned::to_owned);
136 let state_dir = toml_resolver.state_dir()?;
137 let outbound_networking = runtime_config_dir
138 .clone()
139 .map(OutboundNetworkingSpinRuntimeConfig::new);
140 let key_value_resolver = key_value_config_resolver(runtime_config_dir, state_dir.clone());
141 let sqlite_resolver = sqlite_config_resolver(state_dir.clone())
142 .context("failed to resolve sqlite runtime config")?;
143
144 let toml = toml_resolver.toml();
145 let log_dir = toml_resolver.log_dir()?;
146 let max_instance_memory = toml_resolver.max_instance_memory()?;
147
148 let source = TomlRuntimeConfigSource::new(
149 toml_resolver,
150 &key_value_resolver,
151 outbound_networking.as_ref(),
152 &sqlite_resolver,
153 );
154
155 let runtime_config: T = source.try_into().map_err(Into::into)?;
159
160 Ok(Self {
161 runtime_config,
162 key_value_resolver,
163 sqlite_resolver,
164 state_dir,
165 log_dir,
166 max_instance_memory,
167 toml,
168 })
169 }
170
171 pub fn state_dir(&self) -> Option<PathBuf> {
173 self.state_dir.clone()
174 }
175
176 pub fn log_dir(&self) -> Option<PathBuf> {
178 self.log_dir.clone()
179 }
180
181 pub fn max_instance_memory(&self) -> Option<usize> {
183 self.max_instance_memory
184 }
185}
186
187#[derive(Clone, Debug)]
188pub struct TomlResolver<'a> {
190 table: TomlKeyTracker<'a>,
191 local_app_dir: Option<PathBuf>,
193 state_dir: UserProvidedPath,
195 log_dir: UserProvidedPath,
197}
198
199impl<'a> TomlResolver<'a> {
200 pub fn new(
202 table: &'a toml::Table,
203 local_app_dir: Option<PathBuf>,
204 state_dir: UserProvidedPath,
205 log_dir: UserProvidedPath,
206 ) -> Self {
207 Self {
208 table: TomlKeyTracker::new(table),
209 local_app_dir,
210 state_dir,
211 log_dir,
212 }
213 }
214
215 pub fn state_dir(&self) -> std::io::Result<Option<PathBuf>> {
219 let mut state_dir = self.state_dir.clone();
220 if matches!(state_dir, UserProvidedPath::Default) {
222 let from_toml =
223 self.table
224 .get("state_dir")
225 .and_then(|v| v.as_str())
226 .map(|toml_value| {
227 if toml_value.is_empty() {
228 UserProvidedPath::Unset
230 } else {
231 UserProvidedPath::Provided(PathBuf::from(toml_value))
233 }
234 });
235 state_dir = from_toml.unwrap_or(state_dir);
237 }
238
239 match (state_dir, &self.local_app_dir) {
240 (UserProvidedPath::Provided(p), _) => Ok(Some(std::path::absolute(p)?)),
241 (UserProvidedPath::Default, Some(local_app_dir)) => {
242 Ok(Some(local_app_dir.join(".spin")))
243 }
244 (UserProvidedPath::Default | UserProvidedPath::Unset, _) => Ok(None),
245 }
246 }
247
248 pub fn log_dir(&self) -> std::io::Result<Option<PathBuf>> {
252 let mut log_dir = self.log_dir.clone();
253 if matches!(log_dir, UserProvidedPath::Default) {
255 let from_toml = self
256 .table
257 .get("log_dir")
258 .and_then(|v| v.as_str())
259 .map(|toml_value| {
260 if toml_value.is_empty() {
261 UserProvidedPath::Unset
263 } else {
264 UserProvidedPath::Provided(PathBuf::from(toml_value))
266 }
267 });
268 log_dir = from_toml.unwrap_or(log_dir);
270 }
271
272 match log_dir {
273 UserProvidedPath::Provided(p) => Ok(Some(std::path::absolute(p)?)),
274 UserProvidedPath::Default => Ok(self.state_dir()?.map(|p| p.join("logs"))),
275 UserProvidedPath::Unset => Ok(None),
276 }
277 }
278
279 pub fn max_instance_memory(&self) -> anyhow::Result<Option<usize>> {
281 self.table
282 .get("max_instance_memory")
283 .and_then(|v| v.as_integer())
284 .map(|toml_value| toml_value.try_into())
285 .transpose()
286 .map_err(Into::into)
287 }
288
289 pub fn validate_all_keys_used(&self) -> spin_factors::Result<()> {
291 self.table.validate_all_keys_used()
292 }
293
294 fn toml(&self) -> toml::Table {
295 self.table.as_ref().clone()
296 }
297}
298
299pub struct TomlRuntimeConfigSource<'a, 'b> {
301 toml: TomlResolver<'b>,
302 key_value: &'a key_value::RuntimeConfigResolver,
303 outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
304 sqlite: &'a sqlite::RuntimeConfigResolver,
305}
306
307impl<'a, 'b> TomlRuntimeConfigSource<'a, 'b> {
308 pub fn new(
309 toml_resolver: TomlResolver<'b>,
310 key_value: &'a key_value::RuntimeConfigResolver,
311 outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
312 sqlite: &'a sqlite::RuntimeConfigResolver,
313 ) -> Self {
314 Self {
315 toml: toml_resolver,
316 key_value,
317 outbound_networking,
318 sqlite,
319 }
320 }
321}
322
323impl FactorRuntimeConfigSource<KeyValueFactor> for TomlRuntimeConfigSource<'_, '_> {
324 fn get_runtime_config(
325 &mut self,
326 ) -> anyhow::Result<Option<spin_factor_key_value::RuntimeConfig>> {
327 Ok(Some(self.key_value.resolve(Some(&self.toml.table))?))
328 }
329}
330
331impl FactorRuntimeConfigSource<OutboundNetworkingFactor> for TomlRuntimeConfigSource<'_, '_> {
332 fn get_runtime_config(
333 &mut self,
334 ) -> anyhow::Result<Option<<OutboundNetworkingFactor as spin_factors::Factor>::RuntimeConfig>>
335 {
336 let Some(tls) = self.outbound_networking else {
337 return Ok(None);
338 };
339 tls.config_from_table(&self.toml.table)
340 }
341}
342
343impl FactorRuntimeConfigSource<VariablesFactor> for TomlRuntimeConfigSource<'_, '_> {
344 fn get_runtime_config(
345 &mut self,
346 ) -> anyhow::Result<Option<<VariablesFactor as spin_factors::Factor>::RuntimeConfig>> {
347 Ok(Some(variables::runtime_config_from_toml(&self.toml.table)?))
348 }
349}
350
351impl FactorRuntimeConfigSource<OutboundPgFactor> for TomlRuntimeConfigSource<'_, '_> {
352 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
353 Ok(None)
354 }
355}
356
357impl FactorRuntimeConfigSource<OutboundMysqlFactor> for TomlRuntimeConfigSource<'_, '_> {
358 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
359 Ok(None)
360 }
361}
362
363impl FactorRuntimeConfigSource<LlmFactor> for TomlRuntimeConfigSource<'_, '_> {
364 fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_llm::RuntimeConfig>> {
365 llm::runtime_config_from_toml(&self.toml.table, self.toml.state_dir()?)
366 }
367}
368
369impl FactorRuntimeConfigSource<OutboundRedisFactor> for TomlRuntimeConfigSource<'_, '_> {
370 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
371 Ok(None)
372 }
373}
374
375impl FactorRuntimeConfigSource<WasiFactor> for TomlRuntimeConfigSource<'_, '_> {
376 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
377 Ok(None)
378 }
379}
380
381impl FactorRuntimeConfigSource<OutboundHttpFactor> for TomlRuntimeConfigSource<'_, '_> {
382 fn get_runtime_config(
383 &mut self,
384 ) -> anyhow::Result<Option<<OutboundHttpFactor as spin_factors::Factor>::RuntimeConfig>> {
385 spin_factor_outbound_http::runtime_config::spin::config_from_table(&self.toml.table)
386 }
387}
388
389impl FactorRuntimeConfigSource<OutboundMqttFactor> for TomlRuntimeConfigSource<'_, '_> {
390 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
391 Ok(None)
392 }
393}
394
395impl FactorRuntimeConfigSource<SqliteFactor> for TomlRuntimeConfigSource<'_, '_> {
396 fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_sqlite::RuntimeConfig>> {
397 Ok(Some(self.sqlite.resolve(&self.toml.table)?))
398 }
399}
400
401impl RuntimeConfigSourceFinalizer for TomlRuntimeConfigSource<'_, '_> {
402 fn finalize(&mut self) -> anyhow::Result<()> {
403 Ok(self.toml.validate_all_keys_used()?)
404 }
405}
406
407const DEFAULT_KEY_VALUE_STORE_LABEL: &str = "default";
408
409pub fn key_value_config_resolver(
415 local_store_base_path: Option<PathBuf>,
416 default_store_base_path: Option<PathBuf>,
417) -> key_value::RuntimeConfigResolver {
418 let mut key_value = key_value::RuntimeConfigResolver::new();
419
420 key_value
423 .register_store_type(spin_key_value_spin::SpinKeyValueStore::new(
424 local_store_base_path.clone(),
425 ))
426 .unwrap();
427 key_value
428 .register_store_type(spin_key_value_redis::RedisKeyValueStore::new())
429 .unwrap();
430 key_value
431 .register_store_type(spin_key_value_azure::AzureKeyValueStore::new(None))
432 .unwrap();
433 key_value
434 .register_store_type(spin_key_value_aws::AwsDynamoKeyValueStore::new())
435 .unwrap();
436
437 let default_store_path = default_store_base_path.map(|p| p.join(DEFAULT_SPIN_STORE_FILENAME));
439 key_value
441 .add_default_store::<SpinKeyValueStore>(
442 DEFAULT_KEY_VALUE_STORE_LABEL,
443 SpinKeyValueRuntimeConfig::new(default_store_path),
444 )
445 .unwrap();
446
447 key_value
448}
449
450const DEFAULT_SPIN_STORE_FILENAME: &str = "sqlite_key_value.db";
452
453fn sqlite_config_resolver(
458 default_database_dir: Option<PathBuf>,
459) -> anyhow::Result<sqlite::RuntimeConfigResolver> {
460 let local_database_dir =
461 std::env::current_dir().context("failed to get current working directory")?;
462 Ok(sqlite::RuntimeConfigResolver::new(
463 default_database_dir,
464 local_database_dir,
465 ))
466}
467
468#[cfg(test)]
469mod tests {
470 use std::{collections::HashMap, sync::Arc};
471
472 use spin_factors::RuntimeFactors;
473 use spin_factors_test::TestEnvironment;
474
475 use super::*;
476
477 macro_rules! define_test_factor {
479 ($field:ident : $factor:ty) => {
480 #[derive(RuntimeFactors)]
481 struct TestFactors {
482 $field: $factor,
483 }
484 impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TestFactorsRuntimeConfig {
485 type Error = anyhow::Error;
486
487 fn try_from(value: TomlRuntimeConfigSource<'_, '_>) -> Result<Self, Self::Error> {
488 Self::from_source(value)
489 }
490 }
491 fn resolve_toml(
492 toml: toml::Table,
493 path: impl AsRef<std::path::Path>,
494 ) -> anyhow::Result<ResolvedRuntimeConfig<TestFactorsRuntimeConfig>> {
495 ResolvedRuntimeConfig::<TestFactorsRuntimeConfig>::new(
496 toml_resolver(&toml),
497 Some(path.as_ref()),
498 )
499 }
500 };
501 }
502
503 #[test]
504 fn sqlite_is_configured_correctly() {
505 define_test_factor!(sqlite: SqliteFactor);
506
507 impl TestFactorsRuntimeConfig {
508 fn connection_creators(
510 &self,
511 ) -> &HashMap<String, Arc<dyn spin_factor_sqlite::ConnectionCreator>> {
512 &self.sqlite.as_ref().unwrap().connection_creators
513 }
514
515 fn configured_labels(&self) -> Vec<&str> {
517 let mut configured_labels = self
518 .connection_creators()
519 .keys()
520 .map(|s| s.as_str())
521 .collect::<Vec<_>>();
522 configured_labels.sort();
524 configured_labels
525 }
526 }
527
528 let toml = toml::toml! {
530 [sqlite_database.foo]
531 type = "spin"
532 };
533 assert_eq!(
534 resolve_toml(toml, ".")
535 .unwrap()
536 .runtime_config
537 .configured_labels(),
538 vec!["default", "foo"]
539 );
540
541 let toml = toml::Table::new();
543 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
544 assert_eq!(runtime_config.configured_labels(), vec!["default"]);
545 }
546
547 #[test]
548 fn key_value_is_configured_correctly() {
549 define_test_factor!(key_value: KeyValueFactor);
550 impl TestFactorsRuntimeConfig {
551 fn has_store_manager(&self, label: &str) -> bool {
553 self.key_value.as_ref().unwrap().has_store_manager(label)
554 }
555 }
556
557 let toml = toml::toml! {
559 [key_value_store.foo]
560 type = "spin"
561 };
562 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
563 assert!(["default", "foo"]
564 .iter()
565 .all(|label| runtime_config.has_store_manager(label)));
566 }
567
568 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
569 async fn custom_spin_key_value_works_with_custom_paths() -> anyhow::Result<()> {
570 use spin_world::v2::key_value::HostStore;
571 define_test_factor!(key_value: KeyValueFactor);
572 let tmp_dir = tempfile::TempDir::with_prefix("example")?;
573 let absolute_path = tmp_dir.path().join("foo/custom.db");
574 let relative_path = tmp_dir.path().join("custom.db");
575 assert!(!absolute_path.exists());
577 assert!(!relative_path.exists());
578
579 let path_str = absolute_path.to_str().unwrap();
580 let runtime_config = toml::toml! {
581 [key_value_store.absolute]
582 type = "spin"
583 path = path_str
584
585 [key_value_store.relative]
586 type = "spin"
587 path = "custom.db"
588 };
589 let factors = TestFactors {
590 key_value: KeyValueFactor::new(),
591 };
592 let env = TestEnvironment::new(factors)
593 .extend_manifest(toml::toml! {
594 [component.test-component]
595 source = "does-not-exist.wasm"
596 key_value_stores = ["absolute", "relative"]
597 })
598 .runtime_config(
599 resolve_toml(runtime_config, tmp_dir.path().join("runtime-config.toml"))
600 .unwrap()
601 .runtime_config,
602 )?;
603 let mut state = env.build_instance_state().await?;
604
605 let store = state.key_value.open("absolute".to_owned()).await??;
607 let _ = state.key_value.get(store, "foo".to_owned()).await??;
608
609 let store = state.key_value.open("relative".to_owned()).await??;
610 let _ = state.key_value.get(store, "foo".to_owned()).await??;
611
612 assert!(absolute_path.exists());
614 assert!(relative_path.exists());
615 Ok(())
616 }
617
618 fn toml_resolver(toml: &toml::Table) -> TomlResolver<'_> {
619 TomlResolver::new(
620 toml,
621 None,
622 UserProvidedPath::Default,
623 UserProvidedPath::Default,
624 )
625 }
626
627 #[test]
628 fn dirs_are_resolved() {
629 define_test_factor!(sqlite: SqliteFactor);
630
631 let toml = toml::toml! {
632 state_dir = "/foo"
633 log_dir = "/bar"
634 };
635 resolve_toml(toml, "config.toml").unwrap();
636 }
637
638 #[test]
639 fn fails_to_resolve_with_unused_key() {
640 define_test_factor!(sqlite: SqliteFactor);
641
642 let toml = toml::toml! {
643 baz = "/baz"
644 };
645 let Err(e) = resolve_toml(toml, "config.toml") else {
647 panic!("Should not be able to resolve unknown key");
648 };
649 assert_eq!(e.to_string(), "unused runtime config key(s): baz");
650 }
651}