1#[cfg(feature = "postgres")]
7use std::sync::Arc;
8
9#[cfg(feature = "postgres")]
10use cordis::{CordisError, FiberId};
11
12#[cfg(feature = "postgres")]
13fn block_on_async<F: std::future::Future>(fut: F) -> F::Output {
14 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(fut))
15}
16
17#[cfg(feature = "postgres")]
18fn block_on_plugin<S: cordis::Service + 'static>(
19 ctx: &Arc<cordis::Context>,
20 svc: S,
21) -> Result<FiberId, CordisError> {
22 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(ctx.plugin(svc)))
23}
24
25pub fn register_plugins(reg: &cordis::PluginRegistry) {
27 #[cfg(feature = "postgres")]
28 {
29 reg.register("Store", Arc::new(factory_store));
30 }
31 #[cfg(not(feature = "postgres"))]
32 {
33 let _ = reg;
34 }
35}
36
37#[cfg(all(feature = "inventory", feature = "postgres"))]
38inventory::submit! {
39 cordis::CordisPluginFactory { name: "Store", make: factory_store }
40}
41
42#[cfg(feature = "postgres")]
43fn factory_store(
44 ctx: &Arc<cordis::Context>,
45 config: &serde_json::Value,
46) -> Result<FiberId, CordisError> {
47 let db: crate::DatabaseConfig =
48 if config.is_null() || config.as_object().is_some_and(|obj| obj.is_empty()) {
49 crate::DatabaseConfig::default()
50 } else {
51 serde_json::from_value(config.clone())
52 .map_err(|e| CordisError::Configuration(e.to_string()))?
53 };
54
55 let url = crate::postgres::resolve_database_url(Some(&db.url));
56 let pg = block_on_async(crate::PostgresClient::new_remote(url, String::new()))
57 .map_err(|e| CordisError::Configuration(e.to_string()))?;
58 let pg_arc = Arc::new(pg);
59 ctx.provide_arc(pg_arc.clone());
60
61 let tenant = crate::TenantDb::new(pg_arc.clone());
62 let fid = block_on_plugin(ctx, tenant)?;
63
64 let pg = ctx
65 .get::<crate::TenantDb>()
66 .ok_or_else(|| CordisError::Configuration("TenantDb missing after Store factory".into()))?;
67 block_on_async(sqlx::migrate!("../../migrations").run(pg.pool())).map_err(|e| {
68 CordisError::Configuration(format!("Failed to run database migrations: {e}"))
69 })?;
70 tracing::info!("Database migrations applied");
71 block_on_async(crate::tenant_agents::seed_default_templates(pg.pool()))
72 .map_err(|e| CordisError::Configuration(format!("Failed to seed agent templates: {e}")))?;
73 tracing::info!("Agent templates seeded");
74
75 let fleet_secrets = crate::FleetSecrets::new();
76 let fleet_store = crate::fleet_provider_secrets::FleetProviderSecretsStore::new(&pg_arc.pool);
77 let master = crate::MasterKey::from_env();
78 match block_on_async(fleet_store.load_all(master.as_ref())) {
79 Ok(providers) => fleet_secrets.store(providers),
80 Err(e) => {
81 tracing::warn!(error = %e, "Failed to load fleet provider secrets");
82 }
83 }
84 block_on_plugin(ctx, fleet_secrets)?;
85 Ok(fid)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::register_plugins;
91 use cordis::PluginRegistry;
92
93 #[test]
94 fn register_plugins_store_key() {
95 let reg = PluginRegistry::new();
96 register_plugins(®);
97 #[cfg(feature = "postgres")]
98 assert!(reg.get("Store").is_some());
99 #[cfg(not(feature = "postgres"))]
100 assert!(reg.get("Store").is_none());
101 }
102}