Skip to content

Commit e1e7cf3

Browse files
committed
test: add unit tests for daemonset and service & validation
1 parent 99cf8f6 commit e1e7cf3

3 files changed

Lines changed: 335 additions & 0 deletions

File tree

rust/operator-binary/src/controller/build/resource/daemonset.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,3 +837,175 @@ fn build_prepare_start_command(merged_config: &OpaConfig, container_name: &str)
837837

838838
prepare_container_args
839839
}
840+
841+
#[cfg(test)]
842+
mod tests {
843+
use serde_json::json;
844+
use stackable_operator::{
845+
commons::networking::DomainName, k8s_openapi::api::core::v1::ServiceAccount,
846+
k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta,
847+
};
848+
849+
use super::*;
850+
use crate::{
851+
controller::build::properties::test_support::validated_cluster_from_spec, crd::OpaRole,
852+
};
853+
854+
fn cluster_info() -> KubernetesClusterInfo {
855+
KubernetesClusterInfo {
856+
cluster_domain: DomainName::try_from("cluster.local").unwrap(),
857+
}
858+
}
859+
860+
fn service_account() -> ServiceAccount {
861+
ServiceAccount {
862+
metadata: ObjectMeta {
863+
name: Some("test-opa-serviceaccount".to_owned()),
864+
..Default::default()
865+
},
866+
..Default::default()
867+
}
868+
}
869+
870+
fn build(cluster: &ValidatedCluster) -> DaemonSet {
871+
let (role_group_name, role_group) = cluster.role_group_configs[&OpaRole::Server]
872+
.iter()
873+
.next()
874+
.expect("the default role group should exist");
875+
build_server_rolegroup_daemonset(
876+
cluster,
877+
role_group_name,
878+
role_group,
879+
"bundle-builder-image",
880+
"user-info-fetcher-image",
881+
&service_account(),
882+
&cluster_info(),
883+
)
884+
.expect("the daemonset should build")
885+
}
886+
887+
fn container_names(ds: &DaemonSet) -> Vec<String> {
888+
ds.spec
889+
.as_ref()
890+
.unwrap()
891+
.template
892+
.spec
893+
.as_ref()
894+
.unwrap()
895+
.containers
896+
.iter()
897+
.map(|c| c.name.clone())
898+
.collect()
899+
}
900+
901+
fn volume_names(ds: &DaemonSet) -> Vec<String> {
902+
ds.spec
903+
.as_ref()
904+
.unwrap()
905+
.template
906+
.spec
907+
.as_ref()
908+
.unwrap()
909+
.volumes
910+
.as_ref()
911+
.unwrap()
912+
.iter()
913+
.map(|v| v.name.clone())
914+
.collect()
915+
}
916+
917+
#[test]
918+
fn daemonset_has_expected_name_and_rolling_update_strategy() {
919+
let ds = build(&validated_cluster_from_spec(json!({
920+
"image": { "productVersion": "1.2.3" },
921+
"servers": { "roleGroups": { "default": {} } },
922+
})));
923+
924+
assert_eq!(ds.metadata.name.as_deref(), Some("test-opa-server-default"));
925+
let strategy = ds.spec.as_ref().unwrap().update_strategy.as_ref().unwrap();
926+
assert_eq!(strategy.type_.as_deref(), Some("RollingUpdate"));
927+
let rolling_update = strategy.rolling_update.as_ref().unwrap();
928+
// A DaemonSet must never take an OPA pod down before the replacement is ready.
929+
assert_eq!(rolling_update.max_unavailable, Some(IntOrString::Int(0)));
930+
}
931+
932+
#[test]
933+
fn daemonset_runs_opa_and_bundle_builder_with_prepare_init_container() {
934+
let ds = build(&validated_cluster_from_spec(json!({
935+
"image": { "productVersion": "1.2.3" },
936+
"servers": { "roleGroups": { "default": {} } },
937+
})));
938+
939+
let containers = container_names(&ds);
940+
assert!(containers.contains(&"opa".to_owned()));
941+
assert!(containers.contains(&"bundle-builder".to_owned()));
942+
// No sidecars without the corresponding cluster config.
943+
assert!(!containers.contains(&"user-info-fetcher".to_owned()));
944+
assert!(!containers.contains(&"vector".to_owned()));
945+
946+
let pod_spec = ds.spec.as_ref().unwrap().template.spec.as_ref().unwrap();
947+
let init_containers: Vec<_> = pod_spec
948+
.init_containers
949+
.as_ref()
950+
.unwrap()
951+
.iter()
952+
.map(|c| c.name.clone())
953+
.collect();
954+
assert_eq!(init_containers, vec!["prepare".to_owned()]);
955+
956+
// The standard volumes are always present; the TLS volume is not (no TLS configured).
957+
let volumes = volume_names(&ds);
958+
for expected in ["config", "bundles", "log"] {
959+
assert!(
960+
volumes.contains(&expected.to_owned()),
961+
"missing volume {expected}"
962+
);
963+
}
964+
assert!(!volumes.contains(&"tls".to_owned()));
965+
}
966+
967+
#[test]
968+
fn daemonset_adds_vector_container_when_agent_enabled() {
969+
let ds = build(&validated_cluster_from_spec(json!({
970+
"image": { "productVersion": "1.2.3" },
971+
"clusterConfig": { "vectorAggregatorConfigMapName": "vector-aggregator-discovery" },
972+
"servers": {
973+
"config": { "logging": { "enableVectorAgent": true } },
974+
"roleGroups": { "default": {} },
975+
},
976+
})));
977+
978+
assert!(container_names(&ds).contains(&"vector".to_owned()));
979+
}
980+
981+
#[test]
982+
fn daemonset_adds_user_info_fetcher_container_when_configured() {
983+
let ds = build(&validated_cluster_from_spec(json!({
984+
"image": { "productVersion": "1.2.3" },
985+
"clusterConfig": {
986+
"userInfo": {
987+
"backend": {
988+
"experimentalXfscAas": {
989+
"hostname": "aas.default.svc.cluster.local",
990+
"port": 5000,
991+
}
992+
}
993+
}
994+
},
995+
"servers": { "roleGroups": { "default": {} } },
996+
})));
997+
998+
assert!(container_names(&ds).contains(&"user-info-fetcher".to_owned()));
999+
}
1000+
1001+
#[test]
1002+
fn daemonset_adds_tls_volume_when_tls_enabled() {
1003+
let ds = build(&validated_cluster_from_spec(json!({
1004+
"image": { "productVersion": "1.2.3" },
1005+
"clusterConfig": { "tls": { "serverSecretClass": "tls" } },
1006+
"servers": { "roleGroups": { "default": {} } },
1007+
})));
1008+
1009+
assert!(volume_names(&ds).contains(&"tls".to_owned()));
1010+
}
1011+
}

rust/operator-binary/src/controller/build/resource/service.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,3 +204,112 @@ fn prometheus_annotations(tls_enabled: bool) -> Annotations {
204204
])
205205
.expect("should be valid annotations")
206206
}
207+
208+
#[cfg(test)]
209+
mod tests {
210+
use serde_json::json;
211+
212+
use super::*;
213+
use crate::{
214+
controller::build::properties::test_support::validated_cluster_from_spec, crd::OpaRole,
215+
};
216+
217+
const ROLE_GROUP_LABEL: &str = "app.kubernetes.io/role-group";
218+
219+
fn cluster(tls: bool) -> ValidatedCluster {
220+
let mut spec = json!({
221+
"image": { "productVersion": "1.2.3" },
222+
"servers": { "roleGroups": { "default": {} } },
223+
});
224+
if tls {
225+
spec["clusterConfig"] = json!({ "tls": { "serverSecretClass": "tls" } });
226+
}
227+
validated_cluster_from_spec(spec)
228+
}
229+
230+
fn default_role_group(cluster: &ValidatedCluster) -> RoleGroupName {
231+
cluster.role_group_configs[&OpaRole::Server]
232+
.keys()
233+
.next()
234+
.expect("the default role group should exist")
235+
.clone()
236+
}
237+
238+
/// Returns the `(name, port)` of the single data port of `service`.
239+
fn single_port(service: &Service) -> (String, i32) {
240+
let ports = service.spec.as_ref().unwrap().ports.as_ref().unwrap();
241+
assert_eq!(ports.len(), 1, "expected exactly one port");
242+
(ports[0].name.clone().unwrap(), ports[0].port)
243+
}
244+
245+
#[test]
246+
fn role_service_is_cluster_internal_with_node_local_traffic() {
247+
let cluster = cluster(false);
248+
let service = build_server_role_service(&cluster);
249+
let spec = service.spec.unwrap();
250+
251+
assert_eq!(service.metadata.name.as_deref(), Some("test-opa-server"));
252+
// Default listener class `cluster-internal` maps to a `ClusterIP` Service.
253+
assert_eq!(spec.type_.as_deref(), Some("ClusterIP"));
254+
assert_eq!(spec.internal_traffic_policy.as_deref(), Some("Local"));
255+
// The role-level service selects the whole role, so it must not pin a role group.
256+
assert!(!spec.selector.unwrap().contains_key(ROLE_GROUP_LABEL));
257+
}
258+
259+
#[test]
260+
fn role_service_port_follows_tls() {
261+
assert_eq!(
262+
single_port(&build_server_role_service(&cluster(false))),
263+
("http".to_owned(), 8081)
264+
);
265+
assert_eq!(
266+
single_port(&build_server_role_service(&cluster(true))),
267+
("https".to_owned(), 8443)
268+
);
269+
}
270+
271+
#[test]
272+
fn headless_service_is_headless_and_role_group_scoped() {
273+
let cluster = cluster(false);
274+
let rg = default_role_group(&cluster);
275+
let service = build_rolegroup_headless_service(&cluster, &rg);
276+
let spec = service.spec.unwrap();
277+
278+
assert_eq!(
279+
service.metadata.name.as_deref(),
280+
Some("test-opa-server-default-headless")
281+
);
282+
assert_eq!(spec.cluster_ip.as_deref(), Some("None"));
283+
assert_eq!(spec.publish_not_ready_addresses, Some(true));
284+
assert!(spec.selector.unwrap().contains_key(ROLE_GROUP_LABEL));
285+
}
286+
287+
#[test]
288+
fn metrics_service_has_prometheus_metadata() {
289+
let cluster = cluster(false);
290+
let rg = default_role_group(&cluster);
291+
let service = build_rolegroup_metrics_service(&cluster, &rg);
292+
let spec = service.spec.as_ref().unwrap();
293+
294+
assert_eq!(
295+
service.metadata.name.as_deref(),
296+
Some("test-opa-server-default-metrics")
297+
);
298+
assert_eq!(spec.cluster_ip.as_deref(), Some("None"));
299+
assert_eq!(single_port(&service), ("metrics".to_owned(), 8081));
300+
301+
let annotations = service.metadata.annotations.unwrap();
302+
assert_eq!(
303+
annotations.get("prometheus.io/scrape").map(String::as_str),
304+
Some("true")
305+
);
306+
assert_eq!(
307+
annotations.get("prometheus.io/scheme").map(String::as_str),
308+
Some("http")
309+
);
310+
assert_eq!(
311+
annotations.get("prometheus.io/port").map(String::as_str),
312+
Some("8081")
313+
);
314+
}
315+
}

rust/operator-binary/src/controller/validate.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,57 @@ pub fn validate(
232232
role_group_configs,
233233
))
234234
}
235+
236+
#[cfg(test)]
237+
mod tests {
238+
use stackable_operator::product_logging::spec::{
239+
AutomaticContainerLogConfig, ContainerLogConfig, ContainerLogConfigChoice,
240+
};
241+
242+
use super::*;
243+
244+
/// A [`Logging`] with a single (automatic) Vector container log config, as the build step
245+
/// expects when the Vector agent is enabled.
246+
fn logging(enable_vector_agent: bool) -> Logging<Container> {
247+
Logging {
248+
enable_vector_agent,
249+
containers: [(
250+
Container::Vector,
251+
ContainerLogConfig {
252+
choice: Some(ContainerLogConfigChoice::Automatic(
253+
AutomaticContainerLogConfig::default(),
254+
)),
255+
},
256+
)]
257+
.into(),
258+
}
259+
}
260+
261+
#[test]
262+
fn validate_logging_disabled_has_no_vector_container() {
263+
let validated = validate_logging(&logging(false), &None).expect("should validate");
264+
assert!(!validated.enable_vector_agent);
265+
assert!(validated.vector_container.is_none());
266+
}
267+
268+
#[test]
269+
fn validate_logging_enabled_requires_aggregator_config_map() {
270+
assert!(matches!(
271+
validate_logging(&logging(true), &None),
272+
Err(Error::MissingVectorAggregatorConfigMapName)
273+
));
274+
}
275+
276+
#[test]
277+
fn validate_logging_enabled_with_aggregator_yields_vector_container() {
278+
let aggregator =
279+
Some(ConfigMapName::from_str("vector-aggregator-discovery").expect("valid name"));
280+
let validated = validate_logging(&logging(true), &aggregator).expect("should validate");
281+
assert!(validated.enable_vector_agent);
282+
let vector = validated.vector_container.expect("vector container config");
283+
assert_eq!(
284+
vector.vector_aggregator_config_map_name.as_ref(),
285+
"vector-aggregator-discovery"
286+
);
287+
}
288+
}

0 commit comments

Comments
 (0)