lmrc_kubernetes/deployment/
spec.rs

1//! Deployment specification types.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Specification for a Kubernetes Deployment.
7///
8/// # Examples
9///
10/// ```
11/// use lmrc_kubernetes::deployment::{DeploymentSpec, ContainerSpec};
12///
13/// let container = ContainerSpec::new("myapp", "myregistry/myapp:v1.0.0")
14///     .with_port(8080)
15///     .with_env("DATABASE_URL", "postgres://...");
16///
17/// let deployment = DeploymentSpec::new("my-deployment")
18///     .with_replicas(3)
19///     .with_container(container);
20/// ```
21#[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    /// Create a new deployment specification.
36    ///
37    /// # Arguments
38    ///
39    /// * `name` - Name of the deployment
40    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    /// Set the number of replicas.
59    pub fn with_replicas(mut self, replicas: i32) -> Self {
60        self.replicas = replicas;
61        // Auto-select strategy based on replica count
62        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    /// Add a container to the deployment.
72    pub fn with_container(mut self, container: ContainerSpec) -> Self {
73        self.containers.push(container);
74        self
75    }
76
77    /// Set the deployment strategy.
78    pub fn with_strategy(mut self, strategy: DeploymentStrategy) -> Self {
79        self.strategy = strategy;
80        self
81    }
82
83    /// Add a label to the deployment.
84    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    /// Add multiple labels to the deployment.
90    pub fn with_labels(mut self, labels: HashMap<String, String>) -> Self {
91        self.labels.extend(labels);
92        self
93    }
94
95    /// Add an init container to the deployment.
96    ///
97    /// Init containers run before the main containers and must complete successfully.
98    pub fn with_init_container(mut self, container: ContainerSpec) -> Self {
99        self.init_containers.push(container);
100        self
101    }
102
103    /// Add an image pull secret for private registries.
104    ///
105    /// # Arguments
106    ///
107    /// * `secret_name` - Name of the secret containing registry credentials
108    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    /// Set the service account for the pods.
114    ///
115    /// # Arguments
116    ///
117    /// * `account` - Name of the service account
118    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    /// Add a volume to the deployment.
124    ///
125    /// Volumes can be mounted into containers using `ContainerSpec::with_volume_mount()`.
126    ///
127    /// # Arguments
128    ///
129    /// * `volume` - The volume to add
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use lmrc_kubernetes::deployment::{DeploymentSpec, Volume, VolumeMount, ContainerSpec};
135    ///
136    /// let volume = Volume::from_config_map("config-vol", "app-config");
137    /// let mount = VolumeMount::new("config-vol", "/etc/config");
138    ///
139    /// let container = ContainerSpec::new("app", "myapp:v1")
140    ///     .with_volume_mount(mount);
141    ///
142    /// let deployment = DeploymentSpec::new("my-app")
143    ///     .with_volume(volume)
144    ///     .with_container(container);
145    /// ```
146    pub fn with_volume(mut self, volume: Volume) -> Self {
147        self.volumes.push(volume);
148        self
149    }
150
151    /// Validate the deployment spec.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error if:
156    /// - No containers are specified
157    /// - Replicas is negative
158    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    /// Get the deployment name.
175    pub fn name(&self) -> &str {
176        &self.name
177    }
178
179    /// Get the number of replicas.
180    pub fn replicas(&self) -> i32 {
181        self.replicas
182    }
183
184    /// Get the containers.
185    pub fn containers(&self) -> &[ContainerSpec] {
186        &self.containers
187    }
188
189    /// Get the deployment strategy.
190    pub fn strategy(&self) -> &DeploymentStrategy {
191        &self.strategy
192    }
193
194    /// Get the labels.
195    pub fn labels(&self) -> &HashMap<String, String> {
196        &self.labels
197    }
198
199    /// Get the init containers.
200    pub fn init_containers(&self) -> &[ContainerSpec] {
201        &self.init_containers
202    }
203
204    /// Get the image pull secrets.
205    pub fn image_pull_secrets(&self) -> &[String] {
206        &self.image_pull_secrets
207    }
208
209    /// Get the service account.
210    pub fn service_account(&self) -> Option<&str> {
211        self.service_account.as_deref()
212    }
213
214    /// Get the volumes.
215    pub fn volumes(&self) -> &[Volume] {
216        &self.volumes
217    }
218}
219
220/// Container specification for a pod.
221#[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    /// Create a new container specification.
238    ///
239    /// # Arguments
240    ///
241    /// * `name` - Name of the container
242    /// * `image` - Container image (e.g., "nginx:1.21")
243    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    /// Add a container port.
260    pub fn with_port(mut self, port: i32) -> Self {
261        self.ports.push(port);
262        self
263    }
264
265    /// Add an environment variable.
266    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    /// Add multiple environment variables.
272    pub fn with_env_map(mut self, env: HashMap<String, String>) -> Self {
273        self.env.extend(env);
274        self
275    }
276
277    /// Set resource requirements.
278    pub fn with_resources(mut self, resources: ResourceRequirements) -> Self {
279        self.resources = Some(resources);
280        self
281    }
282
283    /// Set liveness probe.
284    ///
285    /// Liveness probes determine when to restart a container.
286    pub fn with_liveness_probe(mut self, probe: ProbeSpec) -> Self {
287        self.liveness_probe = Some(probe);
288        self
289    }
290
291    /// Set readiness probe.
292    ///
293    /// Readiness probes determine when a container is ready to accept traffic.
294    pub fn with_readiness_probe(mut self, probe: ProbeSpec) -> Self {
295        self.readiness_probe = Some(probe);
296        self
297    }
298
299    /// Set startup probe.
300    ///
301    /// Startup probes determine when a container has started.
302    pub fn with_startup_probe(mut self, probe: ProbeSpec) -> Self {
303        self.startup_probe = Some(probe);
304        self
305    }
306
307    /// Add a volume mount.
308    pub fn with_volume_mount(mut self, mount: VolumeMount) -> Self {
309        self.volume_mounts.push(mount);
310        self
311    }
312
313    /// Set container command (overrides ENTRYPOINT).
314    pub fn with_command(mut self, command: Vec<String>) -> Self {
315        self.command = Some(command);
316        self
317    }
318
319    /// Set container arguments (overrides CMD).
320    pub fn with_args(mut self, args: Vec<String>) -> Self {
321        self.args = Some(args);
322        self
323    }
324
325    /// Validate the container spec.
326    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    /// Get the container name.
337    pub fn name(&self) -> &str {
338        &self.name
339    }
340
341    /// Get the container image.
342    pub fn image(&self) -> &str {
343        &self.image
344    }
345
346    /// Get the container ports.
347    pub fn ports(&self) -> &[i32] {
348        &self.ports
349    }
350
351    /// Get the environment variables.
352    pub fn env(&self) -> &HashMap<String, String> {
353        &self.env
354    }
355
356    /// Get the resource requirements.
357    pub fn resources(&self) -> Option<&ResourceRequirements> {
358        self.resources.as_ref()
359    }
360
361    /// Get the liveness probe.
362    pub fn liveness_probe(&self) -> Option<&ProbeSpec> {
363        self.liveness_probe.as_ref()
364    }
365
366    /// Get the readiness probe.
367    pub fn readiness_probe(&self) -> Option<&ProbeSpec> {
368        self.readiness_probe.as_ref()
369    }
370
371    /// Get the startup probe.
372    pub fn startup_probe(&self) -> Option<&ProbeSpec> {
373        self.startup_probe.as_ref()
374    }
375
376    /// Get the volume mounts.
377    pub fn volume_mounts(&self) -> &[VolumeMount] {
378        &self.volume_mounts
379    }
380
381    /// Get the command.
382    pub fn command(&self) -> Option<&[String]> {
383        self.command.as_deref()
384    }
385
386    /// Get the args.
387    pub fn args(&self) -> Option<&[String]> {
388        self.args.as_deref()
389    }
390}
391
392/// Deployment strategy configuration.
393#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
394pub enum DeploymentStrategy {
395    /// Recreate: terminate old pods before creating new ones.
396    Recreate,
397    /// RollingUpdate: gradually replace old pods with new ones.
398    RollingUpdate {
399        /// Maximum number of pods above desired replicas during update.
400        max_surge: Option<String>,
401        /// Maximum number of pods that can be unavailable during update.
402        max_unavailable: Option<String>,
403    },
404}
405
406/// Resource requirements for a container.
407#[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    /// Create new resource requirements.
417    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    /// Set CPU request (e.g., "100m", "0.5").
427    pub fn cpu_request<S: Into<String>>(mut self, request: S) -> Self {
428        self.cpu_request = Some(request.into());
429        self
430    }
431
432    /// Set CPU limit (e.g., "1", "2000m").
433    pub fn cpu_limit<S: Into<String>>(mut self, limit: S) -> Self {
434        self.cpu_limit = Some(limit.into());
435        self
436    }
437
438    /// Set memory request (e.g., "128Mi", "1Gi").
439    pub fn memory_request<S: Into<String>>(mut self, request: S) -> Self {
440        self.memory_request = Some(request.into());
441        self
442    }
443
444    /// Set memory limit (e.g., "256Mi", "2Gi").
445    pub fn memory_limit<S: Into<String>>(mut self, limit: S) -> Self {
446        self.memory_limit = Some(limit.into());
447        self
448    }
449
450    /// Get CPU request.
451    pub fn get_cpu_request(&self) -> Option<&str> {
452        self.cpu_request.as_deref()
453    }
454
455    /// Get CPU limit.
456    pub fn get_cpu_limit(&self) -> Option<&str> {
457        self.cpu_limit.as_deref()
458    }
459
460    /// Get memory request.
461    pub fn get_memory_request(&self) -> Option<&str> {
462        self.memory_request.as_deref()
463    }
464
465    /// Get memory limit.
466    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/// Probe specification for health checks.
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub struct ProbeSpec {
480    /// HTTP GET probe configuration.
481    pub http_get: Option<HttpGetProbe>,
482    /// Exec command probe configuration.
483    pub exec: Option<ExecProbe>,
484    /// TCP socket probe configuration.
485    pub tcp_socket: Option<TcpSocketProbe>,
486    /// Initial delay before probing (seconds).
487    pub initial_delay_seconds: i32,
488    /// How often to perform the probe (seconds).
489    pub period_seconds: i32,
490    /// Number of seconds after which the probe times out.
491    pub timeout_seconds: i32,
492    /// Minimum consecutive successes for the probe to be considered successful.
493    pub success_threshold: i32,
494    /// Minimum consecutive failures for the probe to be considered failed.
495    pub failure_threshold: i32,
496}
497
498impl ProbeSpec {
499    /// Create an HTTP GET probe.
500    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    /// Create an exec command probe.
514    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    /// Create a TCP socket probe.
528    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    /// Set initial delay in seconds.
542    pub fn initial_delay_seconds(mut self, seconds: i32) -> Self {
543        self.initial_delay_seconds = seconds;
544        self
545    }
546
547    /// Set period in seconds.
548    pub fn period_seconds(mut self, seconds: i32) -> Self {
549        self.period_seconds = seconds;
550        self
551    }
552
553    /// Set timeout in seconds.
554    pub fn timeout_seconds(mut self, seconds: i32) -> Self {
555        self.timeout_seconds = seconds;
556        self
557    }
558
559    /// Set failure threshold.
560    pub fn failure_threshold(mut self, threshold: i32) -> Self {
561        self.failure_threshold = threshold;
562        self
563    }
564}
565
566/// HTTP GET probe configuration.
567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
568pub struct HttpGetProbe {
569    /// Path to access on the HTTP server.
570    pub path: String,
571    /// Port to access on the container.
572    pub port: i32,
573}
574
575/// Exec command probe configuration.
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577pub struct ExecProbe {
578    /// Command to execute.
579    pub command: Vec<String>,
580}
581
582/// TCP socket probe configuration.
583#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
584pub struct TcpSocketProbe {
585    /// Port to connect to.
586    pub port: i32,
587}
588
589/// Volume mount specification.
590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
591pub struct VolumeMount {
592    /// Name of the volume to mount.
593    pub name: String,
594    /// Path within the container at which the volume should be mounted.
595    pub mount_path: String,
596    /// Mounted read-only if true, read-write otherwise.
597    pub read_only: bool,
598    /// Path within the volume from which the container's volume should be mounted.
599    pub sub_path: Option<String>,
600}
601
602impl VolumeMount {
603    /// Create a new volume mount.
604    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    /// Set read-only flag.
614    pub fn read_only(mut self, read_only: bool) -> Self {
615        self.read_only = read_only;
616        self
617    }
618
619    /// Set sub-path within the volume.
620    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/// Volume source types.
627#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
628pub enum VolumeSource {
629    /// ConfigMap volume source.
630    ConfigMap {
631        /// Name of the ConfigMap.
632        name: String,
633        /// Optional mode bits for files.
634        default_mode: Option<i32>,
635        /// Optional list of items to project from the ConfigMap.
636        items: Vec<KeyToPath>,
637    },
638    /// Secret volume source.
639    Secret {
640        /// Name of the Secret.
641        secret_name: String,
642        /// Optional mode bits for files.
643        default_mode: Option<i32>,
644        /// Optional list of items to project from the Secret.
645        items: Vec<KeyToPath>,
646    },
647    /// EmptyDir volume source.
648    EmptyDir {
649        /// Medium (e.g., "Memory" for tmpfs).
650        medium: Option<String>,
651        /// Size limit.
652        size_limit: Option<String>,
653    },
654    /// HostPath volume source (use with caution in production).
655    HostPath {
656        /// Path on the host.
657        path: String,
658        /// Type of the path (e.g., "Directory", "File").
659        path_type: Option<String>,
660    },
661    /// PersistentVolumeClaim volume source.
662    PersistentVolumeClaim {
663        /// Name of the PersistentVolumeClaim.
664        claim_name: String,
665        /// Whether the volume should be read-only.
666        read_only: bool,
667    },
668}
669
670/// Mapping of key to path for ConfigMap and Secret volumes.
671#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
672pub struct KeyToPath {
673    /// The key to project.
674    pub key: String,
675    /// The relative path for the file.
676    pub path: String,
677    /// Optional mode bits for the file.
678    pub mode: Option<i32>,
679}
680
681impl KeyToPath {
682    /// Create a new key to path mapping.
683    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    /// Set the file mode.
692    pub fn with_mode(mut self, mode: i32) -> Self {
693        self.mode = Some(mode);
694        self
695    }
696}
697
698/// Volume specification.
699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700pub struct Volume {
701    /// Name of the volume.
702    pub name: String,
703    /// Source of the volume.
704    pub source: VolumeSource,
705}
706
707impl Volume {
708    /// Create a new ConfigMap volume.
709    ///
710    /// # Examples
711    ///
712    /// ```
713    /// use lmrc_kubernetes::deployment::Volume;
714    ///
715    /// let volume = Volume::from_config_map("config-vol", "my-config");
716    /// ```
717    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    /// Create a new ConfigMap volume with specific items.
729    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    /// Create a new Secret volume.
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// use lmrc_kubernetes::deployment::Volume;
750    ///
751    /// let volume = Volume::from_secret("secret-vol", "my-secret");
752    /// ```
753    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    /// Create a new EmptyDir volume.
765    ///
766    /// # Examples
767    ///
768    /// ```
769    /// use lmrc_kubernetes::deployment::Volume;
770    ///
771    /// let volume = Volume::empty_dir("cache");
772    /// ```
773    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    /// Create a new EmptyDir volume backed by tmpfs (in-memory).
784    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    /// Create a new HostPath volume.
795    ///
796    /// Warning: Use with caution in production environments.
797    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    /// Create a new PersistentVolumeClaim volume.
808    ///
809    /// # Examples
810    ///
811    /// ```
812    /// use lmrc_kubernetes::deployment::Volume;
813    ///
814    /// let volume = Volume::from_pvc("data-vol", "my-pvc");
815    /// ```
816    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    /// Create a new PersistentVolumeClaim volume (read-only).
827    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    /// Set the default mode for ConfigMap/Secret volumes.
838    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}