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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Support floating tags for product images via the new `spec.image.stackableVersionPolicy` field
([#809]).
- Add `/ready` endpoint to the operator Deployment, which reports the CRD installation status ([#812]).
- Masters and region servers now have a default affinity to the OPA Pods when OPA authorization is configured ([#816]).

### Changed

Expand Down Expand Up @@ -56,6 +57,7 @@
[#803]: https://github.com/stackabletech/hbase-operator/pull/803
[#809]: https://github.com/stackabletech/hbase-operator/pull/809
[#812]: https://github.com/stackabletech/hbase-operator/pull/812
[#816]: https://github.com/stackabletech/hbase-operator/pull/816

## [26.7.0] - 2026-07-21

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ The default affinities created by the operator are:

1. Co-locate all the HBase Pods (weight 20)
2. Co-locate HBase regionservers with the underlying HDFS datanodes (weight 50)
3. Distribute all Pods within the same role across nodes so multiple instances don't end up on the same Kubernetes node (masters, regionservers, rest servers) (weight 70)
3. If OPA authorization is configured: co-locate HBase masters and regionservers with the OPA Pods (weight 50)
4. Distribute all Pods within the same role across nodes so multiple instances don't end up on the same Kubernetes node (masters, regionservers, rest servers) (weight 70)

NOTE: All default affinities are only preferred and not enforced, as we can not expect all setups to have multiple Kubernetes nodes.
If you want to have them enforced, you need to specify you own `requiredDuringSchedulingIgnoredDuringExecution` affinities.
Expand Down Expand Up @@ -108,6 +109,26 @@ The `hdfs-cluster-name` is the name of the HDFS cluster that was configured in t
NOTE: It is important that the `hdfsConfigMapName` property contains the name the HDFS cluster.
You could instead configure ConfigMaps of specific name or data roles, but for the purpose of Pod placement, this leads to faulty behavior.

If OPA authorization is configured, masters and region servers additionally get the following affinity.
Rest servers do not get it, as they do not load the OPA access controller coprocessor.

[source,yaml]
----
affinity:
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/component: server
app.kubernetes.io/instance: opa-cluster-name
app.kubernetes.io/name: opa
topologyKey: kubernetes.io/hostname
weight: 50
----

`opa-cluster-name` is the `configMapName` from `spec.clusterConfig.authorization.opa`, which by convention is the name of the OpaCluster.

== Use custom pod placement
For general configuration of Pod placement, see the xref:concepts:operations/pod_placement.adoc[Pod placement concepts] page.
One example use-case for HBase would be to *require* the HBase masters to run on different Kubernetes nodes as follows:
Expand Down
11 changes: 11 additions & 0 deletions rust/operator-binary/src/controller/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ pub fn validate_cluster(
.vector_aggregator_config_map_name
.clone();

let opa_config = hbase
.spec
.cluster_config
.authorization
.as_ref()
.and_then(|authorization| authorization.opa.as_ref());

for hbase_role in HbaseRole::iter() {
let group_configs = match hbase_role {
HbaseRole::Master => validate_role_group_configs(
Expand All @@ -147,6 +154,7 @@ pub fn validate_cluster(
&hbase_role,
&cluster_name,
hdfs_discovery_cm_name,
opa_config,
),
AnyServiceConfig::Master,
&vector_aggregator_config_map_name,
Expand All @@ -157,6 +165,7 @@ pub fn validate_cluster(
&hbase_role,
&cluster_name,
hdfs_discovery_cm_name,
opa_config,
),
AnyServiceConfig::RegionServer,
&vector_aggregator_config_map_name,
Expand All @@ -167,6 +176,7 @@ pub fn validate_cluster(
&hbase_role,
&cluster_name,
hdfs_discovery_cm_name,
opa_config,
),
AnyServiceConfig::RestServer,
&vector_aggregator_config_map_name,
Expand Down Expand Up @@ -357,6 +367,7 @@ spec:
&HbaseRole::Master,
&hbase.name_any(),
hbase.spec.cluster_config.hdfs_config_map_name.as_ref(),
None,
);

let validated = with_validated_config::<
Expand Down
97 changes: 79 additions & 18 deletions rust/operator-binary/src/crd/affinity.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use stackable_operator::{
commons::affinity::{
StackableAffinityFragment, affinity_between_cluster_pods, affinity_between_role_pods,
commons::{
affinity::{
StackableAffinityFragment, affinity_between_cluster_pods, affinity_between_role_pods,
},
opa::OpaConfig,
},
k8s_openapi::api::core::v1::{PodAffinity, PodAntiAffinity},
};
Expand All @@ -11,18 +14,34 @@ pub fn get_affinity(
cluster_name: &str,
role: &HbaseRole,
hdfs_discovery_cm_name: &str,
opa_config: Option<&OpaConfig>,
) -> StackableAffinityFragment {
// Masters and region servers load the OPA access controller coprocessor, so they are co-located
// with the OPA Pods.
let affinity_to_opa_pods = opa_config.map(|opa_config| {
affinity_between_role_pods(
"opa",
&opa_config.config_map_name, // The discovery cm has the same name as the OpaCluster itself
"server",
50,
)
});
let affinity_between_cluster_pods = affinity_between_cluster_pods(APP_NAME, cluster_name, 20);
match role {
HbaseRole::Master => StackableAffinityFragment {
pod_affinity: Some(PodAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
affinity_between_cluster_pods,
// We would like a affinity to the Zookeeper Pods, but the hbase CRD only contains a ZNode reference.
// We could look up the ZNode and extract the zk cluster from it but that causes network calls
// See https://github.com/stackabletech/zookeeper-operator/issues/644
// Watch out: The zk can be in a different namespace, so the namespaceSelector must be used
]),
preferred_during_scheduling_ignored_during_execution: Some(
vec![
affinity_between_cluster_pods,
// We would like a affinity to the Zookeeper Pods, but the hbase CRD only contains a ZNode reference.
// We could look up the ZNode and extract the zk cluster from it but that causes network calls
// See https://github.com/stackabletech/zookeeper-operator/issues/644
// Watch out: The zk can be in a different namespace, so the namespaceSelector must be used
]
.into_iter()
.chain(affinity_to_opa_pods)
.collect(),
),
required_during_scheduling_ignored_during_execution: None,
}),
pod_anti_affinity: Some(PodAntiAffinity {
Expand All @@ -36,15 +55,20 @@ pub fn get_affinity(
},
HbaseRole::RegionServer => StackableAffinityFragment {
pod_affinity: Some(PodAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
affinity_between_cluster_pods,
affinity_between_role_pods(
"hdfs",
hdfs_discovery_cm_name, // The discovery cm has the same name as the HdfsCluster itself
"datanode",
50,
),
]),
preferred_during_scheduling_ignored_during_execution: Some(
vec![
affinity_between_cluster_pods,
affinity_between_role_pods(
"hdfs",
hdfs_discovery_cm_name, // The discovery cm has the same name as the HdfsCluster itself
"datanode",
50,
),
]
.into_iter()
.chain(affinity_to_opa_pods)
.collect(),
),
required_during_scheduling_ignored_during_execution: None,
}),
pod_anti_affinity: Some(PodAntiAffinity {
Expand All @@ -56,6 +80,8 @@ pub fn get_affinity(
node_affinity: None,
node_selector: None,
},
// The REST server does not load the OPA access controller coprocessor, so it gets no
// affinity to the OPA Pods.
HbaseRole::RestServer => StackableAffinityFragment {
pod_affinity: Some(PodAffinity {
preferred_during_scheduling_ignored_during_execution: Some(vec![
Expand Down Expand Up @@ -111,6 +137,10 @@ mod tests {
clusterConfig:
hdfsConfigMapName: simple-hdfs
zookeeperConfigMapName: simple-znode
authorization:
opa:
configMapName: simple-opa
package: hbase
masters:
roleGroups:
default:
Expand Down Expand Up @@ -183,6 +213,37 @@ mod tests {
HbaseRole::RestServer => (),
};

// Masters and region servers load the OPA access controller coprocessor.
match role {
HbaseRole::Master | HbaseRole::RegionServer => {
expected_affinities.push(WeightedPodAffinityTerm {
pod_affinity_term: PodAffinityTerm {
label_selector: Some(LabelSelector {
match_expressions: None,
match_labels: Some(BTreeMap::from([
("app.kubernetes.io/name".to_string(), "opa".to_string()),
(
"app.kubernetes.io/instance".to_string(),
"simple-opa".to_string(),
),
(
"app.kubernetes.io/component".to_string(),
"server".to_string(),
),
])),
}),
match_label_keys: None,
mismatch_label_keys: None,
namespace_selector: None,
namespaces: None,
topology_key: "kubernetes.io/hostname".to_string(),
},
weight: 50,
});
}
HbaseRole::RestServer => (),
};

assert_eq!(
affinity,
StackableAffinity {
Expand Down
7 changes: 5 additions & 2 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use stackable_operator::{
commons::{
affinity::StackableAffinity,
cluster_operation::ClusterOperation,
opa::OpaConfig,
product_image_selection::ProductImage,
resources::{
CpuLimitsFragment, MemoryLimitsFragment, NoRuntimeLimits, NoRuntimeLimitsFragment,
Expand Down Expand Up @@ -315,6 +316,7 @@ impl HbaseConfigFragment {
role: &HbaseRole,
cluster_name: &str,
hdfs_discovery_cm_name: &str,
opa_config: Option<&OpaConfig>,
) -> Self {
let graceful_shutdown_timeout = match role {
HbaseRole::Master => HbaseRole::DEFAULT_MASTER_GRACEFUL_SHUTDOWN_TIMEOUT,
Expand All @@ -330,7 +332,7 @@ impl HbaseConfigFragment {
hbase_rootdir: Some(default_hbase_rootdir()),
resources: default_resources(role),
logging: product_logging::spec::default_logging(),
affinity: get_affinity(cluster_name, role, hdfs_discovery_cm_name),
affinity: get_affinity(cluster_name, role, hdfs_discovery_cm_name, opa_config),
graceful_shutdown_timeout: Some(graceful_shutdown_timeout),
requested_secret_lifetime: Some(requested_secret_lifetime),
listener_class: Some(
Expand All @@ -347,12 +349,13 @@ impl RegionServerConfigFragment {
role: &HbaseRole,
cluster_name: &str,
hdfs_discovery_cm_name: &str,
opa_config: Option<&OpaConfig>,
) -> Self {
RegionServerConfigFragment {
hbase_rootdir: Some(default_hbase_rootdir()),
resources: default_resources(role),
logging: product_logging::spec::default_logging(),
affinity: get_affinity(cluster_name, role, hdfs_discovery_cm_name),
affinity: get_affinity(cluster_name, role, hdfs_discovery_cm_name, opa_config),
graceful_shutdown_timeout: Some(
HbaseRole::DEFAULT_REGION_SERVER_GRACEFUL_SHUTDOWN_TIMEOUT,
),
Expand Down
Loading