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,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}
0 commit comments