#!/usr/bin/perl # # bin/worker/sns-notifier # # Listens on SNS queues and takes some action when a message comes in. # # Authors: # Mark Smith # # Copyright (c) 2019 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. # use strict; use v5.10; BEGIN { require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; } use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); use List::Util qw/ sum /; use LWP::UserAgent; use Paws; use POSIX qw/ strftime /; use LJ::JSON; my $HOOK_URL = $LJ::BUILD_NOTIFY_HOOK_URL or die; my @ALL_PHASES = qw/ SUBMITTED QUEUED PROVISIONING DOWNLOAD_SOURCE INSTALL PRE_BUILD POST_BUILD UPLOAD_ARTIFACTS FINALIZING COMPLETED /; my %PHASES = ( SUBMITTED => rgb( 255, 255, 128 ), QUEUED => rgb( 128, 128, 255 ), BUILD => rgb( 128, 128, 255 ), COMPLETED => rgb( 128, 255, 128 ), ); ## Establish SQS connection my $paws = Paws->new( config => { region => $LJ::SQS{region}, }, ) or $log->logcroak('Failed to initialize Paws object.'); my $sqs = $paws->service('SQS') or $log->logcroak('Failed to initialize Paws::SQS object.'); my $queue_url = $sqs->GetQueueUrl( QueueName => 'dw-codebuild-notifications' )->QueueUrl; ## Start listening for notifications, one by one while (1) { my $res = eval { $sqs->ReceiveMessage( QueueUrl => $queue_url, MaxNumberOfMessages => 1, WaitTimeSeconds => 20 ); }; if ( $@ && $@->isa('Paws::Exception') ) { die $@->Message; } my $messages = $res->Messages; $log->warn( 'Got ', scalar(@$messages), ' messages.' ); next unless @$messages; foreach my $message (@$messages) { my $msg = LJ::JSON::from_json( $message->Body ); my $d = $msg->{detail}; my $ai = $d->{'additional-information'}; my $project = $d->{'project-name'} // '(unknown)'; my $current = $d->{'current-phase'}; my $completed = $d->{'completed-phase'}; my $next = $ai->{phases} ? $ai->{phases}[-1]{'phase-type'} : undef; my ( $phase, $message ); if ( $current eq 'SUBMITTED' ) { # Job has been submitted (very first phase) $phase = $current; $message = 'Build request submitted, waiting for a VM...'; } elsif ( $current eq 'COMPLETED' ) { # Job is fully complete $phase = $current; $message = 'Upload completed, build was successful!'; } elsif ( $completed eq 'BUILD' ) { # Job has finished building, moving on to uploading $phase = $completed; $message = 'Build stage complete, uploading artifact.'; } elsif ( $completed eq 'QUEUED' ) { # Job has been assigned resources and is beginning to execute # # This might be nice if we ever see lag between SUBMITTED and QUEUED, # but in all our experience so far, it's instant. Let's not send it # for now. # # $phase = $completed; # $message = 'Resources allocated, provisioning build environment.'; } next unless $phase; my $color = $PHASES{$phase}; my $seconds = $d->{'completed-phase-duration-seconds'}; my $elapsed = $ai->{phases} ? sum( map { $_->{'duration-in-seconds'} + 0 } @{ $ai->{phases} } ) : undef; my $embed = make_embed( $project, $message, $color, commit => github_url( $ai->{'source-version'} ), duration => ( $seconds ? timestr($seconds) : '--' ), elapsed => ( $elapsed ? timestr($elapsed) : '--' ), ); send_webhook($embed) or die; } my $idx = 0; $sqs->DeleteMessageBatch( QueueUrl => $queue_url, Entries => [ map { { Id => $idx++, ReceiptHandle => $_->ReceiptHandle } } @$messages ] ); if ( $@ && $@->isa('Paws::Exception') ) { die $@->Message; } } sub make_embed { my ( $project, $msg, $color, @fields ) = @_; my @embed_fields; while ( my @field = splice @fields, 0, 2 ) { push @embed_fields, { name => $field[0], value => $field[1], inline => LJ::JSON->to_boolean(1) }; } my $obj = { 'embeds' => [ { 'title' => sprintf( '[%s] build update', $project ), 'description' => $msg, 'fields' => \@embed_fields, 'color' => $color, 'footer' => { 'text' => 'AWS CodeBuild Notifier' }, 'timestamp' => strftime( '%Y-%m-%dT%H:%M:%S.000Z', gmtime ), } ] }; return $obj; } sub send_webhook { my $obj = $_[0]; my $ua = LWP::UserAgent->new; $ua->agent('Dreamwidth AWS CodeBuild Notifier '); my $res = $ua->post( $HOOK_URL, Content => LJ::JSON::to_json($obj), 'Content-Type' => 'application/json', ); return 1 if $res->is_success; die $res->decoded_content; } sub rgb { return ( $_[0] << 16 ) + ( $_[1] << 8 ) + $_[2]; } sub timestr { my $secs = $_[0]; if ( $secs > 3600 ) { return sprintf( '%dh%s', int( $secs / 3600 ), timestr( $secs % 3600 ) ); } elsif ( $secs > 60 ) { return sprintf( '%dm%s', int( $secs / 60 ), timestr( $secs % 60 ) ); } else { return sprintf( '%ds', $secs ); } } sub github_url { my $hash = substr( $_[0], 0, 8 ); return qq|[$hash](https://github.com/dreamwidth/dreamwidth/commit/$hash)|; }