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 } ;
72
83use snafu:: { OptionExt , ResultExt , Snafu } ;
94
105use crate :: commons:: networking:: DomainName ;
116
12- // Env vars
137const KUBERNETES_CLUSTER_DOMAIN_ENV : & str = "KUBERNETES_CLUSTER_DOMAIN" ;
148const KUBERNETES_SERVICE_HOST_ENV : & str = "KUBERNETES_SERVICE_HOST" ;
15- // Misc
9+
1610const KUBERNETES_CLUSTER_DOMAIN_DEFAULT : & str = "cluster.local" ;
1711const RESOLVE_CONF_FILE_PATH : & str = "/etc/resolv.conf" ;
1812
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
1915#[ derive( Debug , Snafu ) ]
2016pub 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 } ,
2919
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 {
3222 source : crate :: validation:: Errors ,
3323 cluster_domain : String ,
3424 } ,
3525
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 ,
3828
3929 #[ snafu( display( "Could not trim search entry in '{search_entry_line}'." ) ) ]
4030 TrimSearchEntryFailed { search_entry_line : String } ,
@@ -73,152 +63,75 @@ pub enum Error {
7363/// ```
7464pub static KUBERNETES_CLUSTER_DOMAIN : OnceLock < DomainName > = OnceLock :: new ( ) ;
7565
76- pub ( crate ) fn resolve_kubernetes_cluster_domain ( ) -> Result < DomainName , Error > {
66+ pub ( crate ) fn retrieve_cluster_domain ( ) -> Result < DomainName , Error > {
7767 // 1. Read KUBERNETES_CLUSTER_DOMAIN env var
7868 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 ( ) => {
8172 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 } ) ;
8675 }
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 " ) ;
8978 }
9079 } ;
9180
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
9382 // by checking if KUBERNETES_SERVICE_HOST is set: If not default to 'cluster.local'.
9483 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- }
10484
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 ) ?;
11488
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+ ) ;
11793
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+ }
122104}
123105
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 ( ) )
145123 . context ( LookupClusterDomainEntryFailedSnafu ) ?;
146124
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 ( ) )
187127}
188128
189129#[ cfg( test) ]
190130mod 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 ;
211132
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;
222135
223136 #[ test]
224137 fn use_different_kubernetes_cluster_domain_value ( ) {
@@ -230,35 +143,28 @@ mod tests {
230143 }
231144
232145 // initialize the lock
233- let _ = KUBERNETES_CLUSTER_DOMAIN . set ( resolve_kubernetes_cluster_domain ( ) . unwrap ( ) ) ;
146+ let _ = KUBERNETES_CLUSTER_DOMAIN . set ( retrieve_cluster_domain ( ) . unwrap ( ) ) ;
234147
235148 assert_eq ! (
236149 cluster_domain,
237150 KUBERNETES_CLUSTER_DOMAIN . get( ) . unwrap( ) . to_string( )
238151 ) ;
239152 }
240153
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+ ) ;
256162 }
257163
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 ( ) ) ;
263169 }
264170}
0 commit comments