From d06bd4dac67f5f82be0eace0afeeaa46dc45976f Mon Sep 17 00:00:00 2001 From: Jean-Gabriel Gill-Couture Date: Sun, 14 Dec 2025 17:04:40 -0500 Subject: [PATCH] feat: OKD route CRD and OKD specific route score --- harmony/src/modules/network/tls_router.rs | 73 +++--- harmony/src/modules/okd/crd/mod.rs | 1 + harmony/src/modules/okd/crd/route.rs | 285 ++++++++++++++++++++++ harmony/src/modules/okd/route.rs | 136 +++++++++++ 4 files changed, 457 insertions(+), 38 deletions(-) create mode 100644 harmony/src/modules/okd/crd/route.rs create mode 100644 harmony/src/modules/okd/route.rs diff --git a/harmony/src/modules/network/tls_router.rs b/harmony/src/modules/network/tls_router.rs index c5e520d..5f43df3 100644 --- a/harmony/src/modules/network/tls_router.rs +++ b/harmony/src/modules/network/tls_router.rs @@ -13,13 +13,30 @@ use crate::topology::{K8sclient, Topology}; /// Exposes backend services via TLS passthrough (L4 TCP/SNI forwarding). /// Agnostic to underlying router impl (OKD Route, HAProxy, Envoy, etc.). /// +/// TlsPassthroughScore relies on the TlsRouter Capability for its entire functionnality, +/// the implementation depends entirely on how the Topology implements it. +/// /// # Usage /// ``` -/// use harmony::modules::network::TlsRouterScore; -/// let score = TlsRouterScore::new("postgres-cluster-rw", "pg-rw.example.com", 5432); +/// use harmony::modules::network::TlsPassthroughScore; +/// let score = TlsPassthroughScore { +/// backend: "postgres-cluster-rw".to_string(), +/// hostname: "postgres-rw.example.com".to_string(), +/// target_port: 5432, +/// }; /// ``` +/// +/// # Hint +/// +/// **This TlsPassthroughScore should be used whenever possible.** It is effectively +/// an abstraction over the concept of tls passthrough, and it will allow much more flexible +/// usage over multiple types of Topology than using a lower level module such as +/// OKDTlsPassthroughScore. +/// +/// On the other hand, some implementation specific options might not be available or practical +/// to use through this high level TlsPassthroughScore. #[derive(Debug, Clone, Serialize)] -pub struct TlsRouterScore { +pub struct TlsPassthroughScore { /// Backend identifier (k8s Service, HAProxy upstream, IP/FQDN, etc.). pub backend: String, /// Public hostname clients connect to (TLS SNI, port 443 implicit). @@ -28,34 +45,32 @@ pub struct TlsRouterScore { pub target_port: u16, } -impl Default for TlsRouterScore { - fn default() -> Self { - Self { - backend: "default-backend".to_string(), - hostname: "tls.default.public".to_string(), - target_port: 5432, - } +impl Score for TlsPassthroughScore { + fn create_interpret(&self) -> Box> { + let tls_route = TlsRoute { + hostname: self.hostname.clone(), + backend: self.backend.clone(), + target_port: self.target_port, + }; + Box::new(TlsPassthroughInterpret { tls_route }) } -} -impl TlsRouterScore { - pub fn new(backend: &str, hostname: &str, target_port: u16) -> Self { - Self { - backend: backend.to_string(), - hostname: hostname.to_string(), - target_port, - } + fn name(&self) -> String { + format!( + "TlsRouterScore({}:{} → {})", + self.backend, self.target_port, self.hostname + ) } } /// Custom interpret: provisions the TLS passthrough route on the topology. #[derive(Debug, Clone)] -struct TlsRouterInterpret { +struct TlsPassthroughInterpret { tls_route: TlsRoute, } #[async_trait] -impl Interpret for TlsRouterInterpret { +impl Interpret for TlsPassthroughInterpret { fn get_name(&self) -> InterpretName { InterpretName::Custom("TlsRouterInterpret") } @@ -79,21 +94,3 @@ impl Interpret for TlsRout ))) } } - -impl Score for TlsRouterScore { - fn create_interpret(&self) -> Box> { - let tls_route = TlsRoute { - hostname: self.hostname.clone(), - backend: self.backend.clone(), - target_port: self.target_port, - }; - Box::new(TlsRouterInterpret { tls_route }) - } - - fn name(&self) -> String { - format!( - "TlsRouterScore({}:{ } → {})", - self.backend, self.target_port, self.hostname - ) - } -} diff --git a/harmony/src/modules/okd/crd/mod.rs b/harmony/src/modules/okd/crd/mod.rs index 568db3f..71c4d0a 100644 --- a/harmony/src/modules/okd/crd/mod.rs +++ b/harmony/src/modules/okd/crd/mod.rs @@ -1 +1,2 @@ pub mod nmstate; +pub mod route; diff --git a/harmony/src/modules/okd/crd/route.rs b/harmony/src/modules/okd/crd/route.rs new file mode 100644 index 0000000..ad146de --- /dev/null +++ b/harmony/src/modules/okd/crd/route.rs @@ -0,0 +1,285 @@ +use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ListMeta, ObjectMeta, Time}; +use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString; +use k8s_openapi::{NamespaceResourceScope, Resource}; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct LocalObjectReference { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Route { + // #[serde(skip_serializing_if = "Option::is_none")] + // pub api_version: Option, + // + // #[serde(skip_serializing_if = "Option::is_none")] + // pub kind: Option, + pub metadata: ObjectMeta, + + pub spec: RouteSpec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +impl Resource for Route { + const API_VERSION: &'static str = "route.openshift.io/v1"; + const GROUP: &'static str = "route.openshift.io"; + const VERSION: &'static str = "v1"; + const KIND: &'static str = "Route"; + const URL_PATH_SEGMENT: &'static str = "routes"; + type Scope = NamespaceResourceScope; +} + +impl k8s_openapi::Metadata for Route { + type Ty = ObjectMeta; + + fn metadata(&self) -> &Self::Ty { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut Self::Ty { + &mut self.metadata + } +} + +impl Default for Route { + fn default() -> Self { + Route { + metadata: ObjectMeta::default(), + spec: RouteSpec::default(), + status: None, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteList { + pub metadata: ListMeta, + pub items: Vec, +} + +impl Default for RouteList { + fn default() -> Self { + Self { + metadata: ListMeta::default(), + items: Vec::new(), + } + } +} + +impl Resource for RouteList { + const API_VERSION: &'static str = "route.openshift.io/v1"; + const GROUP: &'static str = "route.openshift.io"; + const VERSION: &'static str = "v1"; + const KIND: &'static str = "RouteList"; + const URL_PATH_SEGMENT: &'static str = "routes"; + type Scope = NamespaceResourceScope; +} + +impl k8s_openapi::Metadata for RouteList { + type Ty = ListMeta; + + fn metadata(&self) -> &Self::Ty { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut Self::Ty { + &mut self.metadata + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteSpec { + #[serde(skip_serializing_if = "Option::is_none")] + pub alternate_backends: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub http_headers: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subdomain: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub tls: Option, + + pub to: RouteTargetReference, + + #[serde(skip_serializing_if = "Option::is_none")] + pub wildcard_policy: Option, +} +impl Default for RouteSpec { + fn default() -> RouteSpec { + RouteSpec { + alternate_backends: None, + host: None, + http_headers: None, + path: None, + port: None, + subdomain: None, + tls: None, + to: RouteTargetReference::default(), + wildcard_policy: None, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteTargetReference { + pub kind: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub weight: Option, +} +impl Default for RouteTargetReference { + fn default() -> RouteTargetReference { + RouteTargetReference { + kind: String::default(), + name: String::default(), + weight: None, + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RoutePort { + pub target_port: IntOrString, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TLSConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub ca_certificate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub certificate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub destination_ca_certificate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub external_certificate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub insecure_edge_termination_policy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub key: Option, + + pub termination: String, +} + +impl Default for TLSConfig { + fn default() -> Self { + Self { + ca_certificate: None, + certificate: None, + destination_ca_certificate: None, + external_certificate: None, + insecure_edge_termination_policy: None, + key: None, + termination: "edge".to_string(), + } + } +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteStatus { + #[serde(skip_serializing_if = "Option::is_none")] + pub ingress: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteIngress { + #[serde(skip_serializing_if = "Option::is_none")] + pub host: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub router_canonical_hostname: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub router_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub wildcard_policy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub conditions: Option>, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RouteIngressCondition { + #[serde(skip_serializing_if = "Option::is_none")] + pub last_transition_time: Option