diff --git a/harmony/src/modules/postgresql/capability.rs b/harmony/src/modules/postgresql/capability.rs index b8dbb610..d7bc2270 100644 --- a/harmony/src/modules/postgresql/capability.rs +++ b/harmony/src/modules/postgresql/capability.rs @@ -72,6 +72,7 @@ pub struct PostgreSQLConfig { /// exist before returning. This ensures that callers like `get_endpoint()` /// won't fail with "service not found" immediately after deployment. pub wait_for_ready: bool, + pub extensions: Vec, } impl PostgreSQLConfig { @@ -92,6 +93,7 @@ impl Default for PostgreSQLConfig { namespace: "harmony".to_string(), wait_for_ready: true, version: None, + extensions: Vec::new(), } } } diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs index 9144a0d4..7433c543 100644 --- a/harmony/src/modules/postgresql/cnpg/crd.rs +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -55,6 +55,35 @@ impl Default for ClusterSpec { } } +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug, Default)] +#[kube( + group = "postgresql.cnpg.io", + version = "v1", + kind = "Database", + plural = "databases", + namespaced = true, + derive = "Default", + schema = "disabled" +)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSpec { + pub name: String, + pub owner: String, + pub cluster: ClusterReference, + pub extensions: Vec, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct ClusterReference { + pub name: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct DatabaseExtension { + pub name: String, + pub ensure: String, +} + #[derive(Deserialize, Serialize, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct Storage { diff --git a/harmony/src/modules/postgresql/failover.rs b/harmony/src/modules/postgresql/failover.rs index 40ab4881..5cd1dc91 100644 --- a/harmony/src/modules/postgresql/failover.rs +++ b/harmony/src/modules/postgresql/failover.rs @@ -31,6 +31,7 @@ impl PostgreSQL for FailoverTopology { namespace: config.namespace.clone(), wait_for_ready: config.wait_for_ready, version: config.version.clone(), + extensions: config.extensions.clone(), }; info!( @@ -148,6 +149,7 @@ impl PostgreSQL for FailoverTopology { namespace: config.namespace.clone(), wait_for_ready: config.wait_for_ready, version: config.version.clone(), + extensions: config.extensions.clone(), }; info!( diff --git a/harmony/src/modules/postgresql/score_k8s.rs b/harmony/src/modules/postgresql/score_k8s.rs index 9c0d43c1..18914e75 100644 --- a/harmony/src/modules/postgresql/score_k8s.rs +++ b/harmony/src/modules/postgresql/score_k8s.rs @@ -4,8 +4,8 @@ use crate::inventory::Inventory; use crate::modules::k8s::resource::K8sResourceScore; use crate::modules::postgresql::capability::{PostgreSQLConfig, PostgreSQLRootAccountRef}; use crate::modules::postgresql::cnpg::{ - Bootstrap, Cluster, ClusterSpec, ExternalCluster, Initdb, PgBaseBackup, ReplicaSpec, - SecretKeySelector, Storage, + Bootstrap, Cluster, ClusterReference, ClusterSpec, Database, DatabaseExtension, DatabaseSpec, + ExternalCluster, Initdb, PgBaseBackup, ReplicaSpec, SecretKeySelector, Storage, }; use crate::modules::postgresql::operator::{ CloudNativePgOperatorHelmScore, CloudNativePgOperatorScore, @@ -45,7 +45,7 @@ fn is_forbidden(e: &E) -> bool { /// ``` /// /// # Limitations (Happy Path) -/// - No backups, monitoring, extensions configured. +/// - No backups or monitoring configured. #[derive(Debug, Clone, Serialize, Default)] pub struct K8sPostgreSQLScore { pub config: PostgreSQLConfig, @@ -281,6 +281,32 @@ impl K8sPostgreSQLInterpret { tokio::time::sleep(std::time::Duration::from_secs(2)).await; } } + + fn app_database(&self) -> Database { + Database { + metadata: ObjectMeta { + name: Some(format!("{}-app", self.config.cluster_name)), + namespace: Some(self.config.namespace.clone()), + ..ObjectMeta::default() + }, + spec: DatabaseSpec { + name: "app".to_string(), + owner: "app".to_string(), + cluster: ClusterReference { + name: self.config.cluster_name.clone(), + }, + extensions: self + .config + .extensions + .iter() + .map(|name| DatabaseExtension { + name: name.clone(), + ensure: "present".to_string(), + }) + .collect(), + }, + } + } } #[async_trait] @@ -330,6 +356,16 @@ impl Interpret for K8sPostgr self.wait_for_primary(topology).await?; } + if !self.config.extensions.is_empty() { + K8sResourceScore::single( + self.app_database(), + Some(self.config.namespace.clone()), + ) + .create_interpret() + .execute(inventory, topology) + .await?; + } + Ok(outcome) } super::capability::PostgreSQLClusterRole::Replica(replica_config) => { @@ -498,6 +534,33 @@ mod tests { ); } + #[test] + fn app_database_requests_extensions_on_the_app_database() { + let interpret = K8sPostgreSQLInterpret { + config: PostgreSQLConfig { + cluster_name: "iaai-pg".to_string(), + namespace: "iaai".to_string(), + extensions: vec!["vector".to_string()], + ..Default::default() + }, + }; + + assert_eq!( + serde_json::to_value(interpret.app_database()).unwrap(), + serde_json::json!({ + "apiVersion": "postgresql.cnpg.io/v1", + "kind": "Database", + "metadata": { "name": "iaai-pg-app", "namespace": "iaai" }, + "spec": { + "name": "app", + "owner": "app", + "cluster": { "name": "iaai-pg" }, + "extensions": [{ "name": "vector", "ensure": "present" }] + } + }) + ); + } + #[test] fn cnpg_image_name_maps_tags_and_full_refs() { assert_eq!(cnpg_image_name(None), None);