11use std:: str:: FromStr ;
22
3+ use sha2:: { Digest , Sha256 } ;
4+
35use super :: types:: {
46 kubernetes:: { ConfigMapName , ListenerName , ServiceName , StatefulSetName } ,
57 operator:: { ClusterName , RoleGroupName , RoleName } ,
@@ -27,18 +29,42 @@ pub struct ResourceNames {
2729impl 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}
0 commit comments