octolab 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/octolab +134 -0
  3. data/helpers/ssh +2 -0
  4. data/lib/octolab.rb +311 -0
  5. metadata +48 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ca57dcf29c979b7163dfbafb2d0c52bff63094d3
4
+ data.tar.gz: 06037f1b1de72dfa6684c56b895cfcfa2b11d436
5
+ SHA512:
6
+ metadata.gz: 0e67e12c7b58011040acba8588e1c8e0ddd191520cd0f488862fd98ae36252d4c6b8d635fd9ec4936e7dc74c3fafbd16bfee739ffc36bf2ebe9d9e5f56ffd8f6
7
+ data.tar.gz: 6d9a5aaa6f974ed4c30822edb7f365e5658abeca8084cfef59b4747b19326fa325328be16fd12f59d1fbde9763e213466cb0c92cf0f21a57439d6ed9652fcfae
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ def check_env
4
+ ret = true
5
+ env_vars = ['GITHUB_SECRET',
6
+ 'GITHUB_TOKEN',
7
+ 'GITLAB_TOKEN',
8
+ 'GITLAB_CI_URL',
9
+ 'GIT_DATA_PATH',
10
+ 'PROCESS_UID',
11
+ 'PROCESS_GID',
12
+ 'LOG_DIR_PATH',
13
+ 'LOG_WEB_AGE',
14
+ 'LOG_WEB_SIZE',
15
+ 'LOG_GIT_AGE',
16
+ 'LOG_GIT_SIZE']
17
+
18
+ puts "-" * 20
19
+ puts "\e[1;97mCHECKING ENVIRONMENT\e[0m"
20
+ puts "-" * 20 + "\n\n"
21
+
22
+ env_vars.each { |i|
23
+ if ENV[i].to_s !~ /^$/
24
+ puts sprintf("\e[1;97m%-15s\e[0m= \e[1;92m" + (i =~ /(secret|token)$/i ? "[ *** SET *** ]" : ENV[i].to_s) + "\e[0m", i)
25
+ else
26
+ puts sprintf("\e[1;97m%-15s\e[0m= \e[1;91mmissing\e[0m", i)
27
+ ret = false
28
+ end
29
+ }
30
+
31
+ ret
32
+ end
33
+
34
+ case ARGV[0]
35
+ when 'check'
36
+ exit 1 if !check_env
37
+ exit 0
38
+ when 'start'
39
+ puts 'Starting OctoLab...'
40
+ Dir.mkdir(ENV['LOG_DIR_PATH']) unless File.exist?(ENV['LOG_DIR_PATH'])
41
+
42
+ #if ENV['RACK_ENV'] == 'production'
43
+ # require 'octolab'
44
+ #else
45
+ load 'lib/octolab.rb'
46
+ #end
47
+
48
+ server = 'thin'
49
+ host = ENV['OCTOLAB_LISTEN'] || '0.0.0.0'
50
+ port = ENV['OCTOLAB_PORT'] || '4567'
51
+ web_app = OctoLab.new
52
+
53
+ begin
54
+ Process.egid = ENV['PROCESS_GID'] if ENV['PROCESS_GID'] and Process.egid == 0
55
+ Process.euid = ENV['PROCESS_UID'] if ENV['PROCESS_UID'] and Process.euid == 0
56
+ rescue
57
+ STDERR.puts 'WARNING! Unable to change UID/GID: ' + $!.message
58
+ #exit 1
59
+ end
60
+
61
+ if ENV['RACK_ENV'] == 'production'
62
+ pid = Process.fork
63
+ if pid.nil? then
64
+ Dir.mkdir(ENV['LOG_DIR_PATH']) unless File.exist?(ENV['LOG_DIR_PATH'])
65
+
66
+ $stdout.reopen(ENV['LOG_DIR_PATH'] + '/web.log', 'a')
67
+ $stdout.sync = true
68
+ $stderr.reopen(ENV['LOG_DIR_PATH'] + '/web.log', 'a')
69
+ $stderr.sync = true
70
+
71
+ dispatch = Rack::Builder.app do
72
+ map '/' do
73
+ run web_app
74
+ end
75
+ end
76
+
77
+ Rack::Server.start({
78
+ app: dispatch,
79
+ server: server,
80
+ Host: host,
81
+ Port: port
82
+ })
83
+ else
84
+ if File.exists?(ENV['PID_FILE_PATH'])
85
+ old_pid = File.read(ENV['PID_FILE_PATH'])
86
+ proc_file = '/proc/' + old_pid + '/cmdline'
87
+ if File.exists?(proc_file)
88
+ cmdline = File.read(proc_file).split("\u0000").join(' ')
89
+ if cmdline =~ /^ruby \S+\/bin\/octolab start$/
90
+ puts "OctoLab is running with PID " + old_pid + '.'
91
+ exit 1
92
+ end
93
+ end
94
+ end
95
+
96
+ File.open(ENV['PID_FILE_PATH'], 'w') { |f| f.write(pid) }
97
+ Process.detach(pid)
98
+ end
99
+ else
100
+ dispatch = Rack::Builder.app do
101
+ map '/' do
102
+ run web_app
103
+ end
104
+ end
105
+
106
+ Rack::Server.start({
107
+ app: dispatch,
108
+ server: server,
109
+ Host: host,
110
+ Port: port
111
+ })
112
+ end
113
+ when 'stop'
114
+ Process.kill(15, File.read(ENV['PID_FILE_PATH']).to_i)
115
+ File.delete(ENV['PID_FILE_PATH'])
116
+ puts 'OctoLab has been stopped.'
117
+ when 'status'
118
+ if File.exists?(ENV['PID_FILE_PATH'])
119
+ pid = File.read(ENV['PID_FILE_PATH'])
120
+ proc_file = '/proc/' + pid + '/cmdline'
121
+ if File.exists?(proc_file)
122
+ cmdline = File.read(proc_file).split("\u0000").join(' ')
123
+ if cmdline =~ /^ruby \S+\/bin\/octolab start$/
124
+ puts "OctoLab is running with PID " + pid + '.'
125
+ else
126
+ puts 'OctoLab is stopped, but PID file exists.'
127
+ end
128
+ else
129
+ puts 'OctoLab is stopped, but PID file exists.'
130
+ end
131
+ else
132
+ puts 'OctoLab is stopped.'
133
+ end
134
+ end
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ exec /usr/bin/ssh -o StrictHostKeyChecking=no -i $GIT_SSH_PATH "$@"
@@ -0,0 +1,311 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'logger'
6
+
7
+ Bundler.require
8
+
9
+ module WEBrick
10
+ class HTTPResponse
11
+ def create_error_page
12
+ @body = { :error => HTMLUtils::escape(@reason_phrase) }.to_json
13
+ end
14
+ end
15
+ end
16
+
17
+ class Logger
18
+ def internal(error)
19
+ error '[!] INTERNAL SERVER ERROR - backtrace:'
20
+ error error.inspect
21
+ error error.backtrace
22
+ error '[!] INTERNAL SERVER ERROR - end of backtrace.'
23
+ end
24
+ end
25
+
26
+ module Git
27
+ class Lib
28
+ def remote_add(name, url, opts = {})
29
+ arr_opts = ['add']
30
+ arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
31
+ arr_opts << '-t' << opts[:track] if opts[:track]
32
+ arr_opts << '--mirror=fetch'if opts[:mirror]
33
+ arr_opts << '--'
34
+ arr_opts << name
35
+ arr_opts << url
36
+
37
+ command('remote', arr_opts)
38
+ end
39
+ end
40
+ end
41
+
42
+ class OctoLab < Sinatra::Base
43
+
44
+ GITHUB_SECRET = ENV['GITHUB_SECRET'].to_s
45
+ GITHUB_TOKEN = ENV['GITHUB_TOKEN'].to_s
46
+ GITLAB_TOKEN = ENV['GITLAB_TOKEN'].to_s
47
+ GITLAB_CI_URL = ENV['GITLAB_CI_URL'].to_s
48
+ GIT_DATA_PATH = ENV['GIT_DATA_PATH'].to_s
49
+ PROCESS_UID = ENV['PROCESS_UID'].to_s
50
+ PROCESS_GID = ENV['PROCESS_GID'].to_s
51
+ LOG_DIR_PATH = ENV['LOG_DIR_PATH'].to_s
52
+ LOG_WEB_AGE = ENV['LOG_WEB_AGE'].to_s
53
+ LOG_WEB_SIZE = ENV['LOG_WEB_SIZE'].to_s
54
+ LOG_GIT_AGE = ENV['LOG_GIT_AGE'].to_s
55
+ LOG_GIT_SIZE = ENV['LOG_GIT_SIZE'].to_s
56
+ ROOT_DIR = File.realpath(__FILE__.split('/')[0..-3].join('/'))
57
+
58
+ class HTTPError < StandardError
59
+ end
60
+
61
+ class TriggerError < StandardError
62
+ end
63
+
64
+ configure do
65
+ set :root, __FILE__.split('/')[0..-3].join('/')
66
+ set :server, 'thin'
67
+ set :run, true
68
+ set :haml, { :ugly => true }
69
+ set :clean_trace, true
70
+ enable :logging
71
+
72
+
73
+ if settings.production?
74
+ $log_app = Logger.new(LOG_DIR_PATH + '/app.log', LOG_WEB_AGE, LOG_WEB_SIZE)
75
+ $log_git = Logger.new(LOG_DIR_PATH + '/git.log', LOG_GIT_AGE, LOG_GIT_SIZE)
76
+ $log_app.level = Logger::DEBUG
77
+ $log_git.level = Logger::DEBUG
78
+ else
79
+ $log_app = Logger.new(STDOUT)
80
+ $log_git = Logger.new(STDOUT)
81
+ $log_app.level = Logger::DEBUG
82
+ $log_git.level = Logger::DEBUG
83
+ end
84
+ end
85
+
86
+ before do
87
+ @client ||= Octokit::Client.new(:access_token => GITHUB_TOKEN)
88
+ end
89
+
90
+ post '/events' do
91
+ begin
92
+ verify_signature(request.body.read)
93
+ request.body.rewind
94
+ payload = JSON.parse(request.body.read)
95
+ $log_app.debug JSON.pretty_generate(payload) if settings.development?
96
+ $log_app.info '[*] Received event: ' + request.env['HTTP_X_GITHUB_EVENT'] if request.env['HTTP_X_GITHUB_EVENT']
97
+
98
+ case request.env['HTTP_X_GITHUB_EVENT']
99
+ when 'pull_request'
100
+ git_fetch_github(payload['pull_request']['base']['repo']['full_name'])
101
+ if payload['action'] =~ /(re)*opened/
102
+ ENV['OCTOLAB_COMMITS'] = (ENV['OCTOLAB_COMMITS'].to_s.split(',') + [payload['pull_request']['head']['sha']]).join(',')
103
+ process_pull_request(payload['pull_request'])
104
+ end
105
+ when 'push'
106
+ git_fetch_github(payload['repository']['full_name'])
107
+ process_push(payload)
108
+ end
109
+ rescue
110
+ $log_app.internal($!)
111
+ halt 500, { :error => 'Processing failed' }.to_json
112
+ end
113
+ end
114
+
115
+ post '/builds' do
116
+ begin
117
+ payload = JSON.parse(request.body.read)
118
+ $log_app.debug JSON.pretty_generate(payload) if settings.development?
119
+
120
+ case request.env['HTTP_X_GITLAB_EVENT']
121
+ when 'Build Hook'
122
+ local_name = payload['project_name'].gsub(/\s\/\s/, '/')
123
+ commit = payload['sha']
124
+ git_dir = GIT_DATA_PATH + '/' + local_name + '.git'
125
+ git = Git.open(git_dir, {:log => $log_git, :repository => git_dir, :working_directory => nil })
126
+ remote_name = git.remotes.select { |i| i.name == 'github' }.first.url.split(':').last.gsub(/\.git$/, '')
127
+ build_id = payload['build_id'].to_s
128
+ commit_status = payload['commit']['status']
129
+ build_status = payload['build_status']
130
+ build_status = 'failure' if payload['build_status'] == 'canceled' or payload['build_status'] == 'failed'
131
+ description = 'The build is in progress...'
132
+ description = 'The build succeeded!' if build_status == 'success'
133
+ description = 'The build has failed!' if build_status == 'failure'
134
+ options = {
135
+ :state => build_status,
136
+ :target_url => GITLAB_CI_URL + '/' + remote_name + '/builds/' + build_id,
137
+ :description => description,
138
+ :context => 'continuous-integration/gitlab'
139
+ }
140
+
141
+ $log_app.info '[*] Build status changed: ' + remote_name + ': ' + commit + ': ' + build_status
142
+
143
+ if ENV['OCTOLAB_COMMITS'].to_s.split(',').include?(commit)
144
+ $log_app.info '[*] Sending status update: ' + remote_name + ': ' + commit + ': ' + build_status
145
+ if build_status =~ /pending/
146
+ @client.create_status(remote_name, commit, build_status, options)
147
+ elsif build_status =~ /failure/ or (build_status =~ /success/ and commit_status =~ /success/)
148
+ @client.create_status(remote_name, commit, build_status, options)
149
+ ENV['OCTOLAB_COMMITS'] = ENV['OCTOLAB_COMMITS'].to_s.split(',').delete_if { |i| i == commit }.join(',')
150
+ end
151
+ end
152
+
153
+ return
154
+ end
155
+ rescue
156
+ $log_app.internal($!)
157
+ halt 500, { :error => 'Processing failed' }.to_json
158
+ end
159
+ end
160
+
161
+ helpers do
162
+ def logger
163
+ request.logger
164
+ end
165
+
166
+ def verify_signature(body)
167
+ signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), GITHUB_SECRET, body)
168
+ return halt 500, { :error => "Signatures didn't match!" }.to_json unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE'])
169
+ end
170
+
171
+ def git_fetch_github(full_name)
172
+ $log_app.info '[*] Fetching: ' + full_name
173
+
174
+ Git.configure do |config|
175
+ config.git_ssh = ROOT_DIR + '/helpers/ssh'
176
+ end
177
+
178
+ git_dir = GIT_DATA_PATH + '/' + full_name + '.git'
179
+ git = Git.open(git_dir, { :log => $log_git, :repository => git_dir, :working_directory => nil })
180
+
181
+ if git.remotes.length == 0
182
+ git.add_remote('github', 'git@github.com:' + full_name + '.git', :mirror => true)
183
+ end
184
+
185
+ git.remote('github').fetch
186
+ end
187
+
188
+ def get_project(name)
189
+ begin
190
+ url = GITLAB_CI_URL + '/api/v3/projects'
191
+ uri = URI(url)
192
+ http = Net::HTTP.new(uri.host, uri.port)
193
+ http.use_ssl = true
194
+ headers = { 'PRIVATE-TOKEN' => GITLAB_TOKEN }
195
+ path = uri.path.empty? ? '/' : uri.path
196
+ code = http.head(path, headers).code.to_i
197
+
198
+ if (code >= 200 && code < 300)
199
+ response = http.get(uri.path, headers)
200
+ JSON.parse(response.body).select { |i| i['path_with_namespace'] == name }.first
201
+ else
202
+ raise HTTPError, 'Error code: ' + code.to_s + ' URL: ' + url
203
+ end
204
+ rescue
205
+ raise $!
206
+ end
207
+ end
208
+
209
+ def get_trigger_token(id)
210
+ begin
211
+ uri = URI(GITLAB_CI_URL + '/api/v3/projects/' + id.to_s + '/triggers')
212
+ http = Net::HTTP.new(uri.host, uri.port)
213
+ http.use_ssl = true
214
+ headers = { 'PRIVATE-TOKEN' => GITLAB_TOKEN }
215
+ path = uri.path.empty? ? '/' : uri.path
216
+ code = http.head(path, headers).code.to_i
217
+
218
+ if (code >= 200 && code < 300)
219
+ response = http.get(uri.path, headers)
220
+ res = JSON.parse(response.body).select { |i| i['deleted_at'] == nil }.first
221
+ if res == nil
222
+ raise HTTPError, 'Unable to obtain build trigger token for project id: ' + id.to_s
223
+ end
224
+ res
225
+ else
226
+ raise HTTPError, 'Error code: ' + code.to_s + ' URL: ' + url
227
+ end
228
+ rescue
229
+ raise $!
230
+ end
231
+ end
232
+
233
+ def trigger_build(remote_name, ref)
234
+ begin
235
+ project = get_project(remote_name)
236
+ token = get_trigger_token(project['id'])['token']
237
+ url = GITLAB_CI_URL + '/api/v3/projects/' + project['id'].to_s + '/trigger/builds'
238
+ uri = URI(url)
239
+ response = Net::HTTP.post_form(uri, 'token' => token, 'ref' => ref)
240
+
241
+ if (response.code.to_i >= 200 && response.code.to_i < 300)
242
+ JSON.parse(response.body)
243
+ elsif response.code.to_i == 400
244
+ raise TriggerError, JSON.parse(response.body)['message']
245
+ else
246
+ raise HTTPError, 'Error code: ' + response.code.to_s + ' URL: ' + url
247
+ end
248
+ rescue
249
+ raise $!
250
+ end
251
+ end
252
+
253
+ def process_pull_request(pull_request)
254
+ Thread.new do
255
+ begin
256
+ remote_name = pull_request['base']['repo']['full_name']
257
+ commit = pull_request['head']['sha']
258
+ ref = pull_request['head']['ref']
259
+ build_status = 'pending'
260
+ description = 'The build is in progress...'
261
+ options = {
262
+ :state => build_status,
263
+ :description => description,
264
+ :context => 'continuous-integration/gitlab'
265
+ }
266
+
267
+ $log_app.info '[*] Processing pull request: ' + remote_name + ': ' + commit
268
+ @client.create_status(remote_name, commit, build_status, options)
269
+ trigger_build(remote_name, ref)
270
+ rescue Octokit::Unauthorized
271
+ $log_app.error '[!] ' + $!.message
272
+ rescue HTTPError
273
+ $log_app.error $!
274
+ rescue TriggerError
275
+ $log_app.error $!
276
+ rescue
277
+ $log_app.error '[!] Unable to process request for: ' + remote_name + ' ref: ' + ref
278
+ $log_app.internal($!)
279
+ ensure
280
+ build_status = 'failure'
281
+ description = 'The build has failed!'
282
+ options = {
283
+ :state => build_status,
284
+ :description => description,
285
+ :context => 'continuous-integration/gitlab'
286
+ }
287
+
288
+ @client.create_status(remote_name, commit, build_status, options)
289
+ end
290
+ end
291
+ end
292
+
293
+ def process_push(payload)
294
+ Thread.new do
295
+ begin
296
+ remote_name = payload['repository']['full_name']
297
+ ref = payload['ref'].split("/").last
298
+
299
+ trigger_build(remote_name, ref)
300
+ rescue HTTPError
301
+ $log_app.error $!
302
+ rescue TriggerError
303
+ $log_app.error $!
304
+ rescue
305
+ $log_app.error '[!] Unable to process request for: ' + remote_name + ' ref: ' + ref
306
+ $log_app.internal($!)
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: octolab
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jakub Zubielik
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: OctoLab allows to mirror GitHub repositories in GitLab and to trigger
14
+ build jobs on push requests.
15
+ email: jzubielik@gmail.com
16
+ executables:
17
+ - octolab
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/octolab
22
+ - helpers/ssh
23
+ - lib/octolab.rb
24
+ homepage: http://rubygems.org/gems/octolab
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.5.1
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: GitHub integration for GitLab CI.
48
+ test_files: []