@@ -67,3 +67,213 @@ impl RuntimeConfig {
6767 Ok ( this)
6868 }
6969}
70+
71+ #[ cfg( test) ]
72+ mod tests {
73+ use std:: net:: IpAddr ;
74+ use std:: sync:: Mutex ;
75+
76+ use carbide_test_support:: Outcome :: * ;
77+ use carbide_test_support:: scenarios;
78+
79+ use super :: * ;
80+
81+ static ENV_LOCK : Mutex < ( ) > = Mutex :: new ( ( ) ) ;
82+ const RUNTIME_CONFIG_ENV_KEYS : & [ & str ] = & [
83+ "CARBIDE_PXE_URL" ,
84+ "CARBIDE_API_INTERNAL_URL" ,
85+ "CARBIDE_API_URL" ,
86+ "CARBIDE_STATIC_PXE_URL" ,
87+ "FORGE_ROOT_CAFILE_PATH" ,
88+ "FORGE_CLIENT_CERT_PATH" ,
89+ "FORGE_CLIENT_KEY_PATH" ,
90+ "PXE_BIND_ADDRESS" ,
91+ "PXE_BIND_PORT" ,
92+ "CARBIDE_PXE_TEMPLATE_DIRECTORY" ,
93+ ] ;
94+
95+ #[ derive( Debug ) ]
96+ struct ConfigEnv {
97+ vars : & ' static [ ( & ' static str , & ' static str ) ] ,
98+ }
99+
100+ #[ derive( Debug , PartialEq ) ]
101+ struct RuntimeConfigSummary {
102+ internal_api_url : String ,
103+ client_facing_api_url : String ,
104+ pxe_url : String ,
105+ static_pxe_url : String ,
106+ forge_root_ca_path : String ,
107+ server_cert_path : String ,
108+ server_key_path : String ,
109+ bind_address : IpAddr ,
110+ bind_port : u16 ,
111+ template_directory : String ,
112+ }
113+
114+ struct EnvSnapshot {
115+ values : Vec < ( & ' static str , Option < String > ) > ,
116+ }
117+
118+ impl EnvSnapshot {
119+ fn capture ( keys : & [ & ' static str ] ) -> Self {
120+ Self {
121+ values : keys. iter ( ) . map ( |key| ( * key, env:: var ( key) . ok ( ) ) ) . collect ( ) ,
122+ }
123+ }
124+ }
125+
126+ impl Drop for EnvSnapshot {
127+ fn drop ( & mut self ) {
128+ for ( key, value) in & self . values {
129+ match value {
130+ Some ( value) => set_env ( key, value) ,
131+ None => remove_env ( key) ,
132+ }
133+ }
134+ }
135+ }
136+
137+ fn set_env ( key : & str , value : & str ) {
138+ // SAFETY: these tests hold ENV_LOCK while mutating process environment.
139+ unsafe { env:: set_var ( key, value) }
140+ }
141+
142+ fn remove_env ( key : & str ) {
143+ // SAFETY: these tests hold ENV_LOCK while mutating process environment.
144+ unsafe { env:: remove_var ( key) }
145+ }
146+
147+ fn clear_env ( keys : & [ & str ] ) {
148+ for key in keys {
149+ remove_env ( key) ;
150+ }
151+ }
152+
153+ fn summarize_config ( config : RuntimeConfig ) -> RuntimeConfigSummary {
154+ RuntimeConfigSummary {
155+ internal_api_url : config. internal_api_url ,
156+ client_facing_api_url : config. client_facing_api_url ,
157+ pxe_url : config. pxe_url ,
158+ static_pxe_url : config. static_pxe_url ,
159+ forge_root_ca_path : config. forge_root_ca_path ,
160+ server_cert_path : config. server_cert_path ,
161+ server_key_path : config. server_key_path ,
162+ bind_address : config. bind_address ,
163+ bind_port : config. bind_port ,
164+ template_directory : config. template_directory ,
165+ }
166+ }
167+
168+ /// Caller must hold `ENV_LOCK` before invoking this function.
169+ fn runtime_config_from_env ( input : ConfigEnv ) -> Result < RuntimeConfigSummary , & ' static str > {
170+ clear_env ( RUNTIME_CONFIG_ENV_KEYS ) ;
171+ for ( key, value) in input. vars {
172+ set_env ( key, value) ;
173+ }
174+ RuntimeConfig :: from_env ( )
175+ . map ( summarize_config)
176+ . map_err ( config_error_kind)
177+ }
178+
179+ fn config_error_kind ( error : String ) -> & ' static str {
180+ match error. as_str ( ) {
181+ "Could not extract FORGE_ROOT_CAFILE_PATH from environment" => "missing-root-ca" ,
182+ "Could not extract FORGE_CLIENT_CERT_PATH from environment" => "missing-client-cert" ,
183+ "Could not extract FORGE_CLIENT_KEY_PATH from environment" => "missing-client-key" ,
184+ "not a parsable bind address for runtime config?" => "bad-bind-address" ,
185+ "not a parsable bind port for runtime config?" => "bad-bind-port" ,
186+ other => panic ! ( "unmapped runtime config error: {other:?}" ) ,
187+ }
188+ }
189+
190+ #[ test]
191+ fn builds_runtime_config_from_environment ( ) {
192+ let _lock = ENV_LOCK . lock ( ) . unwrap_or_else ( |error| error. into_inner ( ) ) ;
193+ let _snapshot = EnvSnapshot :: capture ( RUNTIME_CONFIG_ENV_KEYS ) ;
194+
195+ scenarios ! (
196+ run = runtime_config_from_env;
197+ "required values with defaults" {
198+ ConfigEnv {
199+ vars: & [
200+ ( "FORGE_ROOT_CAFILE_PATH" , "/certs/root.pem" ) ,
201+ ( "FORGE_CLIENT_CERT_PATH" , "/certs/client.pem" ) ,
202+ ( "FORGE_CLIENT_KEY_PATH" , "/certs/client.key" ) ,
203+ ] ,
204+ } => Yields ( RuntimeConfigSummary {
205+ internal_api_url: "https://carbide-api.forge-system.svc.cluster.local:1079" . to_string( ) ,
206+ client_facing_api_url: "https://carbide-api.forge" . to_string( ) ,
207+ pxe_url: "http://carbide-pxe.forge" . to_string( ) ,
208+ static_pxe_url: "http://carbide-pxe.forge" . to_string( ) ,
209+ forge_root_ca_path: "/certs/root.pem" . to_string( ) ,
210+ server_cert_path: "/certs/client.pem" . to_string( ) ,
211+ server_key_path: "/certs/client.key" . to_string( ) ,
212+ bind_address: "0.0.0.0" . parse( ) . unwrap( ) ,
213+ bind_port: 8080 ,
214+ template_directory: "/opt/carbide/pxe/templates" . to_string( ) ,
215+ } ) ,
216+ }
217+
218+ "explicit values" {
219+ ConfigEnv {
220+ vars: & [
221+ ( "CARBIDE_API_INTERNAL_URL" , "https://internal.example.com" ) ,
222+ ( "CARBIDE_API_URL" , "https://client.example.com" ) ,
223+ ( "CARBIDE_PXE_URL" , "http://pxe.example.com" ) ,
224+ ( "CARBIDE_STATIC_PXE_URL" , "http://static-pxe.example.com" ) ,
225+ ( "FORGE_ROOT_CAFILE_PATH" , "/explicit/root.pem" ) ,
226+ ( "FORGE_CLIENT_CERT_PATH" , "/explicit/client.pem" ) ,
227+ ( "FORGE_CLIENT_KEY_PATH" , "/explicit/client.key" ) ,
228+ ( "PXE_BIND_ADDRESS" , "127.0.0.1" ) ,
229+ ( "PXE_BIND_PORT" , "9090" ) ,
230+ ( "CARBIDE_PXE_TEMPLATE_DIRECTORY" , "/templates" ) ,
231+ ] ,
232+ } => Yields ( RuntimeConfigSummary {
233+ internal_api_url: "https://internal.example.com" . to_string( ) ,
234+ client_facing_api_url: "https://client.example.com" . to_string( ) ,
235+ pxe_url: "http://pxe.example.com" . to_string( ) ,
236+ static_pxe_url: "http://static-pxe.example.com" . to_string( ) ,
237+ forge_root_ca_path: "/explicit/root.pem" . to_string( ) ,
238+ server_cert_path: "/explicit/client.pem" . to_string( ) ,
239+ server_key_path: "/explicit/client.key" . to_string( ) ,
240+ bind_address: "127.0.0.1" . parse( ) . unwrap( ) ,
241+ bind_port: 9090 ,
242+ template_directory: "/templates" . to_string( ) ,
243+ } ) ,
244+ }
245+
246+ "missing required values" {
247+ ConfigEnv { vars: & [ ] } => FailsWith ( "missing-root-ca" ) ,
248+ ConfigEnv {
249+ vars: & [ ( "FORGE_ROOT_CAFILE_PATH" , "/certs/root.pem" ) ] ,
250+ } => FailsWith ( "missing-client-cert" ) ,
251+ ConfigEnv {
252+ vars: & [
253+ ( "FORGE_ROOT_CAFILE_PATH" , "/certs/root.pem" ) ,
254+ ( "FORGE_CLIENT_CERT_PATH" , "/certs/client.pem" ) ,
255+ ] ,
256+ } => FailsWith ( "missing-client-key" ) ,
257+ }
258+
259+ "invalid bind settings" {
260+ ConfigEnv {
261+ vars: & [
262+ ( "FORGE_ROOT_CAFILE_PATH" , "/certs/root.pem" ) ,
263+ ( "FORGE_CLIENT_CERT_PATH" , "/certs/client.pem" ) ,
264+ ( "FORGE_CLIENT_KEY_PATH" , "/certs/client.key" ) ,
265+ ( "PXE_BIND_ADDRESS" , "not an ip" ) ,
266+ ] ,
267+ } => FailsWith ( "bad-bind-address" ) ,
268+ ConfigEnv {
269+ vars: & [
270+ ( "FORGE_ROOT_CAFILE_PATH" , "/certs/root.pem" ) ,
271+ ( "FORGE_CLIENT_CERT_PATH" , "/certs/client.pem" ) ,
272+ ( "FORGE_CLIENT_KEY_PATH" , "/certs/client.key" ) ,
273+ ( "PXE_BIND_PORT" , "not a port" ) ,
274+ ] ,
275+ } => FailsWith ( "bad-bind-port" ) ,
276+ }
277+ ) ;
278+ }
279+ }
0 commit comments