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::SpinTlsRuntimeConfig;
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 tls_resolver = runtime_config_dir.clone().map(SpinTlsRuntimeConfig::new);
136 let key_value_resolver = key_value_config_resolver(runtime_config_dir, state_dir.clone());
137 let sqlite_resolver = sqlite_config_resolver(state_dir.clone())
138 .context("failed to resolve sqlite runtime config")?;
139
140 let toml = toml_resolver.toml();
141 let log_dir = toml_resolver.log_dir()?;
142 let max_instance_memory = toml_resolver.max_instance_memory()?;
143
144 let source = TomlRuntimeConfigSource::new(
145 toml_resolver,
146 &key_value_resolver,
147 tls_resolver.as_ref(),
148 &sqlite_resolver,
149 );
150
151 let runtime_config: T = source.try_into().map_err(Into::into)?;
155
156 Ok(Self {
157 runtime_config,
158 key_value_resolver,
159 sqlite_resolver,
160 state_dir,
161 log_dir,
162 max_instance_memory,
163 toml,
164 })
165 }
166
167 pub fn state_dir(&self) -> Option<PathBuf> {
169 self.state_dir.clone()
170 }
171
172 pub fn log_dir(&self) -> Option<PathBuf> {
174 self.log_dir.clone()
175 }
176
177 pub fn max_instance_memory(&self) -> Option<usize> {
179 self.max_instance_memory
180 }
181}
182
183#[derive(Clone, Debug)]
184pub struct TomlResolver<'a> {
186 table: TomlKeyTracker<'a>,
187 local_app_dir: Option<PathBuf>,
189 state_dir: UserProvidedPath,
191 log_dir: UserProvidedPath,
193}
194
195impl<'a> TomlResolver<'a> {
196 pub fn new(
198 table: &'a toml::Table,
199 local_app_dir: Option<PathBuf>,
200 state_dir: UserProvidedPath,
201 log_dir: UserProvidedPath,
202 ) -> Self {
203 Self {
204 table: TomlKeyTracker::new(table),
205 local_app_dir,
206 state_dir,
207 log_dir,
208 }
209 }
210
211 pub fn state_dir(&self) -> std::io::Result<Option<PathBuf>> {
215 let mut state_dir = self.state_dir.clone();
216 if matches!(state_dir, UserProvidedPath::Default) {
218 let from_toml =
219 self.table
220 .get("state_dir")
221 .and_then(|v| v.as_str())
222 .map(|toml_value| {
223 if toml_value.is_empty() {
224 UserProvidedPath::Unset
226 } else {
227 UserProvidedPath::Provided(PathBuf::from(toml_value))
229 }
230 });
231 state_dir = from_toml.unwrap_or(state_dir);
233 }
234
235 match (state_dir, &self.local_app_dir) {
236 (UserProvidedPath::Provided(p), _) => Ok(Some(std::path::absolute(p)?)),
237 (UserProvidedPath::Default, Some(local_app_dir)) => {
238 Ok(Some(local_app_dir.join(".spin")))
239 }
240 (UserProvidedPath::Default | UserProvidedPath::Unset, _) => Ok(None),
241 }
242 }
243
244 pub fn log_dir(&self) -> std::io::Result<Option<PathBuf>> {
248 let mut log_dir = self.log_dir.clone();
249 if matches!(log_dir, UserProvidedPath::Default) {
251 let from_toml = self
252 .table
253 .get("log_dir")
254 .and_then(|v| v.as_str())
255 .map(|toml_value| {
256 if toml_value.is_empty() {
257 UserProvidedPath::Unset
259 } else {
260 UserProvidedPath::Provided(PathBuf::from(toml_value))
262 }
263 });
264 log_dir = from_toml.unwrap_or(log_dir);
266 }
267
268 match log_dir {
269 UserProvidedPath::Provided(p) => Ok(Some(std::path::absolute(p)?)),
270 UserProvidedPath::Default => Ok(self.state_dir()?.map(|p| p.join("logs"))),
271 UserProvidedPath::Unset => Ok(None),
272 }
273 }
274
275 pub fn max_instance_memory(&self) -> anyhow::Result<Option<usize>> {
277 self.table
278 .get("max_instance_memory")
279 .and_then(|v| v.as_integer())
280 .map(|toml_value| toml_value.try_into())
281 .transpose()
282 .map_err(Into::into)
283 }
284
285 pub fn validate_all_keys_used(&self) -> spin_factors::Result<()> {
287 self.table.validate_all_keys_used()
288 }
289
290 fn toml(&self) -> toml::Table {
291 self.table.as_ref().clone()
292 }
293}
294
295pub struct TomlRuntimeConfigSource<'a, 'b> {
297 toml: TomlResolver<'b>,
298 key_value: &'a key_value::RuntimeConfigResolver,
299 tls: Option<&'a SpinTlsRuntimeConfig>,
300 sqlite: &'a sqlite::RuntimeConfigResolver,
301}
302
303impl<'a, 'b> TomlRuntimeConfigSource<'a, 'b> {
304 pub fn new(
305 toml_resolver: TomlResolver<'b>,
306 key_value: &'a key_value::RuntimeConfigResolver,
307 tls: Option<&'a SpinTlsRuntimeConfig>,
308 sqlite: &'a sqlite::RuntimeConfigResolver,
309 ) -> Self {
310 Self {
311 toml: toml_resolver,
312 key_value,
313 tls,
314 sqlite,
315 }
316 }
317}
318
319impl FactorRuntimeConfigSource<KeyValueFactor> for TomlRuntimeConfigSource<'_, '_> {
320 fn get_runtime_config(
321 &mut self,
322 ) -> anyhow::Result<Option<spin_factor_key_value::RuntimeConfig>> {
323 Ok(Some(self.key_value.resolve(Some(&self.toml.table))?))
324 }
325}
326
327impl FactorRuntimeConfigSource<OutboundNetworkingFactor> for TomlRuntimeConfigSource<'_, '_> {
328 fn get_runtime_config(
329 &mut self,
330 ) -> anyhow::Result<Option<<OutboundNetworkingFactor as spin_factors::Factor>::RuntimeConfig>>
331 {
332 let Some(tls) = self.tls else {
333 return Ok(None);
334 };
335 tls.config_from_table(&self.toml.table)
336 }
337}
338
339impl FactorRuntimeConfigSource<VariablesFactor> for TomlRuntimeConfigSource<'_, '_> {
340 fn get_runtime_config(
341 &mut self,
342 ) -> anyhow::Result<Option<<VariablesFactor as spin_factors::Factor>::RuntimeConfig>> {
343 Ok(Some(spin_variables::runtime_config_from_toml(
344 &self.toml.table,
345 )?))
346 }
347}
348
349impl FactorRuntimeConfigSource<OutboundPgFactor> for TomlRuntimeConfigSource<'_, '_> {
350 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
351 Ok(None)
352 }
353}
354
355impl FactorRuntimeConfigSource<OutboundMysqlFactor> for TomlRuntimeConfigSource<'_, '_> {
356 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
357 Ok(None)
358 }
359}
360
361impl FactorRuntimeConfigSource<LlmFactor> for TomlRuntimeConfigSource<'_, '_> {
362 fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_llm::RuntimeConfig>> {
363 llm::runtime_config_from_toml(&self.toml.table, self.toml.state_dir()?)
364 }
365}
366
367impl FactorRuntimeConfigSource<OutboundRedisFactor> for TomlRuntimeConfigSource<'_, '_> {
368 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
369 Ok(None)
370 }
371}
372
373impl FactorRuntimeConfigSource<WasiFactor> for TomlRuntimeConfigSource<'_, '_> {
374 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
375 Ok(None)
376 }
377}
378
379impl FactorRuntimeConfigSource<OutboundHttpFactor> for TomlRuntimeConfigSource<'_, '_> {
380 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
381 Ok(None)
382 }
383}
384
385impl FactorRuntimeConfigSource<OutboundMqttFactor> for TomlRuntimeConfigSource<'_, '_> {
386 fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
387 Ok(None)
388 }
389}
390
391impl FactorRuntimeConfigSource<SqliteFactor> for TomlRuntimeConfigSource<'_, '_> {
392 fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_sqlite::RuntimeConfig>> {
393 Ok(Some(self.sqlite.resolve(&self.toml.table)?))
394 }
395}
396
397impl RuntimeConfigSourceFinalizer for TomlRuntimeConfigSource<'_, '_> {
398 fn finalize(&mut self) -> anyhow::Result<()> {
399 Ok(self.toml.validate_all_keys_used()?)
400 }
401}
402
403const DEFAULT_KEY_VALUE_STORE_LABEL: &str = "default";
404
405pub fn key_value_config_resolver(
411 local_store_base_path: Option<PathBuf>,
412 default_store_base_path: Option<PathBuf>,
413) -> key_value::RuntimeConfigResolver {
414 let mut key_value = key_value::RuntimeConfigResolver::new();
415
416 key_value
419 .register_store_type(spin_key_value_spin::SpinKeyValueStore::new(
420 local_store_base_path.clone(),
421 ))
422 .unwrap();
423 key_value
424 .register_store_type(spin_key_value_redis::RedisKeyValueStore::new())
425 .unwrap();
426 key_value
427 .register_store_type(spin_key_value_azure::AzureKeyValueStore::new(None))
428 .unwrap();
429 key_value
430 .register_store_type(spin_key_value_aws::AwsDynamoKeyValueStore::new())
431 .unwrap();
432
433 let default_store_path = default_store_base_path.map(|p| p.join(DEFAULT_SPIN_STORE_FILENAME));
435 key_value
437 .add_default_store::<SpinKeyValueStore>(
438 DEFAULT_KEY_VALUE_STORE_LABEL,
439 SpinKeyValueRuntimeConfig::new(default_store_path),
440 )
441 .unwrap();
442
443 key_value
444}
445
446const DEFAULT_SPIN_STORE_FILENAME: &str = "sqlite_key_value.db";
448
449fn sqlite_config_resolver(
454 default_database_dir: Option<PathBuf>,
455) -> anyhow::Result<sqlite::RuntimeConfigResolver> {
456 let local_database_dir =
457 std::env::current_dir().context("failed to get current working directory")?;
458 Ok(sqlite::RuntimeConfigResolver::new(
459 default_database_dir,
460 local_database_dir,
461 ))
462}
463
464#[cfg(test)]
465mod tests {
466 use std::{collections::HashMap, sync::Arc};
467
468 use spin_factors::RuntimeFactors;
469 use spin_factors_test::TestEnvironment;
470
471 use super::*;
472
473 macro_rules! define_test_factor {
475 ($field:ident : $factor:ty) => {
476 #[derive(RuntimeFactors)]
477 struct TestFactors {
478 $field: $factor,
479 }
480 impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TestFactorsRuntimeConfig {
481 type Error = anyhow::Error;
482
483 fn try_from(value: TomlRuntimeConfigSource<'_, '_>) -> Result<Self, Self::Error> {
484 Self::from_source(value)
485 }
486 }
487 fn resolve_toml(
488 toml: toml::Table,
489 path: impl AsRef<std::path::Path>,
490 ) -> anyhow::Result<ResolvedRuntimeConfig<TestFactorsRuntimeConfig>> {
491 ResolvedRuntimeConfig::<TestFactorsRuntimeConfig>::new(
492 toml_resolver(&toml),
493 Some(path.as_ref()),
494 )
495 }
496 };
497 }
498
499 #[test]
500 fn sqlite_is_configured_correctly() {
501 define_test_factor!(sqlite: SqliteFactor);
502
503 impl TestFactorsRuntimeConfig {
504 fn connection_creators(
506 &self,
507 ) -> &HashMap<String, Arc<dyn spin_factor_sqlite::ConnectionCreator>> {
508 &self.sqlite.as_ref().unwrap().connection_creators
509 }
510
511 fn configured_labels(&self) -> Vec<&str> {
513 let mut configured_labels = self
514 .connection_creators()
515 .keys()
516 .map(|s| s.as_str())
517 .collect::<Vec<_>>();
518 configured_labels.sort();
520 configured_labels
521 }
522 }
523
524 let toml = toml::toml! {
526 [sqlite_database.foo]
527 type = "spin"
528 };
529 assert_eq!(
530 resolve_toml(toml, ".")
531 .unwrap()
532 .runtime_config
533 .configured_labels(),
534 vec!["default", "foo"]
535 );
536
537 let toml = toml::Table::new();
539 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
540 assert_eq!(runtime_config.configured_labels(), vec!["default"]);
541 }
542
543 #[test]
544 fn key_value_is_configured_correctly() {
545 define_test_factor!(key_value: KeyValueFactor);
546 impl TestFactorsRuntimeConfig {
547 fn has_store_manager(&self, label: &str) -> bool {
549 self.key_value.as_ref().unwrap().has_store_manager(label)
550 }
551 }
552
553 let toml = toml::toml! {
555 [key_value_store.foo]
556 type = "spin"
557 };
558 let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
559 assert!(["default", "foo"]
560 .iter()
561 .all(|label| runtime_config.has_store_manager(label)));
562 }
563
564 #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
565 async fn custom_spin_key_value_works_with_custom_paths() -> anyhow::Result<()> {
566 use spin_world::v2::key_value::HostStore;
567 define_test_factor!(key_value: KeyValueFactor);
568 let tmp_dir = tempfile::TempDir::with_prefix("example")?;
569 let absolute_path = tmp_dir.path().join("foo/custom.db");
570 let relative_path = tmp_dir.path().join("custom.db");
571 assert!(!absolute_path.exists());
573 assert!(!relative_path.exists());
574
575 let path_str = absolute_path.to_str().unwrap();
576 let runtime_config = toml::toml! {
577 [key_value_store.absolute]
578 type = "spin"
579 path = path_str
580
581 [key_value_store.relative]
582 type = "spin"
583 path = "custom.db"
584 };
585 let factors = TestFactors {
586 key_value: KeyValueFactor::new(),
587 };
588 let env = TestEnvironment::new(factors)
589 .extend_manifest(toml::toml! {
590 [component.test-component]
591 source = "does-not-exist.wasm"
592 key_value_stores = ["absolute", "relative"]
593 })
594 .runtime_config(
595 resolve_toml(runtime_config, tmp_dir.path().join("runtime-config.toml"))
596 .unwrap()
597 .runtime_config,
598 )?;
599 let mut state = env.build_instance_state().await?;
600
601 let store = state.key_value.open("absolute".to_owned()).await??;
603 let _ = state.key_value.get(store, "foo".to_owned()).await??;
604
605 let store = state.key_value.open("relative".to_owned()).await??;
606 let _ = state.key_value.get(store, "foo".to_owned()).await??;
607
608 assert!(absolute_path.exists());
610 assert!(relative_path.exists());
611 Ok(())
612 }
613
614 fn toml_resolver(toml: &toml::Table) -> TomlResolver<'_> {
615 TomlResolver::new(
616 toml,
617 None,
618 UserProvidedPath::Default,
619 UserProvidedPath::Default,
620 )
621 }
622
623 #[test]
624 fn dirs_are_resolved() {
625 define_test_factor!(sqlite: SqliteFactor);
626
627 let toml = toml::toml! {
628 state_dir = "/foo"
629 log_dir = "/bar"
630 };
631 resolve_toml(toml, "config.toml").unwrap();
632 }
633
634 #[test]
635 fn fails_to_resolve_with_unused_key() {
636 define_test_factor!(sqlite: SqliteFactor);
637
638 let toml = toml::toml! {
639 baz = "/baz"
640 };
641 let Err(e) = resolve_toml(toml, "config.toml") else {
643 panic!("Should not be able to resolve unknown key");
644 };
645 assert_eq!(e.to_string(), "unused runtime config key(s): baz");
646 }
647}