robot_sweatshop 0.1.2
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.
- 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
|