File Coverage

blib/lib/App/Padadoy.pm
Criterion Covered Total %
statement 127 296 42.9
branch 8 90 8.8
condition 12 65 18.4
subroutine 24 42 57.1
pod 15 17 88.2
total 186 510 36.4


line stmt bran cond sub pod time code
1 2     2   31468 use strict;
  2         4  
  2         47  
2 2     2   8 use warnings;
  2         4  
  2         74  
3             package App::Padadoy;
4             {
5             $App::Padadoy::VERSION = '0.123_6';
6             }
7             #ABSTRACT: Simply deploy PSGI applications
8              
9 2     2   29 use 5.010;
  2         11  
10 2     2   747 use autodie;
  2         22975  
  2         9  
11 2     2   13245 use Try::Tiny;
  2         3141  
  2         95  
12 2     2   879 use IPC::System::Simple qw(run capture $EXITVAL);
  2         16074  
  2         202  
13 2     2   933 use File::Slurp;
  2         6045  
  2         114  
14 2     2   13 use List::Util qw(max);
  2         4  
  2         90  
15 2     2   780 use File::ShareDir qw(dist_file);
  2         9518  
  2         100  
16 2     2   19 use File::Path qw(make_path);
  2         4  
  2         84  
17 2     2   12 use File::Basename qw(dirname);
  2         2  
  2         94  
18 2     2   449 use File::Spec::Functions qw(catdir catfile rel2abs);
  2         578  
  2         103  
19 2     2   866 use Git::Repository;
  2         46298  
  2         7  
20 2     2   919 use Sys::Hostname;
  2         1527  
  2         88  
21 2     2   12 use Cwd;
  2         4  
  2         81  
22              
23             # required for deployment
24 2     2   748 use Plack::Handler::Starman qw();
  2         110723  
  2         44  
25 2     2   741 use Carton qw(0.9.4);
  2         3860  
  2         48  
26              
27             # required for testing
28 2     2   698 use Plack::Test qw();
  2         874  
  2         35  
29 2     2   865 use HTTP::Request::Common qw();
  2         34220  
  2         5675  
30              
31             our @commands = qw(init start stop restart config status create checkout
32             deplist cartontest remote version update enable);
33             our @configs = qw(user base repository port pidfile logs errrorlog accesslog
34             quiet remote);
35              
36             # _msg( $fh, [\$caller], $msg [@args] )
37             sub _msg (@) {
38 0     0   0 my $fh = shift;
39 0 0       0 my $caller = ref($_[0]) ? ${(shift)} :
  0 0       0  
40             ((caller(2))[3] =~ /^App::Padadoy::(.+)/ ? $1 : '');
41 0         0 my $text = shift;
42 0 0       0 say $fh (($caller ? "[$caller] " : "")
    0          
43             . (@_ ? sprintf($text, @_) : $text));
44             }
45              
46             sub fail (@) {
47 0     0 0 0 _msg(*STDERR, @_);
48 0         0 exit 1;
49             }
50              
51             sub msg {
52 14     14 0 32 my $self = shift;
53 14 50       46 _msg( *STDOUT, @_ ) unless $self->{quiet};
54             }
55              
56              
57             sub new {
58 1     1 1 3920 my ($class, $config, %values) = @_;
59              
60 1         5 my $self = bless { }, $class;
61              
62 1 50       8 if ($config) {
63             # $self->msg("Reading configuration from $config");
64 0         0 open (my $fh, "<", $config);
65 0         0 while(<$fh>) {
66 0 0       0 next if /^\s*$/;
67 0 0       0 if (/^\s*([a-z]+)\s*[:=]\s*(.*?)\s*$/) {
    0          
68 0   0     0 $self->{$1} = ($2 // '');
69             } elsif ($_ !~ /^\s*#/) {
70 0         0 fail "syntax error in config file: $_";
71             }
72             }
73 0         0 close $fh;
74 0         0 $self->{base} = rel2abs(dirname($config));
75             }
76              
77 1         4 foreach (@configs) {
78 10 100       33 $self->{$_} = $values{$_} if defined $values{$_};
79             }
80              
81 1   33     566 $self->{user} ||= getlogin || getpwuid($<);
      33        
82 1   33     2561 $self->{base} ||= cwd; # '/base/'.$self->{user};
83 1   33     24 $self->{repository} ||= catdir($self->{base},'repository');
84 1   50     11 $self->{port} ||= 6000;
85 1   33     13 $self->{pidfile} ||= catfile($self->{base},'starman.pid');
86 1   33     19 $self->{logs} ||= catdir( $self->{base},'logs');
87 1   33     11 $self->{errorlog} ||= catfile($self->{logs},'error.log');
88 1   33     10 $self->{accesslog} ||= catfile($self->{logs},'access.log');
89              
90             # config file
91 1         2 $self->{config} = $config;
92              
93             # TODO: validate config values
94              
95             fail "Invalid remote value: ".$self->{remote}
96 1 50 33     8 if $self->{remote} and $self->{remote} !~ qr{^[^@]+@[^:]+:[~/].*$};
97              
98 1         11 $self;
99             }
100              
101              
102             sub create {
103 1     1 1 10 my $self = shift;
104 1         2 my $module = shift;
105              
106 1         4 $self->{module} = $module;
107 1 50 33     16 fail("Invalid module name: $module")
108             if $module and $module !~ /^([a-z][a-z0-9]*(::[a-z][a-z0-9]*)*)$/i;
109              
110 1         10 $self->_provide_config('create');
111              
112 1         249 $self->msg('Using base directory '.$self->{base});
113 1         9 chdir $self->{base};
114              
115 1         1608 $self->msg('app/');
116 1         5 mkdir 'app';
117              
118 1         820 $self->msg('app/Makefile.PL');
119 1         12 write_file('app/Makefile.PL',{no_clobber => 1},
120             read_file(dist_file('App-Padadoy','Makefile.PL')));
121              
122 1 50       506 if ( $module ) {
123 1         8 $self->msg("app/app.psgi (calling $module)");
124 1         3 my $content = read_file(dist_file('App-Padadoy','app2.psgi'));
125 1         189 $content =~ s/YOUR_MODULE/$module/mg;
126 1         6 write_file('app/app.psgi',{no_clobber => 1},$content);
127              
128 1         141 my @parts = ('app', 'lib', split('::', $module));
129 1         4 my $name = pop @parts;
130              
131 1         4 my $path = join '/', @parts;
132 1         4 $self->msg("$path/");
133 1         258 make_path ($path);
134              
135 1         8 $self->msg("$path/$name.pm");
136 1         4 $content = read_file(dist_file('App-Padadoy','Module.pm.tpl'));
137 1         186 $content =~ s/YOUR_MODULE/$module/mg;
138 1         9 write_file( "$path/$name.pm", {no_clobber => 1}, $content );
139              
140 1         139 $self->msg('app/t/');
141 1         91 make_path('app/t');
142              
143 1         5 $self->msg('app/t/basic.t');
144 1         6 my $test = read_file(dist_file('App-Padadoy','basic.t'));
145 1         182 $test =~ s/YOUR_MODULE/$module/mg;
146 1         5 write_file('app/t/basic.t',{no_clobber => 1},$test);
147             } else {
148 0         0 $self->msg('app/app.psgi');
149 0         0 write_file('app/app.psgi',{no_clobber => 1},
150             read_file(dist_file('App-Padadoy','app1.psgi')));
151              
152 0         0 $self->msg('app/lib/');
153 0         0 mkdir 'app/lib';
154 0         0 write_file('app/lib/.gitkeep',{no_clobber => 1},''); # TODO: required?
155              
156 0         0 $self->msg('app/t/');
157 0         0 mkdir 'app/t';
158 0         0 write_file('app/t/.gitkeep',{no_clobber => 1},''); # TODO: required?
159             }
160              
161 1         138 $self->msg('data/');
162 1         3 mkdir 'data';
163              
164 1         88 $self->msg('dotcloud.yml');
165 1         5 write_file( 'dotcloud.yml',{no_clobber => 1},
166             "www:\n type: perl\n approot: app" );
167            
168 1         142 my $content = read_file(dist_file('App-Padadoy','index.pl.tpl'));
169 1         178 $self->msg("perl/index.pl");
170 1         86 make_path("perl");
171 1         7 write_file('perl/index.pl',{no_clobber => 1},$content);
172              
173 1         139 my %symlinks = (libs => 'app/lib','app/deplist.txt' => 'deplist.txt');
174 1         10 while (my ($from,$to) = each %symlinks) {
175 2         538 $self->msg("$from -> $to");
176 2         7 symlink $to, $from;
177             }
178              
179             # TODO:
180             # .openshift/ - hooks for OpenShift (o)
181             # action_hooks/ - scripts that get run every git push (o)
182             }
183              
184              
185             sub deplist {
186 0     0 1 0 my $self = shift;
187              
188 0         0 eval "use Perl::PrereqScanner";
189 0 0       0 fail "Perl::PrereqScanner required" if $@;
190              
191 0         0 fail "not implemented yet";
192              
193             # TODO: dependencies should be detectable automatically
194             # with Perl::PrereqScanner::App
195              
196 0         0 $self->msg("You must initialize a git repository and add remotes");
197             }
198              
199              
200             sub init {
201 0     0 1 0 my $self = shift;
202 0         0 $self->msg("Initializing environment");
203              
204             fail "Expected to run in ".$self->{base}
205 0 0       0 unless cwd eq $self->{base};
206             fail 'Expected to run in an EMPTY base directory'
207 0 0       0 if grep { $_ ne $0 and $_ ne 'padadoy.conf' } <*>;
  0 0       0  
208              
209 0         0 $self->_provide_config('init');
210              
211             try {
212 0     0   0 my $out = capture('git', 'init', '--bare', $self->{repository});
213 0         0 $self->msg(\'init',$_) for split "\n", $out;
214             } catch {
215 0     0   0 fail 'Failed to init git repository in ' . $self->{repository};
216 0         0 };
217              
218 0         0 my $file = $self->{repository}.'/hooks/update';
219 0         0 $self->msg("$file as executable");
220 0         0 write_file($file, read_file(dist_file('App-Padadoy','update')));
221 0         0 chmod 0755,$file;
222              
223 0         0 $file = $self->{repository}.'/hooks/post-receive';
224 0         0 $self->msg("$file as executable");
225 0         0 write_file($file, read_file(dist_file('App-Padadoy','post-receive')));
226 0         0 chmod 0755,$file;
227              
228 0         0 $self->msg("logs/");
229 0         0 mkdir 'logs';
230            
231 0         0 $self->msg("app -> current/app");
232 0         0 symlink 'current/app','app';
233              
234             $self->msg("Pushing to git repository %s@%s:%s will update",
235 0         0 $self->{user}, hostname, $self->{repository});
236             }
237              
238              
239             sub config {
240 0     0 1 0 say shift->_config;
241             }
242              
243             sub _config {
244 1     1   3 my $self = shift;
245 1         14 my $max = max map { length } keys %$self;
  11         34  
246 1   50     12 join "\n", map { sprintf( "%-${max}s = %s", $_, $self->{$_} // '' ) }
  11         59  
247             sort keys %$self;
248             }
249              
250              
251             sub restart {
252 0     0 1 0 my $self = shift;
253              
254 0         0 my $pid = $self->_pid;
255 0 0       0 if ($pid) {
256             $self->msg("Gracefully restarting starman as deamon on port %d (pid in %s)",
257 0         0 $self->{port}, $self->{pidfile});
258 0         0 run('kill','-HUP',$pid);
259             } else {
260 0         0 $self->start;
261             }
262             }
263              
264              
265             sub start {
266 0     0 1 0 my $self = shift;
267              
268 0 0       0 fail "No configuration file found" unless $self->{config};
269              
270 0         0 chdir $self->{base}.'/app';
271              
272              
273 0         0 if (0) { # FIXME
274             # check whether dependencies are satisfied
275             my @out = split "\n", capture('carton check --nocolor 2>&1');
276             if (@out > 1) { # carton check always seems to exit with zero (?!)
277             $out[0] =
278             _msg( *STDERR, \'start', $_) for @out;
279             exit 1;
280             }
281             }
282              
283             # make sure log files exist
284 0         0 foreach my $log (qw(errorlog accesslog)) {
285 0         0 my $path = dirname($self->{$log});
286 0 0       0 make_path($path) unless -d $path;
287 0 0       0 if (! -e $self->{$log} ) {
288 0         0 open (my $fh, '>>', $self->{$log});
289 0         0 close $fh;
290             }
291             }
292              
293             $self->msg("Starting starman as deamon on port %d (pid in %s)",
294 0         0 $self->{port}, $self->{pidfile});
295              
296             # TODO: refactor after release of carton 1.0
297 0         0 $ENV{PLACK_ENV} = 'production';
298             my @opt = (
299             'starman','--port' => $self->{port},
300             '-D','--pid' => $self->{pidfile},
301             '--error-log' => $self->{errorlog},
302             '--access-log' => $self->{accesslog},
303 0         0 );
304 0         0 run('carton','exec','-Ilib','--',@opt);
305             }
306              
307              
308             sub stop {
309 0     0 1 0 my $self = shift;
310              
311 0         0 my $pid = $self->_pid;
312 0 0       0 if ( $pid ) {
313 0         0 $self->msg("killing old process");
314 0         0 run('kill',$pid);
315             } else {
316 0         0 $self->msg("no PID file found");
317             }
318             }
319              
320             sub _pid {
321 0     0   0 my $self = shift;
322 0 0 0     0 return unless $self->{pidfile} and -f $self->{pidfile};
323 0   0     0 my $pid = read_file($self->{pidfile}) || 0;
324 0 0       0 return ($pid =~ s/^(\d+).*$/$1/sm ? $pid : 0);
325             }
326              
327              
328             sub status {
329 0     0 1 0 my $self = shift;
330              
331 0 0       0 fail "No configuration file found" unless $self->{config};
332 0         0 $self->msg("Configuration from ".$self->{config});
333              
334             # PID file?
335 0         0 my $pid = $self->_pid;
336 0 0       0 if ($pid) {
337 0         0 $self->msg("Process running: $pid (PID in %s)", $self->{pidfile});
338             } else {
339 0         0 $self->msg("PID file %s not found or broken", $self->{pidfile});
340             }
341              
342 0         0 my $port = $self->{port};
343            
344             # something listening on the port?
345 0         0 my $sock = IO::Socket::INET->new( PeerAddr => "localhost:$port" );
346 0 0       0 $self->msg("Port is $port - " . ($sock ? "currently used" : "not used"));
347              
348             # find out whether this users owns the socket (there should be a better way!)
349 0         0 my ($command,$pid2,$user);
350 0         0 my @lsof = eval { grep /LISTEN/, ( capture('lsof','-i',":$port") ) };
  0         0  
351 0 0       0 if (@lsof) {
352 0         0 foreach (@lsof) { # there may be multiple processes
353 0         0 my @f = split /\s+/, $_;
354 0 0 0     0 ($command,$pid2,$user) = @f if !$pid2 or $f[1] < $pid2;
355             }
356             } else {
357 0         0 $self->msg("Not listening at port $port");
358             }
359              
360 0 0 0     0 if ($sock or $pid2) {
361 0 0 0     0 if ($pid and $pid eq $pid2) {
    0 0        
      0        
362 0         0 $self->msg("Port $port is used by process $pid as given in ".$self->{pidfile});
363             } elsif (!$pid and $user and $user eq $self->{user}) {
364             $self->msg("Looks like " . $self->{pidfile} . " is missing (should contain PID $pid2) ".
365 0         0 "maybe you run another instance as same user ".$self->{user});
366             } else {
367 0         0 $self->msg("Looks like the port $port is used by someone else!");
368             }
369             }
370             }
371              
372             sub _provide_config {
373 1     1   7 my ($self, $caller) = @_;
374 1 50       4 return if $self->{config};
375              
376 1         2526 $self->{config} = cwd.'/padadoy.conf';
377 1         22 $self->msg(\$caller,"Writing default configuration to ".$self->{config});
378             # TODO: better use template with comments instead
379 1         8 write_file( $self->{config}, $self->_config );
380             }
381              
382              
383             sub checkout {
384 0     0 1   my ($self, $revision, $directory, $current) = @_;
385 0   0       $revision ||= 'master';
386 0   0       $directory ||= catdir($self->{base},$revision);
387              
388 0           my $git_dir = $self->{repository};
389 0 0         fail("git repository directory not found: $git_dir") unless -d $git_dir;
390              
391 0           $self->msg("checking out $revision to $directory");
392 0 0         fail("Working directory already exists: $directory")
393             if -e $directory;
394              
395 0 0         if ( $current ) {
396 0 0         fail("Current working directory not found") unless -d $current;
397             } else {
398 0           $current = catdir($self->{base},'current');
399             }
400              
401 0           mkdir $directory;
402 0           my $local = catdir( $current, 'app', 'local' );
403 0 0         if (-d $local) {
404 0           my $newlocal = catdir($directory,'app');
405 0           $self->msg("rsyncing $local into $newlocal");
406 0           mkdir $newlocal;
407 0           run('rsync', '-a', $local, catdir($directory,'app') );
408             }
409              
410 0           $self->msg("repository is $git_dir");
411 0           my $r = Git::Repository->new(
412             work_tree => $directory,
413             git_dir => $git_dir,
414             );
415 0           $r->run( checkout => '-q', '-f', $revision );
416             }
417              
418              
419             sub cartontest {
420 0     0 1   my $self = shift;
421              
422 0           chdir $self->{base}.'/app';
423 0           $self->msg("installing dependencies and testing");
424              
425 0           run('carton install');
426 0           run('perl Makefile.PL');
427 0           run('carton exec -Ilib -- make test');
428 0           run('carton exec -Ilib -- make clean > /dev/null');
429             }
430              
431              
432             sub update {
433 0     0 1   my $self = shift;
434 0   0       my $revision = shift || 'master';
435              
436 0           $self->msg("updating to revision $revision");
437              
438             # check out to $newdir
439 0           $self->checkout($revision);
440 0           my $revdir = catdir($self->{base},$revision);
441 0           my $newdir = catdir($self->{base},'new');
442              
443             # TODO: call directly
444 0           run('padadoy','cartontest',"base=$revdir");
445              
446 0           chdir $self->{base};
447 0           run('rm','-f','new');
448 0           symlink $revision, 'new';
449              
450 0           $self->msg("revision $revision checked out and tested at $newdir");
451             }
452              
453              
454             sub enable {
455 0     0 1   my $self = shift;
456              
457 0 0         fail "Missing directory ".$self->{base} unless -d $self->{base};
458 0           chdir $self->{base};
459              
460 0           my $new = catdir($self->{base},'new');
461 0           my $current = catdir($self->{base},'current');
462              
463 0 0         fail "Missing directory $new" unless -d $new;
464            
465 0           $self->msg("$new -> current");
466 0           run('rm','-f','current');
467 0           run('mv','new','current');
468              
469 0           chdir $current;
470              
471             # TODO: re-read full configuration (?)
472 0           $self->{base} = $current;
473              
474             # graceful restart seems broken
475 0           $self->stop;
476 0           $self->start;
477              
478             # TODO: cleanup old revisions?
479             }
480              
481              
482             sub remote {
483 0     0 1   my $self = shift;
484 0           my $command = shift;
485              
486 0 0         fail 'no remote configured' unless $self->{remote};
487 0 0         fail 'missing remote command' unless $command;
488              
489             fail "command $command not supported on remote"
490 0 0         unless grep { $_ eq $command } qw(init start stop restart config status version);
  0            
491             # TODO: create deplist checkout cartontest
492            
493 0 0         $self->{remote} =~ /^(.+):(.+)$/ or fail 'invalid remote value: '.$self->{remote};
494 0           my ($userhost,$dir) = ($1,$2);
495 0 0         fail 'remote directory should not contain spaces' if $dir =~ /\s/;
496              
497 0           $self->msg("running padadoy on ".$self->{remote});
498              
499 0           run('ssh',$userhost,"cd $dir && padadoy $command ".join ' ', @_);
500             }
501              
502              
503             sub version {
504 0   0 0 1   say 'This is padadoy version '.($App::Padadoy::VERSION || '??');
505 0           exit;
506             }
507              
508             1;
509              
510              
511             __END__