diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f7b2ac..13a96410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,11 @@ All notable changes to this project will be documented in this file. - Internal operator refactoring: introduce a build() step in the reconciler that assembles all relevant Kubernetes resources before anything is applied ([#1053]). - Bump `stackable-operator` to 0.114.0 ([#1063]). +- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac` + functions and carry the full set of recommended labels ([#1060]). [#1053]: https://github.com/stackabletech/zookeeper-operator/pull/1053 +[#1060]: https://github.com/stackabletech/zookeeper-operator/pull/1060 [#1063]: https://github.com/stackabletech/zookeeper-operator/pull/1063 ## [26.7.0] - 2026-07-21 diff --git a/Cargo.toml b/Cargo.toml index 92483b1a..3153cb6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ tokio-zookeeper = "0.4" tracing = "0.1" [patch."https://github.com/stackabletech/operator-rs.git"] -# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "smooth-operator" } +# stackable-operator = { git = "https://github.com/stackabletech//operator-rs.git", branch = "main" } # stackable-operator = { path = "../operator-rs/crates/stackable-operator" } [patch.crates-io] diff --git a/rust/operator-binary/src/crd/mod.rs b/rust/operator-binary/src/crd/mod.rs index 890fde04..6c2417b6 100644 --- a/rust/operator-binary/src/crd/mod.rs +++ b/rust/operator-binary/src/crd/mod.rs @@ -31,6 +31,7 @@ use stackable_operator::{ kubernetes::{ ConfigMapName, ListenerClassName, ListenerName, NamespaceName, ServiceName, }, + operator::RoleName, }, }, versioned::versioned, @@ -335,6 +336,18 @@ pub enum ZookeeperRole { Server, } +impl From for RoleName { + fn from(value: ZookeeperRole) -> Self { + RoleName::from_str(&value.to_string()).expect("a ZookeeperRole is a valid role name") + } +} + +impl From<&ZookeeperRole> for RoleName { + fn from(value: &ZookeeperRole) -> Self { + RoleName::from_str(&value.to_string()).expect("a ZookeeperRole is a valid role name") + } +} + /// Reference to a single `Pod` that is a component of a [`v1alpha1::ZookeeperCluster`] /// /// Used for service discovery. diff --git a/rust/operator-binary/src/zk_controller.rs b/rust/operator-binary/src/zk_controller.rs index 4f81ffa6..d1c8e235 100644 --- a/rust/operator-binary/src/zk_controller.rs +++ b/rust/operator-binary/src/zk_controller.rs @@ -7,19 +7,18 @@ use snafu::{OptionExt, ResultExt, Snafu}; use stackable_operator::{ cli::OperatorEnvironmentOptions, cluster_resources::ClusterResourceApplyStrategy, - commons::rbac::build_rbac_resources, crd::listener::v1alpha1::Listener, k8s_openapi::api::{ apps::v1::StatefulSet, - core::v1::{ConfigMap, Service}, + core::v1::{ConfigMap, Service, ServiceAccount}, policy::v1::PodDisruptionBudget, + rbac::v1::RoleBinding, }, kube::{ api::DynamicObject, core::{DeserializeGuard, error_boundary}, runtime::controller, }, - kvp::LabelError, logging::controller::ReconcilerError, shared::time::Duration, status::condition::{ @@ -31,7 +30,7 @@ use stackable_operator::{ use strum::{EnumDiscriminants, IntoStaticStr}; use crate::{ - APP_NAME, OPERATOR_NAME, ObjectRef, + OPERATOR_NAME, ObjectRef, crd::v1alpha1, zk_controller::{ build::resource::discovery, @@ -106,29 +105,11 @@ pub enum Error { source: stackable_operator::client::Error, }, - #[snafu(display("failed to create RBAC service account"))] - ApplyServiceAccount { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to create RBAC role binding"))] - ApplyRoleBinding { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to build RBAC resources"))] - BuildRbacResources { - source: stackable_operator::commons::rbac::Error, - }, - #[snafu(display("failed to delete orphaned resources"))] DeleteOrphans { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to build label"))] - BuildLabel { source: LabelError }, - #[snafu(display("failed to build object meta data"))] ObjectMeta { source: stackable_operator::builder::meta::Error, @@ -154,11 +135,7 @@ impl ReconcilerError for Error { Error::BuildDiscoveryConfig { .. } => None, Error::ApplyDiscoveryConfig { .. } => None, Error::ApplyStatus { .. } => None, - Error::ApplyServiceAccount { .. } => None, - Error::ApplyRoleBinding { .. } => None, - Error::BuildRbacResources { .. } => None, Error::DeleteOrphans { .. } => None, - Error::BuildLabel { .. } => None, Error::ObjectMeta { .. } => None, } } @@ -173,6 +150,8 @@ pub struct KubernetesResources { pub listeners: Vec, pub config_maps: Vec, pub pod_disruption_budgets: Vec, + pub service_accounts: Vec, + pub role_bindings: Vec, } pub async fn reconcile_zk( @@ -209,30 +188,24 @@ pub async fn reconcile_zk( &validated_cluster.object_overrides, ); - let (rbac_sa, rbac_rolebinding) = build_rbac_resources( - zk, - APP_NAME, - cluster_resources - .get_required_labels() - .context(BuildLabelSnafu)?, - ) - .context(BuildRbacResourcesSnafu)?; - - cluster_resources - .add(client, rbac_sa) - .await - .context(ApplyServiceAccountSnafu)?; - - cluster_resources - .add(client, rbac_rolebinding) - .await - .context(ApplyRoleBindingSnafu)?; - let resources = build::build(&validated_cluster, &client.kubernetes_cluster_info) .context(BuildResourcesSnafu)?; let mut ss_cond_builder = StatefulSetConditionBuilder::default(); + for service_account in resources.service_accounts { + cluster_resources + .add(client, service_account) + .await + .context(ApplyResourceSnafu)?; + } + for role_binding in resources.role_bindings { + cluster_resources + .add(client, role_binding) + .await + .context(ApplyResourceSnafu)?; + } + for service in resources.services { cluster_resources .add(client, service) diff --git a/rust/operator-binary/src/zk_controller/build.rs b/rust/operator-binary/src/zk_controller/build.rs index 1c6dd520..d1b0d653 100644 --- a/rust/operator-binary/src/zk_controller/build.rs +++ b/rust/operator-binary/src/zk_controller/build.rs @@ -13,8 +13,7 @@ use std::str::FromStr; use snafu::{ResultExt, Snafu}; use stackable_operator::{ - utils::cluster_info::KubernetesClusterInfo, - v2::types::operator::{ProductVersion, RoleGroupName}, + utils::cluster_info::KubernetesClusterInfo, v2::types::operator::RoleGroupName, }; use crate::{ @@ -25,6 +24,7 @@ use crate::{ config_map, listener::build_role_listener, pdb::build_pdb, + rbac::{build_role_binding, build_service_account}, service::{ build_server_rolegroup_headless_service, build_server_rolegroup_metrics_service, }, @@ -40,11 +40,7 @@ stackable_operator::constant!(pub(crate) PLACEHOLDER_DISCOVERY_ROLE_GROUP: RoleG // Placeholder role-group name used for the recommended labels of the role-level `Listener` // (which is not tied to a single role group). -stackable_operator::constant!(pub(crate) PLACEHOLDER_LISTENER_ROLE_GROUP: RoleGroupName = "none"); - -// Placeholder product version used for labels on PVC templates, which cannot be modified once -// deployed. A constant value keeps the labels stable across version upgrades. -stackable_operator::constant!(pub(crate) UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none"); +stackable_operator::constant!(pub(crate) NONE_ROLE_GROUP_NAME: RoleGroupName = "none"); pub mod command; pub mod graceful_shutdown; @@ -137,11 +133,15 @@ pub fn build( listeners, config_maps, pod_disruption_budgets, + service_accounts: vec![build_service_account(cluster)], + role_bindings: vec![build_role_binding(cluster)], }) } #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use stackable_operator::kube::Resource; use super::build; @@ -216,4 +216,68 @@ mod tests { ["simple-zookeeper-server"] ); } + + /// Locks the RBAC resource names, the roleRef, and the recommended label set against + /// accidental drift. The fixture's cluster name deliberately differs from the product name so + /// that swapped `name`/`instance` label values cannot pass unnoticed. + #[test] + fn build_produces_rbac() { + let zookeeper_yaml = r#" + apiVersion: zookeeper.stackable.tech/v1alpha1 + kind: ZookeeperCluster + metadata: + name: simple-zookeeper + spec: + image: + productVersion: "3.9.5" + servers: + roleGroups: + default: + replicas: 1 + "#; + let zookeeper = minimal_zk(zookeeper_yaml); + let cluster = validated_cluster(&zookeeper); + + let resources = build(&cluster, &cluster_info()).expect("build succeeds"); + + assert_eq!( + sorted_names(&resources.service_accounts), + ["simple-zookeeper-serviceaccount"] + ); + assert_eq!( + sorted_names(&resources.role_bindings), + ["simple-zookeeper-rolebinding"] + ); + + let expected_labels = BTreeMap::from( + [ + ("app.kubernetes.io/component", "none"), + ("app.kubernetes.io/instance", "simple-zookeeper"), + ( + "app.kubernetes.io/managed-by", + "zookeeper.stackable.tech_zookeepercluster", + ), + ("app.kubernetes.io/name", "zookeeper"), + ("app.kubernetes.io/role-group", "none"), + ("app.kubernetes.io/version", "3.9.5-stackable0.0.0-dev"), + ("stackable.tech/vendor", "Stackable"), + ] + .map(|(key, value)| (key.to_string(), value.to_string())), + ); + let service_account = resources + .service_accounts + .first() + .expect("a ServiceAccount is built"); + assert_eq!( + service_account.metadata.labels, + Some(expected_labels.clone()) + ); + + let role_binding = resources + .role_bindings + .first() + .expect("a RoleBinding is built"); + assert_eq!(role_binding.metadata.labels, Some(expected_labels)); + assert_eq!(role_binding.role_ref.name, "zookeeper-clusterrole"); + } } diff --git a/rust/operator-binary/src/zk_controller/build/properties/zoo_cfg.rs b/rust/operator-binary/src/zk_controller/build/properties/zoo_cfg.rs index a679f178..b7230c1b 100644 --- a/rust/operator-binary/src/zk_controller/build/properties/zoo_cfg.rs +++ b/rust/operator-binary/src/zk_controller/build/properties/zoo_cfg.rs @@ -38,7 +38,7 @@ pub(crate) fn server_addresses( .into_iter() .flatten() { - let resource_names = cluster.resource_names(rg_name); + let resource_names = cluster.role_group_resource_names(rg_name); let headless_service_name = resource_names.headless_service_name(); let stateful_set_name = resource_names.stateful_set_name().to_string(); // An unset replica count (HPA-managed) predicts a single-server quorum entry, matching diff --git a/rust/operator-binary/src/zk_controller/build/resource/config_map.rs b/rust/operator-binary/src/zk_controller/build/resource/config_map.rs index 39364b7d..b108653e 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/config_map.rs @@ -100,7 +100,7 @@ pub fn build_server_rolegroup_config_map( cluster .object_meta( cluster - .resource_names(role_group_name) + .role_group_resource_names(role_group_name) .role_group_config_map() .to_string(), role_group_name, diff --git a/rust/operator-binary/src/zk_controller/build/resource/discovery.rs b/rust/operator-binary/src/zk_controller/build/resource/discovery.rs index 550bbf10..d1742478 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/discovery.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/discovery.rs @@ -15,7 +15,7 @@ use stackable_operator::{ }; use crate::{ - crd::{ZOOKEEPER_SERVER_PORT_NAME, security::ZookeeperSecurity}, + crd::{ZOOKEEPER_SERVER_PORT_NAME, ZookeeperRole, security::ZookeeperSecurity}, zk_controller::{ build::PLACEHOLDER_DISCOVERY_ROLE_GROUP, validate::{ValidatedCluster, operator_name, product_name}, @@ -150,7 +150,7 @@ fn build_discovery_configmap_for_owner( product_version, &operator_name(), &controller_name, - &ValidatedCluster::role_name(), + &ZookeeperRole::Server.into(), &role_group_name, )) .build(), diff --git a/rust/operator-binary/src/zk_controller/build/resource/listener.rs b/rust/operator-binary/src/zk_controller/build/resource/listener.rs index 61547533..0a35ddaf 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/listener.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/listener.rs @@ -6,7 +6,7 @@ use crate::{ crd::{ ZOOKEEPER_SERVER_PORT_NAME, ZookeeperRole, role_listener_name, security::ZookeeperSecurity, }, - zk_controller::{build::PLACEHOLDER_LISTENER_ROLE_GROUP, validate::ValidatedCluster}, + zk_controller::{build::NONE_ROLE_GROUP_NAME, validate::ValidatedCluster}, }; /// Builds the role-level [`Listener`](listener::v1alpha1::Listener) exposing the ZooKeeper servers. @@ -21,7 +21,7 @@ pub fn build_role_listener( metadata: cluster .object_meta( role_listener_name(cluster.name.as_ref(), zk_role), - &PLACEHOLDER_LISTENER_ROLE_GROUP, + &NONE_ROLE_GROUP_NAME, ) .build(), spec: listener::v1alpha1::ListenerSpec { diff --git a/rust/operator-binary/src/zk_controller/build/resource/mod.rs b/rust/operator-binary/src/zk_controller/build/resource/mod.rs index 48396a03..7f57f617 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/mod.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/mod.rs @@ -5,5 +5,6 @@ pub mod config_map; pub mod discovery; pub mod listener; pub mod pdb; +pub mod rbac; pub mod service; pub mod statefulset; diff --git a/rust/operator-binary/src/zk_controller/build/resource/pdb.rs b/rust/operator-binary/src/zk_controller/build/resource/pdb.rs index 6c8099e9..f50d951f 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/pdb.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/pdb.rs @@ -24,7 +24,7 @@ pub fn build_pdb( let pdb = pod_disruption_budget_builder_with_role( cluster, &product_name(), - &ValidatedCluster::role_name(), + &ZookeeperRole::Server.into(), &operator_name(), &controller_name(), ) diff --git a/rust/operator-binary/src/zk_controller/build/resource/rbac.rs b/rust/operator-binary/src/zk_controller/build/resource/rbac.rs new file mode 100644 index 00000000..7fa8db97 --- /dev/null +++ b/rust/operator-binary/src/zk_controller/build/resource/rbac.rs @@ -0,0 +1,42 @@ +//! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups. + +use std::str::FromStr; + +use stackable_operator::{ + k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding}, + kvp::Labels, + v2::{ + rbac, + types::operator::{RoleGroupName, RoleName}, + }, +}; + +use crate::zk_controller::validate::ValidatedCluster; + +stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none"); +stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none"); + +/// Builds the [`ServiceAccount`] that the role-group Pods run under. +pub fn build_service_account(cluster: &ValidatedCluster) -> ServiceAccount { + rbac::build_service_account( + cluster, + &cluster.cluster_resource_names(), + rbac_labels(cluster), + ) +} + +/// Builds the [`RoleBinding`] that binds the [`ServiceAccount`] from [`build_service_account`] to +/// the operator-deployed ClusterRole. +pub fn build_role_binding(cluster: &ValidatedCluster) -> RoleBinding { + rbac::build_role_binding( + cluster, + &cluster.cluster_resource_names(), + rbac_labels(cluster), + ) +} + +/// Both resources are shared by the whole cluster rather than tied to a role or role group, so +/// the recommended labels carry `none` for both values. +fn rbac_labels(cluster: &ValidatedCluster) -> Labels { + cluster.recommended_labels_for(&NONE_ROLE_NAME, &NONE_ROLE_GROUP_NAME) +} diff --git a/rust/operator-binary/src/zk_controller/build/resource/service.rs b/rust/operator-binary/src/zk_controller/build/resource/service.rs index 81ac8b92..e82f073f 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/service.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/service.rs @@ -25,7 +25,7 @@ pub(crate) fn build_server_rolegroup_headless_service( let metadata = cluster .object_meta( cluster - .resource_names(role_group_name) + .role_group_resource_names(role_group_name) .headless_service_name() .to_string(), role_group_name, @@ -72,7 +72,7 @@ pub(crate) fn build_server_rolegroup_metrics_service( let metadata = cluster .object_meta( cluster - .resource_names(role_group_name) + .role_group_resource_names(role_group_name) .metrics_service_name(), role_group_name, ) diff --git a/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs b/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs index 21c104ef..e99f8315 100644 --- a/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/zk_controller/build/resource/statefulset.rs @@ -57,7 +57,6 @@ use crate::{ zk_controller::{ LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME, build::{ - UNVERSIONED_PRODUCT_VERSION, command::create_init_container_command_args, graceful_shutdown::add_graceful_shutdown_config, jvm::{construct_non_heap_jvm_args, construct_zk_server_heap_env}, @@ -156,7 +155,7 @@ pub fn build_server_rolegroup_statefulset( rolegroup_config: &ZookeeperRoleGroupConfig, ) -> Result { let merged_config = &rolegroup_config.config; - let resource_names = cluster.resource_names(role_group_name); + let resource_names = cluster.role_group_resource_names(role_group_name); let resolved_product_image = &cluster.image; let zookeeper_security = &cluster.cluster_config.zookeeper_security; let metrics_port = cluster.metrics_http_port(rolegroup_config); @@ -194,8 +193,7 @@ pub fn build_server_rolegroup_statefulset( // Used for PVC templates that cannot be modified once they are deployed. A constant version // keeps the labels stable across version upgrades. - let unversioned_recommended_labels = - cluster.recommended_labels_for(&UNVERSIONED_PRODUCT_VERSION, role_group_name); + let unversioned_recommended_labels = cluster.unversioned_recommended_labels(role_group_name); let listener_pvc = build_role_listener_pvc( role_listener_name(cluster.name.as_ref(), &ZookeeperRole::Server).as_ref(), @@ -383,7 +381,7 @@ pub fn build_server_rolegroup_statefulset( fs_group: Some(1000), ..PodSecurityContext::default() }) - .service_account_name(cluster.rbac_service_account_name()); + .service_account_name(cluster.cluster_resource_names().service_account_name()); // Use the user-provided custom log ConfigMap if one is configured, otherwise fall back to the // rolegroup's own ConfigMap. This branches on the *validated* logging choice. diff --git a/rust/operator-binary/src/zk_controller/validate.rs b/rust/operator-binary/src/zk_controller/validate.rs index 37bac4ee..60b02b18 100644 --- a/rust/operator-binary/src/zk_controller/validate.rs +++ b/rust/operator-binary/src/zk_controller/validate.rs @@ -41,14 +41,9 @@ use stackable_operator::{ validate_logging_configuration_for_container, }, role_group_utils::ResourceNames, - role_utils::{ - JavaCommonConfig, ResourceNames as RbacResourceNames, RoleGroupConfig, - with_validated_config, - }, + role_utils::{self, JavaCommonConfig, RoleGroupConfig, with_validated_config}, types::{ - kubernetes::{ - ConfigMapName, ListenerClassName, NamespaceName, ServiceAccountName, Uid, - }, + kubernetes::{ConfigMapName, ListenerClassName, NamespaceName, Uid}, operator::{ ClusterName, ControllerName, OperatorName, ProductName, ProductVersion, RoleGroupName, RoleName, @@ -259,6 +254,10 @@ pub struct ValidatedCluster { pub object_overrides: ObjectOverrides, } +// Placeholder product version used for labels on PVC templates, which cannot be modified once +// deployed. A constant value keeps the labels stable across version upgrades. +stackable_operator::constant!(UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none"); + impl ValidatedCluster { #[allow(clippy::too_many_arguments)] pub fn new( @@ -296,41 +295,51 @@ impl ValidatedCluster { } } - /// The one ZooKeeper role name (`server`). - pub fn role_name() -> RoleName { - RoleName::from_str(&ZookeeperRole::Server.to_string()) - .expect("the server role name is a valid role name") - } - /// Type-safe names for the resources of a given role group. - pub(crate) fn resource_names(&self, role_group_name: &RoleGroupName) -> ResourceNames { + pub(crate) fn role_group_resource_names( + &self, + role_group_name: &RoleGroupName, + ) -> ResourceNames { ResourceNames { cluster_name: self.name.clone(), - role_name: Self::role_name(), + role_name: ZookeeperRole::Server.into(), role_group_name: role_group_name.clone(), } } - /// The RBAC ServiceAccount name for this cluster, `-serviceaccount`. - /// - /// Matches the name produced by - /// [`build_rbac_resources`](stackable_operator::commons::rbac::build_rbac_resources) so the - /// StatefulSet can reference the ServiceAccount without depending on the built object. - pub(crate) fn rbac_service_account_name(&self) -> ServiceAccountName { - RbacResourceNames { + /// Type-safe names for the per-cluster RBAC resources: the ServiceAccount shared by all + /// Pods, its (namespaced) RoleBinding, and the operator-deployed ClusterRole it binds. + pub fn cluster_resource_names(&self) -> role_utils::ResourceNames { + role_utils::ResourceNames { cluster_name: self.name.clone(), product_name: product_name(), } - .service_account_name() } - /// Recommended labels for a role-group resource, using the given product version. - /// - /// Used for PVC templates that cannot be modified once deployed: passing a constant version - /// (e.g. `none`) keeps those labels stable across product version upgrades. - pub(crate) fn recommended_labels_for( + pub fn recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_for(&ZookeeperRole::Server.into(), role_group_name) + } + + pub fn recommended_labels_for( + &self, + role_name: &RoleName, + role_group_name: &RoleGroupName, + ) -> Labels { + self.recommended_labels_with(&self.product_version, role_name, role_group_name) + } + + pub fn unversioned_recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_with( + &UNVERSIONED_PRODUCT_VERSION, + &ZookeeperRole::Server.into(), + role_group_name, + ) + } + + fn recommended_labels_with( &self, product_version: &ProductVersion, + role_name: &RoleName, role_group_name: &RoleGroupName, ) -> Labels { recommended_labels( @@ -339,19 +348,19 @@ impl ValidatedCluster { product_version, &operator_name(), &controller_name(), - &Self::role_name(), + role_name, role_group_name, ) } - /// Recommended labels for a role-group resource. - pub fn recommended_labels(&self, role_group_name: &RoleGroupName) -> Labels { - self.recommended_labels_for(&self.product_version, role_group_name) - } - /// Selector labels matching the pods of a role group. pub fn role_group_selector(&self, role_group_name: &RoleGroupName) -> Labels { - role_group_selector(self, &product_name(), &Self::role_name(), role_group_name) + role_group_selector( + self, + &product_name(), + &ZookeeperRole::Server.into(), + role_group_name, + ) } /// Returns an [`ObjectMetaBuilder`] pre-filled with the namespace, an owner reference back to @@ -739,4 +748,14 @@ mod tests { Some(Quantity("3Gi".to_owned())) ); } + + /// Locks the invariant behind the `expect` in the `From for RoleName` impls: + /// every `ZookeeperRole` variant (present and future) must serialise to a valid `RoleName`. + #[test] + fn every_zookeeper_role_serialises_to_a_valid_role_name() { + for role in ZookeeperRole::iter() { + let _: RoleName = (&role).into(); + let _: RoleName = role.into(); + } + } }