Skip to content
Merged
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
85 changes: 67 additions & 18 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 @@ -50,13 +51,10 @@ pub enum Error {
///
/// Does not need a Kubernetes client: every reference to another Kubernetes resource is already
/// dereferenced and validated by this point, so the errors returned here are resource-assembly
/// failures only. `cluster_info` is static cluster metadata (not a client call), and
/// `service_account_name` is the name of the RBAC `ServiceAccount` the role-group Pods run under
/// (RBAC resources are built and applied separately, in the reconcile step).
/// failures only. `cluster_info` is static cluster metadata (not a client call).
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 +81,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 +105,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 +121,8 @@ pub mod role;

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

use stackable_operator::kube::Resource;

use super::build;
Expand All @@ -146,8 +142,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 +181,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;
2 changes: 1 addition & 1 deletion rust/operator-binary/src/controller/build/resource/pdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub fn build_pdb(
let pdb = pod_disruption_budget_builder_with_role(
cluster,
&product_name(),
&ValidatedCluster::role_name(role),
&role.into(),
&operator_name(),
&controller_name(),
)
Expand Down
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
Loading
Loading