A supermarket to rule them all ;-)
$ cd /var/supers-puppet/rex/supers_deploy
$ rex -E production -b supers_checkout master
$ rex -E production -G web -b supers_install master
$ cd /var/supers-puppet/rex/supers_deploy
$ rex -E production -b supers_checkout master
$ rex -E production -G web -b supers_install master
$ mrsh @wf --\
'source /home/deploy/perl5/perlbrew/etc/bashrc; cd somewhere;\
git checkout $branch; git fetch; git diff --name-only FETCH_HEAD > /tmp/pull.last;\
git merge FETCH_HEAD; cat /tmp/pull.last | ./script/ss_installer > /tmp/installer.last'
$ mrsh @wf -- 'source /home/deploy/perl5/perlbrew/etc/bashrc; ubic restart soysuper.db'
dzil new Supers::Bot
package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
require UNIVERSAL::require;
has user => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has is_ready => ( is => 'rw', isa => 'Bool', default => sub{0} );
has plugins => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin => ( is => 'rw', isa => 'HashRef', default => sub{{}} );
has _cv => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp => (
is => 'rw',
lazy => 1,
builder => '_build_xmpp',
clearer => 'reset_connection'
);
sub _build_xmpp {
my $self = shift;
AnyEvent::XMPP::IM::Connection->new(
jid => $self->user,
password => $self->password,
domain => 'gmail.com',
host => 'talk.google.com',
port => 5223,
old_style_ssl => 1,
);
}
sub BUILD {
my $self = shift;
$self->_init_plugins;
$self->_init_events;
}
sub run {
my $self = shift;
$self->connect;
$self->_cv->recv;
}
sub connect { shift->_xmpp->connect }
sub _init_plugins {
my $self = shift;
my $plugins = $self->plugins;
$self->plugins([]);
$self->register_plugin($_) for @{$plugins};
}
sub register_plugin {
my ( $self, $name ) = @_;
die 'Need plugin name to load!' unless $name;
$name = "Supers::Bot::Plugin::$name" unless $name =~ /::/;
if ( $name->use ) {
my $plugin = eval { $name->new( bot => $self ) };
if ($@) {
warn "Failed to initialize plugin '$name': $@";
return;
}
push @{$self->plugins}, $name;
$self->_plugin->{$name} = $plugin;
return $plugin;
}
else { warn "Plugin '$name' not loaded: $@" }
}
sub trigger {
my ( $self, $event ) = ( shift, shift );
for my $plugin_name ( @{$self->plugins} ) {
my $plugin = $self->_plugin->{$plugin_name} || next;
$plugin->$event(@_) if $plugin->can($event);
}
}
sub trigger {
my ( $self, $event ) = ( shift, shift );
for my $plugin_name ( @{$self->plugins} ) {
my $plugin = $self->_plugin->{$plugin_name} || next;
$plugin->$event(@_) if $plugin->can($event);
}
}
sub _init_events {
my $self = shift;
$self->_xmpp->reg_cb(
session_ready => sub {
$self->is_ready(1);
$self->trigger( 'ready' => 'xmpp' );
},
message => sub {
my ( undef, $msg ) = @_;
return unless $msg->body;
$self->trigger( 'message' => $msg );
},
error => sub {
my ( $conn, $error ) = @_;
warn 'error: '. $error->string;
$self->handle_error( $conn, $error );
},
);
}
has _tick => ( is => 'ro', default => sub {
my $self = shift;
AnyEvent->timer ( after => 1, interval => 1,
cb => sub { $self->trigger('tick') if $self->is_ready }
);
});
has _sig_int => ( is => 'ro', default => sub {
my $self = shift;
AnyEvent->signal(
signal => "INT",
cb => sub {
$self->trigger('shutdown');
$self->_cv->send;
}
);
});
#!/usr/bin/env perl
use Supers::Bot;
Supers::Bot->new(
user => 'bender@gmail.com',
password => 'BiteMyShinyAss',
plugins => [] # meh!
)->run;
package Supers::Bot::Plugin::HelloWorld;
use common::sense;
use Moose; with 'Supers::Bot::Plugin';
sub ready {
say "Ready to run!";
shift->send_message(
'diego@soysuper.com' => 'Go go go soysuper!'
);
}
sub tick {
my $self = shift;
say 'Should I check something on this tick?';
}
sub message {
my ( $self, $msg ) = @_;
say sprintf('Message from %s: %s', $msg->from, $msg->body);
# Now do something with that!
}
__PACKAGE__->meta->make_immutable;
#!/usr/bin/env perl
use Supers::Bot;
Supers::Bot->new(
user => 'bender@gmail.com',
password => 'BiteMyShinyAss',
plugins => [qw/ HelloWorld /]
)->run;
> deploy app@some-branch to some-env
package Supers::Bot::Plugin::DeployApps;
use Moose;
with 'Supers::Bot::Plugin';
with 'Supers::Bot::Role::Runner';
has task_running =>
is => 'rw',
clearer => 'task_done',
predicate => 'is_running';
has last_msg => is => 'rw';
# Environments config for commands
has _envs => is => 'ro', default => sub{+{ ... }};
> deploy app@some-branch to some-env
sub message {
my ( $self, $msg ) = @_;
if ( $msg->body =~ /
^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
/x ) {
my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
# Give some help on this deploy command
}
}
> deploy app@some-branch to some-env
sub message {
my ( $self, $msg ) = @_;
if ( $msg->body =~ /
^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
/x ) {
my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
# Give some help on this deploy command
}
}
> deploy app@some-branch to some-env
sub message {
my ( $self, $msg ) = @_;
if ( $msg->body =~ /
^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
/x ) {
my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
# Give some help on this deploy command
}
}
> deploy app@some-branch to some-env
sub message {
my ( $self, $msg ) = @_;
if ( $msg->body =~ /
^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
/x ) {
my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry );
}
elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
# Give some help on this deploy command
}
}
sub _deploy_app {
my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;
# Whatever we need to build the shell commands to run...
my $cmd = _build_cmd( $app, $env, $branch );
|| return $msg->reply("Unknown application `$app` or environment `$env_alias`");
return $msg->reply( "This would run:\n$cmd" ) if $dry;
$msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
$self->last_msg($msg); $self->task_running(1);
$self->run_command($cmd => sub{
my ( $self, $out, $err, $exit ) = @_;
$msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
$self->task_done;
});
}
sub _deploy_app {
my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;
# Whatever we need to build the shell commands to run...
my $cmd = _build_cmd( $app, $env, $branch );
|| return $msg->reply("Unknown application `$app` or environment `$env_alias`");
return $msg->reply( "This would run:\n$cmd" ) if $dry;
$msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
$self->last_msg($msg); $self->task_running(1);
$self->run_command($cmd => sub{
my ( $self, $out, $err, $exit ) = @_;
$msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
$self->task_done;
});
}
sub _deploy_app {
my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;
# Whatever we need to build the shell commands to run...
my $cmd = _build_cmd( $app, $env, $branch );
|| return $msg->reply("Unknown application `$app` or environment `$env_alias`");
return $msg->reply( "This would run:\n$cmd" ) if $dry;
$msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
$self->last_msg($msg); $self->task_running(1);
$self->run_command($cmd => sub{
my ( $self, $out, $err, $exit ) = @_;
$msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
$self->task_done;
});
}
sub _deploy_app {
my ( $self, $msg, $app, $branch, $env_alias, $dry ) = @_;
# Whatever we need to build the shell commands to run...
my $cmd = _build_cmd( $app, $env, $branch );
|| return $msg->reply("Unknown application `$app` or environment `$env_alias`");
return $msg->reply( "This would run:\n$cmd" ) if $dry;
$msg->reply("Deploying branch `$branch` for application `$app` on `$env_alias` environment.");
$self->last_msg($msg); $self->task_running(1);
$self->run_command($cmd => sub{
my ( $self, $out, $err, $exit ) = @_;
$msg->reply( "Done deploying `$app` on `$env_alias` environment!" );
$self->task_done;
});
}
package Supers::Bot::Role::Runner;
use Moose::Role;
use AnyEvent::Util;
=head2 run_command
Helper to run shell commands async.
Accept a command and optionally a callback to be called with bot,
stdout, stderr and exit status when the command finish running.
This is a wrapper around AnyEvent::Util::run_cmd, so command can
be anything accepted by it.
=cut
sub run_command {
my $self = shift;
my $cmd = shift || die 'No command given!';
my $cb = shift;
my $cv = run_cmd $cmd,
"<", "/dev/null",
">" , \my $stdout,
"2>", \my $stderr;
$cv->cb(sub {
my $exit_status = shift->recv;
if ( $cb ) {
$cb->($self, $stdout, $stderr, $exit_status);
}
});
}
package Supers::Bot::Plugin::AppsMonitor;
use Moose;
with 'Supers::Bot::Plugin';
use AnyEvent::HTTP;
use Scalar::Util qw(weaken);
has operator =>
is => 'rw',
default => sub{[ map {$_.'@soysuper.com'} qw/diego unlucky/ ]};
has urls =>
is => 'rw',
isa => 'ArrayRef',
default => sub {[
[ 'http://app1.ss:3000/' => 'Aperitivos' ],
[ 'http://app1.ss:3000/assets/img/logo-print.png' => 0],
# ...
]};
has guard => ( is => 'rw', default => sub{{}} );
has last_time => ( is => 'rw', default => sub{time} );
sub inform_op {
my ( $self, $msg ) = @_;
$self->send_message( $_, $msg ) for @{$self->operator};
}
sub tick {
my $self = shift;
my $now = time;
return unless $now - $self->last_time > 20;
$self->last_time($now);
if ( my $url = shift @{$self->urls} ) {
weaken $self;
$self->guard->{$url->[0]} = http_get( $url->[0] => sub {
my ($body, $heads) = @_;
if ( $heads->{Status} == 200 ) {
if ( my $re = $url->[1] ) {
$self->inform_op( sprintf('Content error on %s (!~ %s)', $url->[0], $re))
unless !$re || $body =~ /$re/;
}
}
else {
$self->inform_op( sprintf('Error fetching %s (status is not 200)', $url->[0]) );
}
push @{$self->urls}, $url;
});
}
}
__PACKAGE__->meta->make_immutable;
package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
use Mojo::SlackRTM;
Supers::Bot::SlackMessage
require UNIVERSAL::require;
has user => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has slack_token => ( is => 'ro', required => 1 );
has plugins => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin => ( is => 'rw', isa => 'HashRef', default => sub{{}} );
has _cv => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp => ( is => 'rw', lazy => 1, builder => '_build_xmpp', clearer => 'reset_connection' );
has _slack => ( is => 'ro', lazy => 1, default => sub { Mojo::SlackRTM->new(token => shift->slack_token) } );
sub BUILD {
my $self = shift;
$self->_init_plugins;
$self->_init_events;
$self->_init_slack;
}
sub _init_slack {
my $self = shift;
$self->_slack->on( hello => sub { $self->trigger( 'ready' => 'slack' ) } );
$self->_slack->on( message => sub {
$self->trigger(
message => Supers::Bot::SlackMessage->new(slack => $_[0], event => $_[1])
) unless $_[1]->{subtype}; # skip announcements ATM
});
}
package Supers::Bot::SlackMessage;
use Moose;
# ABSTRACT: Wrapper for slack message events to make it work as much as possible like xmpp ones
has slack => ( is => 'ro', required => 1 );
has event => ( is => 'ro', required => 1 );
sub reply {
my ( $self, $response ) = @_;
$self->slack->send_message( $self->event->{channel} => $response );
}
sub from {
my $self = shift;
$self->slack->find_user_name($self->event->{user});
}
sub channel {
my $self = shift;
$self->slack->find_channel_name($self->event->{channel});
}
sub body { shift->event->{text} }
__PACKAGE__->meta->make_immutable;
package Supers::Bot::Role::CmdPlugin;
use Moose::Role;
with 'Supers::Bot::Plugin';
# ABSTRACT: Role to implement bot commands/plugins for Supers::Bot
sub message {
my ( $self, $msg ) = @_;
my ( $to_me, $cmd, @toks ) = _parse_msg($msg);
return unless $to_me;
if ( my $method = $self->can("cmd_$cmd") ) {
$method->($self, $msg, [@toks]);
}
}
sub _parse_msg {
my $msg = pop;
my ( $cmd, @toks ) = grep {defined} split /\s+/, $msg->body;
if ( $cmd =~ /^(bot|bender|<\@([^>]+)>):?$/i ) {
my $name = $2 ? $msg->slack->find_user_name($2) : $1;
return ( 0, $cmd, @toks ) unless $name =~ /^(?:bot|bender)$/;
$cmd = shift @toks;
return ( 1, $cmd, @toks );
}
return ( _is_direct_msg($msg), $cmd, @toks );
}
sub _is_direct_msg {
my $msg = pop;
return 1 if ref($msg) =~ /XMPP/;
!$msg->channel && $msg->from;
}
1;
> deploy app@some-branch to some-env
sub message {
my ( $self, $msg ) = @_;
if ( $msg->body =~ /
^deploy \s+ (\w+) (?:\@([^\s\?]+))? (?:\s+to\s+([^\s\?]+))? (\s*\?)?$
/x ) {
my ($app, $branch, $env, $dry) = ($1, $2||'master', $3||'pro', $4);
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
elsif ( $msg->body =~ /^ \s* (?:deploy \s* (?:\?|help) | help ) \s* $/ix ) {
# Give some help on this deploy command
}
}
> deploy app@some-branch to some-env
sub cmd_deploy {
my ( $self, $msg, $args ) = @_;
return unless @$args;
my ($app, $branch, $env, $try) = split(/@/, $args[0]), $arg[2]||'pro', $args[3];
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
sub cmd_help {
# Give some help on this deploy command
}
> deploy help
sub next_cmd {
my ( $self, $msg, $args, $ns ) = @_;
my $subcmd = shift @$args ||'';
$ns ||= 'subcmd';
if ( my $method = $self->can("${ns}_$subcmd") ) {
return $method->($self, $msg, $args);
}
elsif ( $method = $self->can("${ns}_default") ) {
unshift @$args, $subcmd if $subcmd;
return $method->($self, $msg, $args);
}
}
sub cmd_deploy { shift->next_cmd( @_ => 'deploy' ) }
sub deploy_default {
my ( $self, $msg, $args ) = @_;
return $self->cmd_help($msg) unless @$args;
my ($app, $branch, $env, $try) = split(/@/, $args[0]), $arg[2]||'pro', $args[3];
return $msg->reply(
sprintf( 'Still running `%s` for :%s:', $self->last_msg->body, $self->last_msg->from )
) if $self->is_running;
$self->_deploy_app( $msg, $app, $branch, $env, $dry )
}
sub cmd_help {
my ( $self, $msg, $args ) = @_;
$msg->reply('Some help...');
}
$self->bot->trigger( announce => 'Hello some channel' )
$self->bot->trigger( announce => 'Blah!' => '#random' )
package Supers::Bot;
# ABSTRACT: Soysuper jabber bot
use Moose;
use AnyEvent;
use AnyEvent::XMPP::IM::Connection;
use AnyEvent::XMPP::IM::Message;
use Mojo::SlackRTM;
use AnyEvent::HTTPD
Supers::Bot::SlackMessage
Supers::Bot::HttpMessage
require UNIVERSAL::require;
has user => ( is => 'ro', required => 1 );
has password => ( is => 'ro', required => 1 );
has slack_token => ( is => 'ro', required => 1 );
has httpd_port => ( is => 'ro', default => sub {6200} );
has plugins => ( is => 'rw', isa => 'ArrayRef', default => sub{[]} );
has _plugin => ( is => 'rw', isa => 'HashRef', default => sub{{}} );
has _cv => ( is => 'ro', default => sub {AnyEvent->condvar} );
has _xmpp => ( is => 'rw', lazy => 1, builder => '_build_xmpp', clearer => 'reset_connection' );
has _slack => ( is => 'ro', lazy => 1, default => sub { Mojo::SlackRTM->new(token => shift->slack_token) } );
has _httpd => ( is => 'ro', lazy => 1, default => sub { AnyEvent::HTTPD->new(port => shift->httpd_port) } );
sub BUILD {
my $self = shift;
$self->_init_plugins;
$self->_init_events;
$self->_init_slack;
$self->_init_httpd;
}
sub _init_httpd {
my $self = shift;
$self->_httpd->reg_cb(
'/' => sub {
shift->stop_request;
shift->respond({ content => ['text/plain', "Soysuper Bot"] })
},
'' => sub {
$self->trigger( message => Supers::Bot::HttpMessage->new(
httpd => shift,
req => shift
))
}
);
}
package Supers::Bot::HttpMessage;
use Moose;
has httpd => is => 'ro', required => 1;
has req => is => 'ro', required => 1;
has body => is => 'ro', lazy => 1,
default => sub { join ' ', grep {$_} split(/\//, shift->req->url->path) };
has from => is => 'ro', default => 'api-rest';
has channel => is => 'ro', default => '';
has _res =>
is => 'ro',
isa => 'ArrayRef[Str]',
traits => ['Array'],
default => sub {[]},
handles => {
responses => 'elements',
add_response => 'push',
has_response => 'count',
};
sub reply { shift->add_response(shift) }
sub DEMOLISH {
my ( $self, $is_global ) = @_;
return if $is_global;
$self->has_response
? $self->req->respond({ content => ['text/plain', join("\n\n", $self->responses)] })
: $self->req->respond ([ 404, 'not found', { 'Content-Type' => 'text/plain' }, 'Unknown action' ]);
}
__PACKAGE__->meta->make_immutable;
$ curl http://bot.ss:6200/deploy/api
$ sc deploy api
$ sc deploy api + deploy workers - deploy app
#!/usr/bin/env perl
use common::sense;
use Mojo::UserAgent;
use Data::Dump qw/pp/;
my $ua = Mojo::UserAgent->new->inactivity_timeout(360)->request_timeout(360);
for my $plan ( get_plan(@ARGV) ) { Mojo::IOLoop->delay(@$plan)->wait }
sub get_plan {
my $base = 'http://deploy:6200/';
map {
my @tasks = @$_; [
sub{
my $delay = shift;
say '> '. pp(\@tasks);
$ua->get($_ => { 'ss-from' => $ENV{USER} } => $delay->begin) for map {$base.$_} @tasks;
},
sub{
my $delay = shift;
my $count = 0;
say '- '. $tasks[$count++].":\n". $_->res->body for @_;
say '< '. pp(\@tasks) . "\n";
}
]} map {[ split m|/\+/| ]} split( m|/\-/|, join('/', @_) )
}