package Toadfarm::Plugin::Reload; =head1 NAME Toadfarm::Plugin::Reload - Reload toadfarm with new code =head1 DESCRIPTION This L<Mojolicious> plugin allow the L</Toadfarm> server to restart when a resource is hit with a special JSON payload. The payload need to be compatible with the L<post-receive-hook|https://help.github.com/articles/post-receive-hooks> github use. =head1 SETUP =over 4 =item * You need to set up a post receive hook on github to make this reloader work. Go to "https://github.com/jhthorsen/YOUR-REPO/settings/hooks" to set it up. =item * The WebHook URL need to be "http://yourserver.com/some/secret/path". See L<CONFIG|/path> below for details. =back =head1 CONFIG This is a config template for L<Toadfarm>: { apps => [...], plugins => [ Reload => { path => '/some/secret/path', repositories => [ { name => 'cool-repo', branch => 'some-branch', path => '/path/to/cool-repo', remote => 'whatever', # defaults to "origin" }, ], }, # ... ], } Details: =over 4 =item * path This should be the path part of the URL to POST data to reload the server. Make this something semi secret to avoid random requests: perl -le'print join "/", "", "reload", (time.$$.rand(9999999)) =~ /(\w\w)/g' =item * repositories This should contain a mapping between github repository names and local settings: =over 4 =item * branch This need to match the branch which you push to github. It should be something like "production", and not "master" - unless you want every push to master to reload the server. =item * path This is the path on disk to the local git repo. =back =back =cut use Mojo::Base 'Mojolicious::Plugin'; use Mojo::JSON qw( decode_json encode_json ); use Mojo::Util; our $GIT = $ENV{GIT_EXE} || 'git'; $ENV{TOADFARM_GITHUB_DELAY} ||= 2; =head1 METHODS =head2 register $self->register($app, \%config); See L</SYNOPSIS> for C<%config> parameters. =cut sub register { my ($self, $app, $config) = @_; my $t0 = localtime; $self->{log} = $app->log; $self->_valid_config($config) or return; $app->routes->any($config->{path})->to( cb => sub { my $c = shift; my $payload = $c->req->body_params->param('payload'); my $status = "Started: $t0\n\n"; my $args; if ($payload) { $args = decode_json(Mojo::Util::encode('UTF-8', $payload)); $status = $self->_fork_and_reload($args) ? "ok\n" : "nok\n"; } else { for my $config (@{$self->{repositories}}) { $status .= "--- $config->{name}/$config->{branch}\n"; eval { $self->_run( {GIT_DIR => "$config->{path}/.git"}, $GIT => log => -3 => '--format=%s', sub { $status .= "$_[0]\n" }, ); $status .= "\n"; } or do { $self->{log}->error($@); }; } } $c->render(text => $status, format => 'text'); } ); } sub _fork_and_reload { my ($self, $payload) = @_; my $manager_pid = getppid; my $branch = $payload->{ref}; my $name = $payload->{repository}{name}; my $sha1 = $payload->{head_commit}{id}; my $refreshed = 0; my $pid; unless ($branch and $name and $sha1) { $self->{log}->warn("Skip reload on bad payload: " . encode_json($payload)); return; } $SIG{CHLD} = 'IGNORE'; $pid = fork; return 1 if $pid; return 0 if !defined $pid; # child process $branch =~ s!refs/heads/!!; # maybe i need to wait for github? sleep $ENV{TOADFARM_GITHUB_DELAY} if $ENV{TOADFARM_GITHUB_DELAY}; for my $config (@{$self->{repositories}}) { $config->{name} eq $name or next; $config->{branch} eq $branch or next; eval { $self->{log}->info("Reloading repo $name, branch $branch"); $self->_refresh_repo($config, $sha1); ++$refreshed; } or do { $self->{log}->error($@); }; } if ($refreshed) { $self->_run(kill => -USR2 => $manager_pid); } else { $self->{log}->warn("Skip reload on name=$name and branch=$branch"); } exit 0; } sub _refresh_repo { my ($self, $config, $sha1) = @_; my $log = $self->{log}; chdir $config->{path} or die "chdir $config->{path}: $!"; $self->_run($GIT => fetch => $config->{remote}); $self->_run( $GIT => log => '--format=%H', '-n1', "$config->{remote}/$config->{branch}", sub { return $self->{log}->error("Invalid commit: $_[0] ne $sha1") unless $_[0] eq $sha1; $self->_run($GIT => checkout => -f => -B => toadfarm_reload_branch => "$config->{remote}/$config->{branch}"); } ); } sub _run { my ($self, @cmd) = @_; my $env = ref $cmd[0] eq 'HASH' ? shift @cmd : {}; my $cb = ref $cmd[-1] eq 'CODE' ? pop @cmd : sub { $self->{log}->info("<<< $_[0]") }; my @res; local %ENV = %ENV; $ENV{$_} = $env->{$_} for keys %$env; $env = join ', ', map {"$_=$env->{$_}"} sort keys %$env; $env = "[$env] " if $env; # TODO: $self->{log}->debug("${env}run(@cmd)"); open my $CMD, '-|', @cmd or die "@cmd: $!"; while (<$CMD>) { chomp; push @res, $cb->($_); } } sub _valid_config { my ($self, $config) = @_; my $repositories = $config->{repositories}; if (!$config->{path}) { $self->{log}->error('Abort loading Reload: "path" missing in config'); return; } if (ref $repositories eq 'HASH') { $repositories = [ map { $repositories->{$_}{name} = $_; $repositories->{$_}; } keys %$repositories ]; } if (ref $repositories ne 'ARRAY' or !@$repositories) { $self->{log}->error('Abort loading Reload: "repositories" missing in config'); return; } for my $config (@$repositories) { $config->{remote} ||= 'origin'; for my $key (qw/ path branch /) { next if $config->{$key}; $self->{log}->error(qq[Abort loading Reload: "repositories -> $config->{name} -> $key" missing in config]); return; } } $self->{repositories} = $repositories; } =head1 AUTHOR Jan Henning Thorsen - C<jhthorsen@cpan.org> =cut 1;