Skip to content

Commit a9fbbc8

Browse files
Relax the length constraints of cluster, role and role group name (#1222)
* Relax the length constraints of cluster, role and role group name * Adapt compile-time assertions * Check that ensure_max_length is only called with ASCII resource names
1 parent 00b5d04 commit a9fbbc8

4 files changed

Lines changed: 205 additions & 28 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: 197 additions & 17 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,19 +73,71 @@ 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+
// `concatenated_name` contains only ASCII characters.
81+
let sanitized_name = Self::ensure_max_length(
82+
concatenated_name,
83+
QualifiedRoleGroupName::MAX_LENGTH,
84+
HASH_LENGTH,
85+
);
86+
87+
QualifiedRoleGroupName::from_str(&sanitized_name)
88+
.expect("should be a valid QualifiedRoleGroupName")
89+
}
90+
91+
/// Ensures that the given resource name does not exceed the given maximum length.
92+
/// If required, the resource name is truncated and a hex encoded hash is appended with a dash.
93+
///
94+
/// # Panics
95+
///
96+
/// Panics if `resource_name` contains non-ASCII characters or if
97+
/// `max_length < 1 /* character */ + 1 /* dash */ + hash_length`.
98+
///
99+
/// Kubernetes object names cannot contain non-ASCII characters.
100+
fn ensure_max_length(resource_name: String, max_length: usize, hash_length: usize) -> String {
101+
assert!(resource_name.is_ascii());
102+
assert!(max_length >= 1 /* character */ + 1 /* dash */ + hash_length);
103+
104+
if resource_name.len() <= max_length {
105+
resource_name
106+
} else if hash_length == 0 {
107+
let mut truncated_name = resource_name;
108+
truncated_name.truncate(max_length);
109+
truncated_name
110+
} else {
111+
let mut hash = format!("{:x}", Sha256::digest(resource_name.as_bytes()));
112+
hash.truncate(hash_length);
113+
114+
let mut truncated_name = resource_name;
115+
// Truncate the name so that the hash can be appended without exceeding the maximum
116+
// length.
117+
truncated_name.truncate(max_length - hash_length);
118+
119+
let last_char = truncated_name
120+
.pop()
121+
.expect("should be guaranteed by the assertion above");
122+
let second_to_last_char = truncated_name
123+
.pop()
124+
.expect("should be guaranteed by the assertion above");
125+
126+
// If the truncated name already ends with a dash then do not add another one,
127+
// otherwise replace the last character with a dash.
128+
if second_to_last_char == '-' && last_char != '-' {
129+
format!("{truncated_name}{second_to_last_char}{hash}")
130+
} else {
131+
format!("{truncated_name}{second_to_last_char}-{hash}")
132+
}
133+
}
55134
}
56135

57136
pub fn role_group_config_map(&self) -> ConfigMapName {
58137
// compile-time check
59138
const _: () = assert!(
60139
QualifiedRoleGroupName::MAX_LENGTH <= ConfigMapName::MAX_LENGTH,
61-
"The string `<cluster_name>-<role_name>-<role_group_name>` must not exceed the limit of \
62-
ConfigMap names."
140+
"The string `<qualified_role_group_name>` must not exceed the limit of ConfigMap names."
63141
);
64142
let _ = QualifiedRoleGroupName::IS_RFC_1123_SUBDOMAIN_NAME;
65143

@@ -71,8 +149,8 @@ impl ResourceNames {
71149
// compile-time checks
72150
const _: () = assert!(
73151
QualifiedRoleGroupName::MAX_LENGTH <= StatefulSetName::MAX_LENGTH,
74-
"The string `<cluster_name>-<role_name>-<role_group_name>` must not exceed the \
75-
limit of StatefulSet names."
152+
"The string `<qualified_role_group_name>` must not exceed the limit of StatefulSet \
153+
names."
76154
);
77155
let _ = QualifiedRoleGroupName::IS_RFC_1123_LABEL_NAME;
78156
let _ = QualifiedRoleGroupName::IS_VALID_LABEL_VALUE;
@@ -87,8 +165,8 @@ impl ResourceNames {
87165
// compile-time checks
88166
const _: () = assert!(
89167
QualifiedRoleGroupName::MAX_LENGTH + SUFFIX.len() <= ServiceName::MAX_LENGTH,
90-
"The string `<cluster_name>-<role_name>-<role_group_name>-headless` must not exceed the \
91-
limit of Service names."
168+
"The string `<qualified_role_group_name>-headless` must not exceed the limit of \
169+
Service names."
92170
);
93171
let _ = QualifiedRoleGroupName::IS_RFC_1035_LABEL_NAME;
94172
let _ = QualifiedRoleGroupName::IS_VALID_LABEL_VALUE;
@@ -117,8 +195,7 @@ impl ResourceNames {
117195
// compile-time checks
118196
const _: () = assert!(
119197
QualifiedRoleGroupName::MAX_LENGTH <= ListenerName::MAX_LENGTH,
120-
"The string `<cluster_name>-<role_name>-<role_group_name>` must not exceed the limit of \
121-
Listener names."
198+
"The string `<qualified_role_group_name>` must not exceed the limit of Listener names."
122199
);
123200
let _ = QualifiedRoleGroupName::IS_RFC_1123_SUBDOMAIN_NAME;
124201

@@ -170,4 +247,107 @@ mod tests {
170247
resource_names.listener_name()
171248
);
172249
}
250+
251+
#[test]
252+
fn test_fitting_qualified_role_group_name() {
253+
let cluster_name_length = ClusterName::MAX_LENGTH;
254+
let role_name_and_role_group_name_length = QualifiedRoleGroupName::MAX_LENGTH - cluster_name_length - 2 /* dashes */;
255+
let role_name_length = role_name_and_role_group_name_length / 2;
256+
let role_group_name_length = role_name_and_role_group_name_length - role_name_length;
257+
258+
let resource_names = ResourceNames {
259+
cluster_name: ClusterName::from_str_unsafe(&"c".repeat(cluster_name_length)),
260+
role_name: RoleName::from_str_unsafe(&"r".repeat(role_name_length)),
261+
role_group_name: RoleGroupName::from_str_unsafe(&"g".repeat(role_group_name_length)),
262+
};
263+
264+
let qualified_role_group_name = resource_names.qualified_role_group_name();
265+
266+
assert_eq!(
267+
QualifiedRoleGroupName::MAX_LENGTH,
268+
qualified_role_group_name.to_string().len()
269+
);
270+
assert_eq!(
271+
QualifiedRoleGroupName::from_str_unsafe(
272+
"cccccccccccccccccccccccccccccccccccccccc-rrrrr-ggggg"
273+
),
274+
qualified_role_group_name
275+
);
276+
}
277+
278+
#[test]
279+
fn test_hashed_qualified_role_group_name() {
280+
let resource_names = ResourceNames {
281+
cluster_name: ClusterName::from_str_unsafe(&"c".repeat(ClusterName::MAX_LENGTH)),
282+
role_name: RoleName::from_str_unsafe(&"r".repeat(RoleName::MAX_LENGTH)),
283+
role_group_name: RoleGroupName::from_str_unsafe(&"g".repeat(RoleGroupName::MAX_LENGTH)),
284+
};
285+
286+
let qualified_role_group_name = resource_names.qualified_role_group_name();
287+
288+
assert_eq!(
289+
QualifiedRoleGroupName::MAX_LENGTH,
290+
qualified_role_group_name.to_string().len()
291+
);
292+
assert_eq!(
293+
QualifiedRoleGroupName::from_str_unsafe(
294+
"cccccccccccccccccccccccccccccccccccccccc-rrrr-a12cc0"
295+
),
296+
qualified_role_group_name
297+
);
298+
}
299+
300+
#[test]
301+
fn test_ensure_max_length() {
302+
// empty resource name, no hash length
303+
assert_eq!(
304+
String::new(),
305+
ResourceNames::ensure_max_length(String::new(), 2, 0)
306+
);
307+
308+
// resource_name.len() <= max_length
309+
assert_eq!(
310+
"abcdef".to_owned(),
311+
ResourceNames::ensure_max_length("abcdef".to_owned(), 6, 4)
312+
);
313+
314+
// hash_length == 0
315+
assert_eq!(
316+
"abcdef".to_owned(),
317+
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 0)
318+
);
319+
320+
// hash appended with dash
321+
assert_eq!(
322+
"a-7d1a".to_owned(),
323+
ResourceNames::ensure_max_length("abcdefg".to_owned(), 6, 4)
324+
);
325+
326+
// hash appended without an extra dash
327+
assert_eq!(
328+
"ab-a1b1".to_owned(),
329+
ResourceNames::ensure_max_length("ab-defgh".to_owned(), 7, 4)
330+
);
331+
332+
// hash appended without an extra dash
333+
// In this case, the result is one character shorter than the maximum length.
334+
assert_eq!(
335+
"a-3951".to_owned(),
336+
ResourceNames::ensure_max_length("a-cdefgh".to_owned(), 7, 4)
337+
);
338+
339+
// hash appended without an extra dash
340+
// The two dashes in the given resource name are intentionally kept.
341+
assert_eq!(
342+
"a--f7a0".to_owned(),
343+
ResourceNames::ensure_max_length("a--defgh".to_owned(), 7, 4)
344+
);
345+
346+
// A hash_length longer than the produced hash string may not produce the desired result.
347+
// Just use sensible values!
348+
assert_eq!(
349+
"aaaaaaaaa-d476ce01c3787bcab054a2cf48d6af6dd303a0eb549e21a74125132f79d90c36".to_owned(),
350+
ResourceNames::ensure_max_length("a".repeat(1011), 1010, 1000)
351+
);
352+
}
173353
}

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)