robot_sweatshop 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +12 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +45 -0
- data/Rakefile +3 -0
- data/bin/lib/common.rb +49 -0
- data/bin/lib/config.rb +20 -0
- data/bin/lib/inspect.rb +42 -0
- data/bin/lib/job.rb +13 -0
- data/bin/lib/start.rb +11 -0
- data/bin/sweatshop +73 -0
- data/config.rb +24 -0
- data/config.yaml +12 -0
- data/jobs/example.yaml +10 -0
- data/kintama/README.md +3 -0
- data/kintama/data/payload_data.yaml +6 -0
- data/kintama/end-to-end_spec.rb +30 -0
- data/kintama/input_http_spec.rb +45 -0
- data/kintama/job_assembler_spec.rb +72 -0
- data/kintama/job_worker_spec.rb +65 -0
- data/kintama/moneta-queue_spec.rb +48 -0
- data/kintama/payload_parser_spec.rb +71 -0
- data/kintama/queue_broadcaster_spec.rb +39 -0
- data/kintama/queue_handler_spec.rb +54 -0
- data/kintama/run_all.rb +3 -0
- data/kintama/shared/helpers.rb +55 -0
- data/kintama/shared/process_spawning.rb +13 -0
- data/lib/README.md +12 -0
- data/lib/input/http.rb +27 -0
- data/lib/job/assembler.rb +36 -0
- data/lib/job/worker.rb +40 -0
- data/lib/payload/lib/bitbucket.rb +61 -0
- data/lib/payload/lib/github.rb +45 -0
- data/lib/payload/lib/payload.rb +12 -0
- data/lib/payload/parser.rb +23 -0
- data/lib/queue-helper.rb +32 -0
- data/lib/queue/broadcaster.rb +18 -0
- data/lib/queue/handler.rb +23 -0
- data/lib/queue/lib/moneta-queue.rb +49 -0
- data/lib/queue/watcher.rb +18 -0
- data/robot_sweatshop.eye +58 -0
- data/robot_sweatshop.gemspec +26 -0
- data/robot_sweatshop.production.eye +8 -0
- data/robot_sweatshop.testing.eye +8 -0
- data/workspaces/.keep +0 -0
- metadata +233 -0
data/lib/input/http.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'sinatra'
|
3
|
+
require 'ezmq'
|
4
|
+
require 'json'
|
5
|
+
require_relative '../../config'
|
6
|
+
|
7
|
+
configure do
|
8
|
+
set :port, configatron.input.http.port
|
9
|
+
set :bind, configatron.input.http.bind
|
10
|
+
set :output_queue, 'raw-payload'
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/' do
|
14
|
+
'Everything\'s on schedule!'
|
15
|
+
end
|
16
|
+
|
17
|
+
post '/:format/payload-for/:job_name' do
|
18
|
+
puts "Received #{params['format']} payload for #{params['job_name']}"
|
19
|
+
request.body.rewind
|
20
|
+
hash = {
|
21
|
+
payload: request.body.read,
|
22
|
+
format: params['format'],
|
23
|
+
job_name: params['job_name']
|
24
|
+
}
|
25
|
+
client = EZMQ::Client.new port: 5556
|
26
|
+
client.request "#{settings.output_queue} #{JSON.generate hash}"
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'yaml'
|
3
|
+
require_relative '../queue-helper'
|
4
|
+
|
5
|
+
def get_config(for_job_name:)
|
6
|
+
job_directory = "#{__dir__}/../../jobs"
|
7
|
+
job_config_path = "#{job_directory}/#{for_job_name}.yaml"
|
8
|
+
unless File.file? job_config_path
|
9
|
+
puts "No config found for job '#{for_job_name}'"
|
10
|
+
return nil
|
11
|
+
end
|
12
|
+
YAML.load_file job_config_path
|
13
|
+
end
|
14
|
+
|
15
|
+
def assemble_job(data)
|
16
|
+
job_config = get_config for_job_name: data['job_name']
|
17
|
+
return nil unless job_config
|
18
|
+
if job_config['branch_whitelist'].include? data['payload']['branch']
|
19
|
+
context = job_config['environment'].merge(data['payload'])
|
20
|
+
context.each { |key, value| context[key] = value.to_s }
|
21
|
+
{
|
22
|
+
commands: job_config['commands'],
|
23
|
+
context: context,
|
24
|
+
job_name: data['job_name']
|
25
|
+
}
|
26
|
+
else
|
27
|
+
puts "Branch '#{data['payload']['branch']}' is not whitelisted"
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
QueueHelper.wait_for('parsed-payload') do |data|
|
33
|
+
puts "Assembling: #{data}"
|
34
|
+
assembled_job = assemble_job data
|
35
|
+
QueueHelper.enqueue assembled_job, to: 'jobs' if assembled_job
|
36
|
+
end
|
data/lib/job/worker.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'faker'
|
3
|
+
require 'fileutils'
|
4
|
+
require_relative '../queue-helper'
|
5
|
+
|
6
|
+
# TODO: check existing worker ids. it'd be disastrous to have two sharing a workspace
|
7
|
+
@worker_id = ARGV[0] || "#{Faker::Name.first_name}"
|
8
|
+
|
9
|
+
def from_workspace(named: 'no_job_name')
|
10
|
+
workspace = "#{named}-#{@worker_id}"
|
11
|
+
puts "Workspace: #{workspace}"
|
12
|
+
path = File.expand_path "#{__dir__}/../../workspaces/#{workspace}"
|
13
|
+
FileUtils.mkpath path
|
14
|
+
Dir.chdir(path) { yield if block_given? }
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute(context = {}, command)
|
18
|
+
puts "Executing '#{command}'..."
|
19
|
+
# TODO: path.split(' ') to bypass the shell when we're not using env vars
|
20
|
+
|
21
|
+
# Run the command with the context in environment,
|
22
|
+
# printing the output as it's generated
|
23
|
+
IO.popen(context, command) do |io_stream|
|
24
|
+
while line = io_stream.gets
|
25
|
+
puts line
|
26
|
+
end
|
27
|
+
end
|
28
|
+
puts "Execution complete with exit status: #{$?.exitstatus}"
|
29
|
+
end
|
30
|
+
|
31
|
+
QueueHelper.wait_for('jobs') do |data|
|
32
|
+
puts "Running: #{data}"
|
33
|
+
if data['commands'].is_a? Array
|
34
|
+
from_workspace(named: data['job_name']) do
|
35
|
+
context = data['context'] || {}
|
36
|
+
data['commands'].each { |command| execute context, command }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
puts "Job finished.\n\n"
|
40
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
require_relative 'payload'
|
4
|
+
|
5
|
+
# A parser for Bitbucket payload data
|
6
|
+
class BitbucketPayload < Payload
|
7
|
+
def initialize(data)
|
8
|
+
data = URI.decode_www_form(data)[0][1]
|
9
|
+
@data = JSON.parse data || {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def latest_commit
|
13
|
+
return {} if @data['commits'].nil?
|
14
|
+
@data['commits'].last
|
15
|
+
end
|
16
|
+
|
17
|
+
def repository
|
18
|
+
return {} if @data['repository'].nil?
|
19
|
+
@data['repository']
|
20
|
+
end
|
21
|
+
|
22
|
+
def author
|
23
|
+
return {} if latest_commit['raw_author'].nil?
|
24
|
+
name, email = latest_commit['raw_author'].split(/\s+</)
|
25
|
+
email.slice! '>' unless email.nil?
|
26
|
+
{
|
27
|
+
'name' => name,
|
28
|
+
'email' => email || '',
|
29
|
+
'username' => latest_commit['author']
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def clone_url
|
34
|
+
"#{ @data['canon_url'] }#{ repository['absolute_url'] }"
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash
|
38
|
+
latest_commit['raw_node']
|
39
|
+
end
|
40
|
+
|
41
|
+
def branch
|
42
|
+
latest_commit['branch']
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
latest_commit['message']
|
47
|
+
end
|
48
|
+
|
49
|
+
def repo_slug
|
50
|
+
slug = repository['absolute_url']
|
51
|
+
slug.nil? ? nil : slug[1...-1]
|
52
|
+
end
|
53
|
+
|
54
|
+
def source_url
|
55
|
+
return '' if @data['canon_url'].nil? ||
|
56
|
+
repository.empty? ||
|
57
|
+
latest_commit.empty?
|
58
|
+
base_url = @data['canon_url']
|
59
|
+
"#{base_url}/#{repo_slug}/commits/#{hash}/?at=#{branch}"
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'payload'
|
3
|
+
|
4
|
+
# A parser for Github payload data
|
5
|
+
class GithubPayload < Payload
|
6
|
+
def initialize(payload)
|
7
|
+
@payload = JSON.parse payload || {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def commit
|
11
|
+
@payload['head_commit'] || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def repository
|
15
|
+
@payload['repository'] || {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def clone_url
|
19
|
+
repository['clone_url'] || ''
|
20
|
+
end
|
21
|
+
|
22
|
+
def author
|
23
|
+
commit['author']
|
24
|
+
end
|
25
|
+
|
26
|
+
def hash
|
27
|
+
commit['id'] || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def branch
|
31
|
+
@payload['ref'] || ''
|
32
|
+
end
|
33
|
+
|
34
|
+
def message
|
35
|
+
commit['message'] || ''
|
36
|
+
end
|
37
|
+
|
38
|
+
def repo_slug
|
39
|
+
repository['full_name'] || ''
|
40
|
+
end
|
41
|
+
|
42
|
+
def source_url
|
43
|
+
head_commit['url'] || ''
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# The base interface that the payload parser will be expecting
|
2
|
+
class Payload
|
3
|
+
def self.hash_keys
|
4
|
+
%w(author hash branch message repo_slug source_url clone_url)
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_hash
|
8
|
+
keys = Payload.hash_keys
|
9
|
+
values = Payload.hash_keys.map { |method| method(method.to_sym).call }
|
10
|
+
[keys, values].transpose.to_h
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative '../queue-helper'
|
3
|
+
|
4
|
+
def parse(payload = '', of_format:)
|
5
|
+
of_format = of_format.downcase
|
6
|
+
lib_file = "#{__dir__}/lib/#{of_format}.rb"
|
7
|
+
if of_format != 'payload' && File.file?(lib_file)
|
8
|
+
require_relative lib_file
|
9
|
+
Object.const_get("#{of_format.capitalize}Payload").new payload
|
10
|
+
else
|
11
|
+
puts "Dropping bad format: #{of_format}"
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
QueueHelper.wait_for('raw-payload') do |data|
|
17
|
+
puts "Parsing: #{data}"
|
18
|
+
payload = parse data['payload'], of_format: data['format']
|
19
|
+
if payload
|
20
|
+
hash = { payload: payload.to_hash, job_name: data['job_name'] }
|
21
|
+
QueueHelper.enqueue hash, to: 'parsed-payload'
|
22
|
+
end
|
23
|
+
end
|
data/lib/queue-helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'ezmq'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# A collection of common methods for queue interactions with EZMQ
|
6
|
+
module QueueHelper
|
7
|
+
@@client = EZMQ::Client.new port: 5556
|
8
|
+
|
9
|
+
def self.dequeue(queue_name = 'default')
|
10
|
+
data = @@client.request queue_name
|
11
|
+
begin
|
12
|
+
JSON.parse data
|
13
|
+
rescue JSON::ParserError
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.enqueue(object = {}, to: 'default')
|
19
|
+
@@client.request "#{to} #{JSON.generate object}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.wait_for(queue_name = 'default')
|
23
|
+
puts "Waiting for messages on #{queue_name}"
|
24
|
+
subscriber = EZMQ::Subscriber.new port: 5557, topic: 'busy-queues'
|
25
|
+
subscriber.listen do |message|
|
26
|
+
if message == queue_name
|
27
|
+
data = dequeue queue_name
|
28
|
+
yield data unless data.nil?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative 'lib/moneta-queue'
|
3
|
+
require 'ezmq'
|
4
|
+
|
5
|
+
publisher = EZMQ::Publisher.new port: 5557
|
6
|
+
queues = {}
|
7
|
+
MonetaQueue.watched_queues.each do |queue|
|
8
|
+
queues[queue] = MonetaQueue.new queue
|
9
|
+
end
|
10
|
+
|
11
|
+
@wait_time = ARGV[0].to_f || 0.5
|
12
|
+
|
13
|
+
loop do
|
14
|
+
queues.each do |queue_name, queue|
|
15
|
+
publisher.send queue_name, topic: 'busy-queues' if queue.size > 0
|
16
|
+
end
|
17
|
+
sleep @wait_time unless @wait_time == 0
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative 'lib/moneta-queue'
|
3
|
+
require 'ezmq'
|
4
|
+
|
5
|
+
def enqueue(name, item)
|
6
|
+
puts "enqueue #{name} #{item}"
|
7
|
+
queue = MonetaQueue.new name
|
8
|
+
queue.enqueue item
|
9
|
+
queue.size.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def dequeue(name)
|
13
|
+
puts "dequeue #{name}"
|
14
|
+
queue = MonetaQueue.new name
|
15
|
+
queue.dequeue
|
16
|
+
end
|
17
|
+
|
18
|
+
server = EZMQ::Server.new port: 5556
|
19
|
+
server.listen do |message|
|
20
|
+
name, item = message.split ' ', 2
|
21
|
+
is_dequeue_request = item.nil?
|
22
|
+
is_dequeue_request ? dequeue(name) : enqueue(name, item)
|
23
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'moneta'
|
3
|
+
require_relative '../../../config'
|
4
|
+
|
5
|
+
# A class to handle queueing through Moneta's key-value storage
|
6
|
+
class MonetaQueue
|
7
|
+
attr_reader :watched_queues
|
8
|
+
|
9
|
+
@@store = Moneta.new :File, dir: configatron.queue.moneta_directory
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@mirror_name = "mirror-#{name}"
|
14
|
+
@@store[@name] ||= []
|
15
|
+
@@store[@mirror_name] ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.watched_queues
|
19
|
+
%w(raw-payload parsed-payload jobs testing)
|
20
|
+
end
|
21
|
+
|
22
|
+
def enqueue(item)
|
23
|
+
@@store[@name] = @@store[@name].push item
|
24
|
+
@@store[@mirror_name] = @@store[@mirror_name].push item
|
25
|
+
end
|
26
|
+
|
27
|
+
def dequeue
|
28
|
+
return '' if @@store[@name].empty?
|
29
|
+
item = @@store[@name].first
|
30
|
+
@@store[@name] = @@store[@name][1..-1]
|
31
|
+
item
|
32
|
+
end
|
33
|
+
|
34
|
+
def size
|
35
|
+
loop do # Moneta can return nil sometimes, so we give it time to catch up
|
36
|
+
queue = @@store[@name]
|
37
|
+
return queue.size unless queue.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def clear
|
42
|
+
@@store[@mirror_name] = []
|
43
|
+
@@store[@name] = []
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
@@store[@name].inspect
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative 'lib/moneta-queue'
|
3
|
+
|
4
|
+
@queues = []
|
5
|
+
[ARGV[0], "mirror-#{ARGV[0]}"].each do |queue_name|
|
6
|
+
@queues.push(name: queue_name, queue: MonetaQueue.new(queue_name))
|
7
|
+
end
|
8
|
+
|
9
|
+
loop do
|
10
|
+
system 'clear'
|
11
|
+
@queues.each do |q|
|
12
|
+
puts "Queue: #{q[:name]}"
|
13
|
+
puts "Size: #{q[:queue].size}", "#{'|' * q[:queue].size}"
|
14
|
+
puts q[:queue].inspect
|
15
|
+
puts
|
16
|
+
end
|
17
|
+
sleep 0.1
|
18
|
+
end
|
data/robot_sweatshop.eye
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative 'config'
|
3
|
+
|
4
|
+
log_path = configatron.common.logfile_directory
|
5
|
+
pid_path = configatron.common.pidfile_directory
|
6
|
+
|
7
|
+
Eye.config do
|
8
|
+
logger "#{log_path}/eye.log"
|
9
|
+
end
|
10
|
+
|
11
|
+
Eye.application :robot_sweatshop do
|
12
|
+
trigger :flapping, times: 10, within: 1.minute, retry_in: 10.minutes
|
13
|
+
check :cpu, every: 10.seconds, below: 100, times: 3
|
14
|
+
working_dir "#{__dir__}/lib"
|
15
|
+
|
16
|
+
group 'input' do
|
17
|
+
process :http do
|
18
|
+
pid_file "#{pid_path}/input-http.pid"
|
19
|
+
stdall "#{log_path}/input-http.log"
|
20
|
+
start_command "ruby input/http.rb"
|
21
|
+
daemonize true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
group 'queue' do
|
25
|
+
process :handler do
|
26
|
+
pid_file "#{pid_path}/queue-handler.pid"
|
27
|
+
stdall "#{log_path}/queue-handler.log"
|
28
|
+
start_command "ruby queue/handler.rb"
|
29
|
+
daemonize true
|
30
|
+
end
|
31
|
+
process :broadcaster do
|
32
|
+
pid_file "#{pid_path}/queue-broadcaster.pid"
|
33
|
+
stdall "#{log_path}/queue-broadcaster.log"
|
34
|
+
start_command "ruby queue/broadcaster.rb #{configatron.eye.broadcaster_interval}"
|
35
|
+
daemonize true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
group 'job' do
|
39
|
+
process :assembler do
|
40
|
+
pid_file "#{pid_path}/job-assembler.pid"
|
41
|
+
stdall "#{log_path}/job-assembler.log"
|
42
|
+
start_command "ruby job/assembler.rb"
|
43
|
+
daemonize true
|
44
|
+
end
|
45
|
+
process :worker do
|
46
|
+
pid_file "#{pid_path}/job-worker.pid"
|
47
|
+
stdall "#{log_path}/job-worker.log"
|
48
|
+
start_command "ruby job/worker.rb #{configatron.eye.worker_id}"
|
49
|
+
daemonize true
|
50
|
+
end
|
51
|
+
end
|
52
|
+
process :payload_parser do
|
53
|
+
pid_file "#{pid_path}/payload_parser.pid"
|
54
|
+
stdall "#{log_path}/payload_parser.log"
|
55
|
+
start_command "ruby payload/parser.rb"
|
56
|
+
daemonize true
|
57
|
+
end
|
58
|
+
end
|