wip: public postgres many fixes and refactoring to have a more cohesive routing management
Some checks failed
Run Check Script / check (pull_request) Failing after 41s

This commit is contained in:
2025-12-15 17:04:30 -05:00
parent e0da5764fb
commit 446e079595
7 changed files with 88 additions and 71 deletions

View File

@@ -0,0 +1,18 @@
[package]
name = "example-public-postgres"
edition = "2024"
version.workspace = true
readme.workspace = true
license.workspace = true
publish = false
[dependencies]
harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" }
harmony_types = { path = "../../harmony_types" }
cidr = { workspace = true }
tokio = { workspace = true }
harmony_macros = { path = "../../harmony_macros" }
log = { workspace = true }
env_logger = { workspace = true }
url = { workspace = true }

View File

@@ -0,0 +1,34 @@
use harmony::{
inventory::Inventory,
modules::{network::TlsPassthroughScore, postgresql::PostgreSQLScore},
topology::{K8sAnywhereTopology, TlsRoute},
};
#[tokio::main]
async fn main() {
let namespace = "harmony-postgres-example".to_string();
let postgresql = PostgreSQLScore {
name: "harmony-postgres-example".to_string(), // Override default name
namespace: namespace.clone(),
..Default::default() // Use harmony defaults, they are based on CNPG's default values :
// "default" namespace, 1 instance, 1Gi storage
};
let tls_passthrough = TlsPassthroughScore {
route: TlsRoute {
hostname: "postgres.example.com".to_string(), // CNPG creates a -rw service for read-write endpoint
backend: format!("{}-rw", postgresql.name), // Public hostname for TLS SNI
namespace: namespace.clone(),
target_port: 5432, // PostgreSQL default port
},
};
harmony_cli::run(
Inventory::autoload(),
K8sAnywhereTopology::from_env(),
vec![Box::new(postgresql), Box::new(tls_passthrough)],
None,
)
.await
.unwrap();
}

View File

@@ -2,6 +2,7 @@ use std::{collections::BTreeMap, process::Command, sync::Arc, time::Duration};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine, engine::general_purpose}; use base64::{Engine, engine::general_purpose};
use harmony_types::rfc1123::Rfc1123Name;
use k8s_openapi::api::{ use k8s_openapi::api::{
core::v1::Secret, core::v1::Secret,
rbac::v1::{ClusterRoleBinding, RoleRef, Subject}, rbac::v1::{ClusterRoleBinding, RoleRef, Subject},
@@ -35,6 +36,7 @@ use crate::{
}, },
}, },
network::TlsPassthroughScore, network::TlsPassthroughScore,
okd::route::OKDTlsPassthroughScore,
prometheus::{ prometheus::{
k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore, k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore,
prometheus::PrometheusMonitoring, rhob_alerting_score::RHOBAlertingScore, prometheus::PrometheusMonitoring, rhob_alerting_score::RHOBAlertingScore,
@@ -109,9 +111,12 @@ impl TlsRouter for K8sAnywhereTopology {
if let Some(distro) = self.k8s_distribution.get() { if let Some(distro) = self.k8s_distribution.get() {
match distro { match distro {
KubernetesDistribution::OpenshiftFamily => { KubernetesDistribution::OpenshiftFamily => {
TlsPassthroughScore { route } OKDTlsPassthroughScore {
.interpret(&Inventory::empty(), self) name: Rfc1123Name::try_from(route.to_string_short().as_str())?,
.await?; route,
}
.interpret(&Inventory::empty(), self)
.await?;
Ok(()) Ok(())
} }
KubernetesDistribution::K3sFamily | KubernetesDistribution::Default => Err( KubernetesDistribution::K3sFamily | KubernetesDistribution::Default => Err(

View File

@@ -73,8 +73,19 @@ pub struct TlsRoute {
/// Backend TCP port (Postgres: 5432). /// Backend TCP port (Postgres: 5432).
pub target_port: u16, pub target_port: u16,
/// The environment in which it lives.
/// TODO clarify how we handle this in higher level abstractions. The namespace name is a
/// direct mapping to k8s but that could be misleading for other implementations.
pub namespace: String,
} }
impl TlsRoute {
pub fn to_string_short(&self) -> String {
format!("{}-{}:{}", self.hostname, self.backend, self.target_port)
}
}
/// Installs and queries TLS passthrough routes (L4 TCP/SNI forwarding, no TLS termination). /// 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. /// 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). /// Used by PostgreSQL capability to expose CNPG clusters multisite (site1 → site2 replication).

View File

@@ -160,7 +160,7 @@ impl Default for RouteTargetReference {
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RoutePort { pub struct RoutePort {
pub target_port: IntOrString, pub target_port: u16,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]

View File

@@ -15,7 +15,7 @@
// always be dealing only with okd/openshift compatible topologies and is ready to manage the // always be dealing only with okd/openshift compatible topologies and is ready to manage the
// additional maintenance burden that comes with a lower level functionnality. // additional maintenance burden that comes with a lower level functionnality.
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; use harmony_types::rfc1123::Rfc1123Name;
use kube::api::ObjectMeta; use kube::api::ObjectMeta;
use serde::Serialize; use serde::Serialize;
@@ -24,21 +24,7 @@ use crate::modules::okd::crd::route::{
Route, RoutePort, RouteSpec, RouteTargetReference, TLSConfig, Route, RoutePort, RouteSpec, RouteTargetReference, TLSConfig,
}; };
use crate::score::Score; use crate::score::Score;
use crate::topology::{K8sclient, Topology}; use crate::topology::{K8sclient, TlsRoute, Topology};
#[derive(Clone, Debug, Serialize)]
struct OKDRoutePort {
#[serde(rename = "targetPort")]
target_port: String,
}
#[derive(Clone, Debug, Serialize)]
struct OKDTLSConfig {
#[serde(rename = "termination")]
termination: String,
#[serde(rename = "insecureEdgeTerminationPolicy")]
insecure_edge_termination_policy: String,
}
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct OKDRouteScore { pub struct OKDRouteScore {
@@ -78,43 +64,22 @@ impl<T: Topology + K8sclient> Score<T> for OKDRouteScore {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct OKDTlsPassthroughScore { pub struct OKDTlsPassthroughScore {
pub name: String, pub route: TlsRoute,
pub namespace: String, pub name: Rfc1123Name,
pub backend: String,
pub hostname: String,
pub target_port: String,
}
impl OKDTlsPassthroughScore {
pub fn new(
name: &str,
namespace: &str,
backend: &str,
hostname: &str,
target_port: &str,
) -> Self {
Self {
name: name.to_string(),
namespace: namespace.to_string(),
backend: backend.to_string(),
hostname: hostname.to_string(),
target_port: target_port.to_string(),
}
}
} }
impl<T: Topology + K8sclient> Score<T> for OKDTlsPassthroughScore { impl<T: Topology + K8sclient> Score<T> for OKDTlsPassthroughScore {
fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> { fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
let passthrough_spec = RouteSpec { let passthrough_spec = RouteSpec {
host: Some(self.hostname.clone()), host: Some(self.route.hostname.clone()),
wildcard_policy: Some("None".to_string()), wildcard_policy: Some("None".to_string()),
to: RouteTargetReference { to: RouteTargetReference {
kind: "Service".to_string(), kind: "Service".to_string(),
name: self.backend.clone(), name: self.route.backend.clone(),
weight: Some(100), weight: Some(100),
}, },
port: Some(RoutePort { port: Some(RoutePort {
target_port: IntOrString::String(self.target_port.clone()), target_port: self.route.target_port,
}), }),
tls: Some(TLSConfig { tls: Some(TLSConfig {
termination: "passthrough".to_string(), termination: "passthrough".to_string(),
@@ -123,14 +88,14 @@ impl<T: Topology + K8sclient> Score<T> for OKDTlsPassthroughScore {
}), }),
..Default::default() ..Default::default()
}; };
let route_score = OKDRouteScore::new(&self.name, &self.namespace, passthrough_spec); let route_score = OKDRouteScore::new(&self.name.to_string(), &self.route.namespace, passthrough_spec);
route_score.create_interpret() route_score.create_interpret()
} }
fn name(&self) -> String { fn name(&self) -> String {
format!( format!(
"OKDTlsPassthroughScore({}:{}/{}{})", "OKDTlsPassthroughScore({}:{}/{}{})",
self.backend, self.target_port, self.namespace, self.hostname self.route.backend, self.route.target_port, self.route.namespace, self.route.hostname
) )
} }
} }

View File

@@ -6,12 +6,9 @@ use crate::data::Version;
use crate::domain::topology::router::{TlsRoute, TlsRouter}; use crate::domain::topology::router::{TlsRoute, TlsRouter};
use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}; use crate::interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome};
use crate::inventory::Inventory; use crate::inventory::Inventory;
use crate::modules::k8s::resource::K8sResourceScore;
use crate::modules::postgresql::PostgreSQLScore; use crate::modules::postgresql::PostgreSQLScore;
use crate::modules::postgresql::cnpg::{Bootstrap, Cluster, ClusterSpec, Initdb, Storage};
use crate::score::Score; use crate::score::Score;
use crate::topology::{K8sclient, Topology}; 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. /// Deploys a public PostgreSQL cluster: CNPG + TLS passthrough route for RW endpoint.
/// For failover/multisite: exposes single-instance or small HA Postgres publicly. /// For failover/multisite: exposes single-instance or small HA Postgres publicly.
@@ -26,24 +23,15 @@ use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct PublicPostgreSQLScore { pub struct PublicPostgreSQLScore {
/// Inner non-public Postgres cluster config. /// Inner non-public Postgres cluster config.
pub inner: PostgreSQLScore, pub postgres_score: PostgreSQLScore,
/// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432). /// Public hostname for RW TLS passthrough (port 443 → cluster-rw:5432).
pub hostname: String, pub hostname: String,
} }
impl Default for PublicPostgreSQLScore {
fn default() -> Self {
Self {
inner: PostgreSQLScore::default(),
hostname: "postgres.default.public".to_string(),
}
}
}
impl PublicPostgreSQLScore { impl PublicPostgreSQLScore {
pub fn new(namespace: &str, hostname: &str) -> Self { pub fn new(namespace: &str, hostname: &str) -> Self {
Self { Self {
inner: PostgreSQLScore::new(namespace), postgres_score: PostgreSQLScore::new(namespace),
hostname: hostname.to_string(), hostname: hostname.to_string(),
} }
} }
@@ -72,10 +60,7 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Interpret<T> for PublicP
} }
async fn execute(&self, inventory: &Inventory, topo: &T) -> Result<Outcome, InterpretError> { async fn execute(&self, inventory: &Inventory, topo: &T) -> Result<Outcome, InterpretError> {
// Deploy CNPG cluster first (creates -rw service) // Deploy CNPG cluster first (creates -rw service)
self.postgres_score self.postgres_score.interpret(inventory, topo).await?;
.create_interpret()
.execute(inventory, topo)
.await?;
// Expose RW publicly via TLS passthrough // Expose RW publicly via TLS passthrough
topo.install_route(self.tls_route.clone()) topo.install_route(self.tls_route.clone())
@@ -92,15 +77,16 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Interpret<T> for PublicP
impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostgreSQLScore { impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostgreSQLScore {
fn create_interpret(&self) -> Box<dyn Interpret<T>> { fn create_interpret(&self) -> Box<dyn Interpret<T>> {
let rw_backend = format!("{}-rw", self.inner.name); let rw_backend = format!("{}-rw", self.postgres_score.name);
let tls_route = TlsRoute { let tls_route = TlsRoute {
namespace: self.postgres_score.namespace.clone(),
hostname: self.hostname.clone(), hostname: self.hostname.clone(),
backend: rw_backend, backend: rw_backend,
target_port: 5432, target_port: 5432,
}; };
Box::new(PublicPostgreSQLInterpret { Box::new(PublicPostgreSQLInterpret {
postgres_score: self.inner.clone(), postgres_score: self.postgres_score.clone(),
tls_route, tls_route,
}) })
} }
@@ -108,9 +94,7 @@ impl<T: Topology + K8sclient + TlsRouter + Send + Sync> Score<T> for PublicPostg
fn name(&self) -> String { fn name(&self) -> String {
format!( format!(
"PublicPostgreSQLScore({}:{})", "PublicPostgreSQLScore({}:{})",
self.inner.namespace, self.hostname self.postgres_score.namespace, self.hostname
) )
} }
} }
// TODO: Add RO route (separate hostname/backend="cluster-ro"), backups, failover logic.