@@ -51,6 +51,7 @@ COMMANDS:
5151 convert Convert and transform an image file
5252 inspect Show metadata (format, dimensions, alpha) of an image
5353 serve Start the HTTP image-transform server
54+ validate Check server configuration without starting the server
5455 sign Generate a signed public URL for the server
5556 completions Generate shell completion scripts
5657 help Show help for a command (e.g. truss help convert)
@@ -299,6 +300,19 @@ EXAMPLES:
299300 --expires 1700000000 --width 640 --format webp
300301" ;
301302
303+ const HELP_VALIDATE : & str = "\
304+ truss validate - check server configuration without starting the server
305+
306+ USAGE:
307+ truss validate
308+
309+ Parses and validates all environment variables used by `truss serve`.
310+ Exits 0 when the configuration is valid, or exits 1 with a description
311+ of each error found.
312+
313+ Useful in CI/CD pipelines to catch configuration mistakes early.
314+ " ;
315+
302316const HELP_COMPLETIONS : & str = "\
303317 truss completions - generate shell completion scripts
304318
@@ -358,6 +372,9 @@ enum CliSubcommand {
358372 Help { topic : Option < String > } ,
359373 /// Print version information
360374 Version ,
375+ /// Validate server configuration without starting the server
376+ #[ command( disable_help_flag = true ) ]
377+ Validate ( ClapValidateArgs ) ,
361378 /// Generate shell completion scripts
362379 #[ command( disable_help_flag = true ) ]
363380 Completions {
@@ -483,6 +500,13 @@ struct ClapServeArgs {
483500 help : bool ,
484501}
485502
503+ #[ derive( clap:: Args ) ]
504+ struct ClapValidateArgs {
505+ /// Show help for validate
506+ #[ arg( short = 'h' , long = "help" ) ]
507+ help : bool ,
508+ }
509+
486510#[ derive( clap:: Args ) ]
487511struct ClapSignArgs {
488512 /// CDN base URL for the signed request
@@ -716,6 +740,7 @@ enum HelpTopic {
716740 Convert ,
717741 Inspect ,
718742 Serve ,
743+ Validate ,
719744 Sign ,
720745 Completions ,
721746 Version ,
@@ -726,6 +751,7 @@ enum Command {
726751 Help ( HelpTopic ) ,
727752 Version ,
728753 Serve ( ServeCommand ) ,
754+ Validate ,
729755 Inspect ( InspectCommand ) ,
730756 Convert ( ConvertCommand ) ,
731757 Sign ( SignCommand ) ,
@@ -816,6 +842,7 @@ where
816842 HelpTopic :: Convert => HELP_CONVERT . to_string ( ) ,
817843 HelpTopic :: Inspect => HELP_INSPECT . to_string ( ) ,
818844 HelpTopic :: Serve => help_serve ( ) ,
845+ HelpTopic :: Validate => HELP_VALIDATE . to_string ( ) ,
819846 HelpTopic :: Sign => HELP_SIGN . to_string ( ) ,
820847 HelpTopic :: Completions => HELP_COMPLETIONS . to_string ( ) ,
821848 HelpTopic :: Version => HELP_VERSION . to_string ( ) ,
@@ -833,6 +860,10 @@ where
833860 Ok ( ( ) ) => EXIT_SUCCESS ,
834861 Err ( error) => write_error ( stderr, error) ,
835862 } ,
863+ Ok ( Command :: Validate ) => match execute_validate ( stdout) {
864+ Ok ( ( ) ) => EXIT_SUCCESS ,
865+ Err ( error) => write_error ( stderr, error) ,
866+ } ,
836867 Ok ( Command :: Inspect ( command) ) => match execute_inspect ( command, stdin, stdout) {
837868 Ok ( ( ) ) => EXIT_SUCCESS ,
838869 Err ( error) => write_error ( stderr, error) ,
@@ -861,6 +892,7 @@ const KNOWN_SUBCOMMANDS: &[&str] = &[
861892 "convert" ,
862893 "inspect" ,
863894 "serve" ,
895+ "validate" ,
864896 "sign" ,
865897 "help" ,
866898 "completions" ,
@@ -989,6 +1021,7 @@ where
9891021 Some ( CliSubcommand :: Convert ( args) ) => convert_from_clap ( args) ,
9901022 Some ( CliSubcommand :: Inspect ( args) ) => inspect_from_clap ( args) ,
9911023 Some ( CliSubcommand :: Serve ( args) ) => serve_from_clap ( args) ,
1024+ Some ( CliSubcommand :: Validate ( args) ) => validate_from_clap ( args) ,
9921025 Some ( CliSubcommand :: Sign ( args) ) => sign_from_clap ( args) ,
9931026 }
9941027}
@@ -1017,6 +1050,7 @@ fn parse_help_topic(topic: Option<String>) -> Result<Command, CliError> {
10171050 Some ( "convert" ) => Ok ( Command :: Help ( HelpTopic :: Convert ) ) ,
10181051 Some ( "inspect" ) => Ok ( Command :: Help ( HelpTopic :: Inspect ) ) ,
10191052 Some ( "serve" ) => Ok ( Command :: Help ( HelpTopic :: Serve ) ) ,
1053+ Some ( "validate" ) => Ok ( Command :: Help ( HelpTopic :: Validate ) ) ,
10201054 Some ( "sign" ) => Ok ( Command :: Help ( HelpTopic :: Sign ) ) ,
10211055 Some ( "completions" ) => Ok ( Command :: Help ( HelpTopic :: Completions ) ) ,
10221056 Some ( "version" ) => Ok ( Command :: Help ( HelpTopic :: Version ) ) ,
@@ -1025,7 +1059,8 @@ fn parse_help_topic(topic: Option<String>) -> Result<Command, CliError> {
10251059 message : format ! ( "unknown help topic '{other}'" ) ,
10261060 usage : None ,
10271061 hint : Some (
1028- "available topics: convert, inspect, serve, sign, completions, version" . to_string ( ) ,
1062+ "available topics: convert, inspect, serve, validate, sign, completions, version"
1063+ . to_string ( ) ,
10291064 ) ,
10301065 } ) ,
10311066 }
@@ -1173,6 +1208,13 @@ fn serve_from_clap(args: ClapServeArgs) -> Result<Command, CliError> {
11731208 } ) )
11741209}
11751210
1211+ fn validate_from_clap ( args : ClapValidateArgs ) -> Result < Command , CliError > {
1212+ if args. help {
1213+ return Ok ( Command :: Help ( HelpTopic :: Validate ) ) ;
1214+ }
1215+ Ok ( Command :: Validate )
1216+ }
1217+
11761218fn sign_from_clap ( args : ClapSignArgs ) -> Result < Command , CliError > {
11771219 if args. help {
11781220 return Ok ( Command :: Help ( HelpTopic :: Sign ) ) ;
@@ -1446,6 +1488,24 @@ fn execute_serve(command: ServeCommand) -> Result<(), CliError> {
14461488 . map_err ( |error| runtime_error ( EXIT_RUNTIME , & format ! ( "server runtime failed: {error}" ) ) )
14471489}
14481490
1491+ fn execute_validate < W : Write > ( stdout : & mut W ) -> Result < ( ) , CliError > {
1492+ match ServerConfig :: from_env ( ) {
1493+ Ok ( config) => {
1494+ writeln ! ( stdout, "configuration is valid" ) . map_err ( |error| {
1495+ runtime_error ( EXIT_RUNTIME , & format ! ( "failed to write stdout: {error}" ) )
1496+ } ) ?;
1497+ writeln ! ( stdout, " storage root: {}" , config. storage_root. display( ) ) . map_err (
1498+ |error| runtime_error ( EXIT_RUNTIME , & format ! ( "failed to write stdout: {error}" ) ) ,
1499+ ) ?;
1500+ Ok ( ( ) )
1501+ }
1502+ Err ( error) => Err ( runtime_error (
1503+ EXIT_USAGE ,
1504+ & format ! ( "invalid configuration: {error}" ) ,
1505+ ) ) ,
1506+ }
1507+ }
1508+
14491509fn resolve_server_config ( command : ServeCommand ) -> Result < ServerConfig , CliError > {
14501510 let mut config = ServerConfig :: from_env ( ) . map_err ( |error| {
14511511 runtime_error (
@@ -1843,6 +1903,7 @@ mod tests {
18431903 use crate :: { Fit , MediaType , RawArtifact , SignedUrlSource , TransformOptions , sniff_artifact} ;
18441904 use image:: codecs:: png:: PngEncoder ;
18451905 use image:: { ColorType , ImageEncoder , Rgba , RgbaImage } ;
1906+ use serial_test:: serial;
18461907 use std:: env;
18471908 use std:: fs;
18481909 use std:: io:: { Cursor , Read , Write } ;
@@ -2382,6 +2443,61 @@ mod tests {
23822443 assert_eq ! ( result. unwrap( ) , Command :: Help ( HelpTopic :: Serve ) ) ;
23832444 }
23842445
2446+ // ===== Additional test: help validate =====
2447+
2448+ #[ test]
2449+ fn help_validate_shows_validate_help ( ) {
2450+ let result = parse_args ( vec ! [
2451+ "truss" . to_string( ) ,
2452+ "help" . to_string( ) ,
2453+ "validate" . to_string( ) ,
2454+ ] ) ;
2455+ assert_eq ! ( result. unwrap( ) , Command :: Help ( HelpTopic :: Validate ) ) ;
2456+ }
2457+
2458+ #[ test]
2459+ fn parse_args_validate ( ) {
2460+ let result =
2461+ parse_args ( vec ! [ "truss" . to_string( ) , "validate" . to_string( ) ] ) . expect ( "parse validate" ) ;
2462+ assert_eq ! ( result, Command :: Validate ) ;
2463+ }
2464+
2465+ #[ test]
2466+ fn validate_help_flag ( ) {
2467+ let result = parse_args ( vec ! [
2468+ "truss" . to_string( ) ,
2469+ "validate" . to_string( ) ,
2470+ "--help" . to_string( ) ,
2471+ ] ) ;
2472+ assert_eq ! ( result. unwrap( ) , Command :: Help ( HelpTopic :: Validate ) ) ;
2473+ }
2474+
2475+ #[ test]
2476+ #[ serial]
2477+ fn validate_invalid_config ( ) {
2478+ // SAFETY: test-only, single-threaded access to this env var.
2479+ unsafe { env:: set_var ( "TRUSS_MAX_CONCURRENT_TRANSFORMS" , "invalid" ) } ;
2480+ let mut stdout = Vec :: new ( ) ;
2481+ let result = super :: execute_validate ( & mut stdout) ;
2482+ unsafe { env:: remove_var ( "TRUSS_MAX_CONCURRENT_TRANSFORMS" ) } ;
2483+ assert ! ( result. is_err( ) ) ;
2484+ }
2485+
2486+ #[ test]
2487+ #[ serial]
2488+ fn validate_valid_config ( ) {
2489+ let dir = tempfile:: tempdir ( ) . expect ( "create temp dir" ) ;
2490+ let mut stdout = Vec :: new ( ) ;
2491+ // SAFETY: test-only, single-threaded access to this env var.
2492+ unsafe { env:: set_var ( "TRUSS_STORAGE_ROOT" , dir. path ( ) . to_str ( ) . unwrap ( ) ) } ;
2493+ let result = super :: execute_validate ( & mut stdout) ;
2494+ unsafe { env:: remove_var ( "TRUSS_STORAGE_ROOT" ) } ;
2495+ assert ! ( result. is_ok( ) ) ;
2496+ let output = String :: from_utf8 ( stdout) . expect ( "valid utf-8" ) ;
2497+ assert ! ( output. contains( "configuration is valid" ) ) ;
2498+ assert ! ( output. contains( "storage root:" ) ) ;
2499+ }
2500+
23852501 // ===== Additional test: help sign =====
23862502
23872503 #[ test]
0 commit comments