diff --git a/examples/public_postgres/Cargo.toml b/examples/public_postgres/Cargo.toml new file mode 100644 index 0000000..f0e4c83 --- /dev/null +++ b/examples/public_postgres/Cargo.toml @@ -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 } diff --git a/examples/public_postgres/src/main.rs b/examples/public_postgres/src/main.rs new file mode 100644 index 0000000..b1e9e66 --- /dev/null +++ b/examples/public_postgres/src/main.rs @@ -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(); +} diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index caeb576..95b2cdb 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, process::Command, sync::Arc, time::Duration}; use async_trait::async_trait; use base64::{Engine, engine::general_purpose}; +use harmony_types::rfc1123::Rfc1123Name; use k8s_openapi::api::{ core::v1::Secret, rbac::v1::{ClusterRoleBinding, RoleRef, Subject}, @@ -35,6 +36,7 @@ use crate::{ }, }, network::TlsPassthroughScore, + okd::route::OKDTlsPassthroughScore, prometheus::{ k8s_prometheus_alerting_score::K8sPrometheusCRDAlertingScore, prometheus::PrometheusMonitoring, rhob_alerting_score::RHOBAlertingScore, @@ -109,9 +111,12 @@ impl TlsRouter for K8sAnywhereTopology { if let Some(distro) = self.k8s_distribution.get() { match distro { KubernetesDistribution::OpenshiftFamily => { - TlsPassthroughScore { route } - .interpret(&Inventory::empty(), self) - .await?; + OKDTlsPassthroughScore { + name: Rfc1123Name::try_from(route.to_string_short().as_str())?, + route, + } + .interpret(&Inventory::empty(), self) + .await?; Ok(()) } KubernetesDistribution::K3sFamily | KubernetesDistribution::Default => Err( diff --git a/harmony/src/domain/topology/router.rs b/harmony/src/domain/topology/router.rs index b453e99..ded17d2 100644 --- a/harmony/src/domain/topology/router.rs +++ b/harmony/src/domain/topology/router.rs @@ -73,8 +73,19 @@ pub struct TlsRoute { /// Backend TCP port (Postgres: 5432). 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). /// 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). diff --git a/harmony/src/modules/okd/crd/route.rs b/harmony/src/modules/okd/crd/route.rs index ad146de..0532fd4 100644 --- a/harmony/src/modules/okd/crd/route.rs +++ b/harmony/src/modules/okd/crd/route.rs @@ -160,7 +160,7 @@ impl Default for RouteTargetReference { #[derive(Deserialize, Serialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct RoutePort { - pub target_port: IntOrString, + pub target_port: u16, } #[derive(Deserialize, Serialize, Clone, Debug)] diff --git a/harmony/src/modules/okd/route.rs b/harmony/src/modules/okd/route.rs index 7360b04..29a00cf 100644 --- a/harmony/src/modules/okd/route.rs +++ b/harmony/src/modules/okd/route.rs @@ -15,7 +15,7 @@ // 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. -use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; +use harmony_types::rfc1123::Rfc1123Name; use kube::api::ObjectMeta; use serde::Serialize; @@ -24,21 +24,7 @@ use crate::modules::okd::crd::route::{ Route, RoutePort, RouteSpec, RouteTargetReference, TLSConfig, }; use crate::score::Score; -use crate::topology::{K8sclient, 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, -} +use crate::topology::{K8sclient, TlsRoute, Topology}; #[derive(Debug, Clone, Serialize)] pub struct OKDRouteScore { @@ -78,43 +64,22 @@ impl Score for OKDRouteScore { #[derive(Debug, Clone, Serialize)] pub struct OKDTlsPassthroughScore { - pub name: String, - pub namespace: String, - 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(), - } - } + pub route: TlsRoute, + pub name: Rfc1123Name, } impl Score for OKDTlsPassthroughScore { fn create_interpret(&self) -> Box> { let passthrough_spec = RouteSpec { - host: Some(self.hostname.clone()), + host: Some(self.route.hostname.clone()), wildcard_policy: Some("None".to_string()), to: RouteTargetReference { kind: "Service".to_string(), - name: self.backend.clone(), + name: self.route.backend.clone(), weight: Some(100), }, port: Some(RoutePort { - target_port: IntOrString::String(self.target_port.clone()), + target_port: self.route.target_port, }), tls: Some(TLSConfig { termination: "passthrough".to_string(), @@ -123,14 +88,14 @@ impl Score for OKDTlsPassthroughScore { }), ..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() } fn name(&self) -> String { format!( "OKDTlsPassthroughScore({}:{}/{} → {})", - self.backend, self.target_port, self.namespace, self.hostname + self.route.backend, self.route.target_port, self.route.namespace, self.route.hostname ) } } diff --git a/harmony/src/modules/postgresql/score_public.rs b/harmony/src/modules/postgresql/score_public.rs index 73c2193..8202176 100644 --- a/harmony/src/modules/postgresql/score_public.rs +++ b/harmony/src/modules/postgresql/score_public.rs @@ -6,12 +6,9 @@ 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::k8s::resource::K8sResourceScore; use crate::modules::postgresql::PostgreSQLScore; -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 a public PostgreSQL cluster: CNPG + TLS passthrough route for RW endpoint. /// 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)] pub struct PublicPostgreSQLScore { /// Inner non-public Postgres cluster config. - pub inner: PostgreSQLScore, + pub postgres_score: 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), + postgres_score: PostgreSQLScore::new(namespace), hostname: hostname.to_string(), } } @@ -72,10 +60,7 @@ impl Interpret for PublicP } async fn execute(&self, inventory: &Inventory, topo: &T) -> Result { // Deploy CNPG cluster first (creates -rw service) - self.postgres_score - .create_interpret() - .execute(inventory, topo) - .await?; + self.postgres_score.interpret(inventory, topo).await?; // Expose RW publicly via TLS passthrough topo.install_route(self.tls_route.clone()) @@ -92,15 +77,16 @@ impl Interpret for PublicP impl Score for PublicPostgreSQLScore { fn create_interpret(&self) -> Box> { - let rw_backend = format!("{}-rw", self.inner.name); + let rw_backend = format!("{}-rw", self.postgres_score.name); let tls_route = TlsRoute { + namespace: self.postgres_score.namespace.clone(), hostname: self.hostname.clone(), backend: rw_backend, target_port: 5432, }; Box::new(PublicPostgreSQLInterpret { - postgres_score: self.inner.clone(), + postgres_score: self.postgres_score.clone(), tls_route, }) } @@ -108,9 +94,7 @@ impl Score for PublicPostg fn name(&self) -> String { format!( "PublicPostgreSQLScore({}:{})", - self.inner.namespace, self.hostname + self.postgres_score.namespace, self.hostname ) } } - -// TODO: Add RO route (separate hostname/backend="cluster-ro"), backups, failover logic.