juici 0.0.0 → 0.0.1.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +26 -16
  4. data/README.md +45 -6
  5. data/Rakefile +24 -0
  6. data/bin/juicic +54 -0
  7. data/config/mongoid.yml.sample +20 -5
  8. data/juici-interface.gemspec +19 -0
  9. data/juici.gemspec +7 -3
  10. data/lib/juici.rb +8 -3
  11. data/lib/juici/app.rb +19 -23
  12. data/lib/juici/build_environment.rb +1 -1
  13. data/lib/juici/build_logic.rb +10 -1
  14. data/lib/juici/build_queue.rb +30 -3
  15. data/lib/juici/callback.rb +9 -5
  16. data/lib/juici/config.rb +4 -0
  17. data/lib/juici/controllers.rb +6 -0
  18. data/lib/juici/controllers/base.rb +26 -0
  19. data/lib/juici/controllers/build_queue.rb +14 -0
  20. data/lib/juici/controllers/builds.rb +74 -0
  21. data/lib/juici/controllers/index.rb +20 -0
  22. data/lib/juici/controllers/{trigger_controller.rb → trigger.rb} +24 -5
  23. data/lib/juici/database.rb +19 -13
  24. data/lib/juici/exceptions.rb +2 -0
  25. data/lib/juici/find_logic.rb +11 -0
  26. data/lib/juici/helpers/form_helpers.rb +11 -0
  27. data/lib/juici/helpers/html_helpers.rb +4 -0
  28. data/lib/juici/helpers/url_helpers.rb +28 -0
  29. data/lib/juici/interface.rb +13 -0
  30. data/lib/juici/models/build.rb +81 -24
  31. data/lib/juici/models/project.rb +1 -1
  32. data/lib/juici/server.rb +103 -40
  33. data/lib/juici/version.rb +8 -0
  34. data/lib/juici/views/README.markdown +45 -6
  35. data/lib/juici/views/about.erb +5 -5
  36. data/lib/juici/views/builds/edit.erb +23 -0
  37. data/lib/juici/views/builds/list.erb +26 -33
  38. data/lib/juici/views/builds/new.erb +42 -37
  39. data/lib/juici/views/builds/show.erb +4 -28
  40. data/lib/juici/views/index.erb +30 -13
  41. data/lib/juici/views/layout.erb +22 -13
  42. data/lib/juici/views/not_found.erb +3 -0
  43. data/lib/juici/views/partials/builds/debug.erb +12 -18
  44. data/lib/juici/views/partials/builds/output.erb +1 -0
  45. data/lib/juici/views/partials/builds/show.erb +19 -0
  46. data/lib/juici/views/partials/builds/sidebar.erb +13 -0
  47. data/lib/juici/views/partials/index/recently_built.erb +19 -0
  48. data/lib/juici/views/queue/list.erb +6 -0
  49. data/lib/juici/watcher.rb +33 -25
  50. data/public/favicon.ico +0 -0
  51. data/public/images/black_denim.png +0 -0
  52. data/public/styles/builds.css +59 -8
  53. data/public/styles/juici.css +226 -2
  54. data/public/vendor/bootstrap.css +6004 -0
  55. data/public/vendor/bootstrap.js +2036 -0
  56. data/public/vendor/img/glyphicons-halflings-white.png +0 -0
  57. data/public/vendor/jquery.js +9440 -0
  58. data/script/cibuild +10 -0
  59. data/spec/build_callback_spec.rb +46 -1
  60. data/spec/build_process_spec.rb +71 -5
  61. data/spec/build_queue_spec.rb +3 -1
  62. data/spec/controllers/builds_spec.rb +68 -0
  63. data/spec/controllers/index_spec.rb +28 -0
  64. data/spec/juici_app_spec.rb +0 -15
  65. data/spec/models/build_spec.rb +54 -0
  66. data/spec/spec_helper.rb +13 -0
  67. metadata +76 -12
  68. data/lib/juici/controllers/build_controller.rb +0 -0
  69. data/lib/juici/url_helpers.rb +0 -15
@@ -1,11 +1,20 @@
1
1
  require 'fileutils'
2
2
  require 'tempfile'
3
3
  module Juici
4
- module BuildLogic
4
+ module BuildLogic
5
5
 
6
6
  def spawn_build
7
7
  raise "No such work tree" unless FileUtils.mkdir_p(worktree)
8
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
9
18
  end
10
19
 
11
20
  private
@@ -9,6 +9,7 @@ module Juici
9
9
  @child_pids = []
10
10
  # This is never expired, for now
11
11
  @builds_by_pid = {}
12
+ @started = false
12
13
  end
13
14
 
14
15
  def shutdown!
@@ -39,17 +40,28 @@ module Juici
39
40
  end
40
41
  end
41
42
 
43
+ def delete(id)
44
+ purge(:_id, OpenStruct.new(:_id => id))
45
+ end
46
+
42
47
  # Magic hook that starts a process if there's a good reason to.
43
48
  # Stopgap measure that means you can knock on this if there's a chance we
44
49
  # should start a process
45
50
  def bump!
51
+ return unless @started
46
52
  update_children
47
53
  if not_working? && work_to_do?
48
54
  Juici.dbgp "Starting another child process"
49
55
  next_child.tap do |child|
50
- pid = child.build!
51
- @child_pids << pid
52
- @builds_by_pid[pid] = 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
53
65
  end
54
66
  else
55
67
  Juici.dbgp "I have quite enough to do"
@@ -80,5 +92,20 @@ module Juici
80
92
  @builds.length > 0
81
93
  end
82
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
+
83
110
  end
84
111
  end
@@ -2,20 +2,24 @@ require 'net/http'
2
2
  module Juici
3
3
  class Callback
4
4
 
5
- attr_reader :build, :url
5
+ attr_reader :url
6
+ attr_accessor :payload
6
7
 
7
- def initialize(build, url)
8
- @build = build
8
+ def initialize(url, pl=nil)
9
9
  @url = URI(url)
10
+ @payload = pl if pl
10
11
  end
11
12
 
12
13
  def process!
13
14
  Net::HTTP.start(url.host, url.port) do |http|
14
15
  request = Net::HTTP::Post.new(url.request_uri)
15
- request.body = build.to_callback_json
16
+ request.body = payload
16
17
 
17
- response = http.request request # Net::HTTPResponse object
18
+ http.request request # Net::HTTPResponse object
18
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
19
23
  end
20
24
 
21
25
  end
@@ -4,4 +4,8 @@ class Juici::Config; class << self
4
4
  "/tmp/juici/workspace"
5
5
  end
6
6
 
7
+ def builds_per_page
8
+ 10
9
+ end
10
+
7
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
+
@@ -1,15 +1,31 @@
1
- module Juici
2
- class TriggerController
1
+ module Juici::Controllers
2
+ class Trigger
3
3
 
4
4
  attr_reader :project, :params
5
5
  def initialize(project, params)
6
- @project = Project.find_or_create_by(name: project)
6
+ @project = ::Juici::Project.find_or_create_by(name: project)
7
7
  @params = params
8
8
  end
9
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
+
10
26
  def build!
11
- environment = BuildEnvironment.new
12
- Build.new(parent: project.name).tap do |build|
27
+ environment = ::Juici::BuildEnvironment.new
28
+ ::Juici::Build.new(parent: project.name).tap do |build|
13
29
  # The seperation of concerns around this madness is horrifying
14
30
  unless environment.load_json!(params['environment'])
15
31
  build.warn!("Failed to parse environment")
@@ -36,6 +52,9 @@ module Juici
36
52
  # TODO Throw 422 not acceptable if missing
37
53
  params['command'].tap do |c|
38
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")
39
58
  end
40
59
  end
41
60
 
@@ -1,19 +1,25 @@
1
- class Juici::Database
2
- class << self
3
- def mongoid_config_file
4
- %w[mongoid.yml mongoid.yml.sample].each do |file|
5
- path = File.join("config", file)
6
- return path if File.exist?(path)
1
+ require 'mongoid'
2
+ module Juici
3
+ class Database
4
+ class << self
5
+ def mongoid_config_file
6
+ %w[mongoid.yml mongoid.yml.sample].each do |file|
7
+ path = File.join("config", file)
8
+ return path if File.exist?(path)
9
+ end
10
+ raise "No database config file"
7
11
  end
8
- raise "No database config file"
9
- end
10
12
 
11
- def initialize!
12
- if ENV['RACK_ENV'] == "development"
13
- Mongoid.logger.level = Logger::INFO
14
- end
13
+ def initialize!
14
+ if ::Juici.respond_to? :dbgp
15
+ ::Juici.dbgp "initializing Mongoid with environment: #{ENV['RACK_ENV']}"
16
+ end
17
+ if ENV['RACK_ENV'] == "development"
18
+ Mongoid.logger.level = Logger::INFO
19
+ end
15
20
 
16
- Mongoid.load!(mongoid_config_file)
21
+ Mongoid.load!(mongoid_config_file)
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -0,0 +1,2 @@
1
+ class AbortBuild < Exception
2
+ end
@@ -0,0 +1,11 @@
1
+ module Juici
2
+ module FindLogic
3
+
4
+ def find_or_raise(exc, by)
5
+ self.where(by).first.tap do |rec|
6
+ raise exc if rec.nil?
7
+ end
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ def form_at(route, fields, opts={})
2
+ form = ""
3
+ form << %Q{<form action="#{route}" method="post">\n}
4
+
5
+ fields.each do |name, value|
6
+ form << %Q{<input type="hidden" name="#{name}" value="#{value}">\n}
7
+ end
8
+
9
+ form << %Q{<button type="submit" class="btn">#{opts[:submit] || "submit"}</button>}
10
+ form << %Q{</form>}
11
+ end
@@ -0,0 +1,4 @@
1
+ require 'cgi'
2
+ def escaped(content)
3
+ CGI.escapeHTML content
4
+ end
@@ -8,3 +8,31 @@ def build_url_for(entity)
8
8
  "/builds/#{entity[:parent]}/show/#{entity[:_id]}"
9
9
  end
10
10
  end
11
+
12
+ def rebuild_url_for(entity)
13
+ URI.escape case entity
14
+ when ::Juici::Build
15
+ "/builds/#{entity[:parent]}/rebuild/#{entity[:_id]}"
16
+ end
17
+ end
18
+
19
+ def kill_url_for(entity)
20
+ URI.escape case entity
21
+ when ::Juici::Build
22
+ "/builds/kill"
23
+ end
24
+ end
25
+
26
+ def cancel_url_for(entity)
27
+ URI.escape case entity
28
+ when ::Juici::Build
29
+ "/builds/cancel"
30
+ end
31
+ end
32
+
33
+ def edit_url_for(entity)
34
+ URI.escape case entity
35
+ when ::Juici::Build
36
+ "/builds/#{entity[:parent]}/edit/#{entity[:_id]}"
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ module Juici
2
+ module BuildStatus
3
+ PASS = "success"
4
+ FAIL = "failed"
5
+ START = "started"
6
+ WAIT = "waiting"
7
+ end
8
+
9
+ module Routes
10
+ NEW_BUILD = '/builds/new'
11
+ end
12
+
13
+ end
@@ -1,29 +1,43 @@
1
1
  require 'json'
2
- # status enum
3
- # :waiting
4
- # :started
5
- # :failed
6
- # :success
7
- #
8
- # ???
9
- # :profit!
10
- #
2
+
11
3
  module Juici
12
4
  class Build
13
5
  # A wrapper around the build process
14
6
 
15
7
  include Mongoid::Document
16
- include ::Juici.url_helpers("builds")
17
8
  include BuildLogic
9
+ include BuildStatus
10
+ extend FindLogic
18
11
  # TODO Builds should probably be children of projects in the URL?
19
12
 
13
+ # Finder classmethods
14
+ def self.get_recent(n, opts={})
15
+ self.where(opts).
16
+ limit(n).
17
+ desc(:_id)
18
+ end
19
+
20
+ CLONABLE_FIELDS = [:command, :priority, :environment, :callbacks, :title, :parent]
21
+ EDITABLE_ATTRIBUTES = {
22
+ :string => [:priority, :title],
23
+ :array => [:environment, :callbacks]
24
+ }
25
+
26
+ def self.new_from(other)
27
+ new.tap do |b|
28
+ CLONABLE_FIELDS.each do |prop|
29
+ b[prop] = other[prop]
30
+ end
31
+ end
32
+ end
33
+
20
34
  field :parent, type: String
21
35
  field :command, type: String
22
36
  field :environment, type: Hash
23
37
  field :create_time, type: Time, :default => Proc.new { Time.now }
24
38
  field :start_time, type: Time, :default => nil
25
39
  field :end_time, type: Time, :default => nil
26
- field :status, type: Symbol, :default => :waiting
40
+ field :status, type: Symbol, :default => WAIT.to_sym
27
41
  field :priority, type: Fixnum, :default => 1
28
42
  field :pid, type: Fixnum
29
43
  field :buffer, type: String
@@ -32,37 +46,43 @@ module Juici
32
46
  field :title, type: String, :default => Proc.new { Time.now.to_s }
33
47
 
34
48
  def set_status(value)
35
- self[:status] = value
49
+ self.status= value
36
50
  save!
37
51
  end
38
52
 
39
53
  def start!
40
54
  self[:start_time] = Time.now
41
- set_status :started
55
+ set_status START
42
56
  end
43
57
 
44
58
  def success!
45
59
  finish
46
- set_status :success
60
+ set_status PASS
47
61
  process_callbacks
48
62
  end
49
63
 
50
64
  def failure!
51
65
  finish
52
- set_status :failed
66
+ set_status FAIL
53
67
  process_callbacks
54
68
  end
55
69
 
56
70
  def finish
57
71
  self[:end_time] = Time.now
58
72
  self[:output] = get_output
59
- $build_queue.purge(:pid, self)
73
+ $build_queue.purge(:pid, self) if $build_queue
74
+ end
75
+
76
+ def cancel
77
+ warn! "Cancelled"
78
+ set_status FAIL
79
+ $build_queue.delete(self[:_id]) if $build_queue
60
80
  end
61
81
 
62
82
  def build!
83
+ start!
63
84
  case pid = spawn_build
64
85
  when Fixnum
65
- start!
66
86
  Juici.dbgp "#{pid} away!"
67
87
  self[:pid] = pid
68
88
  self[:buffer] = @buffer.path
@@ -80,18 +100,22 @@ module Juici
80
100
 
81
101
  def worktree
82
102
  File.join(Config.workspace, parent)
103
+ rescue TypeError => e
104
+ warn! "Invalid workdir"
105
+ failure!
106
+ raise AbortBuild
83
107
  end
84
108
 
85
109
  # View helpers
86
110
  def heading_color
87
111
  case status
88
- when :waiting
112
+ when WAIT
89
113
  "build-heading-pending"
90
- when :failed
114
+ when FAIL
91
115
  "build-heading-failed"
92
- when :success
116
+ when PASS
93
117
  "build-heading-success"
94
- when :started
118
+ when START
95
119
  "build-heading-started"
96
120
  end
97
121
  end
@@ -109,14 +133,20 @@ module Juici
109
133
  self[:title] || self[:create_time]
110
134
  end
111
135
 
136
+ def link_title
137
+ "#{self[:parent]}/#{display_title}"
138
+ end
139
+
112
140
  def warn!(msg)
113
141
  warnings << msg
114
142
  save!
115
143
  end
116
144
 
117
145
  def process_callbacks
118
- self[:callbacks].each do |callback_url|
119
- Callback.new(self, callback_url).process!
146
+ callbacks.each do |callback_url|
147
+ c = Callback.new(callback_url)
148
+ c.payload = self.to_callback_json
149
+ c.process!
120
150
  end
121
151
  end
122
152
 
@@ -125,9 +155,36 @@ module Juici
125
155
  {
126
156
  "project" => self[:parent],
127
157
  "status" => self[:status],
128
- "url" => build_url_for(self)
158
+ "url" => build_url_for(self),
159
+ "time" => time_elapsed
129
160
  }.to_json
130
161
  end
131
162
 
163
+ def callbacks
164
+ self[:callbacks] || []
165
+ end
166
+
167
+ def time_elapsed
168
+ if self[:end_time]
169
+ self[:end_time] - self[:start_time]
170
+ elsif self[:start_time]
171
+ Time.now - self[:start_time]
172
+ else
173
+ nil
174
+ end
175
+ rescue
176
+ -1 # Throw an obviously impossible build time.
177
+ # This will only occur as a result of old builds.
178
+ end
179
+
180
+ # Use symbols internally
181
+ def status
182
+ self[:status].to_s
183
+ end
184
+
185
+ def status=(s)
186
+ self[:status] = s.to_sym
187
+ end
188
+
132
189
  end
133
190
  end