From b61e4f9a96812eecd346b59c376fb5dc7c5c1e14 Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sat, 13 Dec 2025 09:47:59 -0500 Subject: [PATCH] wip: Expose postgres publicly. Created tlsroute capability and postgres implementations --- harmony/src/domain/topology/router.rs | 81 +++++++++++++++++ harmony/src/modules/postgresql/mod.rs | 2 + .../src/modules/postgresql/score_public.rs | 89 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 harmony/src/modules/postgresql/score_public.rs diff --git a/harmony/src/domain/topology/router.rs b/harmony/src/domain/topology/router.rs index 7c56e6a..3c8721d 100644 --- a/harmony/src/domain/topology/router.rs +++ b/harmony/src/domain/topology/router.rs @@ -1,11 +1,19 @@ +use async_trait::async_trait; use cidr::Ipv4Cidr; use derive_new::new; use super::{IpAddress, LogicalHost}; +/// Basic network router abstraction (L3 IP routing/gateway). +/// Distinguished from TlsRouter (L4 TLS passthrough). pub trait Router: Send + Sync { + /// Gateway IP address for this subnet/router. fn get_gateway(&self) -> IpAddress; + + /// CIDR block managed by this router. fn get_cidr(&self) -> Ipv4Cidr; + + /// Logical host associated with this router. fn get_host(&self) -> LogicalHost; } @@ -38,3 +46,76 @@ impl Router for UnmanagedRouter { todo!() } } + + +#[derive(Clone, Debug)] + +/// Desired state config for a TLS passthrough route. +/// Forwards external TLS (port 443) → backend service:target_port (no termination at router). +/// Inspired by CNPG multisite: exposes `-rw`/`-ro` services publicly via OKD Route/HAProxy/K8s +/// Gateway etc. +/// +/// # Example +/// ``` +/// use harmony::domain::topology::router::TlsRoute; +/// let postgres_rw = TlsRoute { +/// hostname: "postgres-cluster-example.public.domain.io".to_string(), +/// backend: "postgres-cluster-example-rw".to_string(), // k8s Service or HAProxy upstream +/// target_port: 5432, +/// }; +/// ``` +pub struct TlsRoute { + /// Public hostname clients connect to (TLS SNI, port 443 implicit). + /// Router matches this for passthrough forwarding. + pub hostname: String, + + /// Backend/host identifier (k8s Service, HAProxy upstream, IP/FQDN, etc.). + pub backend: String, + + /// Backend TCP port (Postgres: 5432). + pub target_port: u16, +} + +#[async_trait] + +/// Installs and queries TLS passthrough routes (L4 TCP/SNI forwarding, no TLS termination). +/// Agnostic to impl: OKD Route, AWS NLB+HAProxy, k3s Envoy Gateway, Apache ProxyPass. +/// Used by PostgreSQL capability to expose CNPG clusters multisite (site1 → site2 replication). +/// +/// # Usage +/// ```rust,no_run +/// // After CNPG deploy, expose RW endpoint +/// let topology = okd_topology(); +/// let route = TlsRoute { /* ... */ }; +/// topology.install_route(route).await?; // OKD Route, HAProxy reload, etc. +/// +/// // Client: psql \\"host={route.hostname} port=443 sslmode=verify-ca sslnegotiation=direct\\" +/// let public_ep = Endpoint { host: topology.hostname(), port: 443 }; +/// ``` +pub trait TlsRouter: Send + Sync { +/// Provisions the route (idempotent where possible). + /// Example: OKD Route{ host, to: backend:target_port, tls: {passthrough} }; + /// HAProxy frontend→backend \"postgres-upstream\". + async fn install_route(&self, config: TlsRoute) -> Result<(), String>; + + /// Installed route's public hostname. + fn hostname(&self) -> String; + + /// Installed route's backend identifier. + fn backend(&self) -> String; + + /// Installed route's backend port. + fn target_port(&self) -> u16; +} + +impl std::fmt::Debug for dyn TlsRouter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "TlsRouter[hostname={}, backend={}:{}]", + self.hostname(), + self.backend(), + self.target_port() + )) + } +} + diff --git a/harmony/src/modules/postgresql/mod.rs b/harmony/src/modules/postgresql/mod.rs index 377e4d4..11c21aa 100644 --- a/harmony/src/modules/postgresql/mod.rs +++ b/harmony/src/modules/postgresql/mod.rs @@ -1,6 +1,8 @@ pub mod capability; mod score; pub use score::*; +mod score_public; +pub use score_public::*; pub mod failover; mod operator; diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs new file mode 100644 index 0000000..1bbe8fa --- /dev/null +++ b/harmony/src/modules/postgresql/score_public.rs @@ -0,0 +1,89 @@ +use async_trait::async_trait; +use serde::Serialize; + +use crate::domain::topology::router::{TlsRoute, TlsRouter}; +use crate::interpret::Interpret; +use crate::modules::k8s::resource::K8sResourceScore; +use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage}; +use crate::modules::postgresql::PostgreSQLScore; +use crate::score::Score; +use crate::topology::{K8sclient, Topology}; +use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta; + +/// Deploys a public PostgreSQL cluster: CNPG + TLS passthrough route for RW endpoint. +/// For failover/multisite: exposes single-instance or small HA Postgres publicly. +/// +/// Sequence: PostgreSQLScore → TlsRouter::install_route (RW backend). +/// +/// # Usage +/// ``` +/// use harmony::modules::postgresql::PublicPostgreSQLScore; +/// let score = PublicPostgreSQLScore::new("harmony", "pg-rw.example.com"); +/// ``` +#[derive(Debug, Clone, Serialize)] +pub struct PublicPostgreSQLScore { + /// Inner non-public Postgres cluster config. + pub inner: PostgreSQLScore, + /// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432). + pub hostname: String, +} + +impl Default for PublicPostgreSQLScore { + fn default() -> Self { + Self { + inner: PostgreSQLScore::default(), + hostname: "postgres.default.public".to_string(), + } + } +} + +impl PublicPostgreSQLScore { + pub fn new(namespace: &str, hostname: &str) -> Self { + Self { + inner: PostgreSQLScore::new(namespace), + hostname: hostname.to_string(), + } + } +} + +/// Custom interpret: deploy Postgres then install public TLS route. +#[derive(Debug, Clone)] +struct PublicPostgreSQLInterpret { + postgres_score: PostgreSQLScore, + tls_route: TlsRoute, +} + +#[async_trait] +impl Interpret for PublicPostgreSQLInterpret { + async fn interpret(&self, topo: &T) -> Result<(), Box> { + // Deploy CNPG cluster first (creates -rw service) + self.postgres_score.create_interpret().interpret(topo).await?; + + // Expose RW publicly via TLS passthrough + topo.install_route(self.tls_route.clone()).await.map_err(|e| Box::new(std::io::Error::new(std::io::ErrorKind::Other, e)) as Box)?; + + Ok(()) + } +} + +impl Score for PublicPostgreSQLScore { + fn create_interpret(&self) -> Box> { + let rw_backend = format!("{}-rw", self.inner.name); + let tls_route = TlsRoute { + hostname: self.hostname.clone(), + backend: rw_backend, + target_port: 5432, + }; + + Box::new(PublicPostgreSQLInterpret { + postgres_score: self.inner.clone(), + tls_route, + }) + } + + fn name(&self) -> String { + format!("PublicPostgreSQLScore({}:{})", self.inner.namespace, self.hostname) + } +} + +// TODO: Add RO route (separate hostname/backend="cluster-ro"), backups, failover logic.