require 'pathname' require 'thread' require 'json' require 'hipchat' require 'daybreak' require 'sinatra/base' require_relative 'helpers' require_relative 'metadata' Thread.abort_on_exception = true module KitchenHooks class App < Sinatra::Application set :root, File.join(KitchenHooks::ROOT, 'web') enable :sessions def self.db! path=@@db_path if defined? @@db @@db.flush @@db.close end @@db_path = path @@db = Daybreak::DB.new path end def self.tmp! dir ; @@tmp = dir end def self.close! @@sync_worker.kill @@backlog_worker.kill @@db.flush @@db.close end def self.backlog! @@backlog = Queue.new @@backlog_worker = Thread.new do loop do event = @@backlog.shift App.process event end end end def self.sync! @@sync_worker = Thread.new do loop do process_sync sleep @@sync_interval end end end def self.config! config @@config = config @@hipchat = nil if config['hipchat'] @@hipchat = HipChat::Client.new config['hipchat']['token'] @@hipchat_nick = config['hipchat']['nick'] || raise('No HipChat "nick" provided') @@hipchat_room = config['hipchat']['room'] || raise('No HipChat "room" provided') end @@knives = config['knives'].map do |_, knife| Pathname.new(knife).expand_path.realpath.to_s end @@sync_interval = config.fetch 'sync_interval', 3600 # Hourly end get '/backlog' do content_type :json JSON.pretty_generate \ backlog: @@backlog.inspect, length: @@backlog.length end get '/' do App.process_release db_entries = {} @@db.each do |k, v| db_entries[k] = v unless k =~ /^meta/ end erb :app, locals: { database: db_entries.sort_by { |stamp, _| stamp } } end get '/favicon.ico' do send_file File.join(settings.root, 'favicon.ico'), \ :disposition => 'inline' end get %r|/app/(.*)| do |fn| send_file File.join(settings.root, 'app', fn), \ :disposition => 'inline' end post '/' do request.body.rewind event = JSON::parse request.body.read rescue nil @@backlog.push event end private def self.db ; @@db end def self.tmp ; @@tmp ||= '/tmp' end def self.knives ; @@knives ||= [] end def self.hipchat message, color return if @@hipchat.nil? @@hipchat[@@hipchat_room].send @@hipchat_nick, message, \ color: color, notify: false, message_format: 'html' end def self.notify entry color = case entry[:type] when 'failure' ; 'red' when 'release' ; 'purple' when 'unsynced' ; 'yellow' else ; 'green' end hipchat notification(entry), color end # error == nil => success # error == true => success # error == false => nop # otherwise => failure def self.mark event, type, error=nil return if error == false error = nil if error == true entry = { type: type, event: event } entry.merge!(error: error, type: 'failure') if error db.lock do db[Time.now.to_f] = entry end db.flush notify entry end def self.process_release version=KitchenHooks::VERSION return if db['meta_version'] == version db.lock do db.set! 'meta_version', version end mark version, 'release' end def self.process_sync cached_nodes = db['meta_cached_nodes'] cached_nodes ||= {} sync_servers = SyncServers.new knives, cached_nodes db.lock do db.set! 'meta_cached_nodes', sync_servers.cached_nodes end sync = sync_servers.status puts 'Sync completed' db! sync_tag = sync[:num_failures].zero? ? 'synced' : 'unsynced' mark sync, sync_tag db! end def self.process event if event.nil? # JSON parse failed mark event, 'failure', 'Could not parse WebHook payload' return end if commit_to_kitchen?(event) possible_error = begin perform_kitchen_upload event, knives rescue Exception => e report_error e, 'Could not perform kitchen upload: <i>%s</i>' % e.message.lines.first end mark event, 'kitchen upload', possible_error end if tagged_commit_to_cookbook?(event) && tag_name(event) =~ /^v\d+/ # Cookbooks tagged with a version possible_error = begin perform_cookbook_upload event, knives rescue Exception => e report_error e, 'Could not perform cookbook upload: <i>%s</i>' % e.message.lines.first end mark event, 'cookbook upload', possible_error end if tagged_commit_to_realm?(event) && tag_name(event) =~ /^bjn_/ # Realms tagged with an environment possible_error = begin perform_constraint_application event, knives rescue Exception => e report_error e, 'Could not apply constraints: <i>%s</i>' % e.message.lines.first end mark event, 'constraint application', possible_error end end end end