1
- use std:: {
2
- env,
3
- io:: { self , BufRead } ,
4
- path:: Path ,
5
- sync:: OnceLock ,
6
- } ;
1
+ use std:: { env, path:: Path , str:: FromStr , sync:: OnceLock } ;
7
2
8
3
use snafu:: { OptionExt , ResultExt , Snafu } ;
9
4
10
5
use crate :: commons:: networking:: DomainName ;
11
6
12
- // Env vars
13
7
const KUBERNETES_CLUSTER_DOMAIN_ENV : & str = "KUBERNETES_CLUSTER_DOMAIN" ;
14
8
const KUBERNETES_SERVICE_HOST_ENV : & str = "KUBERNETES_SERVICE_HOST" ;
15
- // Misc
9
+
16
10
const KUBERNETES_CLUSTER_DOMAIN_DEFAULT : & str = "cluster.local" ;
17
11
const RESOLVE_CONF_FILE_PATH : & str = "/etc/resolv.conf" ;
18
12
13
+ // TODO (@Techassi): Do we even need this many variants? Can we get rid of a bunch of variants and
14
+ // fall back to defaults instead? Also trace the errors
19
15
#[ derive( Debug , Snafu ) ]
20
16
pub enum Error {
21
- #[ snafu( display( "Env var '{name}' does not exist." ) ) ]
22
- EnvVarNotFound { source : env:: VarError , name : String } ,
23
-
24
- #[ snafu( display( "Could not find '{resolve_conf_file_path}'." ) ) ]
25
- ResolvConfNotFound {
26
- source : io:: Error ,
27
- resolve_conf_file_path : String ,
28
- } ,
17
+ #[ snafu( display( "failed to read resolv.conf" ) ) ]
18
+ ReadResolvConfFile { source : std:: io:: Error } ,
29
19
30
- #[ snafu( display( "The provided cluster domain ' {cluster_domain}' is not valid. " ) ) ]
31
- InvalidDomain {
20
+ #[ snafu( display( "failed to parse {cluster_domain:?} as cluster domain " ) ) ]
21
+ ParseDomainName {
32
22
source : crate :: validation:: Errors ,
33
23
cluster_domain : String ,
34
24
} ,
35
25
36
- #[ snafu( display( "No 'search' entries found in '{RESOLVE_CONF_FILE_PATH}'. " ) ) ]
37
- SearchEntryNotFound { resolve_conf_file_path : String } ,
26
+ #[ snafu( display( "No 'search' entries found in" ) ) ]
27
+ SearchEntryNotFound ,
38
28
39
29
#[ snafu( display( "Could not trim search entry in '{search_entry_line}'." ) ) ]
40
30
TrimSearchEntryFailed { search_entry_line : String } ,
@@ -73,152 +63,75 @@ pub enum Error {
73
63
/// ```
74
64
pub static KUBERNETES_CLUSTER_DOMAIN : OnceLock < DomainName > = OnceLock :: new ( ) ;
75
65
76
- pub ( crate ) fn resolve_kubernetes_cluster_domain ( ) -> Result < DomainName , Error > {
66
+ pub ( crate ) fn retrieve_cluster_domain ( ) -> Result < DomainName , Error > {
77
67
// 1. Read KUBERNETES_CLUSTER_DOMAIN env var
78
68
tracing:: info!( "Trying to determine the Kubernetes cluster domain..." ) ;
79
- match read_env_var ( KUBERNETES_CLUSTER_DOMAIN_ENV ) {
80
- Ok ( cluster_domain) => {
69
+
70
+ match env:: var ( KUBERNETES_CLUSTER_DOMAIN_ENV ) {
71
+ Ok ( cluster_domain) if !cluster_domain. is_empty ( ) => {
81
72
tracing:: info!( "Using Kubernetes cluster domain: '{cluster_domain}'" ) ;
82
- return cluster_domain
83
- . clone ( )
84
- . try_into ( )
85
- . context ( InvalidDomainSnafu { cluster_domain } ) ;
73
+ return DomainName :: from_str ( & cluster_domain)
74
+ . context ( ParseDomainNameSnafu { cluster_domain } ) ;
86
75
}
87
- Err ( _ ) => {
88
- tracing:: info!( "The env var '{KUBERNETES_CLUSTER_DOMAIN_ENV}' is not set. " ) ;
76
+ _ => {
77
+ tracing:: info!( "The env var '{KUBERNETES_CLUSTER_DOMAIN_ENV}' is not set or empty " ) ;
89
78
}
90
79
} ;
91
80
92
- // 2. If no env var is set, check if we run in a clusterized (Kubernetes/Openshift) enviroment
81
+ // 2. If no env var is set, check if we run in a clustered (Kubernetes/Openshift) environment
93
82
// by checking if KUBERNETES_SERVICE_HOST is set: If not default to 'cluster.local'.
94
83
tracing:: info!( "Trying to determine the operator runtime environment..." ) ;
95
- if read_env_var ( KUBERNETES_SERVICE_HOST_ENV ) . is_err ( ) {
96
- tracing:: info!( "The env var '{KUBERNETES_SERVICE_HOST_ENV}' is not set. This means we do not run in Kubernetes / Openshift. Defaulting cluster domain to '{KUBERNETES_CLUSTER_DOMAIN_DEFAULT}'." ) ;
97
- return KUBERNETES_CLUSTER_DOMAIN_DEFAULT
98
- . to_string ( )
99
- . try_into ( )
100
- . context ( InvalidDomainSnafu {
101
- cluster_domain : KUBERNETES_CLUSTER_DOMAIN_DEFAULT . to_string ( ) ,
102
- } ) ;
103
- }
104
84
105
- // 3. Read and parse 'resolv.conf'. We are looking for the last "search" entry and filter for the shortest
106
- // element in that search line
107
- tracing:: info!(
108
- "Running in clusterized environment. Attempting to parse '{RESOLVE_CONF_FILE_PATH}'..."
109
- ) ;
110
- let resolve_conf_lines =
111
- read_file_from_path ( RESOLVE_CONF_FILE_PATH ) . context ( ResolvConfNotFoundSnafu {
112
- resolve_conf_file_path : RESOLVE_CONF_FILE_PATH . to_string ( ) ,
113
- } ) ?;
85
+ match env:: var ( KUBERNETES_SERVICE_HOST_ENV ) {
86
+ Ok ( _) => {
87
+ let cluster_domain = retrieve_cluster_domain_from_resolv_conf ( RESOLVE_CONF_FILE_PATH ) ?;
114
88
115
- let cluster_domain = parse_resolve_config ( resolve_conf_lines) ?;
116
- tracing:: info!( "Using Kubernetes cluster domain: '{cluster_domain}'" ) ;
89
+ tracing:: info!(
90
+ cluster_domain,
91
+ "Using Kubernetes cluster domain from {RESOLVE_CONF_FILE_PATH} file"
92
+ ) ;
117
93
118
- cluster_domain
119
- . clone ( )
120
- . try_into ( )
121
- . context ( InvalidDomainSnafu { cluster_domain } )
94
+ DomainName :: from_str ( & cluster_domain) . context ( ParseDomainNameSnafu { cluster_domain } )
95
+ }
96
+ Err ( _) => {
97
+ tracing:: info!(
98
+ cluster_domain = KUBERNETES_CLUSTER_DOMAIN_DEFAULT ,
99
+ "Using default Kubernetes cluster domain"
100
+ ) ;
101
+ Ok ( DomainName :: from_str ( KUBERNETES_CLUSTER_DOMAIN_DEFAULT ) . expect ( "stuff" ) )
102
+ }
103
+ }
122
104
}
123
105
124
- /// Extract the Kubernetes cluster domain from the vectorized 'resolv.conf'.
125
- /// This will:
126
- /// 1. Use the last entry containing a 'search' prefix.
127
- /// 2. Strip 'search' from the last entry.
128
- /// 3. Return the shortest itme (e.g. 'cluster.local') in the whitespace seperated list.
129
- fn parse_resolve_config ( resolv_conf : Vec < String > ) -> Result < String , Error > {
130
- tracing:: debug!(
131
- "Start parsing '{RESOLVE_CONF_FILE_PATH}' to retrieve the Kubernetes cluster domain..."
132
- ) ;
133
-
134
- let last_search_entry =
135
- find_last_search_entry ( & resolv_conf) . context ( SearchEntryNotFoundSnafu {
136
- resolve_conf_file_path : RESOLVE_CONF_FILE_PATH . to_string ( ) ,
137
- } ) ?;
138
-
139
- let last_search_entry_content =
140
- trim_search_line ( & last_search_entry) . context ( TrimSearchEntryFailedSnafu {
141
- search_entry_line : last_search_entry. to_string ( ) ,
142
- } ) ?;
143
-
144
- let shortest_search_entry = find_shortest_entry ( last_search_entry_content)
106
+ fn retrieve_cluster_domain_from_resolv_conf < P > ( path : P ) -> Result < String , Error >
107
+ where
108
+ P : AsRef < Path > ,
109
+ {
110
+ let content = std:: fs:: read_to_string ( path) . context ( ReadResolvConfFileSnafu ) ?;
111
+
112
+ let last = content
113
+ . lines ( )
114
+ . map ( |l| l. trim ( ) )
115
+ . filter ( |l| l. starts_with ( "search" ) )
116
+ . map ( |l| l. trim_start_matches ( "search" ) )
117
+ . last ( )
118
+ . context ( SearchEntryNotFoundSnafu ) ?;
119
+
120
+ let shortest_entry = last
121
+ . split_ascii_whitespace ( )
122
+ . min_by_key ( |item| item. len ( ) )
145
123
. context ( LookupClusterDomainEntryFailedSnafu ) ?;
146
124
147
- Ok ( shortest_search_entry. into ( ) )
148
- }
149
-
150
- /// Read an ENV variable
151
- fn read_env_var ( name : & str ) -> Result < String , Error > {
152
- env:: var ( name) . context ( EnvVarNotFoundSnafu { name } )
153
- }
154
-
155
- // Function to read the contents of a file and return all lines as Vec<String>
156
- fn read_file_from_path ( resolv_conf_file_path : & str ) -> Result < Vec < String > , io:: Error > {
157
- let file = std:: fs:: File :: open ( Path :: new ( resolv_conf_file_path) ) ?;
158
- let reader = io:: BufReader :: new ( file) ;
159
-
160
- reader. lines ( ) . collect ( )
161
- }
162
-
163
- /// Search the last entry containing the 'search' prefix. We are only interested in
164
- /// the last line (in case there are multiple entries which would be ignored by external tools).
165
- fn find_last_search_entry ( lines : & [ String ] ) -> Option < String > {
166
- lines
167
- . iter ( )
168
- . rev ( ) // Start from the end to find the last occurrence
169
- . find ( |line| line. trim ( ) . starts_with ( "search" ) )
170
- . cloned ( )
171
- }
172
-
173
- /// Extract the content of the 'search' line. Basically stripping the 'search' prefix from the line like:
174
- /// 'search sble-operators.svc.cluster.local svc.cluster.local cluster.local' will become
175
- /// 'sble-operators.svc.cluster.local svc.cluster.local cluster.local'
176
- fn trim_search_line ( search_line : & str ) -> Option < & str > {
177
- search_line. trim ( ) . strip_prefix ( "search" )
178
- }
179
-
180
- /// Extract the shortest entry from a whitespace seperated string like:
181
- /// 'sble-operators.svc.cluster.local svc.cluster.local cluster.local'
182
- /// This will be 'cluster.local' here.
183
- fn find_shortest_entry ( search_content : & str ) -> Option < & str > {
184
- search_content
185
- . split_whitespace ( )
186
- . min_by_key ( |entry| entry. len ( ) )
125
+ // NOTE (@Techassi): This is really sad and bothers me more than I would like to admit
126
+ Ok ( shortest_entry. to_owned ( ) )
187
127
}
188
128
189
129
#[ cfg( test) ]
190
130
mod tests {
191
- use super :: * ;
192
-
193
- const KUBERNETES_RESOLV_CONF : & str = r#"""
194
- search sble-operators.svc.cluster.local svc.cluster.local cluster.local
195
- nameserver 10.243.21.53
196
- options ndots:5
197
- """# ;
198
-
199
- const OPENSHIFT_RESOLV_CONF : & str = r#"""
200
- search openshift-service-ca-operator.svc.cluster.local svc.cluster.local cluster.local cmx.repl-openshift.build
201
- nameserver 172.30.0.10
202
- options ndots:5
203
- """# ;
204
-
205
- const KUBERNETES_RESOLV_CONF_MULTIPLE_SEARCH_ENTRIES : & str = r#"""
206
- search baz svc.foo.bar foo.bar
207
- search sble-operators.svc.cluster.local svc.cluster.local cluster.local
208
- nameserver 10.243.21.53
209
- options ndots:5
210
- """# ;
131
+ use std:: path:: PathBuf ;
211
132
212
- const KUBERNETES_RESOLV_CONF_MISSING_SEARCH_ENTRIES : & str = r#"""
213
- nameserver 10.243.21.53
214
- options ndots:5
215
- """# ;
216
-
217
- // Helper method to read resolv.conf from a string and not from file.
218
- fn read_file_from_string ( contents : & str ) -> Vec < String > {
219
- // Split the string by lines and collect into a Vec<String>
220
- contents. lines ( ) . map ( |line| line. to_string ( ) ) . collect ( )
221
- }
133
+ use super :: * ;
134
+ use rstest:: rstest;
222
135
223
136
#[ test]
224
137
fn use_different_kubernetes_cluster_domain_value ( ) {
@@ -230,35 +143,28 @@ mod tests {
230
143
}
231
144
232
145
// initialize the lock
233
- let _ = KUBERNETES_CLUSTER_DOMAIN . set ( resolve_kubernetes_cluster_domain ( ) . unwrap ( ) ) ;
146
+ let _ = KUBERNETES_CLUSTER_DOMAIN . set ( retrieve_cluster_domain ( ) . unwrap ( ) ) ;
234
147
235
148
assert_eq ! (
236
149
cluster_domain,
237
150
KUBERNETES_CLUSTER_DOMAIN . get( ) . unwrap( ) . to_string( )
238
151
) ;
239
152
}
240
153
241
- #[ test]
242
- fn parse_resolv_conf_success ( ) {
243
- let correct_resolv_configs = vec ! [
244
- KUBERNETES_RESOLV_CONF ,
245
- OPENSHIFT_RESOLV_CONF ,
246
- KUBERNETES_RESOLV_CONF_MULTIPLE_SEARCH_ENTRIES ,
247
- ] ;
248
-
249
- for resolv_conf in correct_resolv_configs {
250
- let lines = read_file_from_string ( resolv_conf) ;
251
- let last_search_entry = find_last_search_entry ( lines. as_slice ( ) ) . unwrap ( ) ;
252
- let search_entry = trim_search_line ( & last_search_entry) . unwrap ( ) ;
253
- let cluster_domain = find_shortest_entry ( search_entry) . unwrap ( ) ;
254
- assert_eq ! ( cluster_domain, KUBERNETES_CLUSTER_DOMAIN_DEFAULT ) ;
255
- }
154
+ #[ rstest]
155
+ fn parse_resolv_conf_pass (
156
+ #[ files( "fixtures/cluster_domain/pass/*.resolv.conf" ) ] path : PathBuf ,
157
+ ) {
158
+ assert_eq ! (
159
+ retrieve_cluster_domain_from_resolv_conf( path) . unwrap( ) ,
160
+ KUBERNETES_CLUSTER_DOMAIN_DEFAULT
161
+ ) ;
256
162
}
257
163
258
- #[ test ]
259
- fn parse_resolv_conf_error_no_search_entry ( ) {
260
- let lines = read_file_from_string ( KUBERNETES_RESOLV_CONF_MISSING_SEARCH_ENTRIES ) ;
261
- let last_search_entry = find_last_search_entry ( lines . as_slice ( ) ) ;
262
- assert_eq ! ( last_search_entry , None ) ;
164
+ #[ rstest ]
165
+ fn parse_resolv_conf_fail (
166
+ # [ files ( "fixtures/cluster_domain/fail/*.resolv.conf" ) ] path : PathBuf ,
167
+ ) {
168
+ assert ! ( retrieve_cluster_domain_from_resolv_conf ( path ) . is_err ( ) ) ;
263
169
}
264
170
}
0 commit comments