Skip to content

Commit b45ed81

Browse files
committed
improve tests: placement and thoroughness
1 parent 06c9f7e commit b45ed81

3 files changed

Lines changed: 193 additions & 113 deletions

File tree

rust/operator-binary/src/controller/build/mod.rs

Lines changed: 7 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -345,15 +345,10 @@ fn role_data_ports(role: &HdfsNodeRole, https_enabled: bool) -> Vec<(String, Por
345345

346346
#[cfg(test)]
347347
mod tests {
348-
use std::collections::BTreeMap;
349-
350348
use stackable_operator::kube::Resource;
351349

352350
use super::build;
353-
use crate::{
354-
controller::build::properties::test_support::{self, cluster_info, validated_cluster},
355-
test_support::deserialize_and_validate_cluster,
356-
};
351+
use crate::controller::build::properties::test_support::{cluster_info, validated_cluster};
357352

358353
/// The sorted `metadata.name`s of a resource collection.
359354
fn sorted_names(resources: &[impl Resource]) -> Vec<String> {
@@ -365,61 +360,6 @@ mod tests {
365360
names
366361
}
367362

368-
/// Every metrics Service must carry the Prometheus scrape label and the
369-
/// `prometheus.io/path|port|scheme|scrape` annotations, or Prometheus stops discovering the
370-
/// endpoints (caught by the HDFS smoke test 2026-07-23 after the labels migration dropped
371-
/// them).
372-
#[test]
373-
fn metrics_services_carry_prometheus_label_and_annotations() {
374-
let cluster = validated_cluster();
375-
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
376-
377-
let metrics_services: Vec<_> = resources
378-
.services
379-
.iter()
380-
.filter(|service| {
381-
service
382-
.metadata
383-
.name
384-
.as_deref()
385-
.is_some_and(|name| name.ends_with("-metrics"))
386-
})
387-
.collect();
388-
assert!(!metrics_services.is_empty(), "no metrics Services built");
389-
390-
for service in metrics_services {
391-
let name = service.metadata.name.as_deref().unwrap_or_default();
392-
let labels = service.metadata.labels.as_ref().expect("labels are set");
393-
assert_eq!(
394-
labels.get("prometheus.io/scrape").map(String::as_str),
395-
Some("true"),
396-
"{name} lacks the scrape label"
397-
);
398-
399-
// The native metrics port of the role, as asserted by the smoke test.
400-
let expected_port = match name {
401-
n if n.contains("-namenode-") => "9870",
402-
n if n.contains("-datanode-") => "9864",
403-
n if n.contains("-journalnode-") => "8480",
404-
other => panic!("unexpected metrics Service {other}"),
405-
};
406-
let expected_annotations = BTreeMap::from(
407-
[
408-
("prometheus.io/path", "/prom"),
409-
("prometheus.io/port", expected_port),
410-
("prometheus.io/scheme", "http"),
411-
("prometheus.io/scrape", "true"),
412-
]
413-
.map(|(key, value)| (key.to_string(), value.to_string())),
414-
);
415-
assert_eq!(
416-
service.metadata.annotations.as_ref(),
417-
Some(&expected_annotations),
418-
"{name} annotations mismatch"
419-
);
420-
}
421-
}
422-
423363
/// The aggregator emits, for the minimal three-role cluster (one `default` role group each):
424364
/// one StatefulSet and one ConfigMap per role group, one headless plus one metrics Service per
425365
/// role group, and one default PDB per role.
@@ -462,6 +402,12 @@ mod tests {
462402
sorted_names(&resources.pod_disruption_budgets),
463403
["hdfs-datanode", "hdfs-journalnode", "hdfs-namenode"]
464404
);
405+
// The cluster-shared RBAC pair.
406+
assert_eq!(
407+
sorted_names(&resources.service_accounts),
408+
["hdfs-serviceaccount"]
409+
);
410+
assert_eq!(sorted_names(&resources.role_bindings), ["hdfs-rolebinding"]);
465411
}
466412

467413
/// Every StatefulSet's (immutable) `serviceName` must reference a headless Service that the
@@ -486,56 +432,4 @@ mod tests {
486432
);
487433
}
488434
}
489-
490-
/// Locks the RBAC resource names, the roleRef, and the recommended label set against
491-
/// accidental drift. The cluster name deliberately differs from the product name so that
492-
/// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named
493-
/// `hdfs`, which would mask exactly that swap).
494-
#[test]
495-
fn build_produces_rbac() {
496-
let cluster = deserialize_and_validate_cluster(
497-
&test_support::MINIMAL_HDFS_YAML.replace("name: hdfs", "name: my-hdfs"),
498-
);
499-
let resources = build(&cluster, &cluster_info()).expect("build succeeds");
500-
501-
assert_eq!(
502-
sorted_names(&resources.service_accounts),
503-
["my-hdfs-serviceaccount"]
504-
);
505-
assert_eq!(
506-
sorted_names(&resources.role_bindings),
507-
["my-hdfs-rolebinding"]
508-
);
509-
510-
let expected_labels = BTreeMap::from(
511-
[
512-
("app.kubernetes.io/component", "none"),
513-
("app.kubernetes.io/instance", "my-hdfs"),
514-
(
515-
"app.kubernetes.io/managed-by",
516-
"hdfs.stackable.tech_hdfs-operator-hdfs-controller",
517-
),
518-
("app.kubernetes.io/name", "hdfs"),
519-
("app.kubernetes.io/role-group", "none"),
520-
("app.kubernetes.io/version", "3.4.0-stackable0.0.0-dev"),
521-
("stackable.tech/vendor", "Stackable"),
522-
]
523-
.map(|(key, value)| (key.to_string(), value.to_string())),
524-
);
525-
let service_account = resources
526-
.service_accounts
527-
.first()
528-
.expect("a ServiceAccount is built");
529-
assert_eq!(
530-
service_account.metadata.labels,
531-
Some(expected_labels.clone())
532-
);
533-
534-
let role_binding = resources
535-
.role_bindings
536-
.first()
537-
.expect("a RoleBinding is built");
538-
assert_eq!(role_binding.metadata.labels, Some(expected_labels));
539-
assert_eq!(role_binding.role_ref.name, "hdfs-clusterrole");
540-
}
541435
}

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,103 @@ pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding {
4040
fn rbac_labels(cluster: &ValidatedCluster) -> Labels {
4141
cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME)
4242
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use serde_json::json;
47+
48+
use super::*;
49+
use crate::{
50+
controller::build::properties::test_support::MINIMAL_HDFS_YAML,
51+
test_support::deserialize_and_validate_cluster,
52+
};
53+
54+
/// The cluster name is deliberately different from the product name (`hdfs`) so that
55+
/// swapped `name`/`instance` label values cannot pass unnoticed.
56+
fn swap_guard_cluster() -> crate::controller::ValidatedCluster {
57+
deserialize_and_validate_cluster(&MINIMAL_HDFS_YAML.replace("name: hdfs", "name: my-hdfs"))
58+
}
59+
60+
#[test]
61+
fn test_service_account() {
62+
let service_account = build_service_account(&swap_guard_cluster());
63+
64+
assert_eq!(
65+
json!({
66+
"apiVersion": "v1",
67+
"kind": "ServiceAccount",
68+
"metadata": {
69+
// The RBAC resources are cluster-shared, so role and role group are `none`.
70+
"labels": {
71+
"app.kubernetes.io/component": "none",
72+
"app.kubernetes.io/instance": "my-hdfs",
73+
"app.kubernetes.io/managed-by": "hdfs.stackable.tech_hdfs-operator-hdfs-controller",
74+
"app.kubernetes.io/name": "hdfs",
75+
"app.kubernetes.io/role-group": "none",
76+
"app.kubernetes.io/version": "3.4.0-stackable0.0.0-dev",
77+
"stackable.tech/vendor": "Stackable"
78+
},
79+
"name": "my-hdfs-serviceaccount",
80+
"namespace": "default",
81+
"ownerReferences": [
82+
{
83+
"apiVersion": "hdfs.stackable.tech/v1alpha1",
84+
"controller": true,
85+
"kind": "HdfsCluster",
86+
"name": "my-hdfs",
87+
"uid": "c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f"
88+
}
89+
]
90+
}
91+
}),
92+
serde_json::to_value(service_account).expect("must be serializable")
93+
);
94+
}
95+
96+
#[test]
97+
fn test_role_binding() {
98+
let role_binding = build_role_binding(&swap_guard_cluster());
99+
100+
assert_eq!(
101+
json!({
102+
"apiVersion": "rbac.authorization.k8s.io/v1",
103+
"kind": "RoleBinding",
104+
"metadata": {
105+
"labels": {
106+
"app.kubernetes.io/component": "none",
107+
"app.kubernetes.io/instance": "my-hdfs",
108+
"app.kubernetes.io/managed-by": "hdfs.stackable.tech_hdfs-operator-hdfs-controller",
109+
"app.kubernetes.io/name": "hdfs",
110+
"app.kubernetes.io/role-group": "none",
111+
"app.kubernetes.io/version": "3.4.0-stackable0.0.0-dev",
112+
"stackable.tech/vendor": "Stackable"
113+
},
114+
"name": "my-hdfs-rolebinding",
115+
"namespace": "default",
116+
"ownerReferences": [
117+
{
118+
"apiVersion": "hdfs.stackable.tech/v1alpha1",
119+
"controller": true,
120+
"kind": "HdfsCluster",
121+
"name": "my-hdfs",
122+
"uid": "c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f"
123+
}
124+
]
125+
},
126+
"roleRef": {
127+
"apiGroup": "rbac.authorization.k8s.io",
128+
"kind": "ClusterRole",
129+
"name": "hdfs-clusterrole"
130+
},
131+
"subjects": [
132+
{
133+
"kind": "ServiceAccount",
134+
"name": "my-hdfs-serviceaccount",
135+
"namespace": "default"
136+
}
137+
]
138+
}),
139+
serde_json::to_value(role_binding).expect("must be serializable")
140+
);
141+
}
142+
}

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,89 @@ pub(crate) fn rolegroup_metrics_service(
130130
status: None,
131131
})
132132
}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use serde_json::json;
137+
use stackable_operator::v2::types::operator::RoleGroupName;
138+
139+
use super::*;
140+
use crate::{
141+
controller::build::properties::test_support::validated_cluster, crd::HdfsNodeRole,
142+
};
143+
144+
#[test]
145+
fn test_rolegroup_metrics_service() {
146+
let cluster = validated_cluster();
147+
let role = &HdfsNodeRole::Name;
148+
let role_group_name: RoleGroupName = "default".parse().expect("valid role group name");
149+
150+
let service =
151+
rolegroup_metrics_service(&cluster, role, &role_group_name).expect("should not fail");
152+
153+
assert_eq!(
154+
json!({
155+
"apiVersion": "v1",
156+
"kind": "Service",
157+
"metadata": {
158+
// Every metrics Service must carry the Prometheus scrape label and the
159+
// `prometheus.io/path|port|scheme|scrape` annotations, or Prometheus stops
160+
// discovering the endpoints.
161+
"annotations": {
162+
"prometheus.io/path": "/prom",
163+
"prometheus.io/port": "9870",
164+
"prometheus.io/scheme": "http",
165+
"prometheus.io/scrape": "true"
166+
},
167+
"labels": {
168+
"app.kubernetes.io/component": "namenode",
169+
"app.kubernetes.io/instance": "hdfs",
170+
"app.kubernetes.io/managed-by": "hdfs.stackable.tech_hdfs-operator-hdfs-controller",
171+
"app.kubernetes.io/name": "hdfs",
172+
"app.kubernetes.io/role-group": "default",
173+
"app.kubernetes.io/version": "3.4.0-stackable0.0.0-dev",
174+
"prometheus.io/scrape": "true",
175+
"stackable.tech/vendor": "Stackable"
176+
},
177+
"name": "hdfs-namenode-default-metrics",
178+
"namespace": "default",
179+
"ownerReferences": [
180+
{
181+
"apiVersion": "hdfs.stackable.tech/v1alpha1",
182+
"controller": true,
183+
"kind": "HdfsCluster",
184+
"name": "hdfs",
185+
"uid": "c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f"
186+
}
187+
]
188+
},
189+
"spec": {
190+
"clusterIP": "None",
191+
"ports": [
192+
{
193+
"name": "metrics",
194+
"port": 9870,
195+
"protocol": "TCP"
196+
},
197+
{
198+
"name": "jmx-metrics",
199+
"port": 8183,
200+
"protocol": "TCP"
201+
}
202+
],
203+
"publishNotReadyAddresses": true,
204+
"selector": {
205+
"app.kubernetes.io/component": "namenode",
206+
"app.kubernetes.io/instance": "hdfs",
207+
"app.kubernetes.io/name": "hdfs",
208+
"app.kubernetes.io/role-group": "default",
209+
"group": "default",
210+
"role": "namenode"
211+
},
212+
"type": "ClusterIP"
213+
}
214+
}),
215+
serde_json::to_value(service).expect("must be serializable")
216+
);
217+
}
218+
}

0 commit comments

Comments
 (0)