juici 0.0.0.alpha1

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 (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