Skip to content

Commit

Permalink
Webhooks notifications based on ENV vars (brotandgames#25)
Browse files Browse the repository at this point in the history
* Add basic structure for Webhooks notifications

* Webhooks final implementation

* Add tests for webhooks notifications

* Fix Rubocop

* Fix typo '#notify' instead of '#notiy'

Co-Authored-By: Brot & Games <[email protected]>

* Add example configuration for RocketChat.

* Create mail notification + introduce url and check_url

* Fix a typo at webhook_configuration.md

* Fix log entry when status changed.
  • Loading branch information
tareksamni authored and brotandgames committed Jul 30, 2019
1 parent 2df58d0 commit c6c69bd
Show file tree
Hide file tree
Showing 19 changed files with 295 additions and 29 deletions.
13 changes: 8 additions & 5 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
inherit_from: .rubocop_todo.yml

Metrics/AbcSize:
Max: 40
Enabled: false

Metrics/BlockLength:
Max: 300
Enabled: false

Metrics/CyclomaticComplexity:
Max: 10
Enabled: false

Metrics/MethodLength:
Max: 35
Enabled: false

Metrics/PerceivedComplexity:
Max: 10
Enabled: false

Metrics/LineLength:
Enabled: false
4 changes: 3 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ gem 'webpacker', '~> 4.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
gem 'jbuilder', '~> 2.9'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use Active Model has_secure_password
Expand Down Expand Up @@ -49,9 +49,11 @@ end
group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15'
gem 'mocha'
gem 'selenium-webdriver'
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
gem 'webmock'
end

group :development, :test do
Expand Down
23 changes: 17 additions & 6 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ GEM
childprocess (1.0.1)
rake (< 13.0)
concurrent-ruby (1.1.5)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.4)
erubi (1.8.0)
et-orbi (1.2.1)
Expand All @@ -92,12 +94,12 @@ GEM
raabro (~> 1.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
hashdiff (1.0.0)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
jaro_winkler (1.5.3)
jbuilder (2.8.0)
jbuilder (2.9.1)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.7.0)
Expand All @@ -113,13 +115,15 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
metaclass (0.0.4)
method_source (0.9.2)
mimemagic (0.3.3)
mini_mime (1.0.1)
mini_portile2 (2.4.0)
minitest (5.11.3)
mocha (1.9.0)
metaclass (~> 0.0.1)
msgpack (1.2.10)
multi_json (1.13.1)
nio4r (2.3.1)
nokogiri (1.10.3)
mini_portile2 (~> 2.4.0)
Expand Down Expand Up @@ -179,6 +183,7 @@ GEM
rubyzip (1.2.2)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
Expand Down Expand Up @@ -238,6 +243,10 @@ GEM
nokogiri (~> 1.6)
rubyzip (~> 1.0)
selenium-webdriver (~> 3.0)
webmock (3.6.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (4.0.2)
activesupport (>= 4.2)
rack-proxy (>= 0.6.1)
Expand All @@ -248,7 +257,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.20)
zeitwerk (2.1.6)
zeitwerk (2.1.9)

PLATFORMS
ruby
Expand All @@ -258,9 +267,10 @@ DEPENDENCIES
bootstrap (~> 4.3.1)
byebug
capybara (>= 2.15)
jbuilder (~> 2.5)
jbuilder (~> 2.9)
letter_opener
listen (>= 3.0.5, < 3.2)
mocha
puma (~> 3.11)
rails (~> 6.0.0.rc1)
rubocop
Expand All @@ -276,11 +286,12 @@ DEPENDENCIES
tzinfo-data
web-console (>= 3.3.0)
webdrivers
webmock
webpacker (~> 4.0)
yard

RUBY VERSION
ruby 2.6.3p62

BUNDLED WITH
2.0.1
2.0.2
2 changes: 0 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }

def authenticate
# rubocop:disable Metrics/LineLength
basic_auth_username = ENV.fetch('BASIC_AUTH_USERNAME', '')
basic_auth_password = ENV.fetch('BASIC_AUTH_PASSWORD', '')

Expand All @@ -14,6 +13,5 @@ def authenticate
authenticate_or_request_with_http_basic('Ciao Application') do |username, password|
username == basic_auth_username && password == basic_auth_password
end
# rubocop:enable Metrics/LineLength
end
end
19 changes: 19 additions & 0 deletions app/lib/ciao/notifications/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Ciao
module Notifications
class Base
def initialize(endpoint = nil,
payload_template = nil,
payload_renderer_cls = Ciao::Renderers::ReplaceRenderer)
@endpoint = endpoint
@payload_renderer = payload_renderer_cls.new(payload_template)
end

def notify(_payload_data = {})
raise NotImplementedError,
'You can not call Ciao::Notifications::Base#notify directly'
end
end
end
end
11 changes: 11 additions & 0 deletions app/lib/ciao/notifications/mail_notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

module Ciao
module Notifications
class MailNotification < Base
def notify(payload_data = {})
CheckMailer.with(payload_data).change_status_mail.deliver
end
end
end
end
22 changes: 22 additions & 0 deletions app/lib/ciao/notifications/webhook_notification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Ciao
module Notifications
class WebhookNotification < Base
def notify(payload_data = {})
uri = URI.parse(@endpoint)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'

request = Net::HTTP::Post.new(
uri.request_uri,
'Content-Type' => 'application/json'
)
request.body = @payload_renderer.render(payload_data)
http.request(request)
rescue *NET_HTTP_ERRORS => e
Rails.logger.error "Ciao::Notifications::WebhookNotification#notify Could not notify webhook(#{@endpoint}) - #{e}"
end
end
end
end
34 changes: 34 additions & 0 deletions app/lib/ciao/parsers/webhook_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Ciao
module Parsers
class WebhookParser
WEBHOOKS_ENDPOINT_PREFIX = 'CIAO_WEBHOOK_ENDPOINT_'
WEBHOOKS_PAYLOAD_PREFIX = 'CIAO_WEBHOOK_PAYLOAD_'

WEBHOOKS_ENDPOINT_FORMAT = "#{WEBHOOKS_ENDPOINT_PREFIX}%s"
WEBHOOKS_PAYLOAD_FORMAT = "#{WEBHOOKS_PAYLOAD_PREFIX}%s"

WEBHOOKS_FORMAT_REGEX = /^#{WEBHOOKS_ENDPOINT_PREFIX}(?<name>[A-Z0-9_]+)$/.freeze

def self.webhooks
names.map do |check_name|
{
endpoint: ENV.fetch(WEBHOOKS_ENDPOINT_FORMAT % check_name, ''),
payload: ENV.fetch(WEBHOOKS_PAYLOAD_FORMAT % check_name, '')
}
end
end

def self.names
matches.map { |match| match[:name] }
end

def self.matches
ENV.map do |k, _v|
k.match(WEBHOOKS_FORMAT_REGEX)
end.compact
end
end
end
end
16 changes: 16 additions & 0 deletions app/lib/ciao/renderers/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Ciao
module Renderers
class Base
def initialize(template)
@template = template
end

def render(_data)
raise NotImplementedError,
'You can not call Ciao::Renderers::Base#render directly'
end
end
end
end
24 changes: 24 additions & 0 deletions app/lib/ciao/renderers/replace_renderer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Ciao
module Renderers
class ReplaceRenderer < Base
CHECK_NAME_PLACEHOLDER = '__name__'
STATUS_AFTER_PLACEHOLDER = '__status_after__'
STATUS_BEFORE_PLACEHOLDER = '__status_before__'
URL_PLACEHOLDER = '__url__'
CHECK_URL_PLACEHOLDER = '__check_url__'

def render(data)
return '' if @template.nil?

@template
.gsub(CHECK_NAME_PLACEHOLDER, data.fetch(:name, '').to_s)
.gsub(STATUS_AFTER_PLACEHOLDER, data.fetch(:status_after, '').to_s)
.gsub(STATUS_BEFORE_PLACEHOLDER, data.fetch(:status_before, '').to_s)
.gsub(URL_PLACEHOLDER, data.fetch(:url, '').to_s)
.gsub(CHECK_URL_PLACEHOLDER, data.fetch(:check_url, '').to_s)
end
end
end
end
14 changes: 10 additions & 4 deletions app/models/check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def self.percentage_healthy
end

def create_job
# rubocop:disable Metrics/LineLength
job =
Rufus::Scheduler.singleton.cron cron, job: true do
url = URI.parse(self.url)
Expand All @@ -62,8 +61,16 @@ def create_job
status_after = self.status
end
if status_before != status_after
CheckMailer.with(name: name, status_before: status_before, status_after: status_after).change_status_mail.deliver
Rails.logger.info "ciao-scheduler Sent 'changed_status' notification mail"
Rails.logger.info "ciao-scheduler Check '#{name}': Status changed from '#{status_before}' to '#{status_after}'"
NOTIFICATIONS.each do |notification|
notification.notify(
name: name,
status_before: status_before,
status_after: status_after,
url: url,
check_url: Rails.application.routes.url_helpers.check_path(self)
)
end
end
end
if job
Expand All @@ -73,7 +80,6 @@ def create_job
Rails.logger.error 'ciao-scheduler Could not create job'
end
job
# rubocop:enable Metrics/LineLength
end

def unschedule_job
Expand Down
4 changes: 2 additions & 2 deletions bin/bundle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ m = Module.new do
bundler_version = nil
update_index = nil
ARGV.each_with_index do |a, i|
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN # rubocop:disable Style/IfUnlessModifier
bundler_version = a
end
next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
Expand Down Expand Up @@ -79,7 +79,7 @@ m = Module.new do
end

def activate_bundler(bundler_version)
if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new('2.0')
if Gem::Version.correct?(bundler_version) && Gem::Version.new(bundler_version).release < Gem::Version.new('2.0') # rubocop:disable Style/IfUnlessModifier
bundler_version = '< 2'
end
gem_error = activation_error_handling do
Expand Down
3 changes: 2 additions & 1 deletion config/initializers/create_background_jobs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Create all Rufus Scheduler Jobs for active checks on Application Start
# Prevent the initializer to be run during rake tasks
if defined?(Rails::Server) && ActiveRecord::Base.connection.table_exists?('checks')

if defined?(Rails::Server) && ActiveRecord::Base.connection.table_exists?('checks') # rubocop:disable Style/IfUnlessModifier
Check.active.each(&:create_job)
end
19 changes: 19 additions & 0 deletions config/initializers/notifications.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

# Some time in the future Rails is not going to auto_load these for us :(
# we have to explictly require it here
Dir[Rails.root.join('app', 'lib', 'ciao', '**', '*.rb')].each { |f| require f }

# export CIAO_WEBHOOK_ENDPOINT_$NAME=https://chat.yourhost.net/*****
# export CIAO_WEBHOOK_PAYLOAD_$NAME=#'{"username":"Brot & Games","icon_url":"https://avatars0.githubusercontent.com/u/43862266?s=400&v=4","text":"Example message","attachments":[{"title":"Rocket.Chat","title_link":"https://rocket.chat","text":"Rocket.Chat, the best open source chat","image_url":"/images/integration-attachment-example.png","color":"#764FA5"}]}'
# `$NAME` can be any word `[A-Z0-9_]+` and must be unique as it is used as an identifier

NOTIFICATIONS = Ciao::Parsers::WebhookParser.webhooks.map do |webhook|
Ciao::Notifications::WebhookNotification.new(
webhook[:endpoint],
webhook[:payload],
Ciao::Renderers::ReplaceRenderer
)
end

NOTIFICATIONS << Ciao::Notifications::MailNotification.new if ENV['SMTP_ADDRESS'].present?
2 changes: 2 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
ENV['RAILS_ENV'] ||= 'test'
require_relative '../config/environment'
require 'rails/test_help'
require 'mocha/minitest'
require 'webmock/minitest'

module ActiveSupport
class TestCase
Expand Down
Loading

0 comments on commit c6c69bd

Please sign in to comment.