use std::collections::BTreeMap; use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec}; use k8s_openapi::api::core::v1::{ ConfigMapVolumeSource, Container, ContainerPort, PodSpec, PodTemplateSpec, SecretVolumeSource, SecurityContext, Volume, VolumeMount, }; use k8s_openapi::apimachinery::pkg::api::resource::Quantity; use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector; use kube::api::ObjectMeta; use kube::Resource; use crate::types::SojuBouncer; const SOJU_IMAGE: &str = "codeberg.org/emersion/soju:latest"; pub fn build_deployment(bouncer: &SojuBouncer, config_hash: &str) -> Deployment { let name = bouncer.metadata.name.clone().unwrap(); let ns = bouncer.metadata.namespace.clone().unwrap(); let oref = bouncer.controller_owner_ref(&()).unwrap(); let labels: BTreeMap = [ ("app.kubernetes.io/name".to_string(), name.clone()), ( "app.kubernetes.io/managed-by".to_string(), "soju-operator".to_string(), ), ] .into(); let mut volumes = vec![ Volume { name: "config".to_string(), config_map: Some(ConfigMapVolumeSource { name: format!("{name}-config"), ..Default::default() }), ..Default::default() }, Volume { name: "db".to_string(), secret: Some(SecretVolumeSource { secret_name: Some(format!("{name}-db")), ..Default::default() }), ..Default::default() }, ]; let mut volume_mounts = vec![ VolumeMount { name: "config".to_string(), mount_path: "/etc/soju/".to_string(), ..Default::default() }, VolumeMount { name: "db".to_string(), mount_path: "/etc/soju/db/".to_string(), ..Default::default() }, ]; if bouncer.spec.tls.is_some() { volumes.push(Volume { name: "tls".to_string(), secret: Some(SecretVolumeSource { secret_name: Some(format!("{name}-tls")), ..Default::default() }), ..Default::default() }); volume_mounts.push(VolumeMount { name: "tls".to_string(), mount_path: "/etc/soju/tls/".to_string(), ..Default::default() }); } let ports: Vec = bouncer .spec .listeners .iter() .filter_map(|l| { l.address .rsplit(':') .next() .and_then(|p| p.parse::().ok()) .map(|port| ContainerPort { container_port: port, ..Default::default() }) }) .collect(); let resources = bouncer.spec.resources.as_ref().map(|r| { let mut k8s_req = k8s_openapi::api::core::v1::ResourceRequirements::default(); if let Some(requests) = &r.requests { let mut map = BTreeMap::new(); if let Some(cpu) = &requests.cpu { map.insert("cpu".to_string(), Quantity(cpu.clone())); } if let Some(memory) = &requests.memory { map.insert("memory".to_string(), Quantity(memory.clone())); } if !map.is_empty() { k8s_req.requests = Some(map); } } if let Some(limits) = &r.limits { let mut map = BTreeMap::new(); if let Some(cpu) = &limits.cpu { map.insert("cpu".to_string(), Quantity(cpu.clone())); } if let Some(memory) = &limits.memory { map.insert("memory".to_string(), Quantity(memory.clone())); } if !map.is_empty() { k8s_req.limits = Some(map); } } k8s_req }); let container = Container { name: "soju".to_string(), image: Some(SOJU_IMAGE.to_string()), image_pull_policy: Some("IfNotPresent".to_string()), args: Some(vec!["-config".to_string(), "/etc/soju/config".to_string()]), ports: Some(ports), volume_mounts: Some(volume_mounts), resources, security_context: Some(SecurityContext { run_as_non_root: Some(true), allow_privilege_escalation: Some(false), ..Default::default() }), ..Default::default() }; Deployment { metadata: ObjectMeta { name: Some(name.clone()), namespace: Some(ns), labels: Some(labels.clone()), owner_references: Some(vec![oref]), ..Default::default() }, spec: Some(DeploymentSpec { replicas: Some(1), selector: LabelSelector { match_labels: Some(labels.clone()), ..Default::default() }, template: PodTemplateSpec { metadata: Some(ObjectMeta { labels: Some(labels), annotations: Some( [("irc.josie.cloud/config-hash".to_string(), config_hash.to_string())].into(), ), ..Default::default() }), spec: Some(PodSpec { containers: vec![container], volumes: Some(volumes), ..Default::default() }), }, ..Default::default() }), ..Default::default() } } #[cfg(test)] mod tests { use super::*; use crate::testutil::test_bouncer; #[test] fn deployment_mounts_configmap() { let dep = build_deployment(&test_bouncer(), "testhash"); let spec = dep.spec.unwrap(); let pod_spec = spec.template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(volumes.iter().any(|v| v.name == "config")); } #[test] fn deployment_mounts_tls_when_configured() { let dep = build_deployment(&test_bouncer(), "testhash"); let spec = dep.spec.unwrap(); let pod_spec = spec.template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(volumes.iter().any(|v| v.name == "tls")); } #[test] fn deployment_has_owner_reference() { let dep = build_deployment(&test_bouncer(), "testhash"); let orefs = dep.metadata.owner_references.unwrap(); assert_eq!(orefs[0].name, "test-bouncer"); } #[test] fn deployment_exposes_listener_ports() { let dep = build_deployment(&test_bouncer(), "testhash"); let containers = dep.spec.unwrap().template.spec.unwrap().containers; let ports = containers[0].ports.as_ref().unwrap(); assert_eq!(ports.len(), 2); assert_eq!(ports[0].container_port, 6697); assert_eq!(ports[1].container_port, 6667); } #[test] fn deployment_mounts_db_secret() { let dep = build_deployment(&test_bouncer(), "testhash"); let spec = dep.spec.unwrap(); let pod_spec = spec.template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(volumes.iter().any(|v| v.name == "db")); } }