use std::collections::BTreeMap; use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec}; use k8s_openapi::api::core::v1::{ ConfigMapVolumeSource, Container, ContainerPort, PersistentVolumeClaimVolumeSource, 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::ErgoNetwork; const ERGO_IMAGE: &str = "ghcr.io/ergochat/ergo:stable"; pub fn build_deployment(network: &ErgoNetwork) -> Deployment { let name = network.metadata.name.clone().unwrap(); let ns = network.metadata.namespace.clone().unwrap(); let oref = network.controller_owner_ref(&()).unwrap(); let labels: BTreeMap = [ ("app.kubernetes.io/name".to_string(), name.clone()), ( "app.kubernetes.io/managed-by".to_string(), "ergo-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: "data".to_string(), persistent_volume_claim: Some(PersistentVolumeClaimVolumeSource { claim_name: format!("{name}-data"), ..Default::default() }), ..Default::default() }, ]; let mut volume_mounts = vec![ VolumeMount { name: "config".to_string(), mount_path: "/config/".to_string(), ..Default::default() }, VolumeMount { name: "data".to_string(), mount_path: "/ircd/".to_string(), ..Default::default() }, ]; if network.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/ergo/tls/".to_string(), ..Default::default() }); } let ports: Vec = network .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 = network.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: "ergo".to_string(), image: Some(ERGO_IMAGE.to_string()), args: Some(vec![ "run".to_string(), "--conf".to_string(), "/ircd/ircd.yaml".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), ..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_network; #[test] fn deployment_uses_ergo_image() { let dep = build_deployment(&test_network()); let containers = dep.spec.unwrap().template.spec.unwrap().containers; assert_eq!(containers[0].image.as_ref().unwrap(), ERGO_IMAGE); } #[test] fn deployment_has_correct_command() { let dep = build_deployment(&test_network()); let containers = dep.spec.unwrap().template.spec.unwrap().containers; let args = containers[0].args.as_ref().unwrap(); assert_eq!(args, &["run", "--conf", "/ircd/ircd.yaml"]); } #[test] fn deployment_mounts_configmap() { let dep = build_deployment(&test_network()); let pod_spec = dep.spec.unwrap().template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(volumes.iter().any(|v| v.name == "config")); } #[test] fn deployment_mounts_pvc() { let dep = build_deployment(&test_network()); let pod_spec = dep.spec.unwrap().template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); let data_vol = volumes.iter().find(|v| v.name == "data").unwrap(); assert_eq!( data_vol .persistent_volume_claim .as_ref() .unwrap() .claim_name, "test-network-data" ); } #[test] fn deployment_mounts_tls_when_configured() { let dep = build_deployment(&test_network()); let pod_spec = dep.spec.unwrap().template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(volumes.iter().any(|v| v.name == "tls")); } #[test] fn deployment_omits_tls_when_not_configured() { let mut network = test_network(); network.spec.tls = None; let dep = build_deployment(&network); let pod_spec = dep.spec.unwrap().template.spec.unwrap(); let volumes = pod_spec.volumes.unwrap(); assert!(!volumes.iter().any(|v| v.name == "tls")); } #[test] fn deployment_exposes_listener_ports() { let dep = build_deployment(&test_network()); 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_has_owner_reference() { let dep = build_deployment(&test_network()); let orefs = dep.metadata.owner_references.unwrap(); assert_eq!(orefs[0].name, "test-network"); } }