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 const DEFAULT_STATE_DIR: &str = ".spin";
29
30pub struct ResolvedRuntimeConfig<T> {
34 pub runtime_config: T,
36 pub key_value_resolver: key_value::RuntimeConfigResolver,
38 pub sqlite_resolver: sqlite::RuntimeConfigResolver,
40 pub state_dir: Option<PathBuf>,
44 pub log_dir: Option<PathBuf>,
48 pub max_instance_memory: Option<usize>,
50 pub toml: toml::Table,
52}
53
54impl<T> ResolvedRuntimeConfig<T> {
55 pub fn summarize(&self, runtime_config_path: Option<&Path>) {
56 let summarize_labeled_typed_tables = |key| {
57 let mut summaries = vec![];
58 if let Some(tables) = self.toml.get(key).and_then(Value::as_table) {
59 for (label, config) in tables {
60 if let Some(ty) = config.get("type").and_then(Value::as_str) {
61 summaries.push(format!("[{key}.{label}: {ty}]"))
62 }
63 }
64 }
65 summaries
66 };
67
68 let mut summaries = vec![];
69 summaries.extend(summarize_labeled_typed_tables("key_value_store"));
71 summaries.extend(summarize_labeled_typed_tables("sqlite_database"));
73 if let Some(table) = self.toml.get("llm_compute").and_then(Value::as_table) {
75 if let Some(ty) = table.get("type").and_then(Value::as_str) {
76 summaries.push(format!("[llm_compute: {ty}"));
77 }
78 }
79 if !summaries.is_empty() {
80 let summaries = summaries.join(", ");
81 let from_path = runtime_config_path
82 .map(|path| format!("from {}", quoted_path(path)))
83 .unwrap_or_default();
84 println!("Using runtime config {summaries} {from_path}");
85 }
86 }
87}
88
89impl<T> ResolvedRuntimeConfig<T>
90where
91 T: for<'a, 'b> TryFrom<TomlRuntimeConfigSource<'a, 'b>>,
92 for<'a, 'b> <T as TryFrom<TomlRuntimeConfigSource<'a, 'b>>>::Error: Into<anyhow::Error>,
93{
94 pub fn from_file(
98 runtime_config_path: Option<&Path>,
99 local_app_dir: Option<PathBuf>,
100 provided_state_dir: UserProvidedPath,
101 provided_log_dir: UserProvidedPath,
102 ) -> anyhow::Result<Self> {
103 let toml = match runtime_config_path {
104 Some(runtime_config_path) => {
105 let file = std::fs::read_to_string(runtime_config_path).with_context(|| {
106 format!(
107 "failed to read runtime config file '{}'",
108 runtime_config_path.display()
109 )
110 })?;
111 toml::from_str(&file).with_context(|| {
112 format!(
113 "failed to parse runtime config file '{}' as toml",
114 runtime_config_path.display()
115 )
116 })?
117 }
118 None => Default::default(),
119 };
120 let toml_resolver =
121 TomlResolver::new(&toml, local_app_dir, provided_state_dir, provided_log_dir);
122
123 Self::new(toml_resolver, runtime_config_path)
124 }
125
126 pub fn new(
128 toml_resolver: TomlResolver<'_>,
129 runtime_config_path: Option<&Path>,
130 ) -> anyhow::Result<Self> {
131 let runtime_config_dir = runtime_config_path
132 .and_then(Path::parent)
133 .map(ToOwned::to_owned);
134 let state_dir = toml_resolver.state_dir()?;
135 let outbound_networking = runtime_config_dir
136 .clone()
137 .map(OutboundNetworkingSpinRuntimeConfig::new);
138 let key_value_resolver = key_value_config_resolver(runtime_config_dir, state_dir.clone());
139 let sqlite_resolver = sqlite_config_resolver(state_dir.clone())
140 .context("failed to resolve sqlite runtime config")?;
141
142 let toml = toml_resolver.toml();
143 let log_dir = toml_resolver.log_dir()?;
144 let max_instance_memory = toml_resolver.max_instance_memory()?;
145
146 let source = TomlRuntimeConfigSource::new(
147 toml_resolver,
148 &key_value_resolver,
149 outbound_networking.as_ref(),
150 &sqlite_resolver,
151 );
152
153 let runtime_config: T = source.try_into().map_err(Into::into)?;
157
158 Ok(Self {
159 runtime_config,
160 key_value_resolver,
161 sqlite_resolver,
162 state_dir,
163 log_dir,
164 max_instance_memory,
165 toml,
166 })
167 }
168
169 pub fn state_dir(&self) -> Option<PathBuf> {
171 self.state_dir.clone()
172 }
173
174 pub fn log_dir(&self) -> Option<PathBuf> {
176 self.log_dir.clone()
177 }
178
179 pub fn max_instance_memory(&self) -> Option<usize> {
181 self.max_instance_memory
182 }
183}
184
185#[derive(Clone, Debug)]
186pub struct TomlResolver<'a> {
188 table: TomlKeyTracker<'a>,
189 local_app_dir: Option<PathBuf>,
191 state_dir: UserProvidedPath,
193 log_dir: UserProvidedPath,
195}
196
197impl<'a> TomlResolver<'a> {
198 pub fn new(
200 table: &'a toml::Table,
201 local_app_dir: Option<PathBuf>,
202 state_dir: UserProvidedPath,
203 log_dir: UserProvidedPath,
204 ) -> Self {
205 Self {
206 table: TomlKeyTracker::new(table),
207 local_app_dir,
208 state_dir,
209 log_dir,
210 }
211 }
212
213 pub fn state_dir(&self) -> std::io::Result<Option<PathBuf>> {
217 let mut state_dir = self.state_dir.clone();
218 if matches!(state_dir, UserProvidedPath::Default) {
220 let from_toml =
221 self.table
222 .get("state_dir")
223 .and_then(|v| v.as_str())
224 .map(|toml_value| {
225 if toml_value.is_empty() {
226 UserProvidedPath::Unset
228 } else {
229 UserProvidedPath::Provided(PathBuf::from(toml_value))
231 }
232 });
233 state_dir = from_toml.unwrap_or(state_dir);
235 }
236
237 match (state_dir, &self.local_app_dir) {
238 (UserProvidedPath::Provided(p), _) => Ok(Some(std::path::absolute(p)?)),
239 (UserProvidedPath::Default, Some(local_app_dir)) => {
240 Ok(Some(local_app_dir.join(".spin")))
241 }
242 (UserProvidedPath::Default | UserProvidedPath::Unset, _) => Ok(None),
243 }
244 }
245
246 pub fn log_dir(&self) -> std::io::Result<Option<PathBuf>> {
250 let mut log_dir = self.log_dir.clone();
251 if matches!(log_dir, UserProvidedPath::Default) {
253 let from_toml = self
254 .table
255 .get("log_dir")
256 .and_then(|v| v.as_str())
257 .map(|toml_value| {
258 if toml_value.is_empty() {
259 UserProvidedPath::Unset
261 } else {
262 UserProvidedPath::Provided(PathBuf::from(toml_value))
264 }
265 });
266 log_dir = from_toml.unwrap_or(log_dir);
268 }
269
270 match log_dir {
271 UserProvidedPath::Provided(p) => Ok(Some(std::path::absolute(p)?)),
272 UserProvidedPath::Default => Ok(self.state_dir()?.map(|p| p.join("logs"))),
273 UserProvidedPath::Unset => Ok(None),
274 }
275 }
276
277 pub fn max_instance_memory(&self) -> anyhow::Result<Option<usize>> {
279 self.table
280 .get("max_instance_memory")
281 .and_then(|v| v.as_integer())
282 .map(|toml_value| toml_value.try_into())
283 .transpose()
284 .map_err(Into::into)
285 }
286
287 pub fn validate_all_keys_used(&self) -> spin_factors::Result<()> {
289 self.table.validate_all_keys_used()
290 }
291
292 fn toml(&self) -> toml::Table {
293 self.table.as_ref().clone()
294 }
295}
296
297pub struct TomlRuntimeConfigSource<'a, 'b> {
299 toml: TomlResolver<'b>,
300 key_value: &'a key_value::RuntimeConfigResolver,
301 outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
302 sqlite: &'a sqlite::RuntimeConfigResolver,
303}
304
305impl<'a, 'b> TomlRuntimeConfigSource<'a, 'b> {
306 pub fn new(
307 toml_resolver: TomlResolver<'b>,
308 key_value: &'a key_value::RuntimeConfigResolver,
309 outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
310 sqlite: &'a sqlite::RuntimeConfigResolver,
311 ) -> Self {
312 Self {
313 toml: toml_resolver,
314 key_value,
315 outbound_networking,
316 sqlite,
317 }
318 }
319}
320
321impl FactorRuntimeConfigSource<KeyValueFactor> for TomlRuntimeConfigSource<'_, '_> {
322 fn get_runtime_config(
323 &mut self,
324 ) -> anyhow::Result<Option<spin_factor_key_value::RuntimeConfig>> {
325 Ok(Some(self.key_value.resolve(Some(&self.toml.table))?))
326 }
327}
328
329impl FactorRuntimeConfigSource<OutboundNetworkingFactor> for TomlRuntimeConfigSource<'_, '_> {
330 fn get_runtime_config(
331 &mut self,
332 ) -> anyhow::Result<Option<<OutboundNetworkingFactor as spin_factors::Factor>::RuntimeConfig>>
333 {
334 let Some(tls) = self.outbound_networking else {
335 return Ok(None);
336 };
337 tls.config_from_table(&self.toml.table)
338 }
339}
340
341impl FactorRuntimeConfigSource<VariablesFactor> for TomlRuntimeConfigSource<'_, '_> {
342 fn get_runtime_config(
343 &mut self,
344 ) -> anyhow::Result<Option<<VariablesFactor as spin_factors::Factor>::RuntimeConfig>> {
345 Ok(Some(spin_variables::runtime_config_from_toml(
346 &self.toml.table,
347 )?))
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(&mut self) -> anyhow::Result<Option<()>> {
383 Ok(None)
384 }
385}
386
387impl FactorRuntimeConfigSource<OutboundMqttFactor> for TomlRuntimeConfigSource<'_, '_> {
388 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
389 Ok(None)
390 }
391}
392
393impl FactorRuntimeConfigSource<SqliteFactor> for TomlRuntimeConfigSource<'_, '_> {
394 fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_sqlite::RuntimeConfig>> {
395 Ok(Some(self.sqlite.resolve(&self.toml.table)?))
396 }
397}
398
399impl RuntimeConfigSourceFinalizer for TomlRuntimeConfigSource<'_, '_> {
400 fn finalize(&mut self) -> anyhow::Result<()> {
401 Ok(self.toml.validate_all_keys_used()?)
402 }
403}
404
405const DEFAULT_KEY_VALUE_STORE_LABEL: &str = "default";
406
407pub fn key_value_config_resolver(
413 local_store_base_path: Option<PathBuf>,
414 default_store_base_path: Option<PathBuf>,
415) -> key_value::RuntimeConfigResolver {
416 let mut key_value = key_value::RuntimeConfigResolver::new();
417
418 key_value
421 .register_store_type(spin_key_value_spin::SpinKeyValueStore::new(
422 local_store_base_path.clone(),
423 ))
424 .unwrap();
425 key_value
426 .register_store_type(spin_key_value_redis::RedisKeyValueStore::new())
427 .unwrap();
428 key_value
429 .register_store_type(spin_key_value_azure::AzureKeyValueStore::new(None))
430 .unwrap();
431 key_value
432 .register_store_type(spin_key_value_aws::AwsDynamoKeyValueStore::new())
433 .unwrap();
434
435 let default_store_path = default_store_base_path.map(|p| p.join(DEFAULT_SPIN_STORE_FILENAME));
437 key_value
439 .add_default_store::<SpinKeyValueStore>(
440 DEFAULT_KEY_VALUE_STORE_LABEL,
441 SpinKeyValueRuntimeConfig::new(default_store_path),
442 )
443 .unwrap();
444
445 key_value
446}
447
448const DEFAULT_SPIN_STORE_FILENAME: &str = "sqlite_key_value.db";
450
451fn sqlite_config_resolver(
456 default_database_dir: Option<PathBuf>,
457) -> anyhow::Result<sqlite::RuntimeConfigResolver> {
458 let local_database_dir =
459 std::env::current_dir().context("failed to get current working directory")?;
460 Ok(sqlite::RuntimeConfigResolver::new(
461 default_database_dir,
462 local_database_dir,
463 ))
464}
465
466#[cfg(test)]
467mod tests {
468 use std::{collections::HashMap, sync::Arc};
469
470 use spin_factors::RuntimeFactors;
471 use spin_factors_test::TestEnvironment;
472
473 use super::*;
474
475 macro_rules! define_test_factor {
477 ($field:ident : $factor:ty) => {
478 #[derive(RuntimeFactors)]
479 struct TestFactors {
480 $field: $factor,
481 }
482 impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TestFactorsRuntimeConfig {
483 type Error = anyhow::Error;
484
485 fn try_from(value: TomlRuntimeConfigSource<'_, '_>) -> Result<Self, Self::Error> {
486 Self::from_source(value)
487 }
488 }
489 fn resolve_toml(
490 toml: toml::Table,
491 path: impl AsRef<std::path::Path>,
492 ) -> anyhow::Result<ResolvedRuntimeConfig<TestFactorsRuntimeConfig>> {
493 ResolvedRuntimeConfig::<TestFactorsRuntimeConfig>::new(
494 toml_resolver(&toml),
495 Some(path.as_ref()),
496 )
497 }
498 };
499 }
500
501 #[test]
502 fn sqlite_is_configured_correctly() {
503 define_test_factor!(sqlite: SqliteFactor);
504
505 impl TestFactorsRuntimeConfig {
506 fn connection_creators(
508 &self,
509 ) -> &HashMap<String, Arc<dyn spin_factor_sqlite::ConnectionCreator>> {
510 &self.sqlite.as_ref().unwrap().connection_creators
511 }
512
513 fn configured_labels(&self) -> Vec<&str> {
515 let mut configured_labels = self
516 .connection_creators()
517 .keys()
518 .map(|s| s.as_str())
519 .collect::<Vec<_>>();
520 configured_labels.sort();
522 configured_labels
523 }
524 }
525
526 let toml = toml::toml! {
528 [sqlite_database.foo]
529 type = "spin"
530 };
531 assert_eq!(
532 resolve_toml(toml, ".")
533 .unwrap()
534 .runtime_config
535 .configured_labels(),
536 vec!["default", "foo"]
537 );
538
539 let toml = toml::Table::new();
541 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
542 assert_eq!(runtime_config.configured_labels(), vec!["default"]);
543 }
544
545 #[test]
546 fn key_value_is_configured_correctly() {
547 define_test_factor!(key_value: KeyValueFactor);
548 impl TestFactorsRuntimeConfig {
549 fn has_store_manager(&self, label: &str) -> bool {
551 self.key_value.as_ref().unwrap().has_store_manager(label)
552 }
553 }
554
555 let toml = toml::toml! {
557 [key_value_store.foo]
558 type = "spin"
559 };
560 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
561 assert!(["default", "foo"]
562 .iter()
563 .all(|label| runtime_config.has_store_manager(label)));
564 }
565
566 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
567 async fn custom_spin_key_value_works_with_custom_paths() -> anyhow::Result<()> {
568 use spin_world::v2::key_value::HostStore;
569 define_test_factor!(key_value: KeyValueFactor);
570 let tmp_dir = tempfile::TempDir::with_prefix("example")?;
571 let absolute_path = tmp_dir.path().join("foo/custom.db");
572 let relative_path = tmp_dir.path().join("custom.db");
573 assert!(!absolute_path.exists());
575 assert!(!relative_path.exists());
576
577 let path_str = absolute_path.to_str().unwrap();
578 let runtime_config = toml::toml! {
579 [key_value_store.absolute]
580 type = "spin"
581 path = path_str
582
583 [key_value_store.relative]
584 type = "spin"
585 path = "custom.db"
586 };
587 let factors = TestFactors {
588 key_value: KeyValueFactor::new(),
589 };
590 let env = TestEnvironment::new(factors)
591 .extend_manifest(toml::toml! {
592 [component.test-component]
593 source = "does-not-exist.wasm"
594 key_value_stores = ["absolute", "relative"]
595 })
596 .runtime_config(
597 resolve_toml(runtime_config, tmp_dir.path().join("runtime-config.toml"))
598 .unwrap()
599 .runtime_config,
600 )?;
601 let mut state = env.build_instance_state().await?;
602
603 let store = state.key_value.open("absolute".to_owned()).await??;
605 let _ = state.key_value.get(store, "foo".to_owned()).await??;
606
607 let store = state.key_value.open("relative".to_owned()).await??;
608 let _ = state.key_value.get(store, "foo".to_owned()).await??;
609
610 assert!(absolute_path.exists());
612 assert!(relative_path.exists());
613 Ok(())
614 }
615
616 fn toml_resolver(toml: &toml::Table) -> TomlResolver<'_> {
617 TomlResolver::new(
618 toml,
619 None,
620 UserProvidedPath::Default,
621 UserProvidedPath::Default,
622 )
623 }
624
625 #[test]
626 fn dirs_are_resolved() {
627 define_test_factor!(sqlite: SqliteFactor);
628
629 let toml = toml::toml! {
630 state_dir = "/foo"
631 log_dir = "/bar"
632 };
633 resolve_toml(toml, "config.toml").unwrap();
634 }
635
636 #[test]
637 fn fails_to_resolve_with_unused_key() {
638 define_test_factor!(sqlite: SqliteFactor);
639
640 let toml = toml::toml! {
641 baz = "/baz"
642 };
643 let Err(e) = resolve_toml(toml, "config.toml") else {
645 panic!("Should not be able to resolve unknown key");
646 };
647 assert_eq!(e.to_string(), "unused runtime config key(s): baz");
648 }
649}