Skip to content
Open
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
4 changes: 3 additions & 1 deletion crates/stackable-operator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ All notable changes to this project will be documented in this file.
### Added

- Add the Cargo feature `kube-cel` that enables the `cel` feature on the `kube` crate ([1259]).
- Add `length_enforcement::ensure_max_length` and `Key::shortened_to_valid_length` helper functions ([#1260]).

[1259]: https://github.com/stackabletech/operator-rs/pull/1259
[#1259]: https://github.com/stackabletech/operator-rs/pull/1259
[#1260]: https://github.com/stackabletech/operator-rs/pull/1260

## [0.115.0] - 2026-08-04

Expand Down
15 changes: 15 additions & 0 deletions crates/stackable-operator/src/kvp/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::{fmt::Display, ops::Deref, str::FromStr, sync::LazyLock};
use regex::Regex;
use snafu::{ResultExt, Snafu, ensure};

use crate::utils::length_enforcement::ensure_max_length;

const KEY_PREFIX_MAX_LEN: usize = 253;
const KEY_NAME_MAX_LEN: usize = 63;

Expand Down Expand Up @@ -135,6 +137,19 @@ impl Deref for Key {
}

impl Key {
/// (Optionally) shortens the `prefix` and `name` to make sure they produce a valid [`Key`].
///
/// See [`ensure_max_length`] for details on the shortening algorithm.
pub fn shortened_to_valid_length(
prefix: impl Into<String>,
name: impl Into<String>,
) -> Result<Self, KeyError> {
let prefix = ensure_max_length(prefix, KEY_PREFIX_MAX_LEN, 8);
let name = ensure_max_length(name, KEY_NAME_MAX_LEN, 8);

Self::from_str(&format!("{prefix}/{name}"))
}

/// Retrieves the key's prefix.
///
/// ```
Expand Down
103 changes: 103 additions & 0 deletions crates/stackable-operator/src/utils/length_enforcement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use sha2::{Digest, Sha256};

/// Ensures that the given input does not exceed the given maximum length.
/// If required, the input is truncated and a hex encoded hash is appended with a dash.
///
/// # Panics
///
/// Panics if `max_length < 1 /* character */ + 1 /* dash */ + hash_length`.
pub fn ensure_max_length(
original: impl Into<String>,
max_length: usize,
hash_length: usize,
) -> String {
assert!(max_length >= 1 /* character */ + 1 /* dash */ + hash_length);

let original = original.into();
if original.len() <= max_length {
original
} else if hash_length == 0 {
let mut truncated_name = original;
truncated_name.truncate(max_length);
truncated_name
} else {
let mut hash = format!("{:x}", Sha256::digest(original.as_bytes()));
hash.truncate(hash_length);

let mut truncated_name = original;
// Truncate the name so that the hash can be appended without exceeding the maximum
// length.
truncated_name.truncate(max_length - hash_length);

let last_char = truncated_name
.pop()
.expect("should be guaranteed by the assertion above");
let second_to_last_char = truncated_name
.pop()
.expect("should be guaranteed by the assertion above");

// If the truncated name already ends with a dash then do not add another one,
// otherwise replace the last character with a dash.
if second_to_last_char == '-' && last_char != '-' {
format!("{truncated_name}{second_to_last_char}{hash}")
} else {
format!("{truncated_name}{second_to_last_char}-{hash}")
}
}
}

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

#[test]
fn test_ensure_max_length() {
// empty resource name, no hash length
assert_eq!(String::new(), ensure_max_length(String::new(), 2, 0));

// resource_name.len() <= max_length
assert_eq!(
"abcdef".to_owned(),
ensure_max_length("abcdef".to_owned(), 6, 4)
);

// hash_length == 0
assert_eq!(
"abcdef".to_owned(),
ensure_max_length("abcdefg".to_owned(), 6, 0)
);

// hash appended with dash
assert_eq!(
"a-7d1a".to_owned(),
ensure_max_length("abcdefg".to_owned(), 6, 4)
);

// hash appended without an extra dash
assert_eq!(
"ab-a1b1".to_owned(),
ensure_max_length("ab-defgh".to_owned(), 7, 4)
);

// hash appended without an extra dash
// In this case, the result is one character shorter than the maximum length.
assert_eq!(
"a-3951".to_owned(),
ensure_max_length("a-cdefgh".to_owned(), 7, 4)
);

// hash appended without an extra dash
// The two dashes in the given resource name are intentionally kept.
assert_eq!(
"a--f7a0".to_owned(),
ensure_max_length("a--defgh".to_owned(), 7, 4)
);

// A hash_length longer than the produced hash string may not produce the desired result.
// Just use sensible values!
assert_eq!(
"aaaaaaaaa-d476ce01c3787bcab054a2cf48d6af6dd303a0eb549e21a74125132f79d90c36".to_owned(),
ensure_max_length("a".repeat(1011), 1010, 1000)
);
}
}
1 change: 1 addition & 0 deletions crates/stackable-operator/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod bash;
pub mod cluster_info;
pub mod crds;
pub mod kubelet;
pub mod length_enforcement;
pub mod logging;
pub mod signal;

Expand Down
107 changes: 4 additions & 103 deletions crates/stackable-operator/src/v2/role_group_utils.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use std::str::FromStr;

use sha2::{Digest, Sha256};

use super::types::{
kubernetes::{
ConfigMapName, DaemonSetName, DeploymentName, ListenerName, ServiceName, StatefulSetName,
},
operator::{ClusterName, RoleGroupName, RoleName},
};
use crate::attributed_string_type;
use crate::{attributed_string_type, utils::length_enforcement::ensure_max_length};

attributed_string_type! {
QualifiedRoleGroupName,
Expand Down Expand Up @@ -80,61 +78,18 @@ impl ResourceNames {
self.cluster_name, self.role_name, self.role_group_name,
);
// `concatenated_name` contains only ASCII characters.
let sanitized_name = Self::ensure_max_length(
assert!(concatenated_name.is_ascii());
let sanitized_name = ensure_max_length(
concatenated_name,
QualifiedRoleGroupName::MAX_LENGTH,
HASH_LENGTH,
);
assert!(sanitized_name.len() <= QualifiedRoleGroupName::MAX_LENGTH);

QualifiedRoleGroupName::from_str(&sanitized_name)
.expect("should be a valid QualifiedRoleGroupName")
}

/// Ensures that the given resource name does not exceed the given maximum length.
/// If required, the resource name is truncated and a hex encoded hash is appended with a dash.
///
/// # Panics
///
/// Panics if `resource_name` contains non-ASCII characters or if
/// `max_length < 1 /* character */ + 1 /* dash */ + hash_length`.
///
/// Kubernetes object names cannot contain non-ASCII characters.
fn ensure_max_length(resource_name: String, max_length: usize, hash_length: usize) -> String {
assert!(resource_name.is_ascii());
assert!(max_length >= 1 /* character */ + 1 /* dash */ + hash_length);

if resource_name.len() <= max_length {
resource_name
} else if hash_length == 0 {
let mut truncated_name = resource_name;
truncated_name.truncate(max_length);
truncated_name
} else {
let mut hash = format!("{:x}", Sha256::digest(resource_name.as_bytes()));
hash.truncate(hash_length);

let mut truncated_name = resource_name;
// Truncate the name so that the hash can be appended without exceeding the maximum
// length.
truncated_name.truncate(max_length - hash_length);

let last_char = truncated_name
.pop()
.expect("should be guaranteed by the assertion above");
let second_to_last_char = truncated_name
.pop()
.expect("should be guaranteed by the assertion above");

// If the truncated name already ends with a dash then do not add another one,
// otherwise replace the last character with a dash.
if second_to_last_char == '-' && last_char != '-' {
format!("{truncated_name}{second_to_last_char}{hash}")
} else {
format!("{truncated_name}{second_to_last_char}-{hash}")
}
}
}

pub fn role_group_config_map(&self) -> ConfigMapName {
// compile-time check
const _: () = assert!(
Expand Down Expand Up @@ -334,58 +289,4 @@ mod tests {
qualified_role_group_name
);
}

#[test]
fn test_ensure_max_length() {
// empty resource name, no hash length
assert_eq!(
String::new(),
ResourceNames::ensure_max_length(String::new(), 2, 0)
);

// resource_name.len() <= max_length
assert_eq!(
"abcdef".to_owned(),
ResourceNames::ensure_max_length("abcdef".to_owned(), 6, 4)
);

// hash_length == 0
assert_eq!(
"abcdef".to_owned(),
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 0)
);

// hash appended with dash
assert_eq!(
"a-7d1a".to_owned(),
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 4)
);

// hash appended without an extra dash
assert_eq!(
"ab-a1b1".to_owned(),
ResourceNames::ensure_max_length("ab-defgh".to_owned(), 7, 4)
);

// hash appended without an extra dash
// In this case, the result is one character shorter than the maximum length.
assert_eq!(
"a-3951".to_owned(),
ResourceNames::ensure_max_length("a-cdefgh".to_owned(), 7, 4)
);

// hash appended without an extra dash
// The two dashes in the given resource name are intentionally kept.
assert_eq!(
"a--f7a0".to_owned(),
ResourceNames::ensure_max_length("a--defgh".to_owned(), 7, 4)
);

// A hash_length longer than the produced hash string may not produce the desired result.
// Just use sensible values!
assert_eq!(
"aaaaaaaaa-d476ce01c3787bcab054a2cf48d6af6dd303a0eb549e21a74125132f79d90c36".to_owned(),
ResourceNames::ensure_max_length("a".repeat(1011), 1010, 1000)
);
}
}
Loading