Skip to content

Commit 0905607

Browse files
soenkeliebauFelix Hennigfhennig
committed
Feature: pod builder affinity and node selection (#520)
## Description part of this ticket: stackabletech/issues#300 Renamed from: https://github.com/stackabletech/operator-rs/tree/feat/affinity_0.25 Description: - Adds new PodBuilder methods for node selection and pod affinity (Also with tests) Co-authored-by: Felix Hennig <felix.hennig@stackable.tech> Co-authored-by: Felix Hennig <fhennig@users.noreply.github.com>
1 parent 6b646e9 commit 0905607

File tree

4 files changed

+231
-53
lines changed

4 files changed

+231
-53
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- Extended the `PodBuilder` with `pod_affinity`, `pod_anti_affinity`, `node_selector` and their `*_opt` variants ([#520])
10+
11+
[#520]: https://github.com/stackabletech/operator-rs/pull/520
12+
713
## [0.29.0] - 2022-12-16
814

915
### Added

src/builder/pod/mod.rs

Lines changed: 199 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@ use crate::builder::meta::ObjectMetaBuilder;
66
use crate::commons::product_image_selection::ResolvedProductImage;
77
use crate::error::{Error, OperatorResult};
88

9+
use super::{ListenerOperatorVolumeSourceBuilder, ListenerReference};
910
use k8s_openapi::{
1011
api::core::v1::{
1112
Affinity, Container, LocalObjectReference, NodeAffinity, NodeSelector,
12-
NodeSelectorRequirement, NodeSelectorTerm, Pod, PodAffinity, PodCondition,
13+
NodeSelectorRequirement, NodeSelectorTerm, Pod, PodAffinity, PodAntiAffinity, PodCondition,
1314
PodSecurityContext, PodSpec, PodStatus, PodTemplateSpec, Toleration, Volume,
1415
},
1516
apimachinery::pkg::apis::meta::v1::{LabelSelector, LabelSelectorRequirement, ObjectMeta},
1617
};
1718
use std::collections::BTreeMap;
1819

19-
use super::{ListenerOperatorVolumeSourceBuilder, ListenerReference};
20-
21-
/// A builder to build [`Pod`] objects.
22-
///
20+
/// A builder to build [`Pod`] or [`PodTemplateSpec`] objects.
2321
#[derive(Clone, Default)]
2422
pub struct PodBuilder {
2523
containers: Vec<Container>,
@@ -29,6 +27,7 @@ pub struct PodBuilder {
2927
node_name: Option<String>,
3028
node_selector: Option<LabelSelector>,
3129
pod_affinity: Option<PodAffinity>,
30+
pod_anti_affinity: Option<PodAntiAffinity>,
3231
status: Option<PodStatus>,
3332
security_context: Option<PodSecurityContext>,
3433
tolerations: Option<Vec<Toleration>>,
@@ -87,11 +86,31 @@ impl PodBuilder {
8786
self
8887
}
8988

89+
pub fn pod_affinity_opt(&mut self, affinity: Option<PodAffinity>) -> &mut Self {
90+
self.pod_affinity = affinity;
91+
self
92+
}
93+
94+
pub fn pod_anti_affinity(&mut self, anti_affinity: PodAntiAffinity) -> &mut Self {
95+
self.pod_anti_affinity = Some(anti_affinity);
96+
self
97+
}
98+
99+
pub fn pod_anti_affinity_opt(&mut self, anti_affinity: Option<PodAntiAffinity>) -> &mut Self {
100+
self.pod_anti_affinity = anti_affinity;
101+
self
102+
}
103+
90104
pub fn node_selector(&mut self, node_selector: LabelSelector) -> &mut Self {
91105
self.node_selector = Some(node_selector);
92106
self
93107
}
94108

109+
pub fn node_selector_opt(&mut self, node_selector: Option<LabelSelector>) -> &mut Self {
110+
self.node_selector = node_selector;
111+
self
112+
}
113+
95114
pub fn phase(&mut self, phase: &str) -> &mut Self {
96115
let mut status = self.status.get_or_insert_with(PodStatus::default);
97116
status.phase = Some(phase.to_string());
@@ -354,18 +373,11 @@ impl PodBuilder {
354373
init_containers: self.init_containers.clone(),
355374
node_name: self.node_name.clone(),
356375
node_selector: node_selector_labels,
357-
affinity: node_affinity
358-
.map(|node_affinity| Affinity {
359-
node_affinity: Some(node_affinity),
360-
pod_affinity: self.pod_affinity.clone(),
361-
..Affinity::default()
362-
})
363-
.or_else(|| {
364-
Some(Affinity {
365-
pod_affinity: self.pod_affinity.clone(),
366-
..Affinity::default()
367-
})
368-
}),
376+
affinity: Some(Affinity {
377+
node_affinity,
378+
pod_affinity: self.pod_affinity.clone(),
379+
pod_anti_affinity: self.pod_anti_affinity.clone(),
380+
}),
369381
security_context: self.security_context.clone(),
370382
tolerations: self.tolerations.clone(),
371383
volumes: self.volumes.clone(),
@@ -406,33 +418,71 @@ impl PodBuilder {
406418

407419
#[cfg(test)]
408420
mod tests {
421+
use super::*;
409422
use crate::builder::{
410423
meta::ObjectMetaBuilder,
411-
pod::{container::ContainerBuilder, volume::VolumeBuilder, PodBuilder},
424+
pod::{container::ContainerBuilder, volume::VolumeBuilder},
412425
};
413426
use k8s_openapi::{
414427
api::core::v1::{LocalObjectReference, PodAffinity, PodAffinityTerm},
415428
apimachinery::pkg::apis::meta::v1::{LabelSelector, LabelSelectorRequirement},
416429
};
430+
use rstest::*;
417431

418-
#[test]
419-
fn test_pod_builder() {
420-
let container = ContainerBuilder::new("containername")
432+
// A simple [`Container`] with a name and image.
433+
#[fixture]
434+
fn dummy_container() -> Container {
435+
ContainerBuilder::new("container")
421436
.expect("ContainerBuilder not created")
422-
.image("stackable/zookeeper:2.4.14")
423-
.command(vec!["zk-server-start.sh".to_string()])
424-
.args(vec!["stackable/conf/zk.properties".to_string()])
425-
.add_volume_mount("zk-worker-1", "conf/")
426-
.build();
437+
.image("private-company/product:2.4.14")
438+
.build()
439+
}
427440

428-
let init_container = ContainerBuilder::new("init-containername")
429-
.expect("ContainerBuilder not created")
430-
.image("stackable/zookeeper:2.4.14")
431-
.command(vec!["wrapper.sh".to_string()])
432-
.args(vec!["12345".to_string()])
433-
.build();
441+
/// A [`PodBuilder`] that already contains the minum setup to build a Pod (name and container).
442+
#[fixture]
443+
fn pod_builder_with_name_and_container(dummy_container: Container) -> PodBuilder {
444+
let mut builder = PodBuilder::new();
445+
builder
446+
.metadata(ObjectMetaBuilder::new().name("testpod").build())
447+
.add_container(dummy_container);
448+
builder
449+
}
450+
451+
// A fixture for a node selector to use on a Pod, and the resulting node selector labels and node affinity.
452+
#[fixture]
453+
fn node_selector1() -> (
454+
LabelSelector,
455+
Option<BTreeMap<String, String>>,
456+
Option<NodeAffinity>,
457+
) {
458+
let labels = BTreeMap::from([("key".to_owned(), "value".to_owned())]);
459+
let label_selector = LabelSelector {
460+
match_expressions: Some(vec![LabelSelectorRequirement {
461+
key: "security".to_owned(),
462+
operator: "In".to_owned(),
463+
values: Some(vec!["S1".to_owned(), "S2".to_owned()]),
464+
}]),
465+
match_labels: Some(labels.clone()),
466+
};
467+
let affinity = Some(NodeAffinity {
468+
required_during_scheduling_ignored_during_execution: Some(NodeSelector {
469+
node_selector_terms: vec![NodeSelectorTerm {
470+
match_expressions: Some(vec![NodeSelectorRequirement {
471+
key: "security".to_owned(),
472+
operator: "In".to_owned(),
473+
values: Some(vec!["S1".to_owned(), "S2".to_owned()]),
474+
}]),
475+
..Default::default()
476+
}],
477+
}),
478+
..Default::default()
479+
});
480+
(label_selector, Some(labels), affinity)
481+
}
434482

435-
let pod_affinity = PodAffinity {
483+
#[fixture]
484+
fn pod_affinity() -> PodAffinity {
485+
PodAffinity {
436486
preferred_during_scheduling_ignored_during_execution: None,
437487
required_during_scheduling_ignored_during_execution: Some(vec![PodAffinityTerm {
438488
label_selector: Some(LabelSelector {
@@ -446,12 +496,39 @@ mod tests {
446496
topology_key: "topology.kubernetes.io/zone".to_string(),
447497
..Default::default()
448498
}]),
449-
};
499+
}
500+
}
501+
502+
#[fixture]
503+
fn pod_anti_affinity(pod_affinity: PodAffinity) -> PodAntiAffinity {
504+
PodAntiAffinity {
505+
preferred_during_scheduling_ignored_during_execution: None,
506+
required_during_scheduling_ignored_during_execution: pod_affinity
507+
.required_during_scheduling_ignored_during_execution,
508+
}
509+
}
510+
511+
#[rstest]
512+
fn test_pod_builder_pod_name() {
513+
let pod = PodBuilder::new()
514+
.metadata_builder(|builder| builder.name("foo"))
515+
.build()
516+
.unwrap();
517+
518+
assert_eq!(pod.metadata.name.unwrap(), "foo");
519+
}
520+
521+
#[rstest]
522+
fn test_pod_builder(pod_affinity: PodAffinity, dummy_container: Container) {
523+
let init_container = ContainerBuilder::new("init-containername")
524+
.expect("ContainerBuilder not created")
525+
.image("stackable/zookeeper:2.4.14")
526+
.build();
450527

451528
let pod = PodBuilder::new()
452529
.pod_affinity(pod_affinity.clone())
453530
.metadata(ObjectMetaBuilder::new().name("testpod").build())
454-
.add_container(container)
531+
.add_container(dummy_container)
455532
.add_init_container(init_container)
456533
.node_name("worker-1.stackable.demo")
457534
.add_volume(
@@ -486,25 +563,11 @@ mod tests {
486563
.and_then(|volume| volume.config_map.as_ref()?.name.clone())),
487564
Some("configmap".to_string())
488565
);
489-
490-
let pod = PodBuilder::new()
491-
.metadata_builder(|builder| builder.name("foo"))
492-
.build()
493-
.unwrap();
494-
495-
assert_eq!(pod.metadata.name.unwrap(), "foo");
496566
}
497567

498-
#[test]
499-
fn test_pod_builder_image_pull_secrets() {
500-
let container = ContainerBuilder::new("container")
501-
.expect("ContainerBuilder not created")
502-
.image("private-comapany/product:2.4.14")
503-
.build();
504-
505-
let pod = PodBuilder::new()
506-
.metadata(ObjectMetaBuilder::new().name("testpod").build())
507-
.add_container(container)
568+
#[rstest]
569+
fn test_pod_builder_image_pull_secrets(mut pod_builder_with_name_and_container: PodBuilder) {
570+
let pod = pod_builder_with_name_and_container
508571
.image_pull_secrets(vec!["company-registry-secret".to_string()].into_iter())
509572
.build()
510573
.unwrap();
@@ -516,4 +579,88 @@ mod tests {
516579
}]
517580
);
518581
}
582+
583+
/// Test if setting a node selector generates the correct node selector labels and node affinity on the Pod.
584+
#[rstest]
585+
fn test_pod_builder_node_selector(
586+
mut pod_builder_with_name_and_container: PodBuilder,
587+
node_selector1: (
588+
LabelSelector,
589+
Option<BTreeMap<String, String>>,
590+
Option<NodeAffinity>,
591+
),
592+
) {
593+
// destruct fixture
594+
let (node_selector, expected_labels, expected_affinity) = node_selector1;
595+
// first test with the normal node_selector function
596+
let pod = pod_builder_with_name_and_container
597+
.clone()
598+
.node_selector(node_selector.clone())
599+
.build()
600+
.unwrap();
601+
602+
let spec = pod.spec.unwrap();
603+
assert_eq!(spec.node_selector, expected_labels);
604+
assert_eq!(spec.affinity.unwrap().node_affinity, expected_affinity);
605+
606+
// test the node_selector_opt function
607+
let pod = pod_builder_with_name_and_container
608+
.node_selector_opt(Some(node_selector))
609+
.build()
610+
.unwrap();
611+
612+
// asserts
613+
let spec = pod.spec.unwrap();
614+
assert_eq!(spec.node_selector, expected_labels);
615+
assert_eq!(spec.affinity.unwrap().node_affinity, expected_affinity);
616+
}
617+
618+
/// Test if setting a node selector generates the correct node selector labels and node affinity on the Pod,
619+
/// while keeping the manually set Pod affinities. Since they are mangled together, it makes sense to make sure that
620+
/// one is not replacing the other
621+
#[rstest]
622+
fn test_pod_builder_node_selector_and_affinity(
623+
mut pod_builder_with_name_and_container: PodBuilder,
624+
node_selector1: (
625+
LabelSelector,
626+
Option<BTreeMap<String, String>>,
627+
Option<NodeAffinity>,
628+
),
629+
pod_affinity: PodAffinity,
630+
pod_anti_affinity: PodAntiAffinity,
631+
) {
632+
// destruct fixture
633+
let (node_selector, expected_labels, expected_affinity) = node_selector1;
634+
// first test with the normal functions
635+
let pod = pod_builder_with_name_and_container
636+
.clone()
637+
.node_selector(node_selector.clone())
638+
.pod_affinity(pod_affinity.clone())
639+
.pod_anti_affinity(pod_anti_affinity.clone())
640+
.build()
641+
.unwrap();
642+
643+
let spec = pod.spec.unwrap();
644+
assert_eq!(spec.node_selector, expected_labels);
645+
let affinity = spec.affinity.unwrap();
646+
assert_eq!(affinity.node_affinity, expected_affinity);
647+
assert_eq!(affinity.pod_affinity, Some(pod_affinity.clone()));
648+
assert_eq!(affinity.pod_anti_affinity, Some(pod_anti_affinity.clone()));
649+
650+
// test the *_opt functions
651+
let pod = pod_builder_with_name_and_container
652+
.node_selector_opt(Some(node_selector))
653+
.pod_affinity_opt(Some(pod_affinity.clone()))
654+
.pod_anti_affinity_opt(Some(pod_anti_affinity.clone()))
655+
.build()
656+
.unwrap();
657+
658+
// asserts
659+
let spec = pod.spec.unwrap();
660+
assert_eq!(spec.node_selector, expected_labels);
661+
let affinity = spec.affinity.unwrap();
662+
assert_eq!(affinity.node_affinity, expected_affinity);
663+
assert_eq!(affinity.pod_affinity, Some(pod_affinity));
664+
assert_eq!(affinity.pod_anti_affinity, Some(pod_anti_affinity));
665+
}
519666
}

0 commit comments

Comments
 (0)