diff --git a/brocade/src/lib.rs b/brocade/src/lib.rs index c0b5b70..4bb86b3 100644 --- a/brocade/src/lib.rs +++ b/brocade/src/lib.rs @@ -56,7 +56,7 @@ enum ExecutionMode { #[derive(Clone, Debug)] pub struct BrocadeInfo { os: BrocadeOs, - version: String, + _version: String, } #[derive(Clone, Debug)] @@ -263,7 +263,7 @@ async fn get_brocade_info(session: &mut BrocadeSession) -> Result[a-zA-Z0-9.\-]+)") @@ -276,7 +276,7 @@ async fn get_brocade_info(session: &mut BrocadeSession) -> Result Result { - todo!() + K8sPostgreSQLScore { + config: config.clone(), + } + .interpret(&Inventory::empty(), self) + .await + .map_err(|e| format!("Failed to deploy k8s postgresql : {e}"))?; + + Ok(config.cluster_name.clone()) } /// Extracts PostgreSQL-specific replication certs (PEM format) from a deployed primary cluster. diff --git a/harmony/src/modules/application/features/helm_argocd_score.rs b/harmony/src/modules/application/features/helm_argocd_score.rs index 2e51a9e..4f4e649 100644 --- a/harmony/src/modules/application/features/helm_argocd_score.rs +++ b/harmony/src/modules/application/features/helm_argocd_score.rs @@ -1,11 +1,9 @@ use async_trait::async_trait; use harmony_macros::hurl; -use kube::{Api, api::GroupVersionKind}; -use log::{debug, warn}; +use kube::api::GroupVersionKind; use non_blank_string_rs::NonBlankString; use serde::Serialize; -use serde::de::DeserializeOwned; -use std::{process::Command, str::FromStr, sync::Arc}; +use std::{str::FromStr, sync::Arc}; use crate::{ data::Version, @@ -13,10 +11,7 @@ use crate::{ inventory::Inventory, modules::helm::chart::{HelmChartScore, HelmRepository}, score::Score, - topology::{ - HelmCommand, K8sclient, PreparationError, PreparationOutcome, Topology, ingress::Ingress, - k8s::K8sClient, - }, + topology::{HelmCommand, K8sclient, Topology, ingress::Ingress, k8s::K8sClient}, }; use harmony_types::id::Id; diff --git a/harmony/src/modules/postgresql/capability.rs b/harmony/src/modules/postgresql/capability.rs index 9bb2148..a2d6a91 100644 --- a/harmony/src/modules/postgresql/capability.rs +++ b/harmony/src/modules/postgresql/capability.rs @@ -30,6 +30,21 @@ pub struct PostgreSQLConfig { pub instances: u32, pub storage_size: StorageSize, pub role: PostgreSQLClusterRole, + /// **Note :** on OpenShfit based clusters, the namespace `default` has security + /// settings incompatible with the default CNPG behavior. + pub namespace: String, +} + +impl Default for PostgreSQLConfig { + fn default() -> Self { + Self { + cluster_name: "harmony-pg".to_string(), + instances: 1, + storage_size: StorageSize::gi(1), + role: PostgreSQLClusterRole::Primary, + namespace: "harmony".to_string(), + } + } } #[derive(Clone, Debug, Serialize)] diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs index 122923e..c8f6126 100644 --- a/harmony/src/modules/postgresql/cnpg/crd.rs +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; )] #[serde(rename_all = "camelCase")] pub struct ClusterSpec { - pub instances: i32, + pub instances: u32, pub image_name: Option, pub storage: Storage, pub bootstrap: Bootstrap, diff --git a/harmony/src/modules/postgresql/failover.rs b/harmony/src/modules/postgresql/failover.rs index 9e54b2f..8e80166 100644 --- a/harmony/src/modules/postgresql/failover.rs +++ b/harmony/src/modules/postgresql/failover.rs @@ -3,6 +3,7 @@ use log::debug; use log::info; use std::collections::HashMap; +use crate::interpret::Outcome; use crate::{ modules::postgresql::capability::{ BootstrapConfig, BootstrapStrategy, ExternalClusterConfig, PostgreSQL, @@ -25,6 +26,7 @@ impl PostgreSQL for FailoverTopology { instances: config.instances, storage_size: config.storage_size.clone(), role: PostgreSQLClusterRole::Primary, + namespace: config.namespace.clone(), }; info!( @@ -91,6 +93,7 @@ impl PostgreSQL for FailoverTopology { instances: config.instances, storage_size: config.storage_size.clone(), role: PostgreSQLClusterRole::Replica(replica_cluster_config), + namespace: config.namespace.clone(), }; info!( @@ -102,7 +105,7 @@ impl PostgreSQL for FailoverTopology { info!( "Replica cluster '{}' deployed successfully; failover topology '{}' ready", - replica_config.cluster_name, config.cluster_name + replica_config.cluster_name, replica_config.cluster_name ); Ok(primary_cluster_name) diff --git a/harmony/src/modules/postgresql/mod.rs b/harmony/src/modules/postgresql/mod.rs index ea50106..75e91d6 100644 --- a/harmony/src/modules/postgresql/mod.rs +++ b/harmony/src/modules/postgresql/mod.rs @@ -1,8 +1,8 @@ pub mod capability; -mod score; +mod score_k8s; mod score_connect; pub use score_connect::*; -pub use score::*; +pub use score_k8s::*; mod score_public; pub use score_public::*; @@ -10,4 +10,7 @@ pub mod failover; mod operator; pub use operator::*; +mod score; +pub use score::*; + pub mod cnpg; diff --git a/harmony/src/modules/postgresql/score.rs b/harmony/src/modules/postgresql/score.rs index 2a5e82f..72023e6 100644 --- a/harmony/src/modules/postgresql/score.rs +++ b/harmony/src/modules/postgresql/score.rs @@ -1,51 +1,42 @@ +use async_trait::async_trait; +use harmony_types::id::Id; use serde::Serialize; -use crate::interpret::Interpret; -use crate::modules::k8s::resource::K8sResourceScore; -use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage}; +use crate::data::Version; +use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; +use crate::inventory::Inventory; +use crate::modules::postgresql::capability::{PostgreSQL, PostgreSQLConfig}; use crate::score::Score; -use crate::topology::{K8sclient, Topology}; -use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use crate::topology::Topology; -/// Deploys an opinionated, highly available PostgreSQL cluster managed by CNPG. +/// High-level, infrastructure-agnostic PostgreSQL deployment score. /// -/// # Goals -/// - Production-ready Postgres HA (3 instances), persistent storage, app DB. +/// Delegates to the Topology's PostgreSQL capability implementation, +/// allowing flexibility in deployment strategy (k8s/CNPG, cloud-managed, etc.). /// /// # Usage /// ``` /// use harmony::modules::postgresql::PostgreSQLScore; -/// let score = PostgreSQLScore::new("my-app-ns"); +/// let score = PostgreSQLScore::new("harmony"); /// ``` /// -/// # Limitations (Happy Path) -/// - Requires CNPG operator installed (use CloudNativePgOperatorScore). -/// - No backups, monitoring, extensions configured. +/// # Design +/// - PostgreSQLScore: High-level, relies on Topology's PostgreSQL implementation +/// - Topology implements PostgreSQL capability (decoupled from score) +/// - K8s topologies use K8sPostgreSQLScore internally for CNPG deployment /// -/// 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 +/// This layered approach gives users choice: +/// - Use PostgreSQLScore for portability across topologies +/// - Use K8sPostgreSQLScore directly for k8s-specific control #[derive(Debug, Clone, Serialize)] pub struct PostgreSQLScore { - pub name: String, - /// **Note :** on OpenShfit based clusters, the namespace `default` has security - /// settings incompatible with the default CNPG behavior. - pub namespace: String, - pub instances: i32, - pub storage_size: String, - pub image_name: Option, + pub config: PostgreSQLConfig, } impl Default for PostgreSQLScore { fn default() -> Self { Self { - name: "harmony-pg".to_string(), - // We are using the namespace harmony by default since some clusters (openshift family) - // have incompatible configuration of the default namespace with cnpg - namespace: "harmony".to_string(), - instances: 1, - storage_size: "1Gi".to_string(), - image_name: None, // This lets cnpg use its default image + config: PostgreSQLConfig::default(), } } } @@ -53,41 +44,63 @@ impl Default for PostgreSQLScore { impl PostgreSQLScore { pub fn new(namespace: &str) -> Self { Self { - namespace: namespace.to_string(), - ..Default::default() + config: PostgreSQLConfig { + namespace: namespace.to_string(), + ..Default::default() + }, } } } -impl Score for PostgreSQLScore { +impl Score for PostgreSQLScore { fn create_interpret(&self) -> Box> { - let metadata = ObjectMeta { - name: Some(self.name.clone()), - 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() + Box::new(PostgreSQLInterpret { + config: self.config.clone(), + }) } fn name(&self) -> String { - format!("PostgreSQLScore({})", self.namespace) + format!( + "PostgreSQLScore({}:{})", + self.config.namespace, self.config.cluster_name + ) + } +} + +/// Interpret implementation that delegates to Topology's PostgreSQL capability. +#[derive(Debug, Clone)] +struct PostgreSQLInterpret { + config: PostgreSQLConfig, +} + +#[async_trait] +impl Interpret for PostgreSQLInterpret { + fn get_name(&self) -> InterpretName { + InterpretName::Custom("PostgreSQLInterpret") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() + } + + async fn execute(&self, _inventory: &Inventory, topo: &T) -> Result { + // Delegate to topology's PostgreSQL capability + let cluster_name = topo + .deploy(&self.config) + .await + .map_err(|e| InterpretError::new(e))?; + + Ok(Outcome::success(format!( + "PostgreSQL cluster '{}' deployed in namespace '{}'", + cluster_name, self.config.namespace + ))) } } diff --git a/harmony/src/modules/postgresql/score_k8s.rs b/harmony/src/modules/postgresql/score_k8s.rs new file mode 100644 index 0000000..5e3cb08 --- /dev/null +++ b/harmony/src/modules/postgresql/score_k8s.rs @@ -0,0 +1,80 @@ +use serde::Serialize; + +use crate::interpret::Interpret; +use crate::modules::k8s::resource::K8sResourceScore; +use crate::modules::postgresql::capability::PostgreSQLConfig; +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. +/// +/// # 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 K8sPostgreSQLScore { + pub config: PostgreSQLConfig, +} + +impl Default for K8sPostgreSQLScore { + fn default() -> Self { + Self { + config: PostgreSQLConfig::default(), + } + } +} + +impl K8sPostgreSQLScore { + pub fn new(namespace: &str) -> Self { + Self { + config: PostgreSQLConfig { + namespace: namespace.to_string(), + ..Default::default() + }, + } + } +} + +impl Score for K8sPostgreSQLScore { + fn create_interpret(&self) -> Box> { + let metadata = ObjectMeta { + name: Some(self.config.cluster_name.clone()), + namespace: Some(self.config.namespace.clone()), + ..ObjectMeta::default() + }; + + let spec = ClusterSpec { + instances: self.config.instances, + storage: Storage { + size: self.config.storage_size.to_string(), + }, + bootstrap: Bootstrap { + initdb: Initdb { + database: "app".to_string(), + owner: "app".to_string(), + }, + }, + ..ClusterSpec::default() + }; + + let cluster = Cluster { metadata, spec }; + + K8sResourceScore::single(cluster, Some(self.config.namespace.clone())).create_interpret() + } + + fn name(&self) -> String { + format!("PostgreSQLScore({})", self.config.namespace) + } +} diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs index d729005..1ebba70 100644 --- a/harmony/src/modules/postgresql/score_public.rs +++ b/harmony/src/modules/postgresql/score_public.rs @@ -6,7 +6,7 @@ use crate::data::Version; use crate::domain::topology::router::{TlsRoute, TlsRouter}; use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; use crate::inventory::Inventory; -use crate::modules::postgresql::PostgreSQLScore; +use crate::modules::postgresql::K8sPostgreSQLScore; use crate::score::Score; use crate::topology::{K8sclient, Topology}; @@ -23,7 +23,7 @@ use crate::topology::{K8sclient, Topology}; #[derive(Debug, Clone, Serialize)] pub struct PublicPostgreSQLScore { /// Inner non-public Postgres cluster config. - pub postgres_score: PostgreSQLScore, + pub postgres_score: K8sPostgreSQLScore, /// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432). pub hostname: String, } @@ -31,7 +31,7 @@ pub struct PublicPostgreSQLScore { impl PublicPostgreSQLScore { pub fn new(namespace: &str, hostname: &str) -> Self { Self { - postgres_score: PostgreSQLScore::new(namespace), + postgres_score: K8sPostgreSQLScore::new(namespace), hostname: hostname.to_string(), } } @@ -39,9 +39,9 @@ impl PublicPostgreSQLScore { impl Score for PublicPostgreSQLScore { fn create_interpret(&self) -> Box> { - let rw_backend = format!("{}-rw", self.postgres_score.name); + let rw_backend = format!("{}-rw", self.postgres_score.config.cluster_name); let tls_route = TlsRoute { - namespace: self.postgres_score.namespace.clone(), + namespace: self.postgres_score.config.namespace.clone(), hostname: self.hostname.clone(), backend: rw_backend, target_port: 5432, @@ -56,7 +56,7 @@ impl Score for PublicPostg fn name(&self) -> String { format!( "PublicPostgreSQLScore({}:{})", - self.postgres_score.namespace, self.hostname + self.postgres_score.config.namespace, self.hostname ) } } @@ -64,7 +64,7 @@ impl Score for PublicPostg /// Custom interpret: deploy Postgres then install public TLS route. #[derive(Debug, Clone)] struct PublicPostgreSQLInterpret { - postgres_score: PostgreSQLScore, + postgres_score: K8sPostgreSQLScore, tls_route: TlsRoute, } @@ -93,7 +93,7 @@ impl Interpret for PublicP Ok(Outcome::success(format!( "Public CNPG cluster '{}' deployed with TLS passthrough route '{}'", - self.postgres_score.name.clone(), + self.postgres_score.config.cluster_name.clone(), self.tls_route.hostname ))) } diff --git a/harmony_types/src/storage.rs b/harmony_types/src/storage.rs index a0a3094..28a7ef4 100644 --- a/harmony_types/src/storage.rs +++ b/harmony_types/src/storage.rs @@ -5,6 +5,8 @@ use std::fmt; pub struct StorageSize { size_bytes: u64, #[serde(skip)] + display_value: Option, + #[serde(skip)] display_suffix: Option, } @@ -12,6 +14,7 @@ impl StorageSize { pub fn new(size_bytes: u64) -> Self { Self { size_bytes, + display_value: None, display_suffix: None, } } @@ -19,6 +22,7 @@ impl StorageSize { pub fn b(size: u64) -> Self { Self { size_bytes: size, + display_value: Some(size), display_suffix: Some("B".to_string()), } } @@ -26,6 +30,7 @@ impl StorageSize { pub fn kb(size: u64) -> Self { Self { size_bytes: size * 1024, + display_value: Some(size), display_suffix: Some("KB".to_string()), } } @@ -33,6 +38,7 @@ impl StorageSize { pub fn mb(size: u64) -> Self { Self { size_bytes: size * 1024 * 1024, + display_value: Some(size), display_suffix: Some("MB".to_string()), } } @@ -40,6 +46,7 @@ impl StorageSize { pub fn gb(size: u64) -> Self { Self { size_bytes: size * 1024 * 1024 * 1024, + display_value: Some(size), display_suffix: Some("GB".to_string()), } } @@ -47,13 +54,15 @@ impl StorageSize { pub fn gi(size: u64) -> Self { Self { size_bytes: size * 1024 * 1024 * 1024, - display_suffix: Some("GiB".to_string()), + display_value: Some(size), + display_suffix: Some("Gi".to_string()), } } pub fn tb(size: u64) -> Self { Self { size_bytes: size * 1024 * 1024 * 1024 * 1024, + display_value: Some(size), display_suffix: Some("TB".to_string()), } } @@ -61,6 +70,7 @@ impl StorageSize { pub fn ti(size: u64) -> Self { Self { size_bytes: size * 1024 * 1024 * 1024 * 1024, + display_value: Some(size), display_suffix: Some("TiB".to_string()), } } @@ -73,7 +83,8 @@ impl StorageSize { impl fmt::Display for StorageSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(suffix) = &self.display_suffix { - write!(f, "{}{}", self.size_bytes, suffix) + let value = self.display_value.unwrap_or(self.size_bytes); + write!(f, "{}{}", value, suffix) } else { write!(f, "{}B", self.size_bytes) } @@ -95,42 +106,42 @@ mod tests { fn test_kilobytes() { let size = StorageSize::kb(2); assert_eq!(size.bytes(), 2048); - assert_eq!(size.to_string(), "2048KB"); + assert_eq!(size.to_string(), "2KB"); } #[test] fn test_megabytes() { let size = StorageSize::mb(3); assert_eq!(size.bytes(), 3 * 1024 * 1024); - assert_eq!(size.to_string(), "3145728MB"); + assert_eq!(size.to_string(), "3MB"); } #[test] fn test_gigabytes() { let size = StorageSize::gb(4); assert_eq!(size.bytes(), 4 * 1024 * 1024 * 1024); - assert_eq!(size.to_string(), "4294967296GB"); + assert_eq!(size.to_string(), "4GB"); } #[test] fn test_gibibytes() { let size = StorageSize::gi(1); assert_eq!(size.bytes(), 1024 * 1024 * 1024); - assert_eq!(size.to_string(), "1073741824GiB"); + assert_eq!(size.to_string(), "1Gi"); } #[test] fn test_terabytes() { let size = StorageSize::tb(5); assert_eq!(size.bytes(), 5 * 1024 * 1024 * 1024 * 1024); - assert_eq!(size.to_string(), "5497558138880TB"); + assert_eq!(size.to_string(), "5TB"); } #[test] fn test_tebibytes() { let size = StorageSize::ti(1); assert_eq!(size.bytes(), 1024 * 1024 * 1024 * 1024); - assert_eq!(size.to_string(), "1099511627776TiB"); + assert_eq!(size.to_string(), "1Ti"); } #[test] @@ -155,6 +166,6 @@ mod tests { fn test_ord() { let one_gb = StorageSize::gb(1); let one_gi = StorageSize::gi(1); - assert!(one_gb < one_gi); // 1GB = 1000MB, 1GiB = 1024MB + assert!(one_gb < one_gi); // 1GB = 1000MB, 1Gi = 1024MB } }