File Coverage

bin/aep
Criterion Covered Total %
statement 144 152 94.7
branch 24 38 63.1
condition 19 41 46.3
subroutine 20 20 100.0
pod n/a
total 207 251 82.4


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3             # Core
4 12     12   43431 use warnings;
  12         13  
  12         572  
5 12     12   40 use strict;
  12         15  
  12         295  
6 12     12   4269 use utf8;
  12         2624  
  12         53  
7 12     12   428 use v5.28;
  12         44  
8              
9             # Experimental (stable)
10 12     12   4582 use experimental 'signatures';
  12         37316  
  12         50  
11              
12             # External modules
13 12     12   6058 use YAML::XS;
  12         30791  
  12         658  
14 12     12   5712 use Getopt::Long::Descriptive;
  12         488076  
  12         78  
15 12     12   8678 use POE qw(Session::PlainCall);
  12         426605  
  12         67  
16              
17             # Our modules
18 12     12   618524 use App::aep;
  12         49  
  12         663  
19              
20             # Debug
21 12     12   71 use Data::Dumper;
  12         18  
  12         708  
22 12     12   54 use Carp qw(cluck longmess shortmess);
  12         26  
  12         985540  
23              
24 12         1707753 STDOUT->autoflush(1);
25 12         605 STDERR->autoflush(1);
26              
27             # Version of this software
28 12         263 our $VERSION = '0.013';
29              
30             # Config defaults
31 12         51 my $opt_conf_def = { 'AEP_SOCKETPATH' => '/tmp/aep.sock', };
32              
33             # Option specs
34             my @opt_desc = (
35             'aep %o ',
36             [
37             'config-env',
38             'Read config values from the environment only.',
39             {
40             'default' => 0,
41             },
42             ],
43             [
44             'config-file=s',
45             'Read config from a config file only.',
46             {
47             'default' => '$unset',
48             'callbacks' => {
49 2     2   7432 'exists' => sub { _check_exists( shift ) },
50             },
51             },
52             ],
53             [
54             'config-args',
55             'Read config values from arguments only.',
56             {
57             'default' => 0,
58             },
59             ],
60             [
61             'config-merge',
62             'Merge together env, file and args to generate a config.',
63             {
64             'default' => 1,
65             },
66             ],
67             [
68             'config-order=s',
69             'The order to merge options together, comma separated, default is: env,file,args',
70             {
71             'default' => 'env,file,args',
72             },
73             ],
74             [],
75             [
76             'env-prefix=s',
77             'What prefix to look for aep config environmentals, default is AEP_',
78             {
79             'default' => 'AEP_',
80             },
81             ],
82             [],
83             [
84             'command=s',
85             'What to actually run within the container, default is print aes help.',
86             {
87             'default' => 'aep --help',
88             }
89             ],
90             [
91             'command-args=s',
92             'The arguments to add to the command comma separated, default is nothing.',
93             {
94             'default' => "",
95             },
96             ],
97             [ 'command-norestart', 'If the command exits then do not attempt to restart it.', ],
98             [
99             'command-restart=i',
100             'If the command exits how many times to retry it, default 0. Set to -1 for infinite.',
101             {
102             'default' => 0,
103             'callbacks' => {
104 1     1   4252 'is_positive' => sub { _check_positive_number( shift ) },
105             },
106             },
107 12         740 ],
108             [
109             'command-restart-delay=i',
110             'The time in milliseconds to wait before retrying the command, default 1000',
111             {
112             'default' => 1000,
113             },
114             ],
115             [],
116             [
117             'lock-server',
118             'Act like a lock server, this means we will expect other apps to '
119             . 'connect to us, we in turn will say when they should actually start, '
120             . 'this is to counter-act race issues when starting multi image '
121             . 'containers such as docker-compose, default: no',
122             {
123             'default' => 0,
124             },
125             ],
126             [
127             'lock-server-host=s',
128             'What host to bind to, defaults to 0.0.0.0',
129             {
130             'default' => '0.0.0.0',
131             },
132             ],
133             [
134             'lock-server-port=i',
135             'What port to bind to, defaults to 60000',
136             {
137             'default' => 60000,
138             },
139             ],
140             [
141             'lock-server-default=s',
142             'If we get sent an ID we do not know, the default action to take. '
143             . 'Valid options are: "ignore", "run" or "runlast" the default is ignore.',
144             {
145             'default' => 'ignore',
146             },
147             ],
148             [
149             'lock-server-order=s',
150             'The list of ids and the order to allow them to run, '
151             . 'comma separated. Use || for parallel groups, e.g.: db,redis1||redis2,nginx',
152             {
153             'default' => '',
154             },
155             ],
156             [
157             'lock-server-exhaust-action=s',
158             'What to do when all clients in the order have started. '
159             . 'Valid options: "exit", "idle", "restart", "execute". Default is idle.',
160             {
161             'default' => 'idle',
162             },
163             ],
164             [],
165             [
166             'lock-client',
167             'Become a lock client, this will mean your aep will connect to '
168             . 'another aep to learn when it should run its command.',
169             {
170             'default' => 0,
171             },
172             ],
173             [ 'lock-client-noretry', 'If the connection to a master fails, do not retry - overrides lock-client-retry', ],
174             [
175             'lock-client-retry=i',
176             'If the connection to a master fails, do not fail retry n many times, '
177             . 'if this is set to 0 it will retry infinately, defaults to: 3 (seconds)',
178             {
179             'default' => 3,
180             },
181             ],
182             [
183             'lock-client-retry-delay=i',
184             'How long to wait before retrying, default 5 (seconds)',
185             {
186             'default' => 5,
187             },
188             ],
189             [
190             'lock-client-timeout=i',
191             'Maximum seconds to wait for the lock server to send run. 0 = wait forever. Default 0.',
192             {
193             'default' => 0,
194             },
195             ],
196             [
197             'lock-transport=s',
198             'Transport to use for lock client: tcp, unix, or auto (try TCP then Unix). Default auto.',
199             {
200             'default' => 'auto',
201             },
202             ],
203             [],
204             [
205             'lock-client-host=s',
206             'What host to connect to, defaults to: aep-master',
207             {
208             'default' => 'aep-master'
209             },
210             ],
211             [
212             'lock-client-port=i',
213             'What port to connect to, defaults to 60000',
214             {
215             'default' => 60000,
216             },
217             ],
218             [
219             'lock-trigger=s',
220             'Please read --help-config lock-trigger, default is: none:time:10000 (milliseconds)',
221             {
222             'default' => 'none:time:10000'
223             },
224             ],
225             [
226             'lock-id=s',
227             'What ID we should say we are, mandatory when acting as a lock-client',
228             {
229             'default' => $$,
230             },
231             ],
232             [],
233             [
234             'quiet',
235             'Suppress informational output, only show errors.',
236             {
237             'default' => 0,
238             },
239             ],
240             [
241             'verbose',
242             'Show detailed debug output including packet contents.',
243             {
244             'default' => 0,
245             },
246             ],
247             [],
248             [
249             'help',
250             'print usage message and exit',
251             {
252             'shortcircuit' => 1,
253             },
254             ],
255             [
256             'help-config=s',
257             'print configuration examples for: config-env, config-files, '
258             . 'config-arg, config-merge or lock-trigger eg: help-config config-env',
259             {
260             'shortcircuit' => 1,
261             },
262             ],
263             [
264             'docker-health-check',
265             'Call this from docker-compose for a health report, returns an exit of 0(success) or 1(failure)',
266             {
267             'default' => 0,
268             },
269             ],
270             );
271             # Read in our options
272 12         108 my ( $opt, $usage ) = describe_options( @opt_desc );
273              
274 10 100       54133 if ( $opt->help )
275             {
276 1         9 say $usage->text;
277 1         0 exit 0;
278             }
279              
280             # Define our main function that starts out perl POE kernel
281             sub main ( @args )
282 9     9   12 {
  9         20  
  9         14  
283 9         28 my $options = {};
284              
285             # Default exit code of error
286 9         28 my $exit_code = 1;
287 9         29 my $exit_reason = 'Unknown';
288              
289             # Create a function to handle setting exit
290             my $exit_func = sub {
291 9     9   86 my ( $code, $reason ) = @_;
292 9 50 33     163 if ( defined $code && $code =~ m#^\d+$# ) { $exit_code = $code }
  9         30  
293 0         0 else { $exit_code = 1 }
294 9 50       42 if ( defined $reason ) { $exit_reason = $reason }
  9         45  
295 0         0 else { $reason = 'Unknown' }
296 9         33 };
297              
298             # Create an appropriate heap for our session
299 9         33 my $func_map = _create_heap( $args[ 0 ], $args[ 1 ] );
300              
301 9 50       24 my $log_level = $opt->quiet ? 'quiet' : ( $opt->verbose ? 'verbose' : 'info' );
    50          
302 9         68 $func_map->{ '_' }->{ 'log_level' } = $log_level;
303 9     80   41 $func_map->{ '_' }->{ 'debug' } = sub { _func_debug( $log_level, @_ ) };
  80         622  
304 9         19 $func_map->{ '_' }->{ 'set_exit' } = $exit_func;
305              
306 9         154 my $session = POE::Session::PlainCall->create(
307             'package' => 'App::aep',
308             'ctor_args' => [ $options ],
309             'heap' => $func_map,
310             'states' => [
311             qw(
312             _start sig_int sig_term sig_chld sig_usr scheduler
313             command_start command_stdout command_stderr command_close command_error
314             lock_trigger_fire lock_trigger_connect lock_trigger_script
315             lock_client_timeout_fire lock_client_timeout_cancel
316             )
317             ],
318             );
319              
320 9         1157 $poe_kernel->run();
321              
322             # Return an appropriate code and reason
323 9         14124 return ( $exit_code, $exit_reason );
324             }
325              
326 9         12 sub _create_heap ( $opt, $usage )
327 9     9   14 {
  9         62  
  9         33  
328 9         21 my $map = { '_' => {}, };
329 9         26 $map->{ '_' }->{ 'opt' } = $opt;
330 9         15 $map->{ '_' }->{ 'usage' } = $usage;
331 9         18 $map->{ '_' }->{ 'default' } = $opt_conf_def;
332 9         20 $map->{ '_' }->{ 'config' } = {};
333              
334             # As it will be accessed a lot, keep a copy here
335 9         24 my $env_prefix = $opt->env_prefix;
336              
337             # Collect appropriate environmental variables and stash them in the funcmap/heap
338 9         84 foreach my $env_key ( keys %ENV )
339             {
340 244         232 my $env_val = $ENV{ $env_key };
341 244 100       464 if ( $env_key =~ m{^\Q$env_prefix\E} )
342             {
343 1         3 $map->{ '_' }->{ 'aep' }->{ $env_key } = $env_val;
344             }
345             else
346             {
347 243         388 $map->{ '_' }->{ 'env' }->{ $env_key } = $env_val;
348             }
349             }
350              
351             # Process the resultant config by merging sources in the specified order
352 9         20 my $merged = { %{ $opt_conf_def } };
  9         23  
353              
354             # Read config file if specified
355 9         15 my $file_config = {};
356 9         23 my $config_file = $opt->config_file;
357 9 100 66     88 if ( defined $config_file && $config_file ne '$unset' && -e $config_file )
      66        
358             {
359 1         2 my $yaml_content = do {
360 1 50       48 open my $fh, '<', $config_file or die "Cannot open config file '$config_file': $!";
361 1         5 local $/;
362 1         40 <$fh>;
363             };
364 1   50     64 $file_config = YAML::XS::Load( $yaml_content ) || {};
365             }
366              
367             # Build source maps for merging
368             my $sources = {
369 9   100     56 'env' => $map->{ '_' }->{ 'aep' } || {},
370             'file' => $file_config,
371             'args' => {},
372             };
373              
374             # Use AEP_ prefixed env vars as config keys directly (preserve full key name)
375 9         15 my $env_config = {};
376 9         12 for my $key ( keys %{ $sources->{ 'env' } } )
  9         36  
377             {
378 1         3 $env_config->{ $key } = $sources->{ 'env' }->{ $key };
379             }
380 9         17 $sources->{ 'env' } = $env_config;
381              
382             # Determine merge order
383 9   50     22 my $order_str = $opt->config_order || 'env,file,args';
384 9         55 my @order = split( /,/, $order_str );
385              
386             # Apply sources in order (later overrides earlier)
387 9         17 for my $source_name ( @order )
388             {
389 27         80 $source_name =~ s{^\s+|\s+$}{}g;
390 27   50     46 my $source_data = $sources->{ $source_name } || {};
391 27         18 for my $key ( keys %{ $source_data } )
  27         42  
392             {
393 2         4 $merged->{ $key } = $source_data->{ $key };
394             }
395             }
396              
397 9         57 $map->{ '_' }->{ 'config' } = $merged;
398              
399 9         64 return $map;
400             }
401              
402             sub _check_exists ( $val )
403 2     2   6 {
  2         5  
  2         4  
404 2 50       9 if ( $val ne '$unset' ) { return $val ? -e $val : undef }
  2 50       177  
405 0         0 else { return 1 }
406             }
407              
408             sub _check_positive_number ( $val )
409 1     1   2 {
  1         2  
  1         2  
410 1 50 33     18 return ( $val >= 0 || $val == -1 ) ? 1 : undef;
411             }
412              
413             # Show debug messages
414             # $log_level: 'quiet', 'info', or 'verbose' (global setting from --quiet/--verbose)
415             # $level: 'error', 'info', or 'debug' (per-message level, defaults to 'info')
416 89         150 sub _func_debug ( $log_level, $pipe, $line, $message, $level = 'info' )
  89         173  
  89         117  
  89         108  
  89         160  
417 89     89   99 {
  89         56  
418             # Filter messages based on log level
419 89 50 33     192 if ( $log_level eq 'quiet' && $level ne 'error' )
420             {
421 0         0 return;
422             }
423 89 50 33     317 if ( $log_level eq 'info' && $level eq 'debug' )
424             {
425 0         0 return;
426             }
427             # 'verbose' shows everything
428              
429 89         87 my @buffer;
430              
431 89 100 66     296 if ( $message && ref( $message ) eq 'HASH' )
432             {
433              
434             # Fancy message - Add a default for lefttab (add more later)
435 9 50 33     76 if ( $message->{ 'lines' } && ref( $message->{ 'lines' } ) eq 'ARRAY' )
436             {
437              
438             # nothing to do, makes sense
439 9         15 @buffer = @{ $message->{ 'lines' } };
  9         28  
440             }
441             else
442             {
443             # No idea what to do, re-pop it as dumper
444 0         0 push @buffer, 'Unexpected multiline format log packet, using data::dumper';
445 0         0 push @buffer, split( /\n/, Dumper( $message->{ 'lines' } ) );
446             }
447             }
448             else
449             {
450 80         91 push @buffer, $message;
451             }
452              
453 89         90 my $msg_first = 1;
454 89         111 my $left_buffer_size = 0;
455 89         133 foreach my $out_msg ( @buffer )
456             {
457 107 100       192 if ( $msg_first++ == 1 )
458             {
459 89         149 my $msg_prefix = "AEP($pipe:$line) ";
460 89         74 $left_buffer_size = length( $msg_prefix );
461 89         3342 say STDERR "\r$msg_prefix$out_msg";
462             }
463             else
464             {
465 18         351 say STDERR ' ' x $left_buffer_size, $out_msg;
466             }
467             }
468              
469 89         245 return;
470             }
471              
472             # Call the main function with selected options
473             exit do
474 9         72 {
475 9         36 my ( $exit_code, $exit_reason ) = main( $opt, $usage );
476              
477 9 50 33     1391 if ( ( !defined $exit_code ) || ( $exit_code !~ m#^\d+$# ) || ( $exit_code > 255 ) || ( $exit_code < 0 ) )
      33        
      33        
478             {
479 0         0 $exit_code = 255;
480             }
481              
482 9 50       60 my $final_log_level = $opt->quiet ? 'quiet' : ( $opt->verbose ? 'verbose' : 'info' );
    50          
483 9         251 _func_debug(
484             $final_log_level, 'STDERR', __LINE__,
485             {
486             'lines' => [ "Child exiting with: $exit_code", "Status: $exit_reason", 'Perl exception: ' . $!, ]
487             }
488             );
489              
490 9         0 $exit_code;
491             };
492              
493             __END__