@@ -16,6 +16,29 @@ pub mod reloader;
1616#[ cfg( test) ]
1717mod tests;
1818
19+ fn yaml_to_duration_secs ( v : & Yaml ) -> Option < Duration > {
20+ v. as_f64 ( )
21+ . or_else ( || v. as_i64 ( ) . map ( |i| i as f64 ) )
22+ . filter ( |s| s. is_finite ( ) && * s >= 0.0 )
23+ . map ( Duration :: from_secs_f64)
24+ }
25+
26+ fn parse_duration_secs ( s : & str ) -> anyhow:: Result < Duration > {
27+ let f: f64 = s. parse ( ) ?;
28+ if !f. is_finite ( ) || f < 0.0 {
29+ bail ! ( "value must be a non-negative finite number, got {f}" ) ;
30+ }
31+ Ok ( Duration :: from_secs_f64 ( f) )
32+ }
33+
34+ fn parse_positive_duration_secs ( s : & str ) -> anyhow:: Result < Duration > {
35+ let d = parse_duration_secs ( s) ?;
36+ if d. is_zero ( ) {
37+ bail ! ( "value must be greater than zero" ) ;
38+ }
39+ Ok ( d)
40+ }
41+
1942const CONFIG_FILES : [ & str ; 4 ] = [
2043 "/etc/stackrox/fact.yml" ,
2144 "/etc/stackrox/fact.yaml" ,
@@ -218,20 +241,11 @@ impl TryFrom<Vec<Yaml>> for FactConfig {
218241 config. hotreload = Some ( hotreload) ;
219242 }
220243 "scan_interval" => {
221- // scan_internal == 0 disables the scanner
222- if let Some ( scan_interval) = v. as_f64 ( ) {
223- if scan_interval < 0.0 {
224- bail ! ( "invalid scan_interval: {scan_interval}" ) ;
225- }
226- config. scan_interval = Some ( Duration :: from_secs_f64 ( scan_interval) ) ;
227- } else if let Some ( scan_interval) = v. as_i64 ( ) {
228- if scan_interval < 0 {
229- bail ! ( "invalid scan_interval: {scan_interval}" ) ;
230- }
231- config. scan_interval = Some ( Duration :: from_secs ( scan_interval as u64 ) )
232- } else {
233- bail ! ( "scan_interval field has incorrect type: {v:?}" ) ;
234- }
244+ // scan_interval == 0 disables the scanner
245+ config. scan_interval = Some (
246+ yaml_to_duration_secs ( v)
247+ . with_context ( || format ! ( "invalid scan_interval: {v:?}" ) ) ?,
248+ ) ;
235249 }
236250 "rate_limit" => {
237251 // rate_limit == 0 means unlimited (no throttling)
@@ -328,10 +342,67 @@ impl TryFrom<&yaml::Hash> for EndpointConfig {
328342 }
329343}
330344
345+ #[ derive( Debug , Default , PartialEq , Eq , Clone ) ]
346+ pub struct BackoffConfig {
347+ initial : Option < Duration > ,
348+ max : Option < Duration > ,
349+ }
350+
351+ impl BackoffConfig {
352+ fn update ( & mut self , from : & BackoffConfig ) {
353+ if let Some ( initial) = from. initial {
354+ self . initial = Some ( initial) ;
355+ }
356+ if let Some ( max) = from. max {
357+ self . max = Some ( max) ;
358+ }
359+ }
360+
361+ pub fn initial ( & self ) -> Duration {
362+ self . initial . unwrap_or ( Duration :: from_secs ( 1 ) )
363+ }
364+
365+ pub fn max ( & self ) -> Duration {
366+ self . max . unwrap_or ( Duration :: from_secs ( 60 ) )
367+ }
368+ }
369+
370+ impl TryFrom < & yaml:: Hash > for BackoffConfig {
371+ type Error = anyhow:: Error ;
372+
373+ fn try_from ( value : & yaml:: Hash ) -> Result < Self , Self :: Error > {
374+ let mut backoff = BackoffConfig :: default ( ) ;
375+ for ( k, v) in value. iter ( ) {
376+ let Some ( k) = k. as_str ( ) else {
377+ bail ! ( "key is not string: {k:?}" ) ;
378+ } ;
379+ match k {
380+ "initial" => {
381+ backoff. initial = Some (
382+ yaml_to_duration_secs ( v)
383+ . filter ( |d| !d. is_zero ( ) )
384+ . with_context ( || format ! ( "invalid grpc.backoff.initial: {v:?}" ) ) ?,
385+ ) ;
386+ }
387+ "max" => {
388+ backoff. max = Some (
389+ yaml_to_duration_secs ( v)
390+ . filter ( |d| !d. is_zero ( ) )
391+ . with_context ( || format ! ( "invalid grpc.backoff.max: {v:?}" ) ) ?,
392+ ) ;
393+ }
394+ name => bail ! ( "Invalid field 'grpc.backoff.{name}' with value: {v:?}" ) ,
395+ }
396+ }
397+ Ok ( backoff)
398+ }
399+ }
400+
331401#[ derive( Debug , Default , PartialEq , Eq , Clone ) ]
332402pub struct GrpcConfig {
333403 url : Option < String > ,
334404 certs : Option < PathBuf > ,
405+ pub backoff : BackoffConfig ,
335406}
336407
337408impl GrpcConfig {
@@ -343,6 +414,8 @@ impl GrpcConfig {
343414 if let Some ( certs) = from. certs . as_deref ( ) {
344415 self . certs = Some ( certs. to_owned ( ) ) ;
345416 }
417+
418+ self . backoff . update ( & from. backoff ) ;
346419 }
347420
348421 pub fn url ( & self ) -> Option < & str > {
@@ -377,6 +450,12 @@ impl TryFrom<&yaml::Hash> for GrpcConfig {
377450 } ;
378451 grpc. certs = Some ( PathBuf :: from ( certs) ) ;
379452 }
453+ "backoff" => {
454+ let Some ( backoff) = v. as_hash ( ) else {
455+ bail ! ( "grpc.backoff section has incorrect type: {v:?}" ) ;
456+ } ;
457+ grpc. backoff = BackoffConfig :: try_from ( backoff) ?;
458+ }
380459 name => bail ! ( "Invalid field 'grpc.{name}' with value: {v:?}" ) ,
381460 }
382461 }
@@ -465,6 +544,18 @@ pub struct FactCli {
465544 #[ arg( short, long, env = "FACT_CERTS" ) ]
466545 certs : Option < PathBuf > ,
467546
547+ /// Initial backoff delay in seconds for gRPC reconnection
548+ ///
549+ /// Default value is 1 second
550+ #[ arg( long, env = "FACT_GRPC_BACKOFF_INITIAL" , value_parser = parse_positive_duration_secs) ]
551+ backoff_initial : Option < Duration > ,
552+
553+ /// Maximum backoff delay in seconds for gRPC reconnection
554+ ///
555+ /// Default value is 60 seconds
556+ #[ arg( long, env = "FACT_GRPC_BACKOFF_MAX" , value_parser = parse_positive_duration_secs) ]
557+ backoff_max : Option < Duration > ,
558+
468559 /// The port to bind for all exposed endpoints
469560 #[ arg( long, short, env = "FACT_ENDPOINT_ADDRESS" ) ]
470561 address : Option < SocketAddr > ,
@@ -535,8 +626,8 @@ pub struct FactCli {
535626 /// The seconds can use a decimal point for fractions of seconds.
536627 ///
537628 /// Default value is 30 seconds
538- #[ arg( long, short, env = "FACT_SCAN_INTERVAL" ) ]
539- scan_interval : Option < f64 > ,
629+ #[ arg( long, short, env = "FACT_SCAN_INTERVAL" , value_parser = parse_duration_secs ) ]
630+ scan_interval : Option < Duration > ,
540631
541632 /// Maximum number of file events to allow per second
542633 ///
@@ -555,6 +646,10 @@ impl FactCli {
555646 grpc : GrpcConfig {
556647 url : self . url . clone ( ) ,
557648 certs : self . certs . clone ( ) ,
649+ backoff : BackoffConfig {
650+ initial : self . backoff_initial ,
651+ max : self . backoff_max ,
652+ } ,
558653 } ,
559654 endpoint : EndpointConfig {
560655 address : self . address ,
@@ -568,7 +663,7 @@ impl FactCli {
568663 skip_pre_flight : resolve_bool_arg ( self . skip_pre_flight , self . no_skip_pre_flight ) ,
569664 json : resolve_bool_arg ( self . json , self . no_json ) ,
570665 hotreload : resolve_bool_arg ( self . hotreload , self . no_hotreload ) ,
571- scan_interval : self . scan_interval . map ( Duration :: from_secs_f64 ) ,
666+ scan_interval : self . scan_interval ,
572667 rate_limit : self . rate_limit ,
573668 }
574669 }
0 commit comments