-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.rb
189 lines (163 loc) · 6.86 KB
/
app.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# frozen_string_literal: true
require 'yaml'
require 'json'
require 'open3'
require 'sinatra'
require 'uri'
require 'net/http'
require 'octokit'
require 'redis'
require 'sidekiq'
##
# Add dig to the hash for nested deep queries
class Hash
def dig(*path)
path.inject(self) do |location, key|
location.respond_to?(:keys) ? location[key] : nil
end
end
end
##
# A module to expose slack notification functionality to the worker and the sinatra app
module SlackNotifier
module_function
##
# Send a slack message to hubot endpoint. Depends on a hubot endpoint capable of handling a token
# and message to send to a slack channel.
# ie. http://some.server/hubot/msg/{channel}
#
# @param config [Hash] the settings.config (sinatra.yml)
# @param msg [String] the message to send to slack
# @param channel [String] the channel name (without the hash symbol) to send a message to
def slack_message(config, msg, channel)
uri = URI.parse(config['slack']['message_url'].gsub('{channel}', channel.to_s))
payload = { token: config['slack']['token'], msg: msg }
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/x-www-form-urlencoded'
req.body = "payload=#{payload.to_json}"
Net::HTTP.start(uri.host, uri.port) do |http|
http.request(req)
end
end
end
##
# Setup redis connection for background notifications
include SlackNotifier
environment = ENV.fetch('RAILS_ENV', 'development')
redis_conn = { url: "redis://#{environment == 'development' ? 'redis' : '127.0.0.1'}:6379" }
Sidekiq.configure_client do |config|
config.redis = redis_conn.merge(namespace: 'deployment', size: 1)
end
Sidekiq.configure_server do |config|
config.redis = redis_conn.merge(namespace: 'deployment')
end
##
# Configure the sinatra application with the application base configurations in config/app.yml
configure do
set(:config) { load_config("config/#{File.basename(__FILE__, '.*')}.yml") }
end
##
# /events expects a Github deployment JSON object to be POSTed. See: https://developer.github.com/v3/repos/deployments/
# The endpoint that this Sinatra app is listening on should be the target of a webhook for "Deployment Status" and "Deployment" for
# the repository to be deployed using this application.
post '/events' do
# JSON data payload comes from github webook
data = JSON.parse request.body.read
# variables for slack notifications
app_name = data['repository']['name']
environment = data['deployment']['environment']
channel = settings.config['slack']['channel']
# load the config specific to the app being deployed
deploy_app_config = settings.config['deploy_app_config']
app_config = load_config(deploy_app_config.gsub('{app_name}', app_name))
# set the variables related to the app being deployed
channel = get_channel(app_config, data)
command = app_config['deployment']['command']
servers = app_config['deployment'][environment]['servers']
username = app_config['deployment'][environment]['username']
app_path = app_config['deployment'][environment]['app_path']
# get all the users gists that are like {app_name}_deploy, keep the most recent 3 and delete the rest
github_client = Octokit::Client.new(access_token: settings.config['github']['gist_token'], api_endpoint: settings.config['github']['api_endpoint'])
gists = github_client.gists(settings.config['github']['username'])
.select { |g| g.files.to_hash.key?("#{app_name}_deploy".to_sym) }
.sort_by(&:created_at)
gists.drop(3).each do |g|
github_client.delete_gist(g.id)
end
# fire off a worker to deploy the app to each configured server
servers.each do |server|
Worker.perform_async(server, username, app_path, command, environment, app_name, channel, settings.config)
end
# return a quick 200 to github deployment event server
200
rescue StandardError => e
slack_message(settings.config, "#{app_name} : :x: : Unable to deploy to #{environment}, exception: #{e.message}", channel)
raise e
end
##
# Load a config yml file
#
# @param file_relative_path [String] the relative path to the config file
def load_config(file_relative_path)
YAML.load_file(File.join(File.dirname(__FILE__), file_relative_path))
end
##
# Return the appropriate Slack channel to send the message to
#
# @param app_config [Hash] the application configuration
# @param deployment_data [Hash] the deployment message from Github
def get_channel(app_config, deployment_data)
channel = app_config['deployment']['slack_channel'] unless app_config['deployment']['slack_channel'].empty?
channel = deployment_data['deployment']['payload']['notify']['room'] if deployment_data.dig('deployment','payload','notify','room')
channel
end
##
# Sidekiq worker to perform the remote deploy process, update github gists, and notify slack with messages
class Worker
include Sidekiq::Worker
include SlackNotifier
##
# Perform the task of remote deploying the application, sending a slack message, and posting a gist
def perform(server, username, app_path, command, environment, app_name, channel, config)
slack_message(config, "#{app_name} : Deploying #{environment} environment to #{server}.", channel)
cmd = "ssh #{username}@#{server} 'cd -- #{app_path} && #{command.gsub('{environment}', environment)}'"
o, s = Open3.capture2e(cmd)
gist = post_gist(config, app_name, o)
status_message = set_deploy_status(o, s)
status_text = "#{app_name} : #{status_message} See gist for logs: #{gist['html_url']}"
slack_message(config, status_text, channel)
puts "Deployed #{app_name} to #{s}, command=#{cmd}, gist=#{gist['html_url']}, result: output=#{o}, status=#{s}"
end
##
# Determine and set the deployment status for the slack message
#
# @param cap_out [String] the output of the capistrano ssh command
# @param status [Process::Status] the process status returned
# @return [String] a deployment status message string for slack
def set_deploy_status(cap_out, status)
if status.success? && !cap_out.include?('deploy:rollback')
':thumbsup: : Successfully deployed.'
else
':x: : Failed to deploy.'
end
end
##
# Post a new gist to github with the output from the capistrano remote command
#
# @param config [Hash] the settings.config (sinatra.yml)
# @param app_name [String] the application name being deployed
# @param cap_out [String] the output from the deployment command
# @return [Sawyer::Resource] the gist that was created using Octokit
def post_gist(config, app_name, cap_out)
cap_out ||= 'No output.'
payload = {
description: "#{app_name} deployment on #{Time.now}",
public: false,
files: {
"#{app_name}_deploy" => { :content => cap_out.force_encoding('ISO-8859-1') }
}
}
github_client = Octokit::Client.new(access_token: config['github']['gist_token'], api_endpoint: config['github']['api_endpoint'])
github_client.create_gist(payload)
end
end