diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs new file mode 100644 index 0000000..fad1003 --- /dev/null +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -0,0 +1,57 @@ +use kube::{api::ObjectMeta, CustomResource}; +use serde::{Deserialize, Serialize}; + +#[derive(CustomResource, Deserialize, Serialize, Clone, Debug)] +#[kube( + group = "postgresql.cnpg.io", + version = "v1", + kind = "Cluster", + plural = "clusters", + namespaced = true, + schema = "disabled" +)] +pub struct ClusterSpec { + pub instances: i32, + pub image_name: Option, + pub storage: Storage, + pub bootstrap: Bootstrap, +} + +impl Default for Cluster { + fn default() -> Self { + Cluster { + metadata: ObjectMeta::default(), + spec: ClusterSpec::default(), + } + } +} + +impl Default for ClusterSpec { + fn default() -> Self { + Self { + instances: 1, + image_name: None, + storage: Storage::default(), + bootstrap: Bootstrap::default(), + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct Storage { + pub size: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct Bootstrap { + pub initdb: Initdb, +} + +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct Initdb { + pub database: String, + pub owner: String, +} diff --git a/harmony/src/modules/postgresql/cnpg/mod.rs b/harmony/src/modules/postgresql/cnpg/mod.rs new file mode 100644 index 0000000..0237c67 --- /dev/null +++ b/harmony/src/modules/postgresql/cnpg/mod.rs @@ -0,0 +1,2 @@ +mod crd; +pub use crd::*; diff --git a/harmony/src/modules/postgresql/mod.rs b/harmony/src/modules/postgresql/mod.rs index 539a298..377e4d4 100644 --- a/harmony/src/modules/postgresql/mod.rs +++ b/harmony/src/modules/postgresql/mod.rs @@ -1,6 +1,9 @@ pub mod capability; mod score; +pub use score::*; pub mod failover; mod operator; pub use operator::*; + +pub mod cnpg; diff --git a/harmony/src/modules/postgresql/score.rs b/harmony/src/modules/postgresql/score.rs index 5c6f428..d32c1fc 100644 --- a/harmony/src/modules/postgresql/score.rs +++ b/harmony/src/modules/postgresql/score.rs @@ -1,88 +1,87 @@ -use crate::{ - domain::{data::Version, interpret::InterpretStatus}, - interpret::{Interpret, InterpretError, InterpretName, Outcome}, - inventory::Inventory, - modules::postgresql::capability::PostgreSQL, - score::Score, - topology::Topology, -}; - -use super::capability::*; - -use harmony_types::id::Id; - -use async_trait::async_trait; -use log::info; use serde::Serialize; -#[derive(Clone, Debug, Serialize)] +use crate::interpret::Interpret; +use crate::modules::k8s::resource::K8sResourceScore; +use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage}; +use crate::score::Score; +use crate::topology::{K8sclient, Topology}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; + +/// Deploys an opinionated, highly available PostgreSQL cluster managed by CNPG. +/// +/// # Goals +/// - Production-ready Postgres HA (3 instances), persistent storage, app DB. +/// +/// # Usage +/// ``` +/// use harmony::modules::postgresql::PostgreSQLScore; +/// let score = PostgreSQLScore::new("my-app-ns"); +/// ``` +/// +/// # Limitations (Happy Path) +/// - Requires CNPG operator installed (use CloudNativePgOperatorScore). +/// - No backups, monitoring, extensions configured. +/// +/// TODO : refactor this to declare a clean dependency on cnpg operator. Then cnpg operator will +/// self-deploy either using operatorhub or helm chart depending on k8s flavor. This is cnpg +/// specific behavior +#[derive(Debug, Clone, Serialize)] pub struct PostgreSQLScore { - config: PostgreSQLConfig, + pub namespace: String, + pub instances: i32, + pub storage_size: String, + pub image_name: Option, } -#[derive(Debug, Clone)] -pub struct PostgreSQLInterpret { - config: PostgreSQLConfig, - version: Version, - status: InterpretStatus, -} - -impl PostgreSQLInterpret { - pub fn new(config: PostgreSQLConfig) -> Self { - let version = Version::from("1.0.0").expect("Version should be valid"); +impl Default for PostgreSQLScore { + fn default() -> Self { Self { - config, - version, - status: InterpretStatus::QUEUED, + namespace: "default".to_string(), + instances: 1, + storage_size: "1Gi".to_string(), + image_name: None, // This lets cnpg use its default image } } } -impl Score for PostgreSQLScore { - fn name(&self) -> String { - "PostgreSQLScore".to_string() +impl PostgreSQLScore { + pub fn new(namespace: &str) -> Self { + Self { + namespace: namespace.to_string(), + ..Default::default() + } } +} +impl Score for PostgreSQLScore { fn create_interpret(&self) -> Box> { - Box::new(PostgreSQLInterpret::new(self.config.clone())) - } -} - -#[async_trait] -impl Interpret for PostgreSQLInterpret { - fn get_name(&self) -> InterpretName { - InterpretName::Custom("PostgreSQLInterpret") - } - - fn get_version(&self) -> crate::domain::data::Version { - self.version.clone() - } - - fn get_status(&self) -> InterpretStatus { - self.status.clone() - } - - fn get_children(&self) -> Vec { - todo!() - } - - async fn execute( - &self, - _inventory: &Inventory, - topology: &T, - ) -> Result { - info!( - "Executing PostgreSQLInterpret with config {:?}", - self.config - ); - - let cluster_name = topology - .deploy(&self.config) - .await - .map_err(|e| InterpretError::from(e))?; - - Ok(Outcome::success(format!( - "Deployed PostgreSQL cluster `{cluster_name}`" - ))) + let metadata = ObjectMeta { + name: Some("postgres".to_string()), + namespace: Some(self.namespace.clone()), + ..ObjectMeta::default() + }; + + let spec = ClusterSpec { + instances: self.instances, + image_name: self.image_name.clone(), + storage: Storage { + size: self.storage_size.clone(), + }, + bootstrap: Bootstrap { + initdb: Initdb { + database: "app".to_string(), + owner: "app".to_string(), + }, + }, + ..ClusterSpec::default() + }; + + let cluster = Cluster { metadata, spec }; + + K8sResourceScore::single(cluster, Some(self.namespace.clone())).create_interpret() + } + + fn name(&self) -> String { + format!("PostgreSQLScore({})", self.namespace) } }