From 724ab0b888e71f3d54bafad2a947eeb01fdac62b Mon Sep 17 00:00:00 2001 From: wjro Date: Fri, 13 Feb 2026 15:18:23 -0500 Subject: [PATCH 1/6] wip: removed hardcoding and added fn to trait tlsrouter --- examples/multisite_postgres/src/main.rs | 1 - examples/public_postgres/src/main.rs | 1 - .../topology/k8s_anywhere/k8s_anywhere.rs | 4 ++ harmony/src/domain/topology/router.rs | 2 + harmony/src/modules/network/failover.rs | 12 +++++- harmony/src/modules/postgresql/capability.rs | 1 + harmony/src/modules/postgresql/failover.rs | 17 +++++++- .../src/modules/postgresql/score_public.rs | 39 +++++++++---------- 8 files changed, 52 insertions(+), 25 deletions(-) diff --git a/examples/multisite_postgres/src/main.rs b/examples/multisite_postgres/src/main.rs index 7ae4beb..739198f 100644 --- a/examples/multisite_postgres/src/main.rs +++ b/examples/multisite_postgres/src/main.rs @@ -14,7 +14,6 @@ async fn main() { ..Default::default() // Use harmony defaults, they are based on CNPG's default values : // "default" namespace, 1 instance, 1Gi storage }, - hostname: "postgrestest.sto1.nationtech.io".to_string(), }; harmony_cli::run( diff --git a/examples/public_postgres/src/main.rs b/examples/public_postgres/src/main.rs index 029080e..c7fbc59 100644 --- a/examples/public_postgres/src/main.rs +++ b/examples/public_postgres/src/main.rs @@ -16,7 +16,6 @@ async fn main() { ..Default::default() // Use harmony defaults, they are based on CNPG's default values : // 1 instance, 1Gi storage }, - hostname: "postgrestest.sto1.nationtech.io".to_string(), }; let test_connection = PostgreSQLConnectionScore { diff --git a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs index a188a92..41b457a 100644 --- a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs @@ -127,6 +127,10 @@ impl K8sclient for K8sAnywhereTopology { #[async_trait] impl TlsRouter for K8sAnywhereTopology { + async fn get_public_domain(&self) -> Result { + todo!() + } + async fn get_internal_domain(&self) -> Result, String> { match self.get_k8s_distribution().await.map_err(|e| { format!( diff --git a/harmony/src/domain/topology/router.rs b/harmony/src/domain/topology/router.rs index 30d5b46..59382d5 100644 --- a/harmony/src/domain/topology/router.rs +++ b/harmony/src/domain/topology/router.rs @@ -122,4 +122,6 @@ pub trait TlsRouter: Send + Sync { /// Returns the port that this router exposes externally. async fn get_router_port(&self) -> u16; + + async fn get_public_domain(&self) -> Result; } diff --git a/harmony/src/modules/network/failover.rs b/harmony/src/modules/network/failover.rs index d5fd8c0..ebb7f6c 100644 --- a/harmony/src/modules/network/failover.rs +++ b/harmony/src/modules/network/failover.rs @@ -4,7 +4,17 @@ use log::warn; use crate::topology::{FailoverTopology, TlsRoute, TlsRouter}; #[async_trait] -impl TlsRouter for FailoverTopology { +impl TlsRouter for FailoverTopology { + async fn get_public_domain(&self) -> Result { + let primary_domain = self + .primary + .get_public_domain() + .await + .map_err(|e| e.to_string())?; + + Ok(primary_domain) + } + async fn get_internal_domain(&self) -> Result, String> { todo!() } diff --git a/harmony/src/modules/postgresql/capability.rs b/harmony/src/modules/postgresql/capability.rs index 81ca83e..0530b69 100644 --- a/harmony/src/modules/postgresql/capability.rs +++ b/harmony/src/modules/postgresql/capability.rs @@ -37,6 +37,7 @@ pub struct PostgreSQLConfig { /// settings incompatible with the default CNPG behavior. pub namespace: String, } + impl PostgreSQLConfig { pub fn with_namespace(&self, namespace: &str) -> PostgreSQLConfig { let mut new = self.clone(); diff --git a/harmony/src/modules/postgresql/failover.rs b/harmony/src/modules/postgresql/failover.rs index 10fd654..f27dd05 100644 --- a/harmony/src/modules/postgresql/failover.rs +++ b/harmony/src/modules/postgresql/failover.rs @@ -49,11 +49,24 @@ impl PostgreSQL for FailoverTopology { // TODO we should be getting the public endpoint for a service by calling a method on // TlsRouter capability. // Something along the lines of `TlsRouter::get_hostname_for_service(...).await?;` + + //TODO hard coding this here means that the host name is set manually twice which is a + //proble + let host = format!( + "{}.{}.{}", + config.cluster_name, + config.namespace, + self.primary + .get_internal_domain() + .await + .expect("failed to retrieve internal domain") + .expect("domain was none") + .to_string() + ); let endpoint = PostgreSQLEndpoint { - host: "postgrestest.sto1.nationtech.io".to_string(), + host, port: self.primary.get_router_port().await, }; - info!( "Public endpoint '{}:{}' retrieved for primary", endpoint.host, endpoint.port diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs index eaf3c88..f7ed16c 100644 --- a/harmony/src/modules/postgresql/score_public.rs +++ b/harmony/src/modules/postgresql/score_public.rs @@ -24,40 +24,25 @@ use crate::topology::Topology; pub struct PublicPostgreSQLScore { /// Inner non-public Postgres cluster config. pub config: PostgreSQLConfig, - /// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432). - pub hostname: String, } impl PublicPostgreSQLScore { - pub fn new(namespace: &str, hostname: &str) -> Self { + pub fn new(namespace: &str) -> Self { Self { config: PostgreSQLConfig::default().with_namespace(namespace), - hostname: hostname.to_string(), } } } impl Score for PublicPostgreSQLScore { fn create_interpret(&self) -> Box> { - let rw_backend = format!("{}-rw", self.config.cluster_name); - let tls_route = TlsRoute { - namespace: self.config.namespace.clone(), - hostname: self.hostname.clone(), - backend: rw_backend, - target_port: 5432, - }; - Box::new(PublicPostgreSQLInterpret { config: self.config.clone(), - tls_route, }) } fn name(&self) -> String { - format!( - "PublicPostgreSQLScore({}:{})", - self.config.namespace, self.hostname - ) + format!("PublicPostgreSQLScore({})", self.config.namespace) } } @@ -65,7 +50,6 @@ impl Score for PublicPostgreSQLScore { #[derive(Debug, Clone)] struct PublicPostgreSQLInterpret { config: PostgreSQLConfig, - tls_route: TlsRoute, } #[async_trait] @@ -76,15 +60,30 @@ impl Interpret for PublicPostgreSQLInte .await .map_err(|e| InterpretError::new(e))?; + let hostname = format!( + "{}.{}.{}", + self.config.cluster_name, + self.config.namespace, + topo.get_internal_domain() + .await? + .expect("could not find internal domain") + ); + let rw_backend = format!("{}-rw", self.config.cluster_name); + let tls_route = TlsRoute { + hostname, + backend: rw_backend, + target_port: 5432, + namespace: self.config.namespace.clone(), + }; // Expose RW publicly via TLS passthrough - topo.install_route(self.tls_route.clone()) + topo.install_route(tls_route.clone()) .await .map_err(|e| InterpretError::new(e))?; Ok(Outcome::success(format!( "Public CNPG cluster '{}' deployed with TLS passthrough route '{}'", self.config.cluster_name.clone(), - self.tls_route.hostname + tls_route.hostname ))) } -- 2.39.5 From 6ab0f3a6ab7f83f63cebb6cd4d30840014ebc674 Mon Sep 17 00:00:00 2001 From: Sylvain Tremblay Date: Fri, 13 Feb 2026 15:48:24 -0500 Subject: [PATCH 2/6] wip --- Cargo.lock | 20 +++++++++++++++++++ .../topology/k8s_anywhere/k8s_anywhere.rs | 10 +++++++++- harmony/src/modules/network/failover.rs | 3 +++ .../src/modules/postgresql/score_public.rs | 4 +--- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed76c0e..1287cfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3638,6 +3638,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "json-prompt" +version = "0.1.0" +dependencies = [ + "brocade", + "cidr", + "env_logger", + "harmony", + "harmony_cli", + "harmony_macros", + "harmony_secret", + "harmony_secret_derive", + "harmony_types", + "log", + "schemars 0.8.22", + "serde", + "tokio", + "url", +] + [[package]] name = "jsonpath-rust" version = "0.7.5" diff --git a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs index 41b457a..1e70c48 100644 --- a/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere/k8s_anywhere.rs @@ -128,7 +128,10 @@ impl K8sclient for K8sAnywhereTopology { #[async_trait] impl TlsRouter for K8sAnywhereTopology { async fn get_public_domain(&self) -> Result { - todo!() + match &self.config.public_domain { + Some(public_domain) => Ok(public_domain.to_string()), + None => Err("Public domain not available".to_string()), + } } async fn get_internal_domain(&self) -> Result, String> { @@ -1173,6 +1176,7 @@ pub struct K8sAnywhereConfig { /// /// If the context name is not found, it will fail to initialize. pub k8s_context: Option, + public_domain: Option, } impl K8sAnywhereConfig { @@ -1200,6 +1204,7 @@ impl K8sAnywhereConfig { let mut kubeconfig: Option = None; let mut k8s_context: Option = None; + let mut public_domain: Option = None; for part in env_var_value.split(',') { let kv: Vec<&str> = part.splitn(2, '=').collect(); @@ -1207,6 +1212,7 @@ impl K8sAnywhereConfig { match kv[0].trim() { "kubeconfig" => kubeconfig = Some(kv[1].trim().to_string()), "context" => k8s_context = Some(kv[1].trim().to_string()), + "public_domain" => public_domain = Some(kv[1].trim().to_string()), _ => {} } } @@ -1224,6 +1230,7 @@ impl K8sAnywhereConfig { K8sAnywhereConfig { kubeconfig, k8s_context, + public_domain, use_system_kubeconfig, autoinstall: false, use_local_k3d: false, @@ -1266,6 +1273,7 @@ impl K8sAnywhereConfig { use_local_k3d: std::env::var("HARMONY_USE_LOCAL_K3D") .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(true)), k8s_context: std::env::var("HARMONY_K8S_CONTEXT").ok(), + public_domain: std::env::var("HARMONY_PUBLIC_DOMAIN").ok(), } } } diff --git a/harmony/src/modules/network/failover.rs b/harmony/src/modules/network/failover.rs index ebb7f6c..6e9b0fb 100644 --- a/harmony/src/modules/network/failover.rs +++ b/harmony/src/modules/network/failover.rs @@ -6,6 +6,7 @@ use crate::topology::{FailoverTopology, TlsRoute, TlsRouter}; #[async_trait] impl TlsRouter for FailoverTopology { async fn get_public_domain(&self) -> Result { + /* let primary_domain = self .primary .get_public_domain() @@ -13,6 +14,8 @@ impl TlsRouter for FailoverTopology { .map_err(|e| e.to_string())?; Ok(primary_domain) + */ + todo!() } async fn get_internal_domain(&self) -> Result, String> { diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs index f7ed16c..6e1ee7c 100644 --- a/harmony/src/modules/postgresql/score_public.rs +++ b/harmony/src/modules/postgresql/score_public.rs @@ -64,9 +64,7 @@ impl Interpret for PublicPostgreSQLInte "{}.{}.{}", self.config.cluster_name, self.config.namespace, - topo.get_internal_domain() - .await? - .expect("could not find internal domain") + topo.get_public_domain().await? ); let rw_backend = format!("{}-rw", self.config.cluster_name); let tls_route = TlsRoute { -- 2.39.5 From e709de531d1b1f5dfa91cc52939aa37f417c5f46 Mon Sep 17 00:00:00 2001 From: wjro Date: Fri, 13 Feb 2026 16:08:05 -0500 Subject: [PATCH 3/6] fix: added route building to failover topology --- harmony/src/modules/postgresql/failover.rs | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/harmony/src/modules/postgresql/failover.rs b/harmony/src/modules/postgresql/failover.rs index f27dd05..ebda63b 100644 --- a/harmony/src/modules/postgresql/failover.rs +++ b/harmony/src/modules/postgresql/failover.rs @@ -3,6 +3,8 @@ use log::debug; use log::info; use std::collections::HashMap; +use crate::interpret::InterpretError; +use crate::topology::TlsRoute; use crate::topology::TlsRouter; use crate::{ modules::postgresql::capability::{ @@ -67,11 +69,49 @@ impl PostgreSQL for FailoverTopology { host, port: self.primary.get_router_port().await, }; + info!( "Public endpoint '{}:{}' retrieved for primary", endpoint.host, endpoint.port ); + info!("installing primary postgres route"); + let prim_hostname = format!( + "{}.{}.{}", + config.cluster_name, + config.namespace, + self.primary.get_public_domain().await? + ); + let rw_backend = format!("{}-rw", config.cluster_name); + let tls_route = TlsRoute { + hostname: prim_hostname, + backend: rw_backend, + target_port: 5432, + namespace: config.namespace.clone(), + }; + // Expose RW publicly via TLS passthrough + self.primary.install_route(tls_route.clone()) + .await + .map_err(|e| InterpretError::new(e))?; + + info!("installing replica postgres route"); + let rep_hostname = format!( + "{}.{}.{}", + config.cluster_name, + config.namespace, + self.replica.get_public_domain().await? + ); + let rw_backend = format!("{}-rw", config.cluster_name); + let tls_route = TlsRoute { + hostname: rep_hostname, + backend: rw_backend, + target_port: 5432, + namespace: config.namespace.clone(), + }; + // Expose RW publicly via TLS passthrough + self.replica.install_route(tls_route.clone()) + .await + .map_err(|e| InterpretError::new(e))?; info!("Configuring replica connection parameters and bootstrap"); let mut connection_parameters = HashMap::new(); -- 2.39.5 From 16016febcfa971831e1a4c60df4df1462f5905b6 Mon Sep 17 00:00:00 2001 From: wjro Date: Mon, 16 Feb 2026 16:22:30 -0500 Subject: [PATCH 4/6] wip: adding impl details for deploying connected replica cluster --- harmony/src/modules/postgresql/cnpg/crd.rs | 46 +++++++++ harmony/src/modules/postgresql/score_k8s.rs | 102 ++++++++++++++++---- 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs index c8f6126..5a78011 100644 --- a/harmony/src/modules/postgresql/cnpg/crd.rs +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -1,6 +1,12 @@ +use std::collections::HashMap; + use kube::{CustomResource, api::ObjectMeta}; use serde::{Deserialize, Serialize}; +//TODO this fails right now since barmanObjectStore not set one bootstrap method etc. +//needs to have replica set in the crd as per +//https://cloudnative-pg.io/docs/1.25/replica_cluster/#setting-up-a-replica-cluster +// #[derive(CustomResource, Deserialize, Serialize, Clone, Debug)] #[kube( group = "postgresql.cnpg.io", @@ -13,9 +19,12 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct ClusterSpec { pub instances: u32, + #[serde(skip_serializing_if = "Option::is_none")] pub image_name: Option, pub storage: Storage, pub bootstrap: Bootstrap, + #[serde(skip_serializing_if = "Option::is_none")] + pub external_clusters: Option>, } impl Default for Cluster { @@ -34,6 +43,7 @@ impl Default for ClusterSpec { image_name: None, storage: Storage::default(), bootstrap: Bootstrap::default(), + external_clusters: None, } } } @@ -48,6 +58,8 @@ pub struct Storage { #[serde(rename_all = "camelCase")] pub struct Bootstrap { pub initdb: Initdb, + #[serde(skip_serializing_if = "Option::is_none")] + pub recovery: Option, } #[derive(Deserialize, Serialize, Clone, Debug, Default)] @@ -56,3 +68,37 @@ pub struct Initdb { pub database: String, pub owner: String, } + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Recovery { + pub source: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExternalCluster { + pub name: String, + pub connection_parameters: HashMap, + + pub ssl_key: Option, + pub ssl_cert: Option, + pub ssl_root_cert: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ConnectionParameters { + pub host: String, + pub user: String, + pub dbname: String, + pub sslmode: String, + pub sslnegotiation: String, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SecretKeySelector { + pub name: String, + pub key: String, +} diff --git a/harmony/src/modules/postgresql/score_k8s.rs b/harmony/src/modules/postgresql/score_k8s.rs index 5e3cb08..235e6d5 100644 --- a/harmony/src/modules/postgresql/score_k8s.rs +++ b/harmony/src/modules/postgresql/score_k8s.rs @@ -3,7 +3,10 @@ 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::modules::postgresql::cnpg::{ + Bootstrap, Cluster, ClusterSpec, ConnectionParameters, ExternalCluster, Initdb, Recovery, + SecretKeySelector, Storage, +}; use crate::score::Score; use crate::topology::{K8sclient, Topology}; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; @@ -49,29 +52,86 @@ impl K8sPostgreSQLScore { 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() - }; + match &self.config.role { + super::capability::PostgreSQLClusterRole::Primary => { + 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 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(), + }, + recovery: None, + }, + ..ClusterSpec::default() + }; - let cluster = Cluster { metadata, spec }; + let cluster = Cluster { metadata, spec }; - K8sResourceScore::single(cluster, Some(self.config.namespace.clone())).create_interpret() + K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) + .create_interpret() + } + super::capability::PostgreSQLClusterRole::Replica(replica_config) => { + 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::default(), + recovery: Some(Recovery { + source: replica_config.primary_cluster_name.clone(), + }), + }, + external_clusters: Some(vec![ExternalCluster { + name: replica_config.primary_cluster_name.clone(), + ssl_key: Some(SecretKeySelector { + name: "tls.key".into(), + key: replica_config + .replication_certs + .streaming_replica_key_pem + .clone(), + }), + ssl_cert: Some(SecretKeySelector { + name: "tls.crt".into(), + key: replica_config + .replication_certs + .streaming_replica_cert_pem + .clone(), + }), + ssl_root_cert: Some(SecretKeySelector { + name: "ca.crt".into(), + key: replica_config.replication_certs.ca_cert_pem.clone(), + }), + connection_parameters: replica_config + .external_cluster + .connection_parameters + .clone(), + }]), + ..ClusterSpec::default() + }; + + let cluster = Cluster { metadata, spec }; + + K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) + .create_interpret() + } + } } fn name(&self) -> String { -- 2.39.5 From 2cb7aeefc0826c3517932dc157eb1f0758a2c868 Mon Sep 17 00:00:00 2001 From: wjro Date: Tue, 17 Feb 2026 15:02:00 -0500 Subject: [PATCH 5/6] fix: deploys replicated postgresql with site 2 as standby --- harmony/src/modules/postgresql/cnpg/crd.rs | 28 +++- harmony/src/modules/postgresql/failover.rs | 15 +- harmony/src/modules/postgresql/score_k8s.rs | 165 ++++++++++++++++---- 3 files changed, 161 insertions(+), 47 deletions(-) diff --git a/harmony/src/modules/postgresql/cnpg/crd.rs b/harmony/src/modules/postgresql/cnpg/crd.rs index 5a78011..aa4cbd7 100644 --- a/harmony/src/modules/postgresql/cnpg/crd.rs +++ b/harmony/src/modules/postgresql/cnpg/crd.rs @@ -3,10 +3,6 @@ use std::collections::HashMap; use kube::{CustomResource, api::ObjectMeta}; use serde::{Deserialize, Serialize}; -//TODO this fails right now since barmanObjectStore not set one bootstrap method etc. -//needs to have replica set in the crd as per -//https://cloudnative-pg.io/docs/1.25/replica_cluster/#setting-up-a-replica-cluster -// #[derive(CustomResource, Deserialize, Serialize, Clone, Debug)] #[kube( group = "postgresql.cnpg.io", @@ -25,6 +21,8 @@ pub struct ClusterSpec { pub bootstrap: Bootstrap, #[serde(skip_serializing_if = "Option::is_none")] pub external_clusters: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub replica: Option, } impl Default for Cluster { @@ -44,6 +42,7 @@ impl Default for ClusterSpec { storage: Storage::default(), bootstrap: Bootstrap::default(), external_clusters: None, + replica: None, } } } @@ -57,9 +56,13 @@ pub struct Storage { #[derive(Deserialize, Serialize, Clone, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct Bootstrap { - pub initdb: Initdb, + #[serde(skip_serializing_if = "Option::is_none")] + pub initdb: Option, #[serde(skip_serializing_if = "Option::is_none")] pub recovery: Option, + #[serde(rename = "pg_basebackup")] + #[serde(skip_serializing_if = "Option::is_none")] + pub pg_basebackup: Option, } #[derive(Deserialize, Serialize, Clone, Debug, Default)] @@ -75,12 +78,16 @@ pub struct Recovery { pub source: String, } +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct PgBaseBackup { + pub source: String, +} + #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct ExternalCluster { pub name: String, pub connection_parameters: HashMap, - pub ssl_key: Option, pub ssl_cert: Option, pub ssl_root_cert: Option, @@ -96,6 +103,15 @@ pub struct ConnectionParameters { pub sslnegotiation: String, } +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ReplicaSpec { + pub enabled: bool, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub primary: Option, +} + #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct SecretKeySelector { diff --git a/harmony/src/modules/postgresql/failover.rs b/harmony/src/modules/postgresql/failover.rs index ebda63b..b4ae813 100644 --- a/harmony/src/modules/postgresql/failover.rs +++ b/harmony/src/modules/postgresql/failover.rs @@ -51,18 +51,14 @@ impl PostgreSQL for FailoverTopology { // TODO we should be getting the public endpoint for a service by calling a method on // TlsRouter capability. // Something along the lines of `TlsRouter::get_hostname_for_service(...).await?;` - - //TODO hard coding this here means that the host name is set manually twice which is a - //proble let host = format!( "{}.{}.{}", config.cluster_name, config.namespace, self.primary - .get_internal_domain() + .get_public_domain() .await - .expect("failed to retrieve internal domain") - .expect("domain was none") + .expect("failed to retrieve public domain") .to_string() ); let endpoint = PostgreSQLEndpoint { @@ -90,7 +86,8 @@ impl PostgreSQL for FailoverTopology { namespace: config.namespace.clone(), }; // Expose RW publicly via TLS passthrough - self.primary.install_route(tls_route.clone()) + self.primary + .install_route(tls_route.clone()) .await .map_err(|e| InterpretError::new(e))?; @@ -108,8 +105,10 @@ impl PostgreSQL for FailoverTopology { target_port: 5432, namespace: config.namespace.clone(), }; + // Expose RW publicly via TLS passthrough - self.replica.install_route(tls_route.clone()) + self.replica + .install_route(tls_route.clone()) .await .map_err(|e| InterpretError::new(e))?; info!("Configuring replica connection parameters and bootstrap"); diff --git a/harmony/src/modules/postgresql/score_k8s.rs b/harmony/src/modules/postgresql/score_k8s.rs index 235e6d5..3c3a2e5 100644 --- a/harmony/src/modules/postgresql/score_k8s.rs +++ b/harmony/src/modules/postgresql/score_k8s.rs @@ -1,15 +1,20 @@ -use serde::Serialize; - -use crate::interpret::Interpret; +use crate::data::Version; +use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; +use crate::inventory::Inventory; use crate::modules::k8s::resource::K8sResourceScore; use crate::modules::postgresql::capability::PostgreSQLConfig; use crate::modules::postgresql::cnpg::{ - Bootstrap, Cluster, ClusterSpec, ConnectionParameters, ExternalCluster, Initdb, Recovery, + Bootstrap, Cluster, ClusterSpec, ExternalCluster, Initdb, PgBaseBackup, ReplicaSpec, SecretKeySelector, Storage, }; use crate::score::Score; use crate::topology::{K8sclient, Topology}; +use async_trait::async_trait; +use harmony_types::id::Id; +use k8s_openapi::ByteString; +use k8s_openapi::api::core::v1::Secret; use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; +use serde::Serialize; /// Deploys an opinionated, highly available PostgreSQL cluster managed by CNPG. /// @@ -52,6 +57,28 @@ impl K8sPostgreSQLScore { impl Score for K8sPostgreSQLScore { fn create_interpret(&self) -> Box> { + Box::new(K8sPostgreSQLInterpret { + config: self.config.clone(), + }) + } + + fn name(&self) -> String { + format!("PostgreSQLScore({})", self.config.namespace) + } +} + +#[derive(Debug)] +pub struct K8sPostgreSQLInterpret { + config: PostgreSQLConfig, +} + +#[async_trait] +impl Interpret for K8sPostgreSQLInterpret { + async fn execute( + &self, + inventory: &Inventory, + topology: &T, + ) -> Result { match &self.config.role { super::capability::PostgreSQLClusterRole::Primary => { let metadata = ObjectMeta { @@ -66,21 +93,77 @@ impl Score for K8sPostgreSQLScore { size: self.config.storage_size.to_string(), }, bootstrap: Bootstrap { - initdb: Initdb { + initdb: Some(Initdb { database: "app".to_string(), owner: "app".to_string(), - }, + }), recovery: None, + pg_basebackup: None, }, ..ClusterSpec::default() }; - let cluster = Cluster { metadata, spec }; - K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) - .create_interpret() + Ok( + K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) + .create_interpret() + .execute(inventory, topology) + .await?, + ) } super::capability::PostgreSQLClusterRole::Replica(replica_config) => { + let metadata = ObjectMeta { + name: Some("streaming-replica-certs".to_string()), + namespace: Some(self.config.namespace.clone()), + ..ObjectMeta::default() + }; + + // The data must be base64-encoded. If you already have PEM strings in your config, encode them: + let mut data = std::collections::BTreeMap::new(); + data.insert( + "tls.key".to_string(), + ByteString( + replica_config + .replication_certs + .streaming_replica_key_pem + .as_bytes() + .to_vec(), + ), + ); + data.insert( + "tls.crt".to_string(), + ByteString( + replica_config + .replication_certs + .streaming_replica_cert_pem + .as_bytes() + .to_vec(), + ), + ); + data.insert( + "ca.crt".to_string(), + ByteString( + replica_config + .replication_certs + .ca_cert_pem + .as_bytes() + .to_vec(), + ), + ); + + let secret = Secret { + metadata, + data: Some(data), + string_data: None, // You could use string_data if you prefer raw strings + type_: Some("Opaque".to_string()), + ..Secret::default() + }; + + K8sResourceScore::single(secret, Some(self.config.namespace.clone())) + .create_interpret() + .execute(inventory, topology) + .await?; + let metadata = ObjectMeta { name: Some(self.config.cluster_name.clone()), namespace: Some(self.config.namespace.clone()), @@ -93,48 +176,64 @@ impl Score for K8sPostgreSQLScore { size: self.config.storage_size.to_string(), }, bootstrap: Bootstrap { - initdb: Initdb::default(), - recovery: Some(Recovery { + initdb: None, + recovery: None, + pg_basebackup: Some(PgBaseBackup { source: replica_config.primary_cluster_name.clone(), }), }, external_clusters: Some(vec![ExternalCluster { name: replica_config.primary_cluster_name.clone(), - ssl_key: Some(SecretKeySelector { - name: "tls.key".into(), - key: replica_config - .replication_certs - .streaming_replica_key_pem - .clone(), - }), - ssl_cert: Some(SecretKeySelector { - name: "tls.crt".into(), - key: replica_config - .replication_certs - .streaming_replica_cert_pem - .clone(), - }), - ssl_root_cert: Some(SecretKeySelector { - name: "ca.crt".into(), - key: replica_config.replication_certs.ca_cert_pem.clone(), - }), connection_parameters: replica_config .external_cluster .connection_parameters .clone(), + ssl_key: Some(SecretKeySelector { + name: "streaming-replica-certs".to_string(), + key: "tls.key".to_string(), + }), + ssl_cert: Some(SecretKeySelector { + name: "streaming-replica-certs".to_string(), + key: "tls.crt".to_string(), + }), + ssl_root_cert: Some(SecretKeySelector { + name: "streaming-replica-certs".to_string(), + key: "ca.crt".to_string(), + }), }]), + replica: Some(ReplicaSpec { + enabled: true, + source: replica_config.primary_cluster_name.clone(), + primary: None, + }), ..ClusterSpec::default() }; let cluster = Cluster { metadata, spec }; - K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) - .create_interpret() + Ok( + K8sResourceScore::single(cluster, Some(self.config.namespace.clone())) + .create_interpret() + .execute(inventory, topology) + .await?, + ) } } } - fn name(&self) -> String { - format!("PostgreSQLScore({})", self.config.namespace) + fn get_name(&self) -> InterpretName { + InterpretName::Custom("K8sPostgreSQLInterpret") + } + + fn get_version(&self) -> Version { + todo!() + } + + fn get_status(&self) -> InterpretStatus { + todo!() + } + + fn get_children(&self) -> Vec { + todo!() } } -- 2.39.5 From d8ab9d52a45df692f4ac023585af309da8555e01 Mon Sep 17 00:00:00 2001 From: wjro Date: Tue, 17 Feb 2026 15:34:42 -0500 Subject: [PATCH 6/6] fix:broken test --- Cargo.lock | 20 ------------------- .../src/modules/postgresql/score_public.rs | 2 +- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1287cfc..ed76c0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3638,26 +3638,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "json-prompt" -version = "0.1.0" -dependencies = [ - "brocade", - "cidr", - "env_logger", - "harmony", - "harmony_cli", - "harmony_macros", - "harmony_secret", - "harmony_secret_derive", - "harmony_types", - "log", - "schemars 0.8.22", - "serde", - "tokio", - "url", -] - [[package]] name = "jsonpath-rust" version = "0.7.5" diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs index 6e1ee7c..a7ef5a8 100644 --- a/harmony/src/modules/postgresql/score_public.rs +++ b/harmony/src/modules/postgresql/score_public.rs @@ -18,7 +18,7 @@ use crate::topology::Topology; /// # Usage /// ``` /// use harmony::modules::postgresql::PublicPostgreSQLScore; -/// let score = PublicPostgreSQLScore::new("harmony", "pg-rw.example.com"); +/// let score = PublicPostgreSQLScore::new("harmony"); /// ``` #[derive(Debug, Clone, Serialize)] pub struct PublicPostgreSQLScore { -- 2.39.5