diff --git a/CHANGELOG.md b/CHANGELOG.md index 4696be96..e50d7b50 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 ([#985]). - Bump stackable-operator to 0.114.0 ([#994]). +- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac` + functions and carry the full set of recommended labels ([#990]). [#985]: https://github.com/stackabletech/kafka-operator/pull/985 +[#990]: https://github.com/stackabletech/kafka-operator/pull/990 [#994]: https://github.com/stackabletech/kafka-operator/pull/994 ## [26.7.0] - 2026-07-21 diff --git a/rust/operator-binary/src/controller.rs b/rust/operator-binary/src/controller.rs index 4162801d..3bbf7978 100644 --- a/rust/operator-binary/src/controller.rs +++ b/rust/operator-binary/src/controller.rs @@ -21,15 +21,17 @@ use stackable_operator::{ crd::listener, k8s_openapi::api::{ apps::v1::StatefulSet, - core::v1::{ConfigMap, Service}, + core::v1::{ConfigMap, Service, ServiceAccount}, policy::v1::PodDisruptionBudget, + rbac::v1::RoleBinding, }, kube::{ - Resource, ResourceExt, + Resource, api::{DynamicObject, ObjectMeta}, core::{DeserializeGuard, error_boundary}, runtime::{controller::Action, reflector::ObjectRef}, }, + kvp::Labels, logging::controller::ReconcilerError, shared::time::Duration, status::condition::{ @@ -39,7 +41,9 @@ use stackable_operator::{ v2::{ HasName, HasUid, NameIsValidLabelValue, cluster_resources::cluster_resources_new, + kvp::label::{recommended_labels, role_group_selector}, role_group_utils::ResourceNames, + role_utils, types::{ kubernetes::{ConfigMapName, ListenerName, NamespaceName, Uid}, operator::{ClusterName, ControllerName, OperatorName, ProductName, ProductVersion}, @@ -59,11 +63,7 @@ pub(crate) mod validate; pub use stackable_operator::v2::types::operator::{RoleGroupName, RoleName}; use crate::{ - controller::{ - build::resource::rbac::{build_rbac_role_binding, build_rbac_service_account}, - node_id_hasher::node_id_hash32_offset, - security::ValidatedKafkaSecurity, - }, + controller::{node_id_hasher::node_id_hash32_offset, security::ValidatedKafkaSecurity}, crd::{ APP_NAME, KafkaClusterStatus, KafkaPodDescriptor, MetadataManager, OPERATOR_NAME, authorization::KafkaAuthorizationConfig, @@ -75,6 +75,9 @@ use crate::{ pub const KAFKA_CONTROLLER_NAME: &str = "kafkacluster"; pub const KAFKA_FULL_CONTROLLER_NAME: &str = concatcp!(KAFKA_CONTROLLER_NAME, '.', OPERATOR_NAME); +// Placeholder version label value for resources whose labels must not change after deployment. +stackable_operator::constant!(UNVERSIONED_PRODUCT_VERSION: ProductVersion = "none"); + #[derive(Snafu, Debug)] pub enum PodDescriptorsError { #[snafu(display( @@ -99,6 +102,8 @@ pub struct KubernetesResources { pub listeners: Vec, pub config_maps: Vec, pub pod_disruption_budgets: Vec, + pub service_accounts: Vec, + pub role_bindings: Vec, } /// The validated cluster. Carries everything the build steps need, resolved once @@ -169,8 +174,7 @@ impl ValidatedCluster { /// node-id hash offsets must be unique across the whole cluster, so collisions are detected /// across all role groups regardless of `requested_kafka_role`. /// - /// Resource names reuse [`Self::resource_names`] (the canonical - /// `--` naming) so they stay in sync with the StatefulSet and + /// Resource names reuse resource names so they stay in sync with the StatefulSet and /// headless Service this descriptor refers to. pub fn pod_descriptors( &self, @@ -199,7 +203,7 @@ impl ValidatedCluster { seen_hashes.insert(node_id_hash_offset, (role.clone(), role_group_name.clone())); if requested_kafka_role.is_none() || Some(role) == requested_kafka_role { - let resource_names = self.resource_names(role, role_group_name); + let resource_names = self.role_group_resource_names(role, role_group_name); let role_group_statefulset_name = resource_names.stateful_set_name(); let role_group_service_name = resource_names.headless_service_name(); // Pods must be predicted from a concrete count (e.g. for KRaft quorum @@ -228,8 +232,17 @@ impl ValidatedCluster { RoleName::from_str(&role.to_string()).expect("a KafkaRole is a valid role name") } + /// 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(), + } + } + /// Type-safe names for the resources of a given role group. - pub(crate) fn resource_names( + pub(crate) fn role_group_resource_names( &self, role: &KafkaRole, role_group_name: &RoleGroupName, @@ -241,6 +254,61 @@ impl ValidatedCluster { } } + /// Recommended labels for a role-group resource. + pub fn recommended_labels(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + self.recommended_labels_for(&Self::role_name(role), role_group_name) + } + + /// Recommended labels for a resource that is not tied to a concrete [`KafkaRole`], using a free-form role/role-group label value. + 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) + } + + /// Recommended labels with the constant [`UNVERSIONED_PRODUCT_VERSION`], for PVC templates + /// that cannot be modified after deployment (keeps the labels stable across version upgrades). + pub fn unversioned_recommended_labels( + &self, + role: &KafkaRole, + role_group_name: &RoleGroupName, + ) -> Labels { + self.recommended_labels_with( + &UNVERSIONED_PRODUCT_VERSION, + &Self::role_name(role), + role_group_name, + ) + } + + fn recommended_labels_with( + &self, + product_version: &ProductVersion, + role_name: &RoleName, + role_group_name: &RoleGroupName, + ) -> Labels { + recommended_labels( + self, + &product_name(), + product_version, + &operator_name(), + &controller_name(), + role_name, + role_group_name, + ) + } + + /// Selector labels matching the pods of a role group. + pub fn role_group_selector(&self, role: &KafkaRole, role_group_name: &RoleGroupName) -> Labels { + role_group_selector( + self, + &product_name(), + &Self::role_name(role), + role_group_name, + ) + } + /// The name of the broker rolegroup's bootstrap [`Listener`](stackable_operator::crd::listener), /// `---bootstrap`. pub fn bootstrap_listener_name( @@ -250,7 +318,7 @@ impl ValidatedCluster { ) -> ListenerName { ListenerName::from_str(&format!( "{}-bootstrap", - self.resource_names(role, role_group_name) + self.role_group_resource_names(role, role_group_name) .stateful_set_name() )) .expect("the bootstrap listener name is a valid Listener name") @@ -423,27 +491,11 @@ pub enum Error { source: stackable_operator::cluster_resources::Error, }, - #[snafu(display("failed to patch service account"))] - ApplyServiceAccount { - source: stackable_operator::cluster_resources::Error, - }, - - #[snafu(display("failed to patch role binding"))] - ApplyRoleBinding { - source: stackable_operator::cluster_resources::Error, - }, - #[snafu(display("failed to update status"))] ApplyStatus { source: stackable_operator::client::Error, }, - #[snafu(display("failed to get required Labels"))] - GetRequiredLabels { - source: - stackable_operator::kvp::KeyValuePairError, - }, - #[snafu(display("KafkaCluster object is invalid"))] InvalidKafkaCluster { source: error_boundary::InvalidObject, @@ -465,10 +517,7 @@ impl ReconcilerError for Error { Error::BuildDiscoveryConfig { .. } => None, Error::ApplyDiscoveryConfig { .. } => None, Error::DeleteOrphans { .. } => None, - Error::ApplyServiceAccount { .. } => None, - Error::ApplyRoleBinding { .. } => None, Error::ApplyStatus { .. } => None, - Error::GetRequiredLabels { .. } => None, Error::InvalidKafkaCluster { .. } => None, } } @@ -519,34 +568,27 @@ pub async fn reconcile_kafka( let mut ss_cond_builder = StatefulSetConditionBuilder::default(); - let required_labels = cluster_resources - .get_required_labels() - .context(GetRequiredLabelsSnafu)?; - let rbac_sa = build_rbac_service_account(&validated_cluster, required_labels.clone()); - let rbac_rolebinding = build_rbac_role_binding(&validated_cluster, required_labels); - - let rbac_sa = cluster_resources - .add(client, rbac_sa.clone()) - .await - .context(ApplyServiceAccountSnafu)?; - // The ServiceAccount name is deterministic, so the statefulset builders only need the name, - // not the applied object. - let service_account_name = rbac_sa.name_any(); - cluster_resources - .add(client, rbac_rolebinding) - .await - .context(ApplyRoleBindingSnafu)?; - // Build every Kubernetes resource up front (client-free). The discovery ConfigMap is not part // of this, as it depends on the applied bootstrap Listeners' status (see below). - let resources = - build::build(&validated_cluster, &service_account_name).context(BuildResourcesSnafu)?; + let resources = build::build(&validated_cluster).context(BuildResourcesSnafu)?; // Apply order: Services, then Listeners (collecting the applied bootstrap Listeners for the // discovery ConfigMap), then ConfigMaps, then PodDisruptionBudgets, and finally the // StatefulSets. The StatefulSets must be applied after all ConfigMaps and Secrets they mount to // prevent unnecessary Pod restarts. // See https://github.com/stackabletech/commons-operator/issues/111 for details. + 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) @@ -680,8 +722,10 @@ pub(crate) mod test_support { mod tests { use std::collections::BTreeSet; + use strum::IntoEnumIterator; + use super::{ - PodDescriptorsError, + PodDescriptorsError, ValidatedCluster, test_support::{minimal_kafka, validated_cluster}, }; use crate::crd::role::KafkaRole; @@ -763,4 +807,13 @@ mod tests { let node_ids: BTreeSet = descriptors.iter().map(|d| d.node_id).collect(); assert_eq!(node_ids.len(), 3, "node ids must be unique: {node_ids:?}"); } + + /// Locks the invariant behind the `expect` in [`ValidatedCluster::role_name`]: every + /// `KafkaRole` variant (present and future) must serialise to a valid `RoleName`. + #[test] + fn every_kafka_role_serialises_to_a_valid_role_name() { + for role in KafkaRole::iter() { + ValidatedCluster::role_name(&role); + } + } } diff --git a/rust/operator-binary/src/controller/build/labels.rs b/rust/operator-binary/src/controller/build/labels.rs deleted file mode 100644 index db86b3cf..00000000 --- a/rust/operator-binary/src/controller/build/labels.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Recommended-label and selector construction for the KafkaCluster build step. -//! -//! These build Kubernetes labels/selectors from a [`ValidatedCluster`]. They live here rather -//! than as methods on [`ValidatedCluster`] so the validated model only exposes views on its -//! properties, not build-step helpers. - -use std::str::FromStr; - -use stackable_operator::{ - kvp::Labels, - v2::{kvp::label, types::operator::ProductVersion}, -}; - -use crate::{ - controller::{RoleGroupName, ValidatedCluster, controller_name, operator_name, product_name}, - crd::role::KafkaRole, -}; - -/// Recommended labels for a role-group resource, using the given product version. -fn recommended_labels_for( - cluster: &ValidatedCluster, - product_version: &ProductVersion, - role: &KafkaRole, - role_group_name: &RoleGroupName, -) -> Labels { - label::recommended_labels( - cluster, - &product_name(), - product_version, - &operator_name(), - &controller_name(), - &ValidatedCluster::role_name(role), - role_group_name, - ) -} - -/// Recommended labels for a role-group resource. -pub fn recommended_labels( - cluster: &ValidatedCluster, - role: &KafkaRole, - role_group_name: &RoleGroupName, -) -> Labels { - recommended_labels_for(cluster, &cluster.product_version, role, role_group_name) -} - -/// Recommended labels without a version, for PVC templates that cannot be modified once -/// deployed. -pub fn unversioned_recommended_labels( - cluster: &ValidatedCluster, - role: &KafkaRole, - role_group_name: &RoleGroupName, -) -> Labels { - // A version value is required, and we do want to use the "recommended" format for the - // other desired labels. - let none_version = ProductVersion::from_str("none").expect("'none' is a valid product version"); - recommended_labels_for(cluster, &none_version, role, role_group_name) -} - -/// Selector labels matching the pods of a role group. -pub fn role_group_selector( - cluster: &ValidatedCluster, - role: &KafkaRole, - role_group_name: &RoleGroupName, -) -> Labels { - label::role_group_selector( - cluster, - &product_name(), - &ValidatedCluster::role_name(role), - role_group_name, - ) -} diff --git a/rust/operator-binary/src/controller/build/mod.rs b/rust/operator-binary/src/controller/build/mod.rs index f33b2f30..47e0be99 100644 --- a/rust/operator-binary/src/controller/build/mod.rs +++ b/rust/operator-binary/src/controller/build/mod.rs @@ -13,6 +13,7 @@ use crate::{ config_map::build_rolegroup_config_map, listener::build_broker_rolegroup_bootstrap_listener, pdb::build_pdb, + rbac::{build_role_binding, build_service_account}, service::{build_rolegroup_headless_service, build_rolegroup_metrics_service}, statefulset::{ build_broker_rolegroup_statefulset, build_controller_rolegroup_statefulset, @@ -27,7 +28,6 @@ pub mod command; pub mod graceful_shutdown; pub mod jvm; pub mod kerberos; -pub mod labels; pub mod properties; pub mod resource; pub mod security; @@ -59,10 +59,7 @@ pub enum Error { /// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under. /// The RBAC resources are built and applied separately, in the reconcile step; the name is /// deterministic, so the build step does not depend on the applied `ServiceAccount`. -pub fn build( - cluster: &ValidatedCluster, - service_account_name: &str, -) -> Result { +pub fn build(cluster: &ValidatedCluster) -> Result { let mut stateful_sets = vec![]; let mut services = vec![]; let mut listeners = vec![]; @@ -118,19 +115,14 @@ pub fn build( ); let stateful_set = match role { - KafkaRole::Broker => build_broker_rolegroup_statefulset( - role, - role_group_name, - cluster, - validated_rg, - service_account_name, - ), + KafkaRole::Broker => { + build_broker_rolegroup_statefulset(role, role_group_name, cluster, validated_rg) + } KafkaRole::Controller => build_controller_rolegroup_statefulset( role, role_group_name, cluster, validated_rg, - service_account_name, ), } .context(StatefulSetSnafu { @@ -156,11 +148,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; @@ -236,7 +232,7 @@ mod tests { #[test] fn build_produces_expected_resource_names() { let cluster = kraft_cluster(); - let resources = build(&cluster, "simple-kafka-serviceaccount").expect("build succeeds"); + let resources = build(&cluster).expect("build succeeds"); // One StatefulSet per role group. assert_eq!( @@ -281,7 +277,7 @@ mod tests { #[test] fn build_zookeeper_mode_has_no_controller_resources() { let cluster = zookeeper_cluster(); - let resources = build(&cluster, "simple-kafka-serviceaccount").expect("build succeeds"); + let resources = build(&cluster).expect("build succeeds"); assert_eq!( sorted_names(&resources.stateful_sets), @@ -303,4 +299,53 @@ mod tests { ["simple-kafka-broker"] ); } + + /// 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 cluster = kraft_cluster(); + let resources = build(&cluster).expect("build succeeds"); + + assert_eq!( + sorted_names(&resources.service_accounts), + ["simple-kafka-serviceaccount"] + ); + assert_eq!( + sorted_names(&resources.role_bindings), + ["simple-kafka-rolebinding"] + ); + + let expected_labels = BTreeMap::from( + [ + ("app.kubernetes.io/component", "none"), + ("app.kubernetes.io/instance", "simple-kafka"), + ( + "app.kubernetes.io/managed-by", + "kafka.stackable.tech_kafkacluster", + ), + ("app.kubernetes.io/name", "kafka"), + ("app.kubernetes.io/role-group", "none"), + ("app.kubernetes.io/version", "3.9.2-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, "kafka-clusterrole"); + } } diff --git a/rust/operator-binary/src/controller/build/properties/listener.rs b/rust/operator-binary/src/controller/build/properties/listener.rs index d58cba74..3ba733bf 100644 --- a/rust/operator-binary/src/controller/build/properties/listener.rs +++ b/rust/operator-binary/src/controller/build/properties/listener.rs @@ -27,7 +27,7 @@ pub fn get_kafka_listener_config( role_group_name: &RoleGroupName, ) -> KafkaListenerConfig { let headless_service_name = validated_cluster - .resource_names(role, role_group_name) + .role_group_resource_names(role, role_group_name) .headless_service_name(); let pod_fqdn = pod_fqdn( &validated_cluster.namespace, @@ -241,7 +241,7 @@ mod tests { internal_host = pod_fqdn( &validated.namespace, validated - .resource_names(&KafkaRole::Broker, &role_group_name) + .role_group_resource_names(&KafkaRole::Broker, &role_group_name) .headless_service_name() .as_ref(), &cluster_info.cluster_domain @@ -303,7 +303,7 @@ mod tests { internal_host = pod_fqdn( &validated.namespace, validated - .resource_names(&KafkaRole::Broker, &role_group_name) + .role_group_resource_names(&KafkaRole::Broker, &role_group_name) .headless_service_name() .as_ref(), &cluster_info.cluster_domain @@ -366,7 +366,7 @@ mod tests { internal_host = pod_fqdn( &validated.namespace, validated - .resource_names(&KafkaRole::Broker, &role_group_name) + .role_group_resource_names(&KafkaRole::Broker, &role_group_name) .headless_service_name() .as_ref(), &cluster_info.cluster_domain @@ -466,7 +466,7 @@ mod tests { internal_host = pod_fqdn( &validated.namespace, validated - .resource_names(&KafkaRole::Broker, &role_group_name) + .role_group_resource_names(&KafkaRole::Broker, &role_group_name) .headless_service_name() .as_ref(), &cluster_info.cluster_domain diff --git a/rust/operator-binary/src/controller/build/resource/config_map.rs b/rust/operator-binary/src/controller/build/resource/config_map.rs index 6df8f2db..284a281b 100644 --- a/rust/operator-binary/src/controller/build/resource/config_map.rs +++ b/rust/operator-binary/src/controller/build/resource/config_map.rs @@ -14,7 +14,6 @@ use crate::{ controller::{ RoleGroupName, ValidatedCluster, ValidatedRoleGroupConfig, build::{ - labels, properties::{ ConfigFileName, config_file_name, product_logging::role_group_config_map_data, }, @@ -125,7 +124,7 @@ pub fn build_rolegroup_config_map( .name_and_namespace(validated_cluster) .name( validated_cluster - .resource_names(&role, role_group_name) + .role_group_resource_names(&role, role_group_name) .role_group_config_map() .to_string(), ) @@ -134,11 +133,7 @@ pub fn build_rolegroup_config_map( None, Some(true), )) - .with_labels(labels::recommended_labels( - validated_cluster, - &role, - role_group_name, - )) + .with_labels(validated_cluster.recommended_labels(&role, role_group_name)) .build(), ) .add_data( diff --git a/rust/operator-binary/src/controller/build/resource/discovery.rs b/rust/operator-binary/src/controller/build/resource/discovery.rs index 6a9a9c07..de25fd3a 100644 --- a/rust/operator-binary/src/controller/build/resource/discovery.rs +++ b/rust/operator-binary/src/controller/build/resource/discovery.rs @@ -9,7 +9,7 @@ use stackable_operator::{ }; use crate::{ - controller::{RoleGroupName, ValidatedCluster, build::labels}, + controller::{RoleGroupName, ValidatedCluster}, crd::role::KafkaRole, }; @@ -57,12 +57,13 @@ pub fn build_discovery_configmap( None, Some(true), )) - .with_labels(labels::recommended_labels( - validated_cluster, - &KafkaRole::Broker, - &RoleGroupName::from_str("discovery") - .expect("'discovery' is a valid role group name"), - )) + .with_labels( + validated_cluster.recommended_labels( + &KafkaRole::Broker, + &RoleGroupName::from_str("discovery") + .expect("'discovery' is a valid role group name"), + ), + ) .build(), ) .add_data("KAFKA", bootstrap_servers) diff --git a/rust/operator-binary/src/controller/build/resource/listener.rs b/rust/operator-binary/src/controller/build/resource/listener.rs index c8497d31..6e5adc88 100644 --- a/rust/operator-binary/src/controller/build/resource/listener.rs +++ b/rust/operator-binary/src/controller/build/resource/listener.rs @@ -4,9 +4,7 @@ use stackable_operator::{ }; use crate::{ - controller::{ - RoleGroupName, ValidatedCluster, build::labels, security::ValidatedKafkaSecurity, - }, + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, crd::role::{KafkaRole, broker::BrokerConfig}, }; @@ -30,11 +28,7 @@ pub fn build_broker_rolegroup_bootstrap_listener( None, Some(true), )) - .with_labels(labels::recommended_labels( - validated_cluster, - role, - role_group_name, - )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .build(), spec: listener::v1alpha1::ListenerSpec { class_name: Some(merged_config.bootstrap_listener_class.to_string()), diff --git a/rust/operator-binary/src/controller/build/resource/rbac.rs b/rust/operator-binary/src/controller/build/resource/rbac.rs index dfe0e057..e59a8818 100644 --- a/rust/operator-binary/src/controller/build/resource/rbac.rs +++ b/rust/operator-binary/src/controller/build/resource/rbac.rs @@ -1,81 +1,42 @@ -//! Builds the cluster-wide RBAC resources (`ServiceAccount` and `RoleBinding`). -//! -//! The names come from [`ResourceNames`] -//! and are identical to the previously used `commons::rbac::build_rbac_resources` -//! (`-serviceaccount`, `-rolebinding`, `-clusterrole`), so switching to -//! this builder does not rename any RBAC objects. +//! Builds the RBAC resources (ServiceAccount + RoleBinding) shared by all role groups. + +use std::str::FromStr; use stackable_operator::{ - builder::meta::ObjectMetaBuilder, - k8s_openapi::api::{ - core::v1::ServiceAccount, - rbac::v1::{RoleBinding, RoleRef, Subject}, - }, + k8s_openapi::api::{core::v1::ServiceAccount, rbac::v1::RoleBinding}, kvp::Labels, - v2::{builder::meta::ownerreference_from_resource, role_utils::ResourceNames}, + v2::{ + rbac, + types::operator::{RoleGroupName, RoleName}, + }, }; -use crate::controller::{ValidatedCluster, product_name}; +use crate::controller::ValidatedCluster; -/// Type-safe RBAC resource names for this cluster. -fn rbac_resource_names(validated_cluster: &ValidatedCluster) -> ResourceNames { - ResourceNames { - cluster_name: validated_cluster.name.clone(), - product_name: product_name(), - } -} - -/// Builds the [`ServiceAccount`] shared by all role groups, named `-serviceaccount`. -pub fn build_rbac_service_account( - validated_cluster: &ValidatedCluster, - labels: Labels, -) -> ServiceAccount { - let resource_names = rbac_resource_names(validated_cluster); +stackable_operator::constant!(NONE_ROLE_NAME: RoleName = "none"); +stackable_operator::constant!(NONE_ROLE_GROUP_NAME: RoleGroupName = "none"); - ServiceAccount { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(validated_cluster) - .name(resource_names.service_account_name().to_string()) - .ownerreference(ownerreference_from_resource( - validated_cluster, - None, - Some(true), - )) - .with_labels(labels) - .build(), - ..ServiceAccount::default() - } +/// 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`] (named `-rolebinding`) that binds the -/// [`ServiceAccount`] to the `-clusterrole` `ClusterRole`. -pub fn build_rbac_role_binding( - validated_cluster: &ValidatedCluster, - labels: Labels, -) -> RoleBinding { - let resource_names = rbac_resource_names(validated_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), + ) +} - RoleBinding { - metadata: ObjectMetaBuilder::new() - .name_and_namespace(validated_cluster) - .name(resource_names.role_binding_name().to_string()) - .ownerreference(ownerreference_from_resource( - validated_cluster, - None, - Some(true), - )) - .with_labels(labels) - .build(), - role_ref: RoleRef { - api_group: Some("rbac.authorization.k8s.io".to_owned()), - kind: "ClusterRole".to_string(), - name: resource_names.cluster_role_name().to_string(), - }, - subjects: Some(vec![Subject { - kind: "ServiceAccount".to_string(), - name: resource_names.service_account_name().to_string(), - namespace: Some(validated_cluster.namespace.to_string()), - ..Subject::default() - }]), - } +/// 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/controller/build/resource/service.rs b/rust/operator-binary/src/controller/build/resource/service.rs index 07703051..5f574531 100644 --- a/rust/operator-binary/src/controller/build/resource/service.rs +++ b/rust/operator-binary/src/controller/build/resource/service.rs @@ -8,9 +8,7 @@ use stackable_operator::{ }; use crate::{ - controller::{ - RoleGroupName, ValidatedCluster, build::labels, security::ValidatedKafkaSecurity, - }, + controller::{RoleGroupName, ValidatedCluster, security::ValidatedKafkaSecurity}, crd::{METRICS_PORT, METRICS_PORT_NAME, role::KafkaRole}, }; @@ -28,7 +26,7 @@ pub fn build_rolegroup_headless_service( .name_and_namespace(validated_cluster) .name( validated_cluster - .resource_names(role, role_group_name) + .role_group_resource_names(role, role_group_name) .headless_service_name() .to_string(), ) @@ -37,17 +35,15 @@ pub fn build_rolegroup_headless_service( None, Some(true), )) - .with_labels(labels::recommended_labels( - validated_cluster, - role, - role_group_name, - )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .build(), spec: Some(ServiceSpec { cluster_ip: Some("None".to_string()), ports: Some(headless_ports(kafka_security)), selector: Some( - labels::role_group_selector(validated_cluster, role, role_group_name).into(), + validated_cluster + .role_group_selector(role, role_group_name) + .into(), ), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() @@ -67,7 +63,7 @@ pub fn build_rolegroup_metrics_service( .name_and_namespace(validated_cluster) .name( validated_cluster - .resource_names(role, role_group_name) + .role_group_resource_names(role, role_group_name) .metrics_service_name() .to_string(), ) @@ -76,11 +72,7 @@ pub fn build_rolegroup_metrics_service( None, Some(true), )) - .with_labels(labels::recommended_labels( - validated_cluster, - role, - role_group_name, - )) + .with_labels(validated_cluster.recommended_labels(role, role_group_name)) .with_labels(prometheus_labels(&Scraping::Enabled)) .with_annotations(prometheus_annotations( &Scraping::Enabled, @@ -95,7 +87,9 @@ pub fn build_rolegroup_metrics_service( cluster_ip: Some("None".to_string()), ports: Some(metrics_ports()), selector: Some( - labels::role_group_selector(validated_cluster, role, role_group_name).into(), + validated_cluster + .role_group_selector(role, role_group_name) + .into(), ), publish_not_ready_addresses: Some(true), ..ServiceSpec::default() diff --git a/rust/operator-binary/src/controller/build/resource/statefulset.rs b/rust/operator-binary/src/controller/build/resource/statefulset.rs index 70997d08..57104c50 100644 --- a/rust/operator-binary/src/controller/build/resource/statefulset.rs +++ b/rust/operator-binary/src/controller/build/resource/statefulset.rs @@ -53,7 +53,6 @@ use crate::{ }, graceful_shutdown::add_graceful_shutdown_config, kerberos::add_kerberos_pod_config, - labels, properties::product_logging::MAX_KAFKA_LOG_FILES_SIZE, security::{ add_broker_volume_and_volume_mounts, add_controller_volume_and_volume_mounts, @@ -188,17 +187,15 @@ pub fn build_broker_rolegroup_statefulset( role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, validated_rg: &ValidatedRoleGroupConfig, - service_account_name: &str, ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.config.config; - let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); - let recommended_labels = - labels::recommended_labels(validated_cluster, kafka_role, role_group_name); + let resource_names = validated_cluster.role_group_resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); // Used for PVC templates that cannot be modified once they are deployed let unversioned_recommended_labels = - labels::unversioned_recommended_labels(validated_cluster, kafka_role, role_group_name); + validated_cluster.unversioned_recommended_labels(kafka_role, role_group_name); let kcat_prober_container_name = BrokerContainer::KcatProber.to_string(); let mut cb_kcat_prober = @@ -394,7 +391,14 @@ pub fn build_broker_rolegroup_statefulset( .add_container(cb_kcat_prober.build()) .affinity(&merged_config.affinity); - add_common_pod_config(&mut pod_builder, &resource_names, service_account_name)?; + add_common_pod_config( + &mut pod_builder, + &resource_names, + validated_cluster + .cluster_resource_names() + .service_account_name() + .as_ref(), + )?; add_vector_container( &mut pod_builder, @@ -432,7 +436,8 @@ pub fn build_broker_rolegroup_statefulset( replicas: validated_rg.replicas.map(i32::from), selector: LabelSelector { match_labels: Some( - labels::role_group_selector(validated_cluster, kafka_role, role_group_name) + validated_cluster + .role_group_selector(kafka_role, role_group_name) .into(), ), ..LabelSelector::default() @@ -452,14 +457,12 @@ pub fn build_controller_rolegroup_statefulset( role_group_name: &RoleGroupName, validated_cluster: &ValidatedCluster, validated_rg: &ValidatedRoleGroupConfig, - service_account_name: &str, ) -> Result { let kafka_security = &validated_cluster.cluster_config.kafka_security; let resolved_product_image = &validated_cluster.image; let merged_config = &validated_rg.config.config; - let resource_names = validated_cluster.resource_names(kafka_role, role_group_name); - let recommended_labels = - labels::recommended_labels(validated_cluster, kafka_role, role_group_name); + let resource_names = validated_cluster.role_group_resource_names(kafka_role, role_group_name); + let recommended_labels = validated_cluster.recommended_labels(kafka_role, role_group_name); let kafka_container_name = ControllerContainer::Kafka.to_string(); let mut cb_kafka = @@ -577,7 +580,14 @@ pub fn build_controller_rolegroup_statefulset( .add_container(kafka_container) .affinity(&merged_config.affinity); - add_common_pod_config(&mut pod_builder, &resource_names, service_account_name)?; + add_common_pod_config( + &mut pod_builder, + &resource_names, + validated_cluster + .cluster_resource_names() + .service_account_name() + .as_ref(), + )?; add_vector_container( &mut pod_builder, @@ -615,7 +625,8 @@ pub fn build_controller_rolegroup_statefulset( replicas: validated_rg.replicas.map(i32::from), selector: LabelSelector { match_labels: Some( - labels::role_group_selector(validated_cluster, kafka_role, role_group_name) + validated_cluster + .role_group_selector(kafka_role, role_group_name) .into(), ), ..LabelSelector::default()