File Coverage

blib/lib/App/Padadoy.pm
Criterion Covered Total %
statement 133 298 44.6
branch 13 82 15.8
condition 9 54 16.6
subroutine 26 46 56.5
pod 16 18 88.8
total 197 498 39.5


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