Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
are no longer created with the placeholder `app.kubernetes.io/component: none` and
`app.kubernetes.io/role-group: none` labels.
StatefulSet selectors and volume claim templates are unchanged, so upgrading is non-breaking.
- Make operations infallible where dependent on static inputs ([#824]).

### Fixed

Expand All @@ -39,6 +40,7 @@ All notable changes to this project will be documented in this file.
[#814]: https://github.com/stackabletech/hdfs-operator/pull/814
[#819]: https://github.com/stackabletech/hdfs-operator/pull/819
[#821]: https://github.com/stackabletech/hdfs-operator/pull/821
[#824]: https://github.com/stackabletech/hdfs-operator/pull/824

## [26.7.0] - 2026-07-21

Expand Down
192 changes: 90 additions & 102 deletions rust/operator-binary/src/controller/build/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ pub enum Error {
#[snafu(display("failed to construct JVM arguments fro role {role:?}"))]
ConstructJvmArguments { source: jvm::Error, role: String },

#[snafu(display(
"could not determine any ContainerConfig actions for {container_name:?}. Container not recognized."
))]
UnrecognizedContainerName { container_name: String },

#[snafu(display("failed to build secret volume for {volume_name:?}"))]
BuildSecretVolume {
source: SecretOperatorVolumeSourceBuilderError,
Expand Down Expand Up @@ -347,7 +342,7 @@ impl ContainerConfig {
match role {
HdfsNodeRole::Name => {
// Zookeeper fail over container
let zkfc_container_config = Self::try_from(NameNodeContainer::Zkfc.to_string())?;
let zkfc_container_config = Self::zkfc();
pb.add_volumes(zkfc_container_config.volumes(
merged_config,
&object_name,
Expand All @@ -363,8 +358,7 @@ impl ContainerConfig {
)?);

// Format namenode init container
let format_namenodes_container_config =
Self::try_from(NameNodeContainer::FormatNameNodes.to_string())?;
let format_namenodes_container_config = Self::format_namenodes();
pb.add_volumes(format_namenodes_container_config.volumes(
merged_config,
&object_name,
Expand All @@ -381,8 +375,7 @@ impl ContainerConfig {
)?);

// Format ZooKeeper init container
let format_zookeeper_container_config =
Self::try_from(NameNodeContainer::FormatZooKeeper.to_string())?;
let format_zookeeper_container_config = Self::format_zookeeper();
pb.add_volumes(format_zookeeper_container_config.volumes(
merged_config,
&object_name,
Expand All @@ -400,8 +393,7 @@ impl ContainerConfig {
}
HdfsNodeRole::Data => {
// Wait for namenode init container
let wait_for_namenodes_container_config =
Self::try_from(DataNodeContainer::WaitForNameNodes.to_string())?;
let wait_for_namenodes_container_config = Self::wait_for_namenodes();
pb.add_volumes(wait_for_namenodes_container_config.volumes(
merged_config,
&object_name,
Expand Down Expand Up @@ -436,7 +428,7 @@ impl ContainerConfig {
.build_ephemeral()
.context(BuildListenerVolumeSnafu)?
.volume_claim_template
.unwrap();
.expect("The listener volume source builder always sets a volume claim template.");

let pvcs = vec![
node.resources.storage.data.build_pvc(
Expand All @@ -446,7 +438,9 @@ impl ContainerConfig {
PersistentVolumeClaim {
metadata: ObjectMeta {
name: Some(LISTENER_VOLUME_NAME.to_string()),
..listener.metadata.unwrap()
..listener.metadata.expect(
"The listener volume claim template always carries metadata.",
)
},
spec: Some(listener.spec),
..Default::default()
Expand Down Expand Up @@ -1146,8 +1140,9 @@ impl ContainerConfig {

// Adding this for all containers, as not only the main container needs Kerberos or TLS
if cluster.has_kerberos_enabled() {
volume_mounts
.push(VolumeMountBuilder::new("kerberos", KERBEROS_CONTAINER_PATH).build());
volume_mounts.push(
VolumeMountBuilder::new(&*KERBEROS_VOLUME_NAME, KERBEROS_CONTAINER_PATH).build(),
);
}
if cluster.has_https_enabled() {
// This volume will be propagated by the create-tls-cert-bundle container
Expand Down Expand Up @@ -1419,41 +1414,56 @@ impl From<HdfsNodeRole> for ContainerConfig {
}
}

impl TryFrom<String> for ContainerConfig {
type Error = Error;

fn try_from(container_name: String) -> Result<Self, Self::Error> {
match HdfsNodeRole::from_str(container_name.as_str()) {
Ok(role) => Ok(ContainerConfig::from(role)),
// No hadoop main process container
Err(_) => match container_name {
// namenode side container
name if name == NameNodeContainer::Zkfc.to_string() => Ok(Self::Zkfc {
volume_mounts: ContainerVolumeDirs::try_from(name.as_str())?,
container_name: name,
}),
// namenode init containers
name if name == NameNodeContainer::FormatNameNodes.to_string() => {
Ok(Self::FormatNameNodes {
volume_mounts: ContainerVolumeDirs::try_from(name.as_str())?,
container_name: name,
})
}
name if name == NameNodeContainer::FormatZooKeeper.to_string() => {
Ok(Self::FormatZooKeeper {
volume_mounts: ContainerVolumeDirs::try_from(name.as_str())?,
container_name: name,
})
}
// datanode init containers
name if name == DataNodeContainer::WaitForNameNodes.to_string() => {
Ok(Self::WaitForNameNodes {
volume_mounts: ContainerVolumeDirs::try_from(name.as_str())?,
container_name: name,
})
}
_ => Err(Error::UnrecognizedContainerName { container_name }),
},
impl ContainerConfig {
/// The ZooKeeper fail-over controller side container of the namenodes.
fn zkfc() -> Self {
let container_name = NameNodeContainer::Zkfc.to_string();
Self::Zkfc {
volume_mounts: ContainerVolumeDirs::for_container(
&container_name,
Self::ZKFC_CONFIG_VOLUME_MOUNT_NAME,
Self::ZKFC_LOG_VOLUME_MOUNT_NAME,
),
container_name,
}
}

/// The init container formatting the namenodes.
fn format_namenodes() -> Self {
let container_name = NameNodeContainer::FormatNameNodes.to_string();
Self::FormatNameNodes {
volume_mounts: ContainerVolumeDirs::for_container(
&container_name,
Self::FORMAT_NAMENODES_CONFIG_VOLUME_MOUNT_NAME,
Self::FORMAT_NAMENODES_LOG_VOLUME_MOUNT_NAME,
),
container_name,
}
}

/// The init container formatting ZooKeeper for the namenodes.
fn format_zookeeper() -> Self {
let container_name = NameNodeContainer::FormatZooKeeper.to_string();
Self::FormatZooKeeper {
volume_mounts: ContainerVolumeDirs::for_container(
&container_name,
Self::FORMAT_ZOOKEEPER_CONFIG_VOLUME_MOUNT_NAME,
Self::FORMAT_ZOOKEEPER_LOG_VOLUME_MOUNT_NAME,
),
container_name,
}
}

/// The init container of the datanodes waiting for the namenodes.
fn wait_for_namenodes() -> Self {
let container_name = DataNodeContainer::WaitForNameNodes.to_string();
Self::WaitForNameNodes {
volume_mounts: ContainerVolumeDirs::for_container(
&container_name,
Self::WAIT_FOR_NAMENODES_CONFIG_VOLUME_MOUNT_NAME,
Self::WAIT_FOR_NAMENODES_LOG_VOLUME_MOUNT_NAME,
),
container_name,
}
}
}
Expand Down Expand Up @@ -1546,59 +1556,22 @@ impl From<&HdfsNodeRole> for ContainerVolumeDirs {
}
}

impl TryFrom<&str> for ContainerVolumeDirs {
type Error = Error;

fn try_from(container_name: &str) -> Result<Self, Error> {
if let Ok(role) = HdfsNodeRole::from_str(container_name) {
return Ok(ContainerVolumeDirs::from(role));
}

let (config_mount_name, log_mount_name) = match container_name {
// namenode side container
name if name == NameNodeContainer::Zkfc.to_string() => (
ContainerConfig::ZKFC_CONFIG_VOLUME_MOUNT_NAME.to_string(),
ContainerConfig::ZKFC_LOG_VOLUME_MOUNT_NAME.to_string(),
),
// namenode init containers
name if name == NameNodeContainer::FormatNameNodes.to_string() => (
ContainerConfig::FORMAT_NAMENODES_CONFIG_VOLUME_MOUNT_NAME.to_string(),
ContainerConfig::FORMAT_NAMENODES_LOG_VOLUME_MOUNT_NAME.to_string(),
),
name if name == NameNodeContainer::FormatZooKeeper.to_string() => (
ContainerConfig::FORMAT_ZOOKEEPER_CONFIG_VOLUME_MOUNT_NAME.to_string(),
ContainerConfig::FORMAT_ZOOKEEPER_LOG_VOLUME_MOUNT_NAME.to_string(),
impl ContainerVolumeDirs {
/// The volume dirs of a side or init container with the given fixed name and mount names.
fn for_container(container_name: &str, config_mount_name: &str, log_mount_name: &str) -> Self {
ContainerVolumeDirs {
final_config_dir: format!("{base}/{container_name}", base = Self::NODE_BASE_CONFIG_DIR),
config_mount: format!(
"{base}/{container_name}",
base = Self::NODE_BASE_CONFIG_DIR_MOUNT
),
// datanode init containers
name if name == DataNodeContainer::WaitForNameNodes.to_string() => (
ContainerConfig::WAIT_FOR_NAMENODES_CONFIG_VOLUME_MOUNT_NAME.to_string(),
ContainerConfig::WAIT_FOR_NAMENODES_LOG_VOLUME_MOUNT_NAME.to_string(),
config_mount_name: config_mount_name.to_owned(),
log_mount: format!(
"{base}/{container_name}",
base = Self::NODE_BASE_LOG_DIR_MOUNT
),
_ => {
return Err(Error::UnrecognizedContainerName {
container_name: container_name.to_string(),
});
}
};

let final_config_dir =
format!("{base}/{container_name}", base = Self::NODE_BASE_CONFIG_DIR);
let config_mount = format!(
"{base}/{container_name}",
base = Self::NODE_BASE_CONFIG_DIR_MOUNT
);
let log_mount = format!(
"{base}/{container_name}",
base = Self::NODE_BASE_LOG_DIR_MOUNT
);

Ok(ContainerVolumeDirs {
final_config_dir,
config_mount,
config_mount_name,
log_mount,
log_mount_name,
})
log_mount_name: log_mount_name.to_owned(),
}
}
}

Expand Down Expand Up @@ -1638,3 +1611,18 @@ fn bash_capture_shell_helper(container_name: &str) -> String {
"###
}
}

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

#[test]
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *TLS_STORE_VOLUME_NAME;
let _ = *KERBEROS_VOLUME_NAME;
let _ = *VECTOR_CONTAINER_NAME;
let _ = *VECTOR_CONFIG_VOLUME_NAME;
let _ = *VECTOR_LOG_VOLUME_NAME;
}
}
5 changes: 0 additions & 5 deletions rust/operator-binary/src/controller/build/resource/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ use crate::{

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("failed to build object meta data"))]
ObjectMeta {
source: stackable_operator::builder::meta::Error,
},

#[snafu(display("failed to build roleGroup selector labels"))]
RoleGroupSelectorLabels { source: LabelError },
}
Expand Down
22 changes: 21 additions & 1 deletion rust/operator-binary/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use stackable_operator::{
kube::{Resource, api::ObjectMeta},
v2::{
HasName, HasUid, NameIsValidLabelValue,
role_group_utils::ResourceNames,
role_group_utils::{QualifiedRoleGroupName, ResourceNames},
role_utils::{self, RoleGroupConfig},
types::{
kubernetes::{ConfigMapName, NamespaceName, ServiceName, Uid},
Expand Down Expand Up @@ -211,6 +211,13 @@ impl ValidatedCluster {
role: &HdfsNodeRole,
role_group_name: &RoleGroupName,
) -> ServiceName {
const _: () = assert!(
QualifiedRoleGroupName::MAX_LENGTH <= ServiceName::MAX_LENGTH,
"The string `<qualified_role_group_name>` must not exceed the limit of Service names."
);
let _ = QualifiedRoleGroupName::IS_RFC_1035_LABEL_NAME;
let _ = QualifiedRoleGroupName::IS_VALID_LABEL_VALUE;

ServiceName::from_str(
self.role_group_resource_names(role, role_group_name)
.qualified_role_group_name()
Expand Down Expand Up @@ -337,3 +344,16 @@ impl ValidatedClusterConfig {
pub struct ValidatedRoleConfig {
pub pdb: stackable_operator::commons::pdb::PdbConfig,
}

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

#[test]
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *PRODUCT_NAME;
let _ = *OPERATOR_NAME;
let _ = *CONTROLLER_NAME;
}
}
19 changes: 17 additions & 2 deletions rust/operator-binary/src/crd/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::str::FromStr;
use stackable_operator::{
constant,
shared::time::Duration,
v2::types::{common::Port, kubernetes::VolumeName},
v2::types::{
common::Port,
kubernetes::{ListenerClassName, VolumeName},
},
};

pub const DEFAULT_DFS_REPLICATION_FACTOR: u8 = 3;
Expand All @@ -23,7 +26,7 @@ pub const SERVICE_PORT_NAME_DATA: &str = "data";
pub const SERVICE_PORT_NAME_METRICS: &str = "metrics";
pub const SERVICE_PORT_NAME_JMX_METRICS: &str = "jmx-metrics";

pub const DEFAULT_LISTENER_CLASS: &str = "cluster-internal";
constant!(pub DEFAULT_LISTENER_CLASS: ListenerClassName = "cluster-internal");

pub const DEFAULT_NAME_NODE_METRICS_PORT: Port = Port(8183);
pub const DEFAULT_NAME_NODE_NATIVE_METRICS_HTTP_PORT: Port = Port(9870);
Expand Down Expand Up @@ -91,3 +94,15 @@ pub const DATANODE_ROOT_DATA_DIR_SUFFIX: &str = "/datanode";

constant!(pub LISTENER_VOLUME_NAME: VolumeName = "listener");
pub const LISTENER_VOLUME_DIR: &str = "/stackable/listener";

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

#[test]
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *LISTENER_VOLUME_NAME;
let _ = *DEFAULT_LISTENER_CLASS;
}
}
Loading
Loading