Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

- Internal operator refactoring: introduce a build() step in the reconciler that
assembles all relevant Kubernetes resources before anything is applied ([#776]).
- The RBAC ServiceAccount and RoleBinding are now built with the operator-rs `v2::rbac`
functions and carry the full set of recommended labels ([#782]).
- Bump stackable-operator to 0.114.0 ([#786]).

[#776]: https://github.com/stackabletech/hbase-operator/pull/776
[#782]: https://github.com/stackabletech/hbase-operator/pull/782
[#786]: https://github.com/stackabletech/hbase-operator/pull/786

## [26.7.0] - 2026-07-21
Expand Down
81 changes: 66 additions & 15 deletions rust/operator-binary/src/controller/build/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Builders that turn a [`ValidatedCluster`](crate::controller::ValidatedCluster) into
//! Builders that turn a [`ValidatedCluster`] into
//! Kubernetes resources.

use std::str::FromStr;
Expand All @@ -15,6 +15,7 @@ use crate::{
config_map::{self, build_rolegroup_config_map},
discovery::{self, build_discovery_config_map},
pdb::build_pdb,
rbac::{build_role_binding, build_service_account},
service::{build_rolegroup_metrics_service, build_rolegroup_service},
statefulset::{self, build_rolegroup_statefulset},
},
Expand Down Expand Up @@ -56,7 +57,6 @@ pub enum Error {
pub fn build(
cluster: &ValidatedCluster,
cluster_info: &KubernetesClusterInfo,
service_account_name: &str,
) -> Result<KubernetesResources, Error> {
let mut stateful_sets = vec![];
let mut services = vec![];
Expand All @@ -83,17 +83,11 @@ pub fn build(
})?,
);
stateful_sets.push(
build_rolegroup_statefulset(
cluster,
hbase_role,
role_group_name,
rg_config,
service_account_name,
)
.with_context(|_| StatefulSetSnafu {
hbase_role: hbase_role.clone(),
role_group: role_group_name.clone(),
})?,
build_rolegroup_statefulset(cluster, hbase_role, role_group_name, rg_config)
.with_context(|_| StatefulSetSnafu {
hbase_role: hbase_role.clone(),
role_group: role_group_name.clone(),
})?,
);
}

Expand All @@ -113,6 +107,8 @@ pub fn build(
services,
config_maps,
pod_disruption_budgets,
service_accounts: vec![build_service_account(cluster)],
role_bindings: vec![build_role_binding(cluster)],
})
}

Expand All @@ -127,6 +123,8 @@ pub mod role;

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;

use stackable_operator::kube::Resource;

use super::build;
Expand All @@ -146,8 +144,7 @@ mod tests {
fn build_produces_expected_resource_names() {
let cluster = test_utils::validated_cluster();
let cluster_info = test_utils::cluster_info();
let resources =
build(&cluster, &cluster_info, "hbase-serviceaccount").expect("build succeeds");
let resources = build(&cluster, &cluster_info).expect("build succeeds");

// One StatefulSet per role group (one `default` group for each of the three roles).
assert_eq!(
Expand Down Expand Up @@ -186,4 +183,58 @@ mod tests {
["hbase-master", "hbase-regionserver", "hbase-restserver"]
);
}

/// Locks the RBAC resource names, the roleRef, and the recommended label set against
/// accidental drift. The cluster name deliberately differs from the product name so that
/// swapped `name`/`instance` label values cannot pass unnoticed (the shared fixture is named
/// `hbase`, which would mask exactly that swap).
#[test]
fn build_produces_rbac() {
let hbase = test_utils::hbase_from_yaml(
&test_utils::MINIMAL_HBASE_YAML.replace("name: hbase", "name: my-hbase"),
);
let cluster = test_utils::validated_cluster_from(&hbase);
let cluster_info = test_utils::cluster_info();
let resources = build(&cluster, &cluster_info).expect("build succeeds");

assert_eq!(
sorted_names(&resources.service_accounts),
["my-hbase-serviceaccount"]
);
assert_eq!(
sorted_names(&resources.role_bindings),
["my-hbase-rolebinding"]
);

let expected_labels = BTreeMap::from(
[
("app.kubernetes.io/component", "none"),
("app.kubernetes.io/instance", "my-hbase"),
(
"app.kubernetes.io/managed-by",
"hbase.stackable.com_hbasecluster",
),
("app.kubernetes.io/name", "hbase"),
("app.kubernetes.io/role-group", "none"),
("app.kubernetes.io/version", "2.6.3-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, "hbase-clusterrole");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub fn build_rolegroup_config_map(
let cm_metadata = cluster
.object_meta(
cluster
.resource_names(role, role_group_name)
.role_group_resource_names(role, role_group_name)
.role_group_config_map()
.to_string(),
role,
Expand Down
4 changes: 2 additions & 2 deletions rust/operator-binary/src/controller/build/resource/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! Builders that turn a [`ValidatedCluster`](crate::controller::ValidatedCluster) into
//! Kubernetes resources.
//! Builders for individual Kubernetes resources (one module per resource type).

pub mod config_map;
pub mod discovery;
pub mod listener;
pub mod pdb;
pub mod rbac;
pub mod service;
pub mod statefulset;
42 changes: 42 additions & 0 deletions rust/operator-binary/src/controller/build/resource/rbac.rs
Original file line number Diff line number Diff line change
@@ -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::controller::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)
}
4 changes: 2 additions & 2 deletions rust/operator-binary/src/controller/build/resource/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn build_rolegroup_service(
metadata: cluster
.object_meta(
cluster
.resource_names(hbase_role, role_group_name)
.role_group_resource_names(hbase_role, role_group_name)
.headless_service_name()
.to_string(),
hbase_role,
Expand Down Expand Up @@ -77,7 +77,7 @@ pub fn build_rolegroup_metrics_service(
metadata: cluster
.object_meta(
cluster
.resource_names(hbase_role, role_group_name)
.role_group_resource_names(hbase_role, role_group_name)
.metrics_service_name()
.to_string(),
hbase_role,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,11 @@ pub fn build_rolegroup_statefulset(
hbase_role: &HbaseRole,
role_group_name: &RoleGroupName,
validated_rg_config: &HbaseRoleGroupConfig,
service_account_name: &str,
) -> Result<StatefulSet> {
let resolved_product_image = &cluster.image;
let merged_config = &validated_rg_config.config.config;
let logging = &validated_rg_config.config.logging;
let resource_names = cluster.resource_names(hbase_role, role_group_name);
let resource_names = cluster.role_group_resource_names(hbase_role, role_group_name);
let https_enabled = cluster.has_https_enabled();

let ports = hbase_role
Expand Down Expand Up @@ -239,7 +238,12 @@ pub fn build_rolegroup_statefulset(
)),
)
.context(AddVolumeSnafu)?
.service_account_name(service_account_name)
.service_account_name(
cluster
.cluster_resource_names()
.service_account_name()
.to_string(),
)
.security_context(PodSecurityContextBuilder::new().fs_group(1000).build());

// The HBase container's log config ConfigMap: either the operator-generated one (the
Expand Down
57 changes: 51 additions & 6 deletions rust/operator-binary/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ use stackable_operator::{
k8s_openapi::{
api::{
apps::v1::StatefulSet,
core::v1::{ConfigMap, Service},
core::v1::{ConfigMap, Service, ServiceAccount},
policy::v1::PodDisruptionBudget,
rbac::v1::RoleBinding,
},
apimachinery::pkg::apis::meta::v1::ObjectMeta,
},
Expand All @@ -25,6 +26,7 @@ use stackable_operator::{
builder::meta::ownerreference_from_resource,
kvp::label::{recommended_labels, role_group_selector},
role_group_utils::ResourceNames,
role_utils,
types::{
kubernetes::{ConfigMapName, NamespaceName, SecretClassName, Uid},
operator::{
Expand Down Expand Up @@ -67,6 +69,8 @@ pub struct KubernetesResources {
pub services: Vec<Service>,
pub config_maps: Vec<ConfigMap>,
pub pod_disruption_budgets: Vec<PodDisruptionBudget>,
pub service_accounts: Vec<ServiceAccount>,
pub role_bindings: Vec<RoleBinding>,
}

/// The validated cluster: proves that config merging and validation succeeded for
Expand Down Expand Up @@ -132,8 +136,17 @@ impl ValidatedCluster {
RoleName::from_str(&hbase_role.to_string()).expect("an HbaseRole name 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,
hbase_role: &HbaseRole,
role_group_name: &RoleGroupName,
Expand All @@ -146,18 +159,33 @@ impl ValidatedCluster {
}

/// Recommended labels for a role-group resource.
pub fn recommended_labels(
pub fn recommended_labels(&self, role: &HbaseRole, 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 [`HbaseRole`] (e.g. the
/// Kubernetes executor pod template), using a free-form role/role-group label value.
pub fn recommended_labels_for(
&self,
hbase_role: &HbaseRole,
role_name: &RoleName,
role_group_name: &RoleGroupName,
) -> Labels {
self.recommended_labels_with(&self.product_version, role_name, role_group_name)
}

fn recommended_labels_with(
&self,
product_version: &ProductVersion,
role_name: &RoleName,
role_group_name: &RoleGroupName,
) -> Labels {
recommended_labels(
self,
&product_name(),
&self.product_version,
product_version,
&operator_name(),
&controller_name(),
&Self::role_name(hbase_role),
role_name,
role_group_name,
)
}
Expand Down Expand Up @@ -295,3 +323,20 @@ pub type HbaseRoleGroupConfig = stackable_operator::v2::role_utils::RoleGroupCon
stackable_operator::v2::role_utils::JavaCommonConfig,
v1alpha1::HbaseConfigOverrides,
>;

#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;

use super::ValidatedCluster;
use crate::crd::HbaseRole;

/// Locks the invariant behind the `expect` in [`ValidatedCluster::role_name`]: every
/// `HbaseRole` variant (present and future) must serialise to a valid `RoleName`.
#[test]
fn every_hbase_role_serialises_to_a_valid_role_name() {
for role in HbaseRole::iter() {
ValidatedCluster::role_name(&role);
}
}
}
Loading
Loading