juici 0.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +74 -0
  4. data/Procfile +1 -0
  5. data/README.md +138 -0
  6. data/Rakefile +33 -0
  7. data/TODO.md +28 -0
  8. data/bin/juici +7 -0
  9. data/bin/juicic +54 -0
  10. data/config/mongoid.yml.sample +25 -0
  11. data/juici-interface.gemspec +19 -0
  12. data/juici.gemspec +32 -0
  13. data/lib/juici.rb +24 -0
  14. data/lib/juici/app.rb +67 -0
  15. data/lib/juici/build_environment.rb +38 -0
  16. data/lib/juici/build_logic.rb +37 -0
  17. data/lib/juici/build_queue.rb +111 -0
  18. data/lib/juici/callback.rb +26 -0
  19. data/lib/juici/config.rb +11 -0
  20. data/lib/juici/controllers.rb +6 -0
  21. data/lib/juici/controllers/base.rb +26 -0
  22. data/lib/juici/controllers/build_queue.rb +14 -0
  23. data/lib/juici/controllers/builds.rb +74 -0
  24. data/lib/juici/controllers/index.rb +20 -0
  25. data/lib/juici/controllers/trigger.rb +85 -0
  26. data/lib/juici/database.rb +25 -0
  27. data/lib/juici/exceptions.rb +2 -0
  28. data/lib/juici/find_logic.rb +11 -0
  29. data/lib/juici/helpers/form_helpers.rb +11 -0
  30. data/lib/juici/helpers/html_helpers.rb +4 -0
  31. data/lib/juici/helpers/url_helpers.rb +38 -0
  32. data/lib/juici/interface.rb +13 -0
  33. data/lib/juici/models/build.rb +190 -0
  34. data/lib/juici/models/build_process.rb +7 -0
  35. data/lib/juici/models/project.rb +9 -0
  36. data/lib/juici/server.rb +172 -0
  37. data/lib/juici/version.rb +8 -0
  38. data/lib/juici/views/README.markdown +138 -0
  39. data/lib/juici/views/about.erb +16 -0
  40. data/lib/juici/views/builds.erb +7 -0
  41. data/lib/juici/views/builds/edit.erb +23 -0
  42. data/lib/juici/views/builds/list.erb +27 -0
  43. data/lib/juici/views/builds/new.erb +43 -0
  44. data/lib/juici/views/builds/show.erb +4 -0
  45. data/lib/juici/views/index.erb +30 -0
  46. data/lib/juici/views/layout.erb +44 -0
  47. data/lib/juici/views/not_found.erb +3 -0
  48. data/lib/juici/views/partials/builds/debug.erb +22 -0
  49. data/lib/juici/views/partials/builds/output.erb +1 -0
  50. data/lib/juici/views/partials/builds/show.erb +19 -0
  51. data/lib/juici/views/partials/builds/sidebar.erb +13 -0
  52. data/lib/juici/views/partials/index/recently_built.erb +19 -0
  53. data/lib/juici/views/queue/list.erb +6 -0
  54. data/lib/juici/views/redirect.erb +0 -0
  55. data/lib/juici/views/support.erb +6 -0
  56. data/lib/juici/watcher.rb +48 -0
  57. data/public/favicon.ico +0 -0
  58. data/public/images/black_denim.png +0 -0
  59. data/public/styles/builds.css +62 -0
  60. data/public/styles/juici.css +226 -0
  61. data/public/vendor/bootstrap.css +6004 -0
  62. data/public/vendor/bootstrap.js +2036 -0
  63. data/public/vendor/img/glyphicons-halflings-white.png +0 -0
  64. data/public/vendor/jquery.js +9440 -0
  65. data/sample/mongod.conf +5 -0
  66. data/script/cibuild +10 -0
  67. data/spec/build_callback_spec.rb +54 -0
  68. data/spec/build_environment_spec.rb +53 -0
  69. data/spec/build_process_spec.rb +96 -0
  70. data/spec/build_queue_spec.rb +63 -0
  71. data/spec/controllers/builds_spec.rb +68 -0
  72. data/spec/controllers/index_spec.rb +28 -0
  73. data/spec/juici_app_spec.rb +8 -0
  74. data/spec/models/build_spec.rb +54 -0
  75. data/spec/spec_helper.rb +26 -0
  76. metadata +290 -0
@@ -0,0 +1,67 @@
1
+ at_exit do
2
+ Juici::App.shutdown
3
+ end
4
+
5
+ module Juici
6
+ class App
7
+ @@watchers = []
8
+
9
+ def self.shutdown
10
+ ::Juici.dbgp "Shutting down Juici"
11
+ ::Juici::Watcher.instance.shutdown!
12
+
13
+ shutdown_build_queue
14
+ end
15
+
16
+ attr_reader :opts
17
+ def initialize(opts={})
18
+ Database.initialize!
19
+ @opts = opts
20
+ reset_stale_children
21
+ start_watcher
22
+ init_build_queue
23
+ reload_unfinished_work
24
+ start_queue
25
+ end
26
+
27
+ private
28
+
29
+ def init_build_queue
30
+ $build_queue ||= BuildQueue.new
31
+ end
32
+
33
+ def self.shutdown_build_queue
34
+ if $build_queue
35
+ $build_queue.shutdown!
36
+ $build_queue = nil
37
+ end
38
+
39
+ # Find any remaining children and kill them
40
+ # Ensure that any killed builds will be retried
41
+ end
42
+
43
+ def start_watcher
44
+ ::Juici::Watcher.instance.start
45
+ end
46
+
47
+ def reload_unfinished_work
48
+ # At this point no workers have started yet, we can safely assume that
49
+ # :started means aborted
50
+ Build.where(:status => Juici::BuildStatus::WAIT).each do |build|
51
+ $build_queue << build
52
+ end
53
+ end
54
+
55
+ def reset_stale_children
56
+ Build.where(:status => Juici::BuildStatus::START).each do |build|
57
+ build[:status] = Juici::BuildStatus::WAIT
58
+ build.save!
59
+ end
60
+ end
61
+
62
+ def start_queue
63
+ $build_queue.start!
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,38 @@
1
+ module Juici
2
+ BUILD_SENSITIVE_VARIABLES = %w[RUBYOPT BUNDLE_GEMFILE RACK_ENV MONGOLAB_URI GEM_PATH WORKING_DIR]
3
+ class BuildEnvironment
4
+
5
+ attr_reader :env
6
+ def initialize
7
+ @env = ENV.to_hash.tap do |env|
8
+ BUILD_SENSITIVE_VARIABLES.each do |var|
9
+ env[var] = nil
10
+ end
11
+ env["BUNDLE_CONFIG"] = "/nonexistent"
12
+ end
13
+ end
14
+
15
+ def [](k)
16
+ env[k]
17
+ end
18
+
19
+ # XXX This is spectacular.
20
+ # Not in the good way
21
+ def load_json!(json)
22
+ return true if json == ""
23
+ loaded_json = JSON.load(json)
24
+ if loaded_json.is_a? Hash
25
+ env.merge!(loaded_json)
26
+ return true
27
+ end
28
+ false
29
+ rescue JSON::ParserError
30
+ return false
31
+ end
32
+
33
+ def to_hash
34
+ env
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ require 'fileutils'
2
+ require 'tempfile'
3
+ module Juici
4
+ module BuildLogic
5
+
6
+ def spawn_build
7
+ raise "No such work tree" unless FileUtils.mkdir_p(worktree)
8
+ spawn(command, worktree)
9
+ rescue AbortBuild
10
+ :buildaborted
11
+ end
12
+
13
+ def kill!
14
+ warn! "Killed!"
15
+ if pid = self[:pid]
16
+ Process.kill(15, pid)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def spawn(cmd, dir)
23
+ @buffer = Tempfile.new('juici-xxxx')
24
+ Process.spawn(environment, cmd,
25
+ :chdir => dir,
26
+ :in => "/dev/null",
27
+ :out => @buffer.fileno,
28
+ :err => [:child, :out]
29
+ )
30
+ rescue Errno::ENOENT
31
+ :enoent
32
+ rescue TypeError
33
+ :invalidcommand
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,111 @@
1
+ # An object representing a queue. It merely manages creating child processes
2
+ # and their priority, reaping them is a job for BuildThread
3
+ #
4
+ module Juici
5
+ class BuildQueue
6
+
7
+ def initialize
8
+ @builds = []
9
+ @child_pids = []
10
+ # This is never expired, for now
11
+ @builds_by_pid = {}
12
+ @started = false
13
+ end
14
+
15
+ def shutdown!
16
+ @child_pids.each do |pid|
17
+ ::Juici.dbgp "Killing off child pid #{pid}"
18
+ Process.kill(15, pid)
19
+ end
20
+ end
21
+
22
+ # Pushing a Build object into the BuildQueue is expressing that you want it
23
+ # run
24
+ def <<(other)
25
+ @builds << other
26
+ bump!
27
+ end
28
+
29
+ def current_min_priority
30
+ @builds.collect(&:priority).compact.min || 1
31
+ end
32
+
33
+ def next_child
34
+ @builds.sort_by(&:priority).first
35
+ end
36
+
37
+ def purge(by, build)
38
+ @builds.reject! do |i|
39
+ build.send(by) == i.send(by)
40
+ end
41
+ end
42
+
43
+ def delete(id)
44
+ purge(:_id, OpenStruct.new(:_id => id))
45
+ end
46
+
47
+ # Magic hook that starts a process if there's a good reason to.
48
+ # Stopgap measure that means you can knock on this if there's a chance we
49
+ # should start a process
50
+ def bump!
51
+ return unless @started
52
+ update_children
53
+ if not_working? && work_to_do?
54
+ Juici.dbgp "Starting another child process"
55
+ next_child.tap do |child|
56
+ if pid = child.build!
57
+ Juici.dbgp "Started child: #{pid}"
58
+ @child_pids << pid
59
+ @builds_by_pid[pid] = child
60
+ else
61
+ Juici.dbgp "Child #{child} failed to start"
62
+ bump! # Ruby's recursion isn't great, but re{try,do} may as well be
63
+ # undefined behaviour here.
64
+ end
65
+ end
66
+ else
67
+ Juici.dbgp "I have quite enough to do"
68
+ end
69
+ end
70
+
71
+ def update_children
72
+ @child_pids.select! do |pid|
73
+ return false if pid.nil?
74
+ begin
75
+ Process.kill(0, pid)
76
+ true
77
+ rescue Errno::ESRCH
78
+ false
79
+ end
80
+ end
81
+ end
82
+
83
+ def get_build_by_pid(pid)
84
+ @builds_by_pid[pid]
85
+ end
86
+
87
+ def not_working?
88
+ @child_pids.length == 0
89
+ end
90
+
91
+ def work_to_do?
92
+ @builds.length > 0
93
+ end
94
+
95
+ def currently_building
96
+ @child_pids.map do |pid|
97
+ get_build_by_pid(pid)
98
+ end
99
+ end
100
+
101
+ def start!
102
+ @started = true
103
+ bump!
104
+ end
105
+
106
+ def builds
107
+ @builds
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,26 @@
1
+ require 'net/http'
2
+ module Juici
3
+ class Callback
4
+
5
+ attr_reader :url
6
+ attr_accessor :payload
7
+
8
+ def initialize(url, pl=nil)
9
+ @url = URI(url)
10
+ @payload = pl if pl
11
+ end
12
+
13
+ def process!
14
+ Net::HTTP.start(url.host, url.port) do |http|
15
+ request = Net::HTTP::Post.new(url.request_uri)
16
+ request.body = payload
17
+
18
+ http.request request # Net::HTTPResponse object
19
+ end
20
+ rescue SocketError => e
21
+ # We don't get a reference to build any more, can't warn :(
22
+ # TODO Throw a warning on the build
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ class Juici::Config; class << self
2
+ # XXX Temporary implementation to be replaced by a config reader
3
+ def workspace
4
+ "/tmp/juici/workspace"
5
+ end
6
+
7
+ def builds_per_page
8
+ 10
9
+ end
10
+
11
+ end; end
@@ -0,0 +1,6 @@
1
+ module Juici
2
+ module Controllers
3
+ class Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,26 @@
1
+ module Juici::Controllers
2
+ class Base
3
+
4
+ NotFound = Sinatra::NotFound
5
+
6
+ attr_accessor :params
7
+ def initialize(params)
8
+ @params = params
9
+ end
10
+
11
+ def build_opts(opts)
12
+ default_opts.merge(opts)
13
+ end
14
+
15
+ def default_opts
16
+ {
17
+ :styles => styles
18
+ }
19
+ end
20
+
21
+ def styles
22
+ []
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module Juici::Controllers
2
+ class BuildQueue < Base
3
+
4
+ def list
5
+ builds = $build_queue.builds.sort_by(&:priority)
6
+ yield [:"queue/list", opts(:builds => builds)]
7
+ end
8
+
9
+ def opts(_opts={})
10
+ {:active => :queue}.merge(_opts)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,74 @@
1
+ module Juici::Controllers
2
+ class Builds < Base
3
+
4
+ def list
5
+ params[:page] = params[:page] ? params[:page].to_i : 0
6
+
7
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
8
+
9
+ builds = ::Juici::Build.where(parent: project.name)
10
+
11
+ pages = (builds.count.to_f / ::Juici::Config.builds_per_page).ceil
12
+
13
+ builds = builds.desc(:_id).
14
+ limit(::Juici::Config.builds_per_page).
15
+ skip(params[:page].to_i * ::Juici::Config.builds_per_page)
16
+
17
+ yield [:"builds/list", build_opts({:project => project, :builds => builds, :pages => pages})]
18
+ end
19
+
20
+ def show
21
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
22
+ build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
23
+ # return 404 unless project && build
24
+ yield [:"builds/show", build_opts({:project => project, :build => build})]
25
+ end
26
+
27
+ def kill
28
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
29
+ build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
30
+
31
+ ::Juici.dbgp "Killing off build #{build[:_id]}"
32
+ build.kill! if build.status == ::Juici::BuildStatus::START
33
+ return build
34
+ end
35
+
36
+ def cancel
37
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
38
+ build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
39
+
40
+ ::Juici.dbgp "Cancelling build #{build[:_id]}"
41
+ build.cancel if build.status == ::Juici::BuildStatus::WAIT
42
+ return build
43
+ end
44
+
45
+ def new
46
+ yield [:"builds/new", {:active => :new_build}]
47
+ end
48
+
49
+ def styles
50
+ ["builds"]
51
+ end
52
+
53
+ def edit
54
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
55
+ build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
56
+ # return 404 unless project && build
57
+ yield [:"builds/edit", {:project => project, :build => build}]
58
+ end
59
+
60
+ def update!
61
+ project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
62
+ build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
63
+
64
+ ::Juici::Build::EDITABLE_ATTRIBUTES[:string].each do |attr|
65
+ build[attr] = params[attr] if params[attr]
66
+ end
67
+ # binding.pry
68
+ build.tap do |b|
69
+ b.save!
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,20 @@
1
+ module Juici::Controllers
2
+ class Index
3
+
4
+ def index
5
+ yield [:index, {:active => :index}]
6
+ end
7
+
8
+ def about
9
+ content = GitHub::Markdown.render(File.read("lib/juici/views/README.markdown"))
10
+ content.gsub!(/<h(\d+)>/, '<h\1 class="block-header">')
11
+ yield [:about, {:active => :about, :content => content}]
12
+ end
13
+
14
+ def support
15
+ yield [:support, {:active => :support}]
16
+ end
17
+
18
+ end
19
+ end
20
+
@@ -0,0 +1,85 @@
1
+ module Juici::Controllers
2
+ class Trigger
3
+
4
+ attr_reader :project, :params
5
+ def initialize(project, params)
6
+ @project = ::Juici::Project.find_or_create_by(name: project)
7
+ @params = params
8
+ end
9
+
10
+ # Find an existing build, duplicate the sane parts of it.
11
+ def rebuild!
12
+ unless project = ::Juici::Project.where(name: params[:project]).first
13
+ not_found
14
+ end
15
+ unless build = ::Juici::Build.where(parent: project.name, _id: params[:id]).first
16
+ not_found
17
+ end
18
+
19
+ ::Juici::Build.new_from(build).tap do |new_build|
20
+ new_build.save!
21
+ $build_queue << new_build
22
+ $build_queue.bump!
23
+ end
24
+ end
25
+
26
+ def build!
27
+ environment = ::Juici::BuildEnvironment.new
28
+ ::Juici::Build.new(parent: project.name).tap do |build|
29
+ # The seperation of concerns around this madness is horrifying
30
+ unless environment.load_json!(params['environment'])
31
+ build.warn!("Failed to parse environment")
32
+ end
33
+
34
+ build[:command] = build_command
35
+ build[:priority] = build_priority
36
+ build[:environment] = environment.to_hash
37
+ build[:callbacks] = callbacks
38
+ build[:title] = title if title_given?
39
+
40
+ build.save!
41
+ $build_queue << build
42
+ end
43
+ end
44
+
45
+ # These accessors are not really the concern of a "Controller". On the
46
+ # other hand, this thing is also not a controller realistically, so I'm
47
+ # just going to carry on using this as a mechanism to poke at my model.
48
+ #
49
+ # I later plan to implement a more strictly bounded interface which bails
50
+ # instead of coercing.
51
+ def build_command
52
+ # TODO Throw 422 not acceptable if missing
53
+ params['command'].tap do |c|
54
+ raise "No command given" if c.nil?
55
+ # TODO Detect that we've recieved this from a browser session and only
56
+ # do this replacement there, we can trust API supplied data.
57
+ c.gsub!(/\r\n/, "\n")
58
+ end
59
+ end
60
+
61
+ def build_priority
62
+ if params['priority'] =~ /^[0-9]+$/
63
+ params['priority'].to_i
64
+ else
65
+ 1
66
+ end
67
+ end
68
+
69
+ def callbacks
70
+ JSON.load(params['callbacks'])
71
+ rescue
72
+ []
73
+ end
74
+
75
+ def title
76
+ params['title']
77
+ end
78
+
79
+ def title_given?
80
+ t = params['title']
81
+ !(t.nil? || t.empty?)
82
+ end
83
+
84
+ end
85
+ end