Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
- test: Update OPA to 1.20.2 in integration tests ([#823]).
- All pods now set `securityContext.seccompProfile.type` to `RuntimeDefault` ([#824]).
- Replace the unmaintained `serde_yaml` with `yaml_serde` ([#824]).
- Internal operator refactoring: build each role group through per-role modules (masters,
region servers, rest servers) and a shared role group builder, with each role's configuration
held in typed fields of the validated cluster ([#827]).
- Variable names in `configOverrides` for `hbase-env.sh` are now validated (non-empty, printable
ASCII, no `=`); an invalid name is rejected when the HbaseCluster is validated instead of
producing a broken `hbase-env.sh` ([#827]).

### Fixed

Expand Down Expand Up @@ -67,6 +73,7 @@
[#823]: https://github.com/stackabletech/hbase-operator/pull/823
[#824]: https://github.com/stackabletech/hbase-operator/pull/824
[#825]: https://github.com/stackabletech/hbase-operator/pull/825
[#827]: https://github.com/stackabletech/hbase-operator/pull/827

## [26.7.0] - 2026-07-21

Expand Down
4 changes: 3 additions & 1 deletion docs/modules/hbase/pages/usage-guide/overrides.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ For a role or role group, at the same level of `config`, you can specify: `confi
* `ssl-client.xml`
* `security.properties`

Variable names in `hbase-env.sh` overrides must be valid environment variable names (non-empty, printable ASCII, no `=`); an invalid name is rejected when the HbaseCluster is validated.

NOTE: `hdfs-site.xml` is not listed here, the file is always taken from the referenced HDFS cluster.
If you want to modify it, take a look at xref:hdfs:usage-guide/configuration-environment-overrides.adoc[HDFS configuration overrides].

Expand Down Expand Up @@ -52,7 +54,7 @@ restServers:
----

All override property values must be strings.
The properties are formatted and escaped correctly into the XML file, respectively inserted as is into the `hbase-env.sh` file.
The properties are formatted and escaped correctly into the XML file, respectively inserted as is into the `hbase-env.sh` file after the variable names have been validated.

For a full list of configuration options we refer to the HBase https://hbase.apache.org/book.html#config.files[configuration documentation].

Expand Down
308 changes: 308 additions & 0 deletions rust/operator-binary/src/controller/build/container.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
//! Builds the containers of a role group's pod and collects the volumes they mount.
//!
//! Every HBase role runs the same containers: the `hbase` container and, when the Vector agent is
//! enabled, the Vector sidecar. What differs between roles arrives resolved in the
//! [`RoleGroupInputs`]; the role itself is read only for the values that are functions of it (its
//! CLI name and ports) and for the object names.

use std::str::FromStr;

use indoc::formatdoc;
use snafu::{ResultExt, Snafu};
use stackable_operator::{
builder::pod::volume::VolumeBuilder,
constant,
k8s_openapi::{
api::core::v1::{
ConfigMapVolumeSource, Container, ContainerPort, PersistentVolumeClaim, Probe,
TCPSocketAction, Volume,
},
apimachinery::pkg::util::intstr::IntOrString,
},
product_logging,
v2::{
builder::pod::container::{EnvVarName, EnvVarSet, new_container_builder},
product_logging::framework::{
STACKABLE_LOG_DIR, ValidatedContainerLogConfigChoice, vector_container,
},
types::kubernetes::{ContainerName, VolumeName},
},
};

use crate::{
controller::build::{
kerberos::{self, mount_kerberos_volumes},
properties::product_logging::MAX_HBASE_LOG_FILES_SIZE,
role_group::{ListenerPlacement, RoleGroupInputs},
},
crd::{CONFIG_DIR_NAME, HbaseRole, LISTENER_VOLUME_DIR, LISTENER_VOLUME_NAME},
};

constant!(pub(crate) HBASE_CONTAINER_NAME: ContainerName = "hbase");
constant!(VECTOR_CONTAINER_NAME: ContainerName = "vector");

// Pod volume names. The Vector container reuses the `hbase-config` (rolegroup ConfigMap, which
// carries `vector.yaml`) and `log` volumes, so the produced volume mounts match the rest of the
// Pod.
constant!(pub(crate) HBASE_CONFIG_VOLUME_NAME: VolumeName = "hbase-config");
constant!(pub(crate) HDFS_DISCOVERY_VOLUME_NAME: VolumeName = "hdfs-discovery");
constant!(pub(crate) LOG_CONFIG_VOLUME_NAME: VolumeName = "log-config");
constant!(pub(crate) LOG_VOLUME_NAME: VolumeName = "log");

// Environment variable names set on the HBase container. Declared as typed constants (instead of
// `EnvVarName::from_str_unsafe` at the use site) and validated by `test_constants`.
constant!(HBASE_CONF_DIR_ENV: EnvVarName = "HBASE_CONF_DIR");
constant!(HADOOP_CONF_DIR_ENV: EnvVarName = "HADOOP_CONF_DIR");
constant!(pub(crate) REGION_MOVER_OPTS_ENV: EnvVarName = "REGION_MOVER_OPTS");
constant!(pub(crate) RUN_REGION_MOVER_ENV: EnvVarName = "RUN_REGION_MOVER");
constant!(STACKABLE_LOG_DIR_ENV: EnvVarName = "STACKABLE_LOG_DIR");
constant!(CONTAINERDEBUG_LOG_DIRECTORY_ENV: EnvVarName = "CONTAINERDEBUG_LOG_DIRECTORY");

// These constants are hard coded in hbase-entrypoint.sh
// You need to change them there too.
const HDFS_DISCOVERY_TMP_DIR: &str = "/stackable/tmp/hdfs";
const HBASE_CONFIG_TMP_DIR: &str = "/stackable/tmp/hbase";
const HBASE_LOG_CONFIG_TMP_DIR: &str = "/stackable/tmp/log_config";

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("failed to mount the Kerberos volumes"))]
MountKerberosVolumes { source: kerberos::Error },
}

type Result<T, E = Error> = std::result::Result<T, E>;

/// The containers of a role group's pod and the volumes they mount, in pod order.
pub(crate) struct ContainersAndVolumes {
/// The `hbase` container, then the Vector sidecar when the Vector agent is enabled.
pub(crate) containers: Vec<Container>,
/// The pod volumes in the order the pod lists them; see [`containers_and_volumes`].
pub(crate) pod_volumes: Vec<Volume>,
/// The listener claim template of a rest server role group. `None` for the other roles,
/// which keeps their StatefulSets without a `volumeClaimTemplates` field.
pub(crate) volume_claim_templates: Option<Vec<PersistentVolumeClaim>>,
}

/// Builds the containers every role runs and collects the volumes they mount.
///
/// The pod volumes are collected in the order the pod lists them: the role group's ConfigMap,
/// the HDFS discovery ConfigMap, the log directory, the log config, then the Kerberos keytab and
/// TLS keystore when configured, then the ephemeral listener volume of masters and region
/// servers. This order is part of the pod template, so changing it restarts every pod; it is
/// pinned by `pod_volumes_keep_their_order` in `build/mod.rs`.
pub(crate) fn containers_and_volumes(inputs: &RoleGroupInputs) -> Result<ContainersAndVolumes> {
let cluster = inputs.cluster;
let hbase_role = &inputs.role;
let resolved_product_image = &cluster.image;
let logging = &inputs.logging;
let resource_names = cluster.role_group_resource_names(hbase_role, &inputs.role_group_name);
let https_enabled = cluster.has_https_enabled();

let ports = hbase_role
.ports(https_enabled)
.into_iter()
.map(|(name, value)| ContainerPort {
name: Some(name),
container_port: i32::from(value),
protocol: Some("TCP".to_string()),
..ContainerPort::default()
})
.collect();

let probe_template = Probe {
tcp_socket: Some(TCPSocketAction {
port: IntOrString::String(hbase_role.data_port_name(https_enabled)),
..TCPSocketAction::default()
}),
..Probe::default()
};

let startup_probe = Probe {
failure_threshold: Some(120),
initial_delay_seconds: Some(4),
period_seconds: Some(5),
timeout_seconds: Some(3),
..probe_template.clone()
};
let liveness_probe = Probe {
failure_threshold: Some(3),
period_seconds: Some(10),
timeout_seconds: Some(3),
..probe_template.clone()
};
let readiness_probe = Probe {
failure_threshold: Some(1),
period_seconds: Some(10),
timeout_seconds: Some(2),
..probe_template
};

let merged_env = EnvVarSet::new()
.with_value(&HBASE_CONF_DIR_ENV, CONFIG_DIR_NAME)
// required by phoenix (for cases where Kerberos is enabled): see https://issues.apache.org/jira/browse/PHOENIX-2369
.with_value(&HADOOP_CONF_DIR_ENV, CONFIG_DIR_NAME)
// These env vars are set for all roles to avoid bash's "unbound variable" errors.
.with_value(&REGION_MOVER_OPTS_ENV, inputs.region_mover_opts.clone())
.with_value(&RUN_REGION_MOVER_ENV, inputs.run_region_mover.to_string())
.with_value(&STACKABLE_LOG_DIR_ENV, STACKABLE_LOG_DIR)
// Needed for the `containerdebug` process to log its tracing information to.
.with_value(
&CONTAINERDEBUG_LOG_DIRECTORY_ENV,
format!("{STACKABLE_LOG_DIR}/containerdebug"),
)
// Empty when Kerberos is disabled.
.merge(kerberos::kerberos_env_vars(cluster))
// apply overrides last of all; `EnvVarSet` is keyed by name, so iteration is already
// in a fixed (sorted-by-name) order
.merge(inputs.env_overrides.clone());

let role_name = hbase_role.cli_role_name();
let mut hbase_container = new_container_builder(&HBASE_CONTAINER_NAME);

hbase_container
.image_from_product_image(resolved_product_image)
.command(command())
.args(vec![formatdoc! {"
{entrypoint} {role} {port} {port_name} {ui_port_name}",
entrypoint = "/stackable/hbase/bin/hbase-entrypoint.sh".to_string(),
role = role_name,
port = hbase_role.data_port(),
port_name = hbase_role.data_port_name(https_enabled),
ui_port_name = HbaseRole::ui_port_name(https_enabled),
}])
.add_env_vars(merged_env)
.add_volume_mount(&*HBASE_CONFIG_VOLUME_NAME, HBASE_CONFIG_TMP_DIR)
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(&*HDFS_DISCOVERY_VOLUME_NAME, HDFS_DISCOVERY_TMP_DIR)
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(&*LOG_CONFIG_VOLUME_NAME, HBASE_LOG_CONFIG_TMP_DIR)
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(&*LOG_VOLUME_NAME, STACKABLE_LOG_DIR)
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(LISTENER_VOLUME_NAME, LISTENER_VOLUME_DIR)
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_container_ports(ports)
.resources(inputs.resources.clone().into())
.startup_probe(startup_probe)
.liveness_probe(liveness_probe)
.readiness_probe(readiness_probe);

// The HBase container's log config ConfigMap: either the operator-generated one (the
// rolegroup ConfigMap, which carries the automatic `log4j2.properties`) or a user-provided
// custom ConfigMap. This branches on the *validated* logging choice (see `ValidatedLogging`).
let log_config_config_map = match &logging.hbase_container {
ValidatedContainerLogConfigChoice::Custom(config_map_name) => config_map_name.to_string(),
ValidatedContainerLogConfigChoice::Automatic(_) => {
resource_names.role_group_config_map().to_string()
}
};

let mut pod_volumes = vec![
Volume {
name: HBASE_CONFIG_VOLUME_NAME.to_string(),
config_map: Some(ConfigMapVolumeSource {
name: resource_names.role_group_config_map().to_string(),
..Default::default()
}),
..Default::default()
},
Volume {
name: HDFS_DISCOVERY_VOLUME_NAME.to_string(),
config_map: Some(ConfigMapVolumeSource {
name: cluster.cluster_config.hdfs_config_map_name.to_string(),
..Default::default()
}),
..Default::default()
},
VolumeBuilder::new(&*LOG_VOLUME_NAME)
.with_empty_dir(
None::<String>,
Some(product_logging::framework::calculate_log_volume_size_limit(
&[MAX_HBASE_LOG_FILES_SIZE],
)),
)
.build(),
Volume {
name: LOG_CONFIG_VOLUME_NAME.to_string(),
config_map: Some(ConfigMapVolumeSource {
name: log_config_config_map,
..ConfigMapVolumeSource::default()
}),
..Volume::default()
},
];

if cluster.has_kerberos_enabled() {
pod_volumes.extend(
mount_kerberos_volumes(
cluster,
resource_names.metrics_service_name().as_ref(),
&mut hbase_container,
inputs.requested_secret_lifetime,
)
.context(MountKerberosVolumesSnafu)?,
);
}

let volume_claim_templates = match &inputs.listener {
ListenerPlacement::PodVolume(volume) => {
pod_volumes.push(volume.clone());
None
}
ListenerPlacement::ClaimTemplate(claim) => Some(vec![claim.clone()]),
};

let mut containers = vec![hbase_container.build()];

// Vector sidecar shall be the last container in the list.
if let Some(vector_log_config) = &logging.vector_container {
containers.push(vector_container(
&VECTOR_CONTAINER_NAME,
resolved_product_image,
vector_log_config,
&resource_names,
&HBASE_CONFIG_VOLUME_NAME,
&LOG_VOLUME_NAME,
EnvVarSet::new(),
));
}

Ok(ContainersAndVolumes {
containers,
pod_volumes,
volume_claim_templates,
})
}

/// Returns the container command.
fn command() -> Vec<String> {
vec![
"/bin/bash".to_string(),
"-x".to_string(),
"-euo".to_string(),
"pipefail".to_string(),
"-c".to_string(),
]
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *HBASE_CONTAINER_NAME;
let _ = *VECTOR_CONTAINER_NAME;
let _ = *HBASE_CONFIG_VOLUME_NAME;
let _ = *HDFS_DISCOVERY_VOLUME_NAME;
let _ = *LOG_CONFIG_VOLUME_NAME;
let _ = *LOG_VOLUME_NAME;
let _ = *HBASE_CONF_DIR_ENV;
let _ = *HADOOP_CONF_DIR_ENV;
let _ = *REGION_MOVER_OPTS_ENV;
let _ = *RUN_REGION_MOVER_ENV;
let _ = *STACKABLE_LOG_DIR_ENV;
let _ = *CONTAINERDEBUG_LOG_DIRECTORY_ENV;
}
}
27 changes: 0 additions & 27 deletions rust/operator-binary/src/controller/build/graceful_shutdown.rs

This file was deleted.

Loading