1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct DeploymentSpec {
23 name: String,
24 replicas: i32,
25 containers: Vec<ContainerSpec>,
26 init_containers: Vec<ContainerSpec>,
27 strategy: DeploymentStrategy,
28 labels: HashMap<String, String>,
29 image_pull_secrets: Vec<String>,
30 service_account: Option<String>,
31 volumes: Vec<Volume>,
32}
33
34impl DeploymentSpec {
35 pub fn new<S: Into<String>>(name: S) -> Self {
41 let name = name.into();
42 let mut labels = HashMap::new();
43 labels.insert("app".to_string(), name.clone());
44
45 Self {
46 name,
47 replicas: 1,
48 containers: Vec::new(),
49 init_containers: Vec::new(),
50 strategy: DeploymentStrategy::Recreate,
51 labels,
52 image_pull_secrets: Vec::new(),
53 service_account: None,
54 volumes: Vec::new(),
55 }
56 }
57
58 pub fn with_replicas(mut self, replicas: i32) -> Self {
60 self.replicas = replicas;
61 if replicas > 1 {
63 self.strategy = DeploymentStrategy::RollingUpdate {
64 max_surge: Some("50%".to_string()),
65 max_unavailable: Some("0".to_string()),
66 };
67 }
68 self
69 }
70
71 pub fn with_container(mut self, container: ContainerSpec) -> Self {
73 self.containers.push(container);
74 self
75 }
76
77 pub fn with_strategy(mut self, strategy: DeploymentStrategy) -> Self {
79 self.strategy = strategy;
80 self
81 }
82
83 pub fn with_label<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
85 self.labels.insert(key.into(), value.into());
86 self
87 }
88
89 pub fn with_labels(mut self, labels: HashMap<String, String>) -> Self {
91 self.labels.extend(labels);
92 self
93 }
94
95 pub fn with_init_container(mut self, container: ContainerSpec) -> Self {
99 self.init_containers.push(container);
100 self
101 }
102
103 pub fn with_image_pull_secret<S: Into<String>>(mut self, secret_name: S) -> Self {
109 self.image_pull_secrets.push(secret_name.into());
110 self
111 }
112
113 pub fn with_service_account<S: Into<String>>(mut self, account: S) -> Self {
119 self.service_account = Some(account.into());
120 self
121 }
122
123 pub fn with_volume(mut self, volume: Volume) -> Self {
147 self.volumes.push(volume);
148 self
149 }
150
151 pub fn validate(&self) -> Result<(), String> {
159 if self.containers.is_empty() {
160 return Err("Deployment must have at least one container".to_string());
161 }
162 if self.replicas < 0 {
163 return Err("Replicas cannot be negative".to_string());
164 }
165 for container in &self.containers {
166 container.validate()?;
167 }
168 for init_container in &self.init_containers {
169 init_container.validate()?;
170 }
171 Ok(())
172 }
173
174 pub fn name(&self) -> &str {
176 &self.name
177 }
178
179 pub fn replicas(&self) -> i32 {
181 self.replicas
182 }
183
184 pub fn containers(&self) -> &[ContainerSpec] {
186 &self.containers
187 }
188
189 pub fn strategy(&self) -> &DeploymentStrategy {
191 &self.strategy
192 }
193
194 pub fn labels(&self) -> &HashMap<String, String> {
196 &self.labels
197 }
198
199 pub fn init_containers(&self) -> &[ContainerSpec] {
201 &self.init_containers
202 }
203
204 pub fn image_pull_secrets(&self) -> &[String] {
206 &self.image_pull_secrets
207 }
208
209 pub fn service_account(&self) -> Option<&str> {
211 self.service_account.as_deref()
212 }
213
214 pub fn volumes(&self) -> &[Volume] {
216 &self.volumes
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct ContainerSpec {
223 name: String,
224 image: String,
225 ports: Vec<i32>,
226 env: HashMap<String, String>,
227 resources: Option<ResourceRequirements>,
228 liveness_probe: Option<ProbeSpec>,
229 readiness_probe: Option<ProbeSpec>,
230 startup_probe: Option<ProbeSpec>,
231 volume_mounts: Vec<VolumeMount>,
232 command: Option<Vec<String>>,
233 args: Option<Vec<String>>,
234}
235
236impl ContainerSpec {
237 pub fn new<S: Into<String>>(name: S, image: S) -> Self {
244 Self {
245 name: name.into(),
246 image: image.into(),
247 ports: Vec::new(),
248 env: HashMap::new(),
249 resources: None,
250 liveness_probe: None,
251 readiness_probe: None,
252 startup_probe: None,
253 volume_mounts: Vec::new(),
254 command: None,
255 args: None,
256 }
257 }
258
259 pub fn with_port(mut self, port: i32) -> Self {
261 self.ports.push(port);
262 self
263 }
264
265 pub fn with_env<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
267 self.env.insert(key.into(), value.into());
268 self
269 }
270
271 pub fn with_env_map(mut self, env: HashMap<String, String>) -> Self {
273 self.env.extend(env);
274 self
275 }
276
277 pub fn with_resources(mut self, resources: ResourceRequirements) -> Self {
279 self.resources = Some(resources);
280 self
281 }
282
283 pub fn with_liveness_probe(mut self, probe: ProbeSpec) -> Self {
287 self.liveness_probe = Some(probe);
288 self
289 }
290
291 pub fn with_readiness_probe(mut self, probe: ProbeSpec) -> Self {
295 self.readiness_probe = Some(probe);
296 self
297 }
298
299 pub fn with_startup_probe(mut self, probe: ProbeSpec) -> Self {
303 self.startup_probe = Some(probe);
304 self
305 }
306
307 pub fn with_volume_mount(mut self, mount: VolumeMount) -> Self {
309 self.volume_mounts.push(mount);
310 self
311 }
312
313 pub fn with_command(mut self, command: Vec<String>) -> Self {
315 self.command = Some(command);
316 self
317 }
318
319 pub fn with_args(mut self, args: Vec<String>) -> Self {
321 self.args = Some(args);
322 self
323 }
324
325 pub fn validate(&self) -> Result<(), String> {
327 if self.name.is_empty() {
328 return Err("Container name cannot be empty".to_string());
329 }
330 if self.image.is_empty() {
331 return Err("Container image cannot be empty".to_string());
332 }
333 Ok(())
334 }
335
336 pub fn name(&self) -> &str {
338 &self.name
339 }
340
341 pub fn image(&self) -> &str {
343 &self.image
344 }
345
346 pub fn ports(&self) -> &[i32] {
348 &self.ports
349 }
350
351 pub fn env(&self) -> &HashMap<String, String> {
353 &self.env
354 }
355
356 pub fn resources(&self) -> Option<&ResourceRequirements> {
358 self.resources.as_ref()
359 }
360
361 pub fn liveness_probe(&self) -> Option<&ProbeSpec> {
363 self.liveness_probe.as_ref()
364 }
365
366 pub fn readiness_probe(&self) -> Option<&ProbeSpec> {
368 self.readiness_probe.as_ref()
369 }
370
371 pub fn startup_probe(&self) -> Option<&ProbeSpec> {
373 self.startup_probe.as_ref()
374 }
375
376 pub fn volume_mounts(&self) -> &[VolumeMount] {
378 &self.volume_mounts
379 }
380
381 pub fn command(&self) -> Option<&[String]> {
383 self.command.as_deref()
384 }
385
386 pub fn args(&self) -> Option<&[String]> {
388 self.args.as_deref()
389 }
390}
391
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub enum DeploymentStrategy {
395 Recreate,
397 RollingUpdate {
399 max_surge: Option<String>,
401 max_unavailable: Option<String>,
403 },
404}
405
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct ResourceRequirements {
409 cpu_request: Option<String>,
410 cpu_limit: Option<String>,
411 memory_request: Option<String>,
412 memory_limit: Option<String>,
413}
414
415impl ResourceRequirements {
416 pub fn new() -> Self {
418 Self {
419 cpu_request: None,
420 cpu_limit: None,
421 memory_request: None,
422 memory_limit: None,
423 }
424 }
425
426 pub fn cpu_request<S: Into<String>>(mut self, request: S) -> Self {
428 self.cpu_request = Some(request.into());
429 self
430 }
431
432 pub fn cpu_limit<S: Into<String>>(mut self, limit: S) -> Self {
434 self.cpu_limit = Some(limit.into());
435 self
436 }
437
438 pub fn memory_request<S: Into<String>>(mut self, request: S) -> Self {
440 self.memory_request = Some(request.into());
441 self
442 }
443
444 pub fn memory_limit<S: Into<String>>(mut self, limit: S) -> Self {
446 self.memory_limit = Some(limit.into());
447 self
448 }
449
450 pub fn get_cpu_request(&self) -> Option<&str> {
452 self.cpu_request.as_deref()
453 }
454
455 pub fn get_cpu_limit(&self) -> Option<&str> {
457 self.cpu_limit.as_deref()
458 }
459
460 pub fn get_memory_request(&self) -> Option<&str> {
462 self.memory_request.as_deref()
463 }
464
465 pub fn get_memory_limit(&self) -> Option<&str> {
467 self.memory_limit.as_deref()
468 }
469}
470
471impl Default for ResourceRequirements {
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub struct ProbeSpec {
480 pub http_get: Option<HttpGetProbe>,
482 pub exec: Option<ExecProbe>,
484 pub tcp_socket: Option<TcpSocketProbe>,
486 pub initial_delay_seconds: i32,
488 pub period_seconds: i32,
490 pub timeout_seconds: i32,
492 pub success_threshold: i32,
494 pub failure_threshold: i32,
496}
497
498impl ProbeSpec {
499 pub fn http_get(path: String, port: i32) -> Self {
501 Self {
502 http_get: Some(HttpGetProbe { path, port }),
503 exec: None,
504 tcp_socket: None,
505 initial_delay_seconds: 0,
506 period_seconds: 10,
507 timeout_seconds: 1,
508 success_threshold: 1,
509 failure_threshold: 3,
510 }
511 }
512
513 pub fn exec(command: Vec<String>) -> Self {
515 Self {
516 http_get: None,
517 exec: Some(ExecProbe { command }),
518 tcp_socket: None,
519 initial_delay_seconds: 0,
520 period_seconds: 10,
521 timeout_seconds: 1,
522 success_threshold: 1,
523 failure_threshold: 3,
524 }
525 }
526
527 pub fn tcp_socket(port: i32) -> Self {
529 Self {
530 http_get: None,
531 exec: None,
532 tcp_socket: Some(TcpSocketProbe { port }),
533 initial_delay_seconds: 0,
534 period_seconds: 10,
535 timeout_seconds: 1,
536 success_threshold: 1,
537 failure_threshold: 3,
538 }
539 }
540
541 pub fn initial_delay_seconds(mut self, seconds: i32) -> Self {
543 self.initial_delay_seconds = seconds;
544 self
545 }
546
547 pub fn period_seconds(mut self, seconds: i32) -> Self {
549 self.period_seconds = seconds;
550 self
551 }
552
553 pub fn timeout_seconds(mut self, seconds: i32) -> Self {
555 self.timeout_seconds = seconds;
556 self
557 }
558
559 pub fn failure_threshold(mut self, threshold: i32) -> Self {
561 self.failure_threshold = threshold;
562 self
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568pub struct HttpGetProbe {
569 pub path: String,
571 pub port: i32,
573}
574
575#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577pub struct ExecProbe {
578 pub command: Vec<String>,
580}
581
582#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
584pub struct TcpSocketProbe {
585 pub port: i32,
587}
588
589#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
591pub struct VolumeMount {
592 pub name: String,
594 pub mount_path: String,
596 pub read_only: bool,
598 pub sub_path: Option<String>,
600}
601
602impl VolumeMount {
603 pub fn new<S: Into<String>>(name: S, mount_path: S) -> Self {
605 Self {
606 name: name.into(),
607 mount_path: mount_path.into(),
608 read_only: false,
609 sub_path: None,
610 }
611 }
612
613 pub fn read_only(mut self, read_only: bool) -> Self {
615 self.read_only = read_only;
616 self
617 }
618
619 pub fn sub_path<S: Into<String>>(mut self, path: S) -> Self {
621 self.sub_path = Some(path.into());
622 self
623 }
624}
625
626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
628pub enum VolumeSource {
629 ConfigMap {
631 name: String,
633 default_mode: Option<i32>,
635 items: Vec<KeyToPath>,
637 },
638 Secret {
640 secret_name: String,
642 default_mode: Option<i32>,
644 items: Vec<KeyToPath>,
646 },
647 EmptyDir {
649 medium: Option<String>,
651 size_limit: Option<String>,
653 },
654 HostPath {
656 path: String,
658 path_type: Option<String>,
660 },
661 PersistentVolumeClaim {
663 claim_name: String,
665 read_only: bool,
667 },
668}
669
670#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
672pub struct KeyToPath {
673 pub key: String,
675 pub path: String,
677 pub mode: Option<i32>,
679}
680
681impl KeyToPath {
682 pub fn new(key: impl Into<String>, path: impl Into<String>) -> Self {
684 Self {
685 key: key.into(),
686 path: path.into(),
687 mode: None,
688 }
689 }
690
691 pub fn with_mode(mut self, mode: i32) -> Self {
693 self.mode = Some(mode);
694 self
695 }
696}
697
698#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700pub struct Volume {
701 pub name: String,
703 pub source: VolumeSource,
705}
706
707impl Volume {
708 pub fn from_config_map(name: impl Into<String>, configmap_name: impl Into<String>) -> Self {
718 Self {
719 name: name.into(),
720 source: VolumeSource::ConfigMap {
721 name: configmap_name.into(),
722 default_mode: None,
723 items: Vec::new(),
724 },
725 }
726 }
727
728 pub fn from_config_map_items(
730 name: impl Into<String>,
731 configmap_name: impl Into<String>,
732 items: Vec<KeyToPath>,
733 ) -> Self {
734 Self {
735 name: name.into(),
736 source: VolumeSource::ConfigMap {
737 name: configmap_name.into(),
738 default_mode: None,
739 items,
740 },
741 }
742 }
743
744 pub fn from_secret(name: impl Into<String>, secret_name: impl Into<String>) -> Self {
754 Self {
755 name: name.into(),
756 source: VolumeSource::Secret {
757 secret_name: secret_name.into(),
758 default_mode: None,
759 items: Vec::new(),
760 },
761 }
762 }
763
764 pub fn empty_dir(name: impl Into<String>) -> Self {
774 Self {
775 name: name.into(),
776 source: VolumeSource::EmptyDir {
777 medium: None,
778 size_limit: None,
779 },
780 }
781 }
782
783 pub fn memory_volume(name: impl Into<String>, size_limit: impl Into<String>) -> Self {
785 Self {
786 name: name.into(),
787 source: VolumeSource::EmptyDir {
788 medium: Some("Memory".to_string()),
789 size_limit: Some(size_limit.into()),
790 },
791 }
792 }
793
794 pub fn from_host_path(name: impl Into<String>, path: impl Into<String>) -> Self {
798 Self {
799 name: name.into(),
800 source: VolumeSource::HostPath {
801 path: path.into(),
802 path_type: None,
803 },
804 }
805 }
806
807 pub fn from_pvc(name: impl Into<String>, claim_name: impl Into<String>) -> Self {
817 Self {
818 name: name.into(),
819 source: VolumeSource::PersistentVolumeClaim {
820 claim_name: claim_name.into(),
821 read_only: false,
822 },
823 }
824 }
825
826 pub fn from_pvc_read_only(name: impl Into<String>, claim_name: impl Into<String>) -> Self {
828 Self {
829 name: name.into(),
830 source: VolumeSource::PersistentVolumeClaim {
831 claim_name: claim_name.into(),
832 read_only: true,
833 },
834 }
835 }
836
837 pub fn with_default_mode(mut self, mode: i32) -> Self {
839 match &mut self.source {
840 VolumeSource::ConfigMap { default_mode, .. } => {
841 *default_mode = Some(mode);
842 }
843 VolumeSource::Secret { default_mode, .. } => {
844 *default_mode = Some(mode);
845 }
846 _ => {}
847 }
848 self
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
857 fn test_deployment_spec_builder() {
858 let container = ContainerSpec::new("app", "nginx:1.21")
859 .with_port(80)
860 .with_env("ENV", "production");
861
862 let spec = DeploymentSpec::new("my-app")
863 .with_replicas(3)
864 .with_container(container);
865
866 assert_eq!(spec.name(), "my-app");
867 assert_eq!(spec.replicas(), 3);
868 assert_eq!(spec.containers().len(), 1);
869 assert!(matches!(
870 spec.strategy(),
871 DeploymentStrategy::RollingUpdate { .. }
872 ));
873 }
874
875 #[test]
876 fn test_container_spec_builder() {
877 let mut env = HashMap::new();
878 env.insert("DB_HOST".to_string(), "localhost".to_string());
879
880 let container = ContainerSpec::new("api", "myapp:v1")
881 .with_port(8080)
882 .with_port(9090)
883 .with_env_map(env);
884
885 assert_eq!(container.name(), "api");
886 assert_eq!(container.image(), "myapp:v1");
887 assert_eq!(container.ports().len(), 2);
888 assert_eq!(
889 container.env().get("DB_HOST"),
890 Some(&"localhost".to_string())
891 );
892 }
893
894 #[test]
895 fn test_resource_requirements() {
896 let resources = ResourceRequirements::new()
897 .cpu_request("100m")
898 .cpu_limit("500m")
899 .memory_request("128Mi")
900 .memory_limit("512Mi");
901
902 assert_eq!(resources.get_cpu_request(), Some("100m"));
903 assert_eq!(resources.get_cpu_limit(), Some("500m"));
904 assert_eq!(resources.get_memory_request(), Some("128Mi"));
905 assert_eq!(resources.get_memory_limit(), Some("512Mi"));
906 }
907
908 #[test]
909 fn test_deployment_strategy_auto_selection() {
910 let spec1 = DeploymentSpec::new("app1").with_replicas(1);
911 assert!(matches!(spec1.strategy(), DeploymentStrategy::Recreate));
912
913 let spec2 = DeploymentSpec::new("app2").with_replicas(3);
914 assert!(matches!(
915 spec2.strategy(),
916 DeploymentStrategy::RollingUpdate { .. }
917 ));
918 }
919}