use kube::CustomResource; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub fn sanitize_bouncer_name(input: &str) -> String { input .to_lowercase() .chars() .map(|c| if c.is_alphanumeric() || c == '-' { c } else { '-' }) .collect::() .trim_matches('-') .to_string() } // Minimal SojuBouncer spec -- just enough for the portal to create CRs. // The full CRD is defined in the soju-operator crate. #[derive(CustomResource, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[kube( group = "irc.now", version = "v1alpha1", kind = "SojuBouncer", namespaced, status = "SojuBouncerStatus" )] pub struct SojuBouncerSpec { pub hostname: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub owner: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub hostname_cloak: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub listeners: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub auth: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct Listener { pub address: String, #[serde(default)] pub tls: bool, #[serde(default)] pub websocket: bool, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct AuthSpec { pub oauth2_url: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub secret_ref: Option, } pub fn default_bouncer_spec(name: &str, owner: &str, issuer_url: &str) -> SojuBouncerSpec { SojuBouncerSpec { hostname: format!("{name}.irc.now"), owner: Some(owner.to_string()), hostname_cloak: Some(format!("{name}.irc.now")), listeners: vec![ Listener { address: ":6667".to_string(), tls: false, websocket: false }, Listener { address: ":8080".to_string(), tls: false, websocket: true }, ], auth: Some(AuthSpec { oauth2_url: issuer_url.to_string(), secret_ref: Some("oidc-soju".to_string()), }), } } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct SojuBouncerStatus { #[serde(default)] pub conditions: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct SojuBouncerCondition { #[serde(rename = "type")] pub type_: String, pub status: String, #[serde(default)] pub reason: Option, #[serde(default)] pub message: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn bouncer_name_sanitizes_input() { assert_eq!(sanitize_bouncer_name("My Bouncer!"), "my-bouncer"); assert_eq!(sanitize_bouncer_name("test_123"), "test-123"); } #[test] fn bouncer_name_trims_dashes() { assert_eq!(sanitize_bouncer_name("--hello--"), "hello"); } #[test] fn bouncer_name_handles_empty() { assert_eq!(sanitize_bouncer_name(""), ""); } }