Skip to content

Commit 5e36ccf

Browse files
Relax the length constraints of cluster, role and role group name
1 parent 1e8099f commit 5e36ccf

4 files changed

Lines changed: 194 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/stackable-operator/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ semver.workspace = true
4949
serde_json.workspace = true
5050
serde_yaml.workspace = true
5151
serde.workspace = true
52+
sha2.workspace = true
5253
snafu.workspace = true
5354
strum.workspace = true
5455
tokio.workspace = true

crates/stackable-operator/src/v2/role_group_utils.rs

Lines changed: 186 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::str::FromStr;
22

3+
use sha2::{Digest, Sha256};
4+
35
use super::types::{
46
kubernetes::{ConfigMapName, ListenerName, ServiceName, StatefulSetName},
57
operator::{ClusterName, RoleGroupName, RoleName},
@@ -27,18 +29,42 @@ pub struct ResourceNames {
2729
impl ResourceNames {
2830
/// Creates a qualified role group name in the format
2931
/// `<cluster_name>-<role_name>-<role_group_name>`
30-
fn qualified_role_group_name(&self) -> QualifiedRoleGroupName {
32+
///
33+
/// If the result would exceed the maximum length of qualified role group names, then it is
34+
/// truncated and a hash is appended. The maximum length of the cluster name is short enough,
35+
/// so that a part of the role name is always rendered. The role group name is barely used and
36+
/// often set to "default", so that the qualified role group name is still meaningful:
37+
///
38+
/// ```rust
39+
/// # use std::str::FromStr;
40+
/// # use stackable_operator::v2::role_group_utils::ResourceNames;
41+
/// # use stackable_operator::v2::types::operator::{ClusterName, RoleGroupName, RoleName};
42+
///
43+
/// let resource_names = ResourceNames {
44+
/// cluster_name: ClusterName::from_str("an-exceptional-long-cluster-name").unwrap(),
45+
/// role_name: RoleName::from_str("dagprocessor").unwrap(),
46+
/// role_group_name: RoleGroupName::from_str("default").unwrap(),
47+
/// };
48+
///
49+
/// assert_eq!(
50+
/// "an-exceptional-long-cluster-name-dagprocessor-6cc08b",
51+
/// resource_names.qualified_role_group_name().to_string()
52+
/// );
53+
/// ```
54+
pub fn qualified_role_group_name(&self) -> QualifiedRoleGroupName {
3155
// compile-time checks
56+
const HASH_LENGTH: usize = 6;
57+
58+
// At least the cluster name should be short enough to not be replaced by the hash.
3259
const _: () = assert!(
3360
ClusterName::MAX_LENGTH
3461
+ 1 // dash
35-
+ RoleName::MAX_LENGTH
36-
+ 1 // dash
37-
+ RoleGroupName::MAX_LENGTH
62+
+ HASH_LENGTH
3863
<= QualifiedRoleGroupName::MAX_LENGTH,
39-
"The string `<cluster_name>-<role_name>-<role_group_name>` must not exceed the limit \
40-
of RFC 1035 label names."
64+
"The string `<cluster_name>-<hash>` must not exceed the limit of qualified role group \
65+
names."
4166
);
67+
4268
// qualified_role_group_name is only an RFC 1035 label name if it starts with an
4369
// alphabetic character, therefore cluster_name must also be an RFC 1035 label name.
4470
// role_name and role_group_name and the middle of the qualified_role_group_name can
@@ -47,11 +73,59 @@ impl ResourceNames {
4773
let _ = RoleName::IS_RFC_1123_LABEL_NAME;
4874
let _ = RoleGroupName::IS_RFC_1123_LABEL_NAME;
4975

50-
QualifiedRoleGroupName::from_str(&format!(
76+
let concatenated_name = format!(
5177
"{}-{}-{}",
5278
self.cluster_name, self.role_name, self.role_group_name,
53-
))
54-
.expect("should be a valid QualifiedRoleGroupName")
79+
);
80+
let sanitized_name = Self::ensure_max_length(
81+
concatenated_name,
82+
QualifiedRoleGroupName::MAX_LENGTH,
83+
HASH_LENGTH,
84+
);
85+
86+
QualifiedRoleGroupName::from_str(&sanitized_name)
87+
.expect("should be a valid QualifiedRoleGroupName")
88+
}
89+
90+
/// Ensures that the given resource name does not exceed the given maximum length.
91+
/// If required, the resource name is truncated and a hex encoded hash is appended with a dash.
92+
///
93+
/// # Panics
94+
///
95+
/// Panics if `max_length < 1 /* character */ + 1 /* dash */ + hash_length`.
96+
fn ensure_max_length(resource_name: String, max_length: usize, hash_length: usize) -> String {
97+
assert!(max_length >= 1 /* character */ + 1 /* dash */ + hash_length);
98+
99+
if resource_name.len() <= max_length {
100+
resource_name
101+
} else if hash_length == 0 {
102+
let mut truncated_name = resource_name;
103+
truncated_name.truncate(max_length);
104+
truncated_name
105+
} else {
106+
let mut hash = format!("{:x}", Sha256::digest(resource_name.as_bytes()));
107+
hash.truncate(hash_length);
108+
109+
let mut truncated_name = resource_name;
110+
// Truncate the name so that the hash can be appended without exceeding the maximum
111+
// length.
112+
truncated_name.truncate(max_length - hash_length);
113+
114+
let last_char = truncated_name
115+
.pop()
116+
.expect("should be guaranteed by the assertion above");
117+
let second_to_last_char = truncated_name
118+
.pop()
119+
.expect("should be guaranteed by the assertion above");
120+
121+
// If the truncated name already ends with a dash then do not add another one,
122+
// otherwise replace the last character with a dash.
123+
if second_to_last_char == '-' && last_char != '-' {
124+
format!("{truncated_name}{second_to_last_char}{hash}")
125+
} else {
126+
format!("{truncated_name}{second_to_last_char}-{hash}")
127+
}
128+
}
55129
}
56130

57131
pub fn role_group_config_map(&self) -> ConfigMapName {
@@ -150,4 +224,107 @@ mod tests {
150224
resource_names.listener_name()
151225
);
152226
}
227+
228+
#[test]
229+
fn test_fitting_qualified_role_group_name() {
230+
let cluster_name_length = ClusterName::MAX_LENGTH;
231+
let role_name_and_role_group_name_length = QualifiedRoleGroupName::MAX_LENGTH - cluster_name_length - 2 /* dashes */;
232+
let role_name_length = role_name_and_role_group_name_length / 2;
233+
let role_group_name_length = role_name_and_role_group_name_length - role_name_length;
234+
235+
let resource_names = ResourceNames {
236+
cluster_name: ClusterName::from_str_unsafe(&"c".repeat(cluster_name_length)),
237+
role_name: RoleName::from_str_unsafe(&"r".repeat(role_name_length)),
238+
role_group_name: RoleGroupName::from_str_unsafe(&"g".repeat(role_group_name_length)),
239+
};
240+
241+
let qualified_role_group_name = resource_names.qualified_role_group_name();
242+
243+
assert_eq!(
244+
QualifiedRoleGroupName::MAX_LENGTH,
245+
qualified_role_group_name.to_string().len()
246+
);
247+
assert_eq!(
248+
QualifiedRoleGroupName::from_str_unsafe(
249+
"cccccccccccccccccccccccccccccccccccccccc-rrrrr-ggggg"
250+
),
251+
qualified_role_group_name
252+
);
253+
}
254+
255+
#[test]
256+
fn test_hashed_qualified_role_group_name() {
257+
let resource_names = ResourceNames {
258+
cluster_name: ClusterName::from_str_unsafe(&"c".repeat(ClusterName::MAX_LENGTH)),
259+
role_name: RoleName::from_str_unsafe(&"r".repeat(RoleName::MAX_LENGTH)),
260+
role_group_name: RoleGroupName::from_str_unsafe(&"g".repeat(RoleGroupName::MAX_LENGTH)),
261+
};
262+
263+
let qualified_role_group_name = resource_names.qualified_role_group_name();
264+
265+
assert_eq!(
266+
QualifiedRoleGroupName::MAX_LENGTH,
267+
qualified_role_group_name.to_string().len()
268+
);
269+
assert_eq!(
270+
QualifiedRoleGroupName::from_str_unsafe(
271+
"cccccccccccccccccccccccccccccccccccccccc-rrrr-a12cc0"
272+
),
273+
qualified_role_group_name
274+
);
275+
}
276+
277+
#[test]
278+
fn test_ensure_max_length() {
279+
// empty resource name, no hash length
280+
assert_eq!(
281+
String::new(),
282+
ResourceNames::ensure_max_length(String::new(), 2, 0)
283+
);
284+
285+
// resource_name.len() <= max_length
286+
assert_eq!(
287+
"abcdef".to_owned(),
288+
ResourceNames::ensure_max_length("abcdef".to_owned(), 6, 4)
289+
);
290+
291+
// hash_length == 0
292+
assert_eq!(
293+
"abcdef".to_owned(),
294+
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 0)
295+
);
296+
297+
// hash appended with dash
298+
assert_eq!(
299+
"a-7d1a".to_owned(),
300+
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 4)
301+
);
302+
303+
// hash appended without an extra dash
304+
assert_eq!(
305+
"ab-a1b1".to_owned(),
306+
ResourceNames::ensure_max_length("ab-defgh".to_owned(), 7, 4)
307+
);
308+
309+
// hash appended without an extra dash
310+
// In this case, the result is one character shorter than the maximum length.
311+
assert_eq!(
312+
"a-3951".to_owned(),
313+
ResourceNames::ensure_max_length("a-cdefgh".to_owned(), 7, 4)
314+
);
315+
316+
// hash appended without an extra dash
317+
// The two dashes in the given resource name are intentionally kept.
318+
assert_eq!(
319+
"a--f7a0".to_owned(),
320+
ResourceNames::ensure_max_length("a--defgh".to_owned(), 7, 4)
321+
);
322+
323+
// A hash_length longer than the produced hash string may not produce the desired result.
324+
// Just use sensible values!
325+
assert_eq!(
326+
"aaaaaaaaa-d476ce01c3787bcab054a2cf48d6af6dd303a0eb549e21a74125132f79d90c36".to_owned(),
327+
ResourceNames::ensure_max_length("a".repeat(1011), 1010, 1000)
328+
);
329+
}
153330
}

crates/stackable-operator/src/v2/types/operator.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ attributed_string_type! {
2626
ClusterName,
2727
"The name of a cluster/stacklet",
2828
"my-opensearch-cluster",
29-
// Suffixes are added to produce resource names. According compile-time checks ensure that
30-
// max_length cannot be set higher.
31-
(max_length = 24),
29+
// Suffixes are added to produce resource names.
30+
//
31+
// 40 characters for cluster names should be sufficient and still allow the operators to append
32+
// custom suffixes to build resource names. Increasing this value could break existing operator
33+
// code.
34+
(max_length = 40),
3235
is_rfc_1035_label_name,
3336
is_valid_label_value
3437
}
@@ -51,10 +54,6 @@ attributed_string_type! {
5154
RoleGroupName,
5255
"The name of a role-group name",
5356
"cluster-manager",
54-
// The role-group name is used to produce resource names. To make sure that all resource names
55-
// are valid, max_length is restricted. Compile-time checks ensure that max_length cannot be
56-
// set higher if not other names like the RoleName are set lower accordingly.
57-
(max_length = 16),
5857
is_rfc_1123_label_name,
5958
is_valid_label_value
6059
}
@@ -63,10 +62,6 @@ attributed_string_type! {
6362
RoleName,
6463
"The name of a role name",
6564
"nodes",
66-
// The role name is used to produce resource names. To make sure that all resource names are
67-
// valid, max_length is restricted. Compile-time checks ensure that max_length cannot be set
68-
// higher if not other names like the RoleGroupName are set lower accordingly.
69-
(max_length = 10),
7065
is_rfc_1123_label_name,
7166
is_valid_label_value
7267
}

0 commit comments

Comments
 (0)