Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webhooks notifications based on ENV vars #25

Merged
merged 10 commits into from
Jul 30, 2019
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
brotandgames marked this conversation as resolved.
Show resolved Hide resolved
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