k8s_openapi_ext/ext/
pod_spec.rs

1use super::*;
2
3pub trait PodSpecExt {
4    /// Build `corev1::PodSpec` with this container
5    ///
6    fn container(container: corev1::Container) -> Self;
7
8    /// Build `corev1::PodSpec` with these containers
9    ///
10    fn containers(containers: impl IntoIterator<Item = corev1::Container>) -> Self;
11
12    /// Set `active_deadline_seconds`
13    ///
14    fn active_deadline_seconds(self, seconds: i64) -> Self;
15
16    /// Set affinity
17    ///
18    fn affinity(self, affinity: impl Into<Option<corev1::Affinity>>) -> Self;
19
20    /// Set `automount_service_account_token`
21    ///
22    fn automount_service_account_token(self, yes: bool) -> Self;
23
24    /// Set `dns_policy`
25    ///
26    fn dns_policy(self, policy: impl ToString) -> Self;
27
28    /// Set `enable_service_links`
29    ///
30    fn enable_service_links(self, yes: bool) -> Self;
31
32    /// Set `hostname`
33    ///
34    fn hostname(self, hostname: impl ToString) -> Self;
35
36    /// Set `host_ipc`
37    ///
38    fn host_ipc(self, yes: bool) -> Self;
39
40    /// Set `host_network`
41    ///
42    fn host_network(self, yes: bool) -> Self;
43
44    /// Set `host_pid`
45    ///
46    fn host_pid(self, yes: bool) -> Self;
47
48    /// Add image pull secret
49    ///
50    fn image_pull_secret(self, name: impl ToString) -> Self;
51
52    /// Set `node_name`
53    ///
54    fn node_name(self, node_name: impl ToString) -> Self;
55
56    /// Add node selector
57    ///
58    fn node_selector(
59        self,
60        node_selector: impl IntoIterator<Item = (impl ToString, impl ToString)>,
61    ) -> Self;
62
63    /// Set `preemption_policy`
64    ///
65    fn preemption_policy(self, policy: impl ToString) -> Self;
66
67    /// Set `priority`
68    ///
69    fn priority(self, priority: i32) -> Self;
70
71    /// Set `priority_class_name`
72    ///
73    fn priority_class_name(self, class_name: impl ToString) -> Self;
74
75    /// Set `restart_policy`
76    ///
77    fn restart_policy(self, policy: impl ToString) -> Self;
78
79    /// Set `runtime_class_name`
80    ///
81    fn runtime_class_name(self, class_name: impl ToString) -> Self;
82
83    /// Set `scheduler_name`
84    ///
85    fn scheduler_name(self, scheduler: impl ToString) -> Self;
86
87    /// Set service account name
88    ///
89    fn service_account_name(self, name: impl ToString) -> Self;
90
91    /// Set `service_account` (deprecated)
92    ///
93    /// **Deprecated:** Use [`service_account_name`] instead.
94    #[deprecated(note = "Use service_account_name instead")]
95    fn service_account(self, name: impl ToString) -> Self;
96
97    /// Set `set_hostname_as_fqdn`
98    ///
99    fn set_hostname_as_fqdn(self, yes: bool) -> Self;
100
101    /// Set `share_process_namespace`
102    ///
103    fn share_process_namespace(self, yes: bool) -> Self;
104
105    /// Set `subdomain`
106    ///
107    fn subdomain(self, subdomain: impl ToString) -> Self;
108
109    /// Set `termination_grace_period_seconds`
110    ///
111    fn termination_grace_period_seconds(self, seconds: i64) -> Self;
112
113    /// Add tolerations
114    ///
115    fn tolerations(self, tolerations: impl IntoIterator<Item = corev1::Toleration>) -> Self;
116
117    /// Add `volumes`
118    ///
119    fn volumes(self, volumes: impl IntoIterator<Item = corev1::Volume>) -> Self;
120}
121
122impl PodSpecExt for corev1::PodSpec {
123    fn container(container: corev1::Container) -> Self {
124        let containers = vec![container];
125        Self {
126            containers,
127            // active_deadline_seconds: todo!(),
128            // affinity: todo!(),
129            // automount_service_account_token: todo!(),
130            // dns_config: todo!(),
131            // dns_policy: todo!(),
132            // enable_service_links: todo!(),
133            // ephemeral_containers: todo!(),
134            // host_aliases: todo!(),
135            // host_ipc: todo!(),
136            // host_network: todo!(),
137            // host_pid: todo!(),
138            // hostname: todo!(),
139            // image_pull_secrets: todo!(),
140            // init_containers: todo!(),
141            // node_name: todo!(),
142            // node_selector: todo!(),
143            // overhead: todo!(),
144            // preemption_policy: todo!(),
145            // priority: todo!(),
146            // priority_class_name: todo!(),
147            // readiness_gates: todo!(),
148            // restart_policy: todo!(),
149            // runtime_class_name: todo!(),
150            // scheduler_name: todo!(),
151            // security_context: todo!(),
152            // service_account: todo!(),
153            // service_account_name: todo!(),
154            // set_hostname_as_fqdn: todo!(),
155            // share_process_namespace: todo!(),
156            // subdomain: todo!(),
157            // termination_grace_period_seconds: todo!(),
158            // tolerations: todo!(),
159            // topology_spread_constraints: todo!(),
160            // volumes: todo!(),
161            ..default()
162        }
163    }
164
165    fn containers(containers: impl IntoIterator<Item = corev1::Container>) -> Self {
166        let containers = Vec::from_iter(containers);
167        Self {
168            containers,
169            ..default()
170        }
171    }
172
173    fn active_deadline_seconds(self, seconds: i64) -> Self {
174        Self {
175            active_deadline_seconds: Some(seconds),
176            ..self
177        }
178    }
179
180    fn affinity(self, affinity: impl Into<Option<corev1::Affinity>>) -> Self {
181        let affinity = affinity.into();
182        Self { affinity, ..self }
183    }
184
185    fn automount_service_account_token(self, yes: bool) -> Self {
186        Self {
187            automount_service_account_token: Some(yes),
188            ..self
189        }
190    }
191
192    fn dns_policy(self, policy: impl ToString) -> Self {
193        let dns_policy = Some(policy.to_string());
194        Self { dns_policy, ..self }
195    }
196
197    fn enable_service_links(self, yes: bool) -> Self {
198        Self {
199            enable_service_links: Some(yes),
200            ..self
201        }
202    }
203
204    fn hostname(self, hostname: impl ToString) -> Self {
205        let hostname = Some(hostname.to_string());
206        Self { hostname, ..self }
207    }
208
209    fn host_ipc(self, yes: bool) -> Self {
210        Self {
211            host_ipc: Some(yes),
212            ..self
213        }
214    }
215
216    fn host_network(self, yes: bool) -> Self {
217        Self {
218            host_network: Some(yes),
219            ..self
220        }
221    }
222
223    fn host_pid(self, yes: bool) -> Self {
224        Self {
225            host_pid: Some(yes),
226            ..self
227        }
228    }
229
230    fn image_pull_secret(mut self, name: impl ToString) -> Self {
231        let secret = corev1::LocalObjectReference::new(name);
232        self.image_pull_secrets.get_or_insert_default().push(secret);
233        self
234    }
235
236    fn node_name(self, node_name: impl ToString) -> Self {
237        let node_name = Some(node_name.to_string());
238        Self { node_name, ..self }
239    }
240
241    fn node_selector(
242        mut self,
243        node_selector: impl IntoIterator<Item = (impl ToString, impl ToString)>,
244    ) -> Self {
245        let node_selector = node_selector
246            .into_iter()
247            .map(|(key, value)| (key.to_string(), value.to_string()));
248        self.node_selector
249            .get_or_insert_default()
250            .extend(node_selector);
251        self
252    }
253
254    fn preemption_policy(self, policy: impl ToString) -> Self {
255        let preemption_policy = Some(policy.to_string());
256        Self {
257            preemption_policy,
258            ..self
259        }
260    }
261
262    fn priority(self, priority: i32) -> Self {
263        Self {
264            priority: Some(priority),
265            ..self
266        }
267    }
268
269    fn priority_class_name(self, class_name: impl ToString) -> Self {
270        let priority_class_name = Some(class_name.to_string());
271        Self {
272            priority_class_name,
273            ..self
274        }
275    }
276
277    fn restart_policy(self, policy: impl ToString) -> Self {
278        let restart_policy = Some(policy.to_string());
279        Self {
280            restart_policy,
281            ..self
282        }
283    }
284
285    fn runtime_class_name(self, class_name: impl ToString) -> Self {
286        let runtime_class_name = Some(class_name.to_string());
287        Self {
288            runtime_class_name,
289            ..self
290        }
291    }
292
293    fn scheduler_name(self, scheduler: impl ToString) -> Self {
294        let scheduler_name = Some(scheduler.to_string());
295        Self {
296            scheduler_name,
297            ..self
298        }
299    }
300
301    fn service_account_name(self, name: impl ToString) -> Self {
302        let service_account_name = Some(name.to_string());
303        Self {
304            service_account_name,
305            ..self
306        }
307    }
308
309    fn service_account(self, name: impl ToString) -> Self {
310        let service_account = Some(name.to_string());
311        Self {
312            service_account,
313            ..self
314        }
315    }
316
317    fn set_hostname_as_fqdn(self, yes: bool) -> Self {
318        Self {
319            set_hostname_as_fqdn: Some(yes),
320            ..self
321        }
322    }
323
324    fn share_process_namespace(self, yes: bool) -> Self {
325        Self {
326            share_process_namespace: Some(yes),
327            ..self
328        }
329    }
330
331    fn subdomain(self, subdomain: impl ToString) -> Self {
332        let subdomain = Some(subdomain.to_string());
333        Self { subdomain, ..self }
334    }
335
336    fn termination_grace_period_seconds(self, seconds: i64) -> Self {
337        Self {
338            termination_grace_period_seconds: Some(seconds),
339            ..self
340        }
341    }
342
343    fn tolerations(mut self, tolerations: impl IntoIterator<Item = corev1::Toleration>) -> Self {
344        self.tolerations.get_or_insert_default().extend(tolerations);
345        self
346    }
347
348    fn volumes(mut self, volumes: impl IntoIterator<Item = corev1::Volume>) -> Self {
349        self.volumes.get_or_insert_default().extend(volumes);
350        self
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    fn test_spec() -> corev1::PodSpec {
359        corev1::PodSpec::container(corev1::Container::new("test-container"))
360    }
361
362    #[test]
363    fn container() {
364        let container = corev1::Container::new("test-container");
365        let pod_spec = corev1::PodSpec::container(container);
366        assert_eq!(pod_spec.containers.len(), 1);
367        assert_eq!(pod_spec.containers[0].name, "test-container");
368    }
369
370    #[test]
371    fn containers() {
372        let containers_vec = vec![
373            corev1::Container::new("container1"),
374            corev1::Container::new("container2"),
375        ];
376        let pod_spec = corev1::PodSpec::containers(containers_vec);
377        assert_eq!(pod_spec.containers.len(), 2);
378        assert_eq!(pod_spec.containers[0].name, "container1");
379        assert_eq!(pod_spec.containers[1].name, "container2");
380    }
381
382    #[test]
383    fn active_deadline_seconds() {
384        let pod_spec = test_spec().active_deadline_seconds(300);
385        assert_eq!(pod_spec.active_deadline_seconds, Some(300));
386    }
387
388    #[test]
389    fn affinity() {
390        let affinity = corev1::Affinity::default();
391        let pod_spec = test_spec().affinity(affinity);
392        assert!(pod_spec.affinity.is_some());
393
394        let pod_spec_none = test_spec().affinity(None);
395        assert!(pod_spec_none.affinity.is_none());
396    }
397
398    #[test]
399    fn automount_service_account_token() {
400        let pod_spec = test_spec().automount_service_account_token(true);
401        assert_eq!(pod_spec.automount_service_account_token, Some(true));
402
403        let pod_spec_false = test_spec().automount_service_account_token(false);
404        assert_eq!(pod_spec_false.automount_service_account_token, Some(false));
405    }
406
407    #[test]
408    fn dns_policy() {
409        let pod_spec = test_spec().dns_policy("ClusterFirst");
410        assert_eq!(pod_spec.dns_policy.unwrap(), "ClusterFirst");
411    }
412
413    #[test]
414    fn enable_service_links() {
415        let pod_spec = test_spec().enable_service_links(false);
416        assert_eq!(pod_spec.enable_service_links, Some(false));
417
418        let pod_spec_true = test_spec().enable_service_links(true);
419        assert_eq!(pod_spec_true.enable_service_links, Some(true));
420    }
421
422    #[test]
423    fn hostname() {
424        let pod_spec = test_spec().hostname("my-pod");
425        assert_eq!(pod_spec.hostname.unwrap(), "my-pod");
426    }
427
428    #[test]
429    fn host_ipc() {
430        let pod_spec = test_spec().host_ipc(true);
431        assert_eq!(pod_spec.host_ipc, Some(true));
432    }
433
434    #[test]
435    fn host_network() {
436        let pod_spec = test_spec().host_network(true);
437        assert_eq!(pod_spec.host_network, Some(true));
438    }
439
440    #[test]
441    fn host_pid() {
442        let pod_spec = test_spec().host_pid(true);
443        assert_eq!(pod_spec.host_pid, Some(true));
444    }
445
446    #[test]
447    fn image_pull_secret() {
448        let pod_spec = test_spec()
449            .image_pull_secret("secret1")
450            .image_pull_secret("secret2");
451
452        let secrets = pod_spec.image_pull_secrets.unwrap_or_default();
453        assert_eq!(secrets.len(), 2);
454        // LocalObjectReference.name field is String
455        assert_eq!(secrets[0].name, "secret1");
456        assert_eq!(secrets[1].name, "secret2");
457    }
458
459    #[test]
460    fn node_name() {
461        let pod_spec = test_spec().node_name("worker-node-1");
462        assert_eq!(pod_spec.node_name.unwrap(), "worker-node-1");
463    }
464
465    #[test]
466    fn node_selector() {
467        let pod = corev1::Pod::default().spec(
468            test_spec().node_selector([("kubernetes.io/os", "linux"), ("node-type", "compute")]),
469        );
470
471        let node_selector = pod.node_selector().unwrap();
472        assert_eq!(node_selector.len(), 2);
473        assert_eq!(node_selector.get("kubernetes.io/os").unwrap(), "linux");
474        assert_eq!(node_selector.get("node-type").unwrap(), "compute");
475    }
476
477    #[test]
478    fn preemption_policy() {
479        let pod_spec = test_spec().preemption_policy("Never");
480        assert_eq!(pod_spec.preemption_policy.unwrap(), "Never");
481    }
482
483    #[test]
484    fn priority() {
485        let pod_spec = test_spec().priority(100);
486        assert_eq!(pod_spec.priority, Some(100));
487    }
488
489    #[test]
490    fn priority_class_name() {
491        let pod_spec = test_spec().priority_class_name("high-priority");
492        assert_eq!(pod_spec.priority_class_name.unwrap(), "high-priority");
493    }
494
495    #[test]
496    fn restart_policy() {
497        let pod_spec = test_spec().restart_policy("OnFailure");
498        assert_eq!(pod_spec.restart_policy.unwrap(), "OnFailure");
499    }
500
501    #[test]
502    fn runtime_class_name() {
503        let pod_spec = test_spec().runtime_class_name("gvisor");
504        assert_eq!(pod_spec.runtime_class_name.unwrap(), "gvisor");
505    }
506
507    #[test]
508    fn scheduler_name() {
509        let pod_spec = test_spec().scheduler_name("custom-scheduler");
510        assert_eq!(pod_spec.scheduler_name.unwrap(), "custom-scheduler");
511    }
512
513    #[test]
514    fn service_account_name() {
515        let pod_spec = test_spec().service_account_name("my-service-account");
516        assert_eq!(pod_spec.service_account_name.unwrap(), "my-service-account");
517    }
518
519    #[test]
520    #[expect(deprecated)]
521    fn service_account_deprecated() {
522        let pod_spec = test_spec().service_account("deprecated-account");
523        assert_eq!(pod_spec.service_account.unwrap(), "deprecated-account");
524    }
525
526    #[test]
527    fn set_hostname_as_fqdn() {
528        let pod_spec = test_spec().set_hostname_as_fqdn(true);
529        assert_eq!(pod_spec.set_hostname_as_fqdn, Some(true));
530    }
531
532    #[test]
533    fn share_process_namespace() {
534        let pod_spec = test_spec().share_process_namespace(true);
535        assert_eq!(pod_spec.share_process_namespace, Some(true));
536    }
537
538    #[test]
539    fn subdomain() {
540        let pod_spec = test_spec().subdomain("my-subdomain");
541        assert_eq!(pod_spec.subdomain.unwrap(), "my-subdomain");
542    }
543
544    #[test]
545    fn termination_grace_period_seconds() {
546        let pod_spec = test_spec().termination_grace_period_seconds(60);
547        assert_eq!(pod_spec.termination_grace_period_seconds, Some(60));
548    }
549
550    #[test]
551    fn tolerations() {
552        let toleration1 =
553            corev1::Toleration::no_schedule("node-role.kubernetes.io/master").exists();
554        let toleration2 = corev1::Toleration::no_execute("node.kubernetes.io/not-ready")
555            .exists()
556            .toleration_seconds(300);
557
558        let pod = corev1::Pod::default().spec(test_spec().tolerations([toleration1, toleration2]));
559
560        let tolerations = pod.tolerations().unwrap();
561        assert_eq!(tolerations.len(), 2);
562        let t0 = &tolerations[0];
563        let t1 = &tolerations[1];
564
565        assert_eq!(t0.key.as_ref().unwrap(), "node-role.kubernetes.io/master");
566        assert_eq!(t0.operator.as_ref().unwrap(), "Exists");
567        assert_eq!(t0.effect.as_ref().unwrap(), "NoSchedule");
568
569        assert_eq!(t1.key.as_ref().unwrap(), "node.kubernetes.io/not-ready");
570        assert_eq!(t1.operator.as_ref().unwrap(), "Exists");
571        assert_eq!(t1.effect.as_ref().unwrap(), "NoExecute");
572        assert_eq!(t1.toleration_seconds, Some(300));
573    }
574
575    #[test]
576    fn volumes() {
577        let volume1 = corev1::Volume {
578            name: "config-vol".to_string(),
579            config_map: Some(corev1::ConfigMapVolumeSource {
580                name: "my-config".to_string(),
581                ..default()
582            }),
583            ..default()
584        };
585        let volume2 = corev1::Volume {
586            name: "empty-vol".to_string(),
587            empty_dir: Some(corev1::EmptyDirVolumeSource::default()),
588            ..default()
589        };
590
591        let pod_spec = test_spec().volumes([volume1, volume2]);
592
593        let volumes = pod_spec.volumes.as_ref().unwrap();
594        assert_eq!(volumes.len(), 2);
595        assert_eq!(volumes[0].name, "config-vol");
596        assert_eq!(volumes[1].name, "empty-vol");
597        assert!(volumes[0].config_map.is_some());
598        assert!(volumes[1].empty_dir.is_some());
599    }
600
601    #[test]
602    fn method_chaining() {
603        let container = corev1::Container::new("test-container");
604
605        let pod_spec = corev1::PodSpec::container(container)
606            .hostname("my-pod")
607            .host_network(true)
608            .dns_policy("ClusterFirst")
609            .restart_policy("Always")
610            .priority(100)
611            .service_account_name("my-sa")
612            .image_pull_secret("my-secret")
613            .node_selector([("zone", "us-west-2a")])
614            .termination_grace_period_seconds(30);
615
616        assert_eq!(pod_spec.containers.len(), 1);
617        assert_eq!(pod_spec.hostname.as_ref().unwrap(), "my-pod");
618        assert_eq!(pod_spec.host_network, Some(true));
619        assert_eq!(pod_spec.dns_policy.as_ref().unwrap(), "ClusterFirst");
620        assert_eq!(pod_spec.restart_policy.as_ref().unwrap(), "Always");
621        assert_eq!(pod_spec.priority, Some(100));
622        assert_eq!(pod_spec.service_account_name.as_ref().unwrap(), "my-sa");
623        assert_eq!(pod_spec.image_pull_secrets.as_ref().unwrap().len(), 1);
624
625        // Use PodGetExt for node_selector - more ergonomic
626        let pod = corev1::Pod::default().spec(pod_spec);
627        assert_eq!(
628            pod.node_selector().unwrap().get("zone").unwrap(),
629            "us-west-2a"
630        );
631        assert_eq!(
632            pod.spec.as_ref().unwrap().termination_grace_period_seconds,
633            Some(30)
634        );
635    }
636}