feat(postgresql): request extensions on CNPG clusters #350

Merged
leokumojin merged 3 commits from feat/postgres-extensions into master 2026-10-19 15:24:40 +00:00
4 changed files with 99 additions and 3 deletions

View File

@@ -72,6 +72,7 @@ pub struct PostgreSQLConfig {
/// exist before returning. This ensures that callers like `get_endpoint()` /// exist before returning. This ensures that callers like `get_endpoint()`
/// won't fail with "service not found" immediately after deployment. /// won't fail with "service not found" immediately after deployment.
pub wait_for_ready: bool, pub wait_for_ready: bool,
pub extensions: Vec<String>,
} }
impl PostgreSQLConfig { impl PostgreSQLConfig {
@@ -92,6 +93,7 @@ impl Default for PostgreSQLConfig {
namespace: "harmony".to_string(), namespace: "harmony".to_string(),
wait_for_ready: true, wait_for_ready: true,
version: None, version: None,
extensions: Vec::new(),
} }
} }
} }

View File

@@ -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<DatabaseExtension>,
}
#[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)] #[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Storage { pub struct Storage {

View File

@@ -31,6 +31,7 @@ impl<T: PostgreSQL + TlsRouter> PostgreSQL for FailoverTopology<T> {
namespace: config.namespace.clone(), namespace: config.namespace.clone(),
wait_for_ready: config.wait_for_ready, wait_for_ready: config.wait_for_ready,
version: config.version.clone(), version: config.version.clone(),
extensions: config.extensions.clone(),
}; };
info!( info!(
@@ -148,6 +149,7 @@ impl<T: PostgreSQL + TlsRouter> PostgreSQL for FailoverTopology<T> {
namespace: config.namespace.clone(), namespace: config.namespace.clone(),
wait_for_ready: config.wait_for_ready, wait_for_ready: config.wait_for_ready,
version: config.version.clone(), version: config.version.clone(),
extensions: config.extensions.clone(),
}; };
info!( info!(

View File

@@ -4,8 +4,8 @@ use crate::inventory::Inventory;
use crate::modules::k8s::resource::K8sResourceScore; use crate::modules::k8s::resource::K8sResourceScore;
use crate::modules::postgresql::capability::{PostgreSQLConfig, PostgreSQLRootAccountRef}; use crate::modules::postgresql::capability::{PostgreSQLConfig, PostgreSQLRootAccountRef};
use crate::modules::postgresql::cnpg::{ use crate::modules::postgresql::cnpg::{
Bootstrap, Cluster, ClusterSpec, ExternalCluster, Initdb, PgBaseBackup, ReplicaSpec, Bootstrap, Cluster, ClusterReference, ClusterSpec, Database, DatabaseExtension, DatabaseSpec,
SecretKeySelector, Storage, ExternalCluster, Initdb, PgBaseBackup, ReplicaSpec, SecretKeySelector, Storage,
}; };
use crate::modules::postgresql::operator::{ use crate::modules::postgresql::operator::{
CloudNativePgOperatorHelmScore, CloudNativePgOperatorScore, CloudNativePgOperatorHelmScore, CloudNativePgOperatorScore,
@@ -45,7 +45,7 @@ fn is_forbidden<E: std::fmt::Display>(e: &E) -> bool {
/// ``` /// ```
/// ///
/// # Limitations (Happy Path) /// # Limitations (Happy Path)
/// - No backups, monitoring, extensions configured. /// - No backups or monitoring configured.
#[derive(Debug, Clone, Serialize, Default)] #[derive(Debug, Clone, Serialize, Default)]
pub struct K8sPostgreSQLScore { pub struct K8sPostgreSQLScore {
pub config: PostgreSQLConfig, pub config: PostgreSQLConfig,
@@ -281,6 +281,32 @@ impl K8sPostgreSQLInterpret {
tokio::time::sleep(std::time::Duration::from_secs(2)).await; 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] #[async_trait]
@@ -330,6 +356,16 @@ impl<T: Topology + K8sclient + HelmCommand + 'static> Interpret<T> for K8sPostgr
self.wait_for_primary(topology).await?; 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) Ok(outcome)
} }
super::capability::PostgreSQLClusterRole::Replica(replica_config) => { 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] #[test]
fn cnpg_image_name_maps_tags_and_full_refs() { fn cnpg_image_name_maps_tags_and_full_refs() {
assert_eq!(cnpg_image_name(None), None); assert_eq!(cnpg_image_name(None), None);