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 @@ -23,6 +23,7 @@ All notable changes to this project will be documented in this file.
StatefulSets created by older operator versions cannot be updated in place: after the
operator upgrade, delete each metastore StatefulSet so that the operator immediately recreates it with
the new labels ([#748]).
- Make operations infallible where dependent on static inputs ([#759]).

### Fixed

Expand All @@ -42,6 +43,7 @@ All notable changes to this project will be documented in this file.
[#741]: https://github.com/stackabletech/hive-operator/pull/741
[#748]: https://github.com/stackabletech/hive-operator/pull/748
[#754]: https://github.com/stackabletech/hive-operator/pull/754
[#759]: https://github.com/stackabletech/hive-operator/pull/759

## [26.7.0] - 2026-07-21

Expand Down
15 changes: 9 additions & 6 deletions rust/operator-binary/src/controller/build/kerberos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ pub enum Error {

#[snafu(display("failed to add needed volume"))]
AddVolume { source: builder::pod::Error },

#[snafu(display("failed to add needed volumeMount"))]
AddVolumeMount {
source: builder::pod::container::Error,
},
}

/// Adds the Kerberos secret-operator volume (providing `krb5.conf` and `keytab`) to the pod
/// builder and mounts it into the container at [`STACKABLE_KERBEROS_DIR`]. Does nothing when
/// Kerberos is disabled.
///
/// # Panics
///
/// Panics if the volume mounts cannot be added to the container builder. Only call this on a
/// container builder whose mount paths are still distinct from the ones added here.
pub fn add_kerberos_pod_config(
cluster: &ValidatedCluster,
role: &HiveRole,
Expand All @@ -81,7 +84,7 @@ pub fn add_kerberos_pod_config(
)
.context(AddVolumeSnafu)?;
cb.add_volume_mount(&*KERBEROS_VOLUME_NAME, STACKABLE_KERBEROS_DIR)
.context(AddVolumeMountSnafu)?;
.expect("The mount paths are statically defined and there should be no duplicates.");
}

Ok(())
Expand Down
11 changes: 11 additions & 0 deletions rust/operator-binary/src/controller/build/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,14 @@ pub fn build_opa_tls_ca_cert_mount_path(opa: &ResolvedOpaConfig) -> Option<Strin
.as_ref()
.map(|_| format!("/stackable/secrets/{}", *OPA_TLS_VOLUME_NAME))
}

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

#[test]
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *OPA_TLS_VOLUME_NAME;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ fn cluster_object_ref(cluster: &ValidatedCluster) -> ObjectRef<v1alpha1::HiveClu
/// Takes the bare cluster name (not [`ValidatedCluster`]) so the dereference step, which runs
/// before validation, can derive the same name.
pub fn discovery_config_map_name(cluster_name: &ClusterName) -> ConfigMapName {
const _: () = assert!(
ClusterName::MAX_LENGTH <= ConfigMapName::MAX_LENGTH,
"The string `<cluster_name>` must not exceed the limit of ConfigMap names."
);
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;

ConfigMapName::from_str(cluster_name.as_ref())
.expect("a valid cluster name is a valid ConfigMap name")
}
Expand Down
18 changes: 12 additions & 6 deletions rust/operator-binary/src/controller/build/resource/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use stackable_operator::{
crd::listener::v1alpha1::{Listener, ListenerIngress, ListenerPort, ListenerSpec},
v2::types::{
kubernetes::{ListenerClassName, ListenerName},
operator::ClusterName,
operator::{ClusterName, RoleName},
},
};

Expand Down Expand Up @@ -48,11 +48,17 @@ pub fn build_listener_connection_string(
/// Takes the bare cluster name (not [`ValidatedCluster`]) so the dereference step, which runs
/// before validation, can derive the same name.
pub fn role_listener_name(cluster_name: &ClusterName, hive_role: &HiveRole) -> ListenerName {
ListenerName::from_str(&format!(
"{cluster_name}-{hive_role}",
hive_role = **hive_role
))
.expect("the role listener name is a valid Listener name")
const _: () = assert!(
ClusterName::MAX_LENGTH + 1 /* dash */ + RoleName::MAX_LENGTH <= ListenerName::MAX_LENGTH,
"The string `<cluster_name>-<role_name>` must not exceed the limit of Listener names."
);
// Both halves are RFC 1123 labels joined by a dash, which is a valid RFC 1123 subdomain.
let _ = ClusterName::IS_RFC_1123_SUBDOMAIN_NAME;
let _ = RoleName::IS_RFC_1123_LABEL_NAME;

let role_name: &RoleName = hive_role;
ListenerName::from_str(&format!("{cluster_name}-{role_name}"))
.expect("The role listener name is a valid Listener name.")
}

// Designed to build a listener per role
Expand Down
36 changes: 21 additions & 15 deletions rust/operator-binary/src/controller/build/resource/statefulset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ pub(crate) fn build_metastore_rolegroup_statefulset(

let mut pod_builder = PodBuilder::new();

// Operator-managed volume mounts use constant mount paths, so they cannot collide with each
// other and those adds are infallible. Volume adds stay fallible. The S3 volumes and mounts
// (named after the user's SecretClasses) are added last, after every operator-managed one.
if let Some(hdfs) = &cluster.cluster_config.hdfs {
pod_builder
.add_volume(
Expand All @@ -207,16 +210,7 @@ pub(crate) fn build_metastore_rolegroup_statefulset(
.context(AddVolumeSnafu)?;
container_builder
.add_volume_mount(&*HDFS_DISCOVERY_VOLUME_NAME, HDFS_CONFIG_MOUNT_DIR)
.context(AddVolumeMountSnafu)?;
}

if let Some(s3) = s3_connection {
s3.add_volumes_and_mounts(&mut pod_builder, vec![&mut container_builder])
.context(ConfigureS3ConnectionSnafu)?;

if s3.tls.uses_tls() && !s3.tls.uses_tls_verification() {
S3TlsNoVerificationNotSupportedSnafu.fail()?;
}
.expect("The mount paths are statically defined and there should be no duplicates.");
}

// Add OPA TLS certs if configured
Expand Down Expand Up @@ -303,19 +297,19 @@ pub(crate) fn build_metastore_rolegroup_statefulset(
hive_opa_config,
))
.add_volume_mount(&*STACKABLE_CONFIG_DIR_NAME, STACKABLE_CONFIG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(
&*STACKABLE_CONFIG_MOUNT_DIR_NAME,
STACKABLE_CONFIG_MOUNT_DIR,
)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(&*STACKABLE_LOG_DIR_NAME, STACKABLE_LOG_DIR)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_volume_mount(
&*STACKABLE_LOG_CONFIG_MOUNT_DIR_NAME,
STACKABLE_LOG_CONFIG_MOUNT_DIR,
)
.context(AddVolumeMountSnafu)?
.expect("The mount paths are statically defined and there should be no duplicates.")
.add_container_port(HIVE_PORT_NAME, HIVE_PORT.into())
.add_container_port(METRICS_PORT_NAME, METRICS_PORT.into())
.resources(merged_config.resources.clone().into())
Expand Down Expand Up @@ -372,7 +366,7 @@ pub(crate) fn build_metastore_rolegroup_statefulset(

container_builder
.add_volume_mount(&*LISTENER_PVC_NAME, LISTENER_VOLUME_DIR)
.context(AddVolumeMountSnafu)?;
.expect("The mount paths are statically defined and there should be no duplicates.");

pod_builder
.metadata(metadata)
Expand Down Expand Up @@ -442,6 +436,18 @@ pub(crate) fn build_metastore_rolegroup_statefulset(
.context(AddKerberosConfigSnafu)?;
}

// S3 volumes and mounts last: their names and mount paths come from the user's SecretClasses,
// so they can collide with the operator-managed ones above. Adding them after every
// operator-managed mount keeps the mount expects above safe from user-derived duplicates.
if let Some(s3) = s3_connection {
s3.add_volumes_and_mounts(&mut pod_builder, vec![&mut *container_builder])
.context(ConfigureS3ConnectionSnafu)?;

if s3.tls.uses_tls() && !s3.tls.uses_tls_verification() {
S3TlsNoVerificationNotSupportedSnafu.fail()?;
}
}

// this is the main container
pod_builder.add_container(container_builder.build());

Expand Down
20 changes: 9 additions & 11 deletions rust/operator-binary/src/crd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub use product_logging::spec::{
};
use security::AuthenticationConfig;
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use stackable_operator::{
commons::{
affinity::StackableAffinity,
Expand Down Expand Up @@ -78,12 +77,12 @@ pub const STACKABLE_TRUST_STORE: &str = "/stackable/truststore.p12";
pub const STACKABLE_TRUST_STORE_PASSWORD: &str = "changeit";

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

// used by crds to define a default listener_class name
/// Serde default for `listenerClass`. Kept as a function because `#[serde(default = "...")]`
/// requires a function path.
pub fn metastore_default_listener_class() -> ListenerClassName {
ListenerClassName::from_str(DEFAULT_LISTENER_CLASS)
.expect("the default listener class is a valid listener class name")
DEFAULT_LISTENER_CLASS.clone()
}

const DEFAULT_METASTORE_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_minutes_unchecked(5);
Expand All @@ -100,12 +99,6 @@ pub type HiveRoleType = Role<
pub type HiveRoleGroupType =
RoleGroup<MetaStoreConfigFragment, JavaCommonConfig, v1alpha1::HiveConfigOverrides>;

#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("the role {role} is not defined"))]
CannotRetrieveHiveRole { role: String },
}

#[versioned(
version(name = "v1alpha1"),
crates(
Expand Down Expand Up @@ -410,6 +403,11 @@ mod tests {
fn test_constants() {
// Test that dereferencing the constants does not panic.
let _ = *METASTORE_ROLE_NAME;
let _ = *DEFAULT_LISTENER_CLASS;
let _ = *STACKABLE_CONFIG_DIR_NAME;
let _ = *STACKABLE_CONFIG_MOUNT_DIR_NAME;
let _ = *STACKABLE_LOG_DIR_NAME;
let _ = *STACKABLE_LOG_CONFIG_MOUNT_DIR_NAME;
}

impl RoundtripTestData for v1alpha1::HiveClusterSpec {
Expand Down
63 changes: 33 additions & 30 deletions tests/templates/kuttl/smoke/60-assert.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ spec:
- -euo
- pipefail
- -c
# Env var order is deterministic: operator-rs `EnvVarSet` sorts referenced variables before the
# variables that reference them via `$(NAME)`. Without such references (the case here) the
# order is alphabetical.
env:
- name: COMMON_VAR
value: group-value
Expand Down Expand Up @@ -266,12 +269,6 @@ spec:
cpu: 250m
memory: 768Mi
volumeMounts:
- mountPath: /stackable/secrets/test-hive-s3-secret-class
name: test-hive-s3-secret-class-s3-credentials
{% if test_scenario['values']['s3-use-tls'] == 'true' %}
- mountPath: /stackable/secrets/minio-tls-certificates
name: minio-tls-certificates-ca-cert
{% endif %}
{% if test_scenario['values']['opa-use-tls'] == 'true' %}
- mountPath: /stackable/secrets/opa-tls
name: opa-tls
Expand All @@ -286,6 +283,12 @@ spec:
name: log-config-mount
- mountPath: /stackable/listener
name: listener
- mountPath: /stackable/secrets/test-hive-s3-secret-class
name: test-hive-s3-secret-class-s3-credentials
{% if test_scenario['values']['s3-use-tls'] == 'true' %}
- mountPath: /stackable/secrets/minio-tls-certificates
name: minio-tls-certificates-ca-cert
{% endif %}
{% if lookup('env', 'VECTOR_AGGREGATOR') %}
- args:
- |-
Expand Down Expand Up @@ -357,12 +360,12 @@ spec:
serviceAccountName: hive-serviceaccount
terminationGracePeriodSeconds: 300
volumes:
{% if test_scenario['values']['opa-use-tls'] == 'true' %}
- ephemeral:
volumeClaimTemplate:
metadata:
annotations:
secrets.stackable.tech/class: test-hive-s3-secret-class
secrets.stackable.tech/provision-parts: public-private
secrets.stackable.tech/provision-parts: public
spec:
accessModes:
- ReadWriteOnce
Expand All @@ -371,14 +374,28 @@ spec:
storage: "1"
storageClassName: secrets.stackable.tech
volumeMode: Filesystem
name: test-hive-s3-secret-class-s3-credentials
{% if test_scenario['values']['s3-use-tls'] == 'true' %}
name: opa-tls
{% endif %}
- emptyDir:
sizeLimit: 10Mi
name: config
- configMap:
defaultMode: 420
name: hive-metastore-default
name: config-mount
- emptyDir:
sizeLimit: 30Mi
name: log
- configMap:
defaultMode: 420
name: hive-metastore-default
name: log-config-mount
- ephemeral:
volumeClaimTemplate:
metadata:
annotations:
secrets.stackable.tech/class: minio-tls-certificates
secrets.stackable.tech/provision-parts: public
secrets.stackable.tech/class: test-hive-s3-secret-class
secrets.stackable.tech/provision-parts: public-private
spec:
accessModes:
- ReadWriteOnce
Expand All @@ -387,13 +404,13 @@ spec:
storage: "1"
storageClassName: secrets.stackable.tech
volumeMode: Filesystem
name: minio-tls-certificates-ca-cert
{% endif %}
{% if test_scenario['values']['opa-use-tls'] == 'true' %}
name: test-hive-s3-secret-class-s3-credentials
{% if test_scenario['values']['s3-use-tls'] == 'true' %}
- ephemeral:
volumeClaimTemplate:
metadata:
annotations:
secrets.stackable.tech/class: minio-tls-certificates
secrets.stackable.tech/provision-parts: public
spec:
accessModes:
Expand All @@ -403,22 +420,8 @@ spec:
storage: "1"
storageClassName: secrets.stackable.tech
volumeMode: Filesystem
name: opa-tls
name: minio-tls-certificates-ca-cert
{% endif %}
- emptyDir:
sizeLimit: 10Mi
name: config
- configMap:
defaultMode: 420
name: hive-metastore-default
name: config-mount
- emptyDir:
sizeLimit: 30Mi
name: log
- configMap:
defaultMode: 420
name: hive-metastore-default
name: log-config-mount
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
Expand Down
Loading