refactor: Build RBAC with recommended labels#806
Conversation
3327a68 to
1c5b9cd
Compare
|
| /// Every metrics Service must carry the Prometheus scrape label and the | ||
| /// `prometheus.io/path|port|scheme|scrape` annotations, or Prometheus stops discovering the | ||
| /// endpoints (caught by the HDFS smoke test 2026-07-23 after the labels migration dropped | ||
| /// them). | ||
| #[test] | ||
| fn metrics_services_carry_prometheus_label_and_annotations() { | ||
| let cluster = validated_cluster(); | ||
| let resources = build(&cluster, &cluster_info()).expect("build succeeds"); | ||
|
|
||
| let metrics_services: Vec<_> = resources | ||
| .services | ||
| .iter() | ||
| .filter(|service| { | ||
| service | ||
| .metadata | ||
| .name | ||
| .as_deref() | ||
| .is_some_and(|name| name.ends_with("-metrics")) | ||
| }) | ||
| .collect(); | ||
| assert!(!metrics_services.is_empty(), "no metrics Services built"); | ||
|
|
||
| for service in metrics_services { | ||
| let name = service.metadata.name.as_deref().unwrap_or_default(); | ||
| let labels = service.metadata.labels.as_ref().expect("labels are set"); | ||
| assert_eq!( | ||
| labels.get("prometheus.io/scrape").map(String::as_str), | ||
| Some("true"), | ||
| "{name} lacks the scrape label" | ||
| ); | ||
|
|
||
| // The native metrics port of the role, as asserted by the smoke test. | ||
| let expected_port = match name { | ||
| n if n.contains("-namenode-") => "9870", | ||
| n if n.contains("-datanode-") => "9864", | ||
| n if n.contains("-journalnode-") => "8480", | ||
| other => panic!("unexpected metrics Service {other}"), | ||
| }; | ||
| let expected_annotations = BTreeMap::from( | ||
| [ | ||
| ("prometheus.io/path", "/prom"), | ||
| ("prometheus.io/port", expected_port), | ||
| ("prometheus.io/scheme", "http"), | ||
| ("prometheus.io/scrape", "true"), | ||
| ] | ||
| .map(|(key, value)| (key.to_string(), value.to_string())), | ||
| ); | ||
| assert_eq!( | ||
| service.metadata.annotations.as_ref(), | ||
| Some(&expected_annotations), | ||
| "{name} annotations mismatch" | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
In my opinion, no specific error cases should be tested in the unit tests. One problem is that you lose track of the test coverage. Which parts are not tested, and which are tested twice?
Instead, I would test each function with a sensible branch or path coverage. The given test case targets the function controller::build::resource::service::rolegroup_metrics_service. You
could define the following test case there:
#[cfg(test)]
mod tests {
use serde_json::json;
use stackable_operator::v2::types::operator::RoleGroupName;
use super::*;
use crate::{
controller::build::properties::test_support::validated_cluster, crd::HdfsNodeRole,
};
#[test]
fn test_rolegroup_metrics_service() {
let cluster = validated_cluster();
let role = &HdfsNodeRole::Name;
let role_group_name = &RoleGroupName::from_str_unsafe("default");
let service =
rolegroup_metrics_service(&cluster, role, role_group_name).expect("should not fail");
assert_eq!(
json!({
"apiVersion": "v1",
"kind": "Service",
"metadata": {
// Every metrics Service must carry the Prometheus scrape label and the
// `prometheus.io/path|port|scheme|scrape` annotations, or Prometheus stops
// discovering the endpoints.
"annotations": {
"prometheus.io/path": "/prom",
"prometheus.io/port": "9870",
"prometheus.io/scheme": "http",
"prometheus.io/scrape": "true"
},
"labels": {
"app.kubernetes.io/component": "namenode",
"app.kubernetes.io/instance": "hdfs",
"app.kubernetes.io/managed-by": "hdfs.stackable.tech_hdfs-operator-hdfs-controller",
"app.kubernetes.io/name": "hdfs",
"app.kubernetes.io/role-group": "default",
"app.kubernetes.io/version": "3.4.0-stackable0.0.0-dev",
"prometheus.io/scrape": "true",
"stackable.tech/vendor": "Stackable"
},
"name": "hdfs-namenode-default-metrics",
"namespace": "default",
"ownerReferences": [
{
"apiVersion": "hdfs.stackable.tech/v1alpha1",
"controller": true,
"kind": "HdfsCluster",
"name": "hdfs",
"uid": "c2c8c5c0-0b5a-4b1e-9f3e-1a2b3c4d5e6f"
}
]
},
"spec": {
"clusterIP": "None",
"ports": [
{
"name": "metrics",
"port": 9870,
"protocol": "TCP"
},
{
"name": "jmx-metrics",
"port": 8183,
"protocol": "TCP"
}
],
"publishNotReadyAddresses": true,
"selector": {
"app.kubernetes.io/component": "namenode",
"app.kubernetes.io/instance": "hdfs",
"app.kubernetes.io/name": "hdfs",
"app.kubernetes.io/role-group": "default",
"group": "default",
"role": "namenode"
},
"type": "ClusterIP"
}
}),
serde_json::to_value(service).expect("must be serializable")
);
}
}The code requires stackable-operator = { workspace = true, features = ["test-support"] } in the dev-dependencies of the rust/operator-binary/Cargo.toml.
There was a problem hiding this comment.
Yes, that's a fair point. That test was added when I discovered I had missed/dropped some elements on the refactor and broke the smoke test, more as a regression test than anything more thorough.
| /// one StatefulSet and one ConfigMap per role group, one headless plus one metrics Service per | ||
| /// role group, and one default PDB per role. | ||
| #[test] | ||
| fn build_produces_expected_resource_names() { |
There was a problem hiding this comment.
This test case is good. It checks that the build function returns all expected resources. It just checks the names and not the contents because it relies on the fact that the builder functions have been tested in their own unit tests.
Service accounts and role bindings were added to the returned resources. They must also be added here and the first half of the test case build_produces_rbac can be removed.
| /// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named | ||
| /// `hdfs`, which would mask exactly that swap). | ||
| #[test] | ||
| fn build_produces_rbac() { |
There was a problem hiding this comment.
The produced RBAC resources should actually be tested in the rbac module.
| let product_version = ProductVersion::from_str(&image.app_version_label_value) | ||
| .expect("the app version label value is a valid product version"); |
There was a problem hiding this comment.
What's the purpose of image.product_version if only image.app_version_label_value is used?
There was a problem hiding this comment.
image.product_version is used when upgrading e.g. https://github.com/stackabletech/hdfs-operator/blob/feat/smooth-operator/build-rbac/rust/operator-binary/src/hdfs_controller.rs#L292.
There was a problem hiding this comment.
Having the whole label there would cause problems.
|
|
||
| /// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, the resource `name`, an owner | ||
| /// reference back to this cluster, and the given recommended `labels`. | ||
| pub(crate) fn object_meta(&self, name: impl Into<String>, labels: Labels) -> ObjectMetaBuilder { |
There was a problem hiding this comment.
Functions which build Kubernetes resources or parts of them, should be located in the build step. To put it somewhat pointedly: If building parts of Kubernetes resources in ValidatedCluster is a good idea, then impl From<ValidatedCluster> for StatefulSet would be even better.
There was a problem hiding this comment.
Would you say the same applies to governing_service_name? (directly under object_meta)?
Description
Part of: stackabletech/issues#869
Definition of Done Checklist
Author
Reviewer
Acceptance
type/deprecationlabel & add to the deprecation scheduletype/experimentallabel & add to the experimental features tracker