hell 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -6,9 +6,12 @@ end
6
6
 
7
7
 
8
8
  gem 'capistrano'
9
- gem 'json'
9
+ gem 'multi_json'
10
10
 
11
+ gem 'pusher'
11
12
  gem 'sinatra'
12
13
  gem 'sinatra-contrib'
14
+ gem 'sinatra-assetpack', :require => 'sinatra/assetpack'
15
+ gem 'sass'
13
16
  gem 'thin'
14
- gem 'websocket'
17
+ gem 'unicorn'
data/README.markdown CHANGED
@@ -64,7 +64,6 @@ Note that the current response is subject to change, and as such is not document
64
64
  The following environment variables are available for your use:
65
65
 
66
66
  - `HELL_APP_ROOT`: Path from which capistrano should execute. Defaults to `Dir.pwd`.
67
- - `HELL_ENVIRONMENTS`: Comma-separated list of environments, Default: `production,staging`.
68
67
  - `HELL_REQUIRE_ENV`: Whether or not to require specifying an environment. Default: `1`.
69
68
  - `HELL_LOG_PATH`: Path to which logs should be written to. Defaults to `Dir.pwd + '/log'`.
70
69
  - `HELL_BASE_DIR`: Base directory to use in web ui. Useful for subdirectories. Defaults to `/`.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.1.0
data/bin/hell CHANGED
@@ -1,12 +1,30 @@
1
- #!/usr/bin/env ruby
2
- #
3
- begin
4
- require 'hell/app.rb'
5
- rescue LoadError => e
6
- require 'rubygems'
7
- path = File.expand_path '../../lib', __FILE__
8
- $:.unshift(path) if File.directory?(path) && !$:.include?(path)
9
- require 'hell/app.rb'
1
+ #!/this/will/be/overwritten/or/wrapped/anyways/do/not/worry/ruby
2
+ # -*- encoding: binary -*-
3
+ require 'unicorn/launcher'
4
+ require 'hell/lib/cli'
5
+
6
+ ENV["RACK_ENV"] ||= "development"
7
+
8
+ unicorn_path = File.expand_path('../../unicorn', __FILE__)
9
+ options, op = Hell::CLI.option_parser(ARGV, unicorn_path)
10
+
11
+ unless File.directory? options[:log_path]
12
+ abort('Missing hell log path %s' % options[:log_path])
13
+ end
14
+
15
+ options.delete(:log_path)
16
+
17
+ app = Unicorn.builder(File.expand_path('../../config.ru', __FILE__), op)
18
+ op = nil
19
+
20
+ if $DEBUG
21
+ require 'pp'
22
+ pp({
23
+ :unicorn_options => options,
24
+ :app => app,
25
+ :daemonize => options[:daemonize],
26
+ })
10
27
  end
11
28
 
12
- Hell::App.run!
29
+ Unicorn::Launcher.daemonize!(options) if options[:daemonize]
30
+ Unicorn::HttpServer.new(app, options).start.join
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/ruby
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler.setup
5
+ require 'hell'
6
+
7
+ run Hell::App
data/hell.gemspec ADDED
@@ -0,0 +1,107 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "hell"
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jose Diaz-Gonzalez"]
12
+ s.date = "2013-02-21"
13
+ s.description = "Hell is an open source web interface that exposes a set of capistrano recipes as a json api, for usage within large teams"
14
+ s.email = "jose@seatgeek.com"
15
+ s.executables = ["hell"]
16
+ s.extra_rdoc_files = [
17
+ "LICENSE.txt",
18
+ "README.markdown"
19
+ ]
20
+ s.files = [
21
+ ".document",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.markdown",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "bin/hell",
28
+ "config.ru",
29
+ "hell.gemspec",
30
+ "lib/hell.rb",
31
+ "lib/hell/app.rb",
32
+ "lib/hell/lib/capistrano/cli.rb",
33
+ "lib/hell/lib/capistrano/configuration.rb",
34
+ "lib/hell/lib/capistrano/task_definition.rb",
35
+ "lib/hell/lib/cli.rb",
36
+ "lib/hell/lib/helpers.rb",
37
+ "lib/hell/lib/monkey_patch.rb",
38
+ "lib/hell/public/assets/css/bootstrap-responsive.css",
39
+ "lib/hell/public/assets/css/bootstrap-responsive.min.css",
40
+ "lib/hell/public/assets/css/bootstrap.css",
41
+ "lib/hell/public/assets/css/bootstrap.min.css",
42
+ "lib/hell/public/assets/css/hell.css",
43
+ "lib/hell/public/assets/ico/favicon.ico",
44
+ "lib/hell/public/assets/ico/favicon.png",
45
+ "lib/hell/public/assets/img/glyphicons-halflings-white.png",
46
+ "lib/hell/public/assets/img/glyphicons-halflings.png",
47
+ "lib/hell/public/assets/js/backbone-localstorage.js",
48
+ "lib/hell/public/assets/js/backbone.js",
49
+ "lib/hell/public/assets/js/bankersbox.js",
50
+ "lib/hell/public/assets/js/bootstrap.growl.js",
51
+ "lib/hell/public/assets/js/bootstrap.js",
52
+ "lib/hell/public/assets/js/hashchange.js",
53
+ "lib/hell/public/assets/js/hell.js",
54
+ "lib/hell/public/assets/js/jquery.js",
55
+ "lib/hell/public/assets/js/timeago.js",
56
+ "lib/hell/public/assets/js/underscore.js",
57
+ "lib/hell/views/index.erb",
58
+ "test/helper.rb",
59
+ "test/test_hell.rb",
60
+ "unicorn"
61
+ ]
62
+ s.homepage = "http://github.com/seatgeek/hell"
63
+ s.licenses = ["MIT"]
64
+ s.require_paths = ["lib"]
65
+ s.rubygems_version = "1.8.23"
66
+ s.summary = "A web interface and api wrapper around Capistrano"
67
+
68
+ if s.respond_to? :specification_version then
69
+ s.specification_version = 3
70
+
71
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
72
+ s.add_runtime_dependency(%q<capistrano>, [">= 0"])
73
+ s.add_runtime_dependency(%q<multi_json>, [">= 0"])
74
+ s.add_runtime_dependency(%q<pusher>, [">= 0"])
75
+ s.add_runtime_dependency(%q<sinatra>, [">= 0"])
76
+ s.add_runtime_dependency(%q<sinatra-contrib>, [">= 0"])
77
+ s.add_runtime_dependency(%q<sinatra-assetpack>, [">= 0"])
78
+ s.add_runtime_dependency(%q<sass>, [">= 0"])
79
+ s.add_runtime_dependency(%q<thin>, [">= 0"])
80
+ s.add_runtime_dependency(%q<unicorn>, [">= 0"])
81
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
82
+ else
83
+ s.add_dependency(%q<capistrano>, [">= 0"])
84
+ s.add_dependency(%q<multi_json>, [">= 0"])
85
+ s.add_dependency(%q<pusher>, [">= 0"])
86
+ s.add_dependency(%q<sinatra>, [">= 0"])
87
+ s.add_dependency(%q<sinatra-contrib>, [">= 0"])
88
+ s.add_dependency(%q<sinatra-assetpack>, [">= 0"])
89
+ s.add_dependency(%q<sass>, [">= 0"])
90
+ s.add_dependency(%q<thin>, [">= 0"])
91
+ s.add_dependency(%q<unicorn>, [">= 0"])
92
+ s.add_dependency(%q<jeweler>, [">= 0"])
93
+ end
94
+ else
95
+ s.add_dependency(%q<capistrano>, [">= 0"])
96
+ s.add_dependency(%q<multi_json>, [">= 0"])
97
+ s.add_dependency(%q<pusher>, [">= 0"])
98
+ s.add_dependency(%q<sinatra>, [">= 0"])
99
+ s.add_dependency(%q<sinatra-contrib>, [">= 0"])
100
+ s.add_dependency(%q<sinatra-assetpack>, [">= 0"])
101
+ s.add_dependency(%q<sass>, [">= 0"])
102
+ s.add_dependency(%q<thin>, [">= 0"])
103
+ s.add_dependency(%q<unicorn>, [">= 0"])
104
+ s.add_dependency(%q<jeweler>, [">= 0"])
105
+ end
106
+ end
107
+
data/lib/hell.rb CHANGED
@@ -0,0 +1,4 @@
1
+
2
+ module Hell
3
+ autoload :App, 'hell/app'
4
+ end
data/lib/hell/app.rb CHANGED
@@ -4,23 +4,36 @@ require 'sinatra'
4
4
  require 'sinatra/base'
5
5
  require 'sinatra/json'
6
6
  require 'sinatra/streaming'
7
+ require 'sinatra/assetpack'
7
8
 
8
- require 'json'
9
+ require 'pusher'
10
+ require 'multi_json'
9
11
  require 'securerandom'
10
- require 'websocket'
11
-
12
- HELL_DIR = Dir.pwd
13
- APP_ROOT = ENV.fetch('HELL_APP_ROOT', HELL_DIR)
14
- ENVIRONMENTS = ENV.fetch('HELL_ENVIRONMENTS', 'production,staging').split(',')
15
- BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
16
- REQUIRE_ENV = ENV.fetch('HELL_REQUIRE_ENV', '1') == '1'
17
- HELL_LOG_PATH = ENV.fetch('HELL_LOG_PATH', File.join(HELL_DIR, 'log'))
18
- HELL_BASE_DIR = ENV.fetch('HELL_BASE_DIR', '/')
19
- SENTINEL_STRINGS = ENV.fetch('HELL_SENTINEL_STRINGS', 'Hellish Task Completed').split(',')
20
12
 
13
+ require 'hell/lib/cli'
21
14
  require 'hell/lib/monkey_patch'
22
15
  require 'hell/lib/helpers'
23
16
 
17
+ options, op = Hell::CLI.option_parser(ARGV, nil, true)
18
+ op = nil
19
+
20
+ HELL_DIR = Dir.pwd
21
+ HELL_APP_ROOT = options[:app_root]
22
+ HELL_BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
23
+ HELL_REQUIRE_ENV = !!options[:require_env]
24
+ HELL_LOG_PATH = options[:log_path]
25
+ HELL_BASE_PATH = options[:base_path]
26
+ HELL_SENTINEL_STRINGS = options[:sentinel]
27
+
28
+ USE_PUSHER = !!(options[:pusher_app_id] && options[:pusher_key] && options[:pusher_secret])
29
+
30
+ PUSHER_APP_ID = options[:pusher_app_id]
31
+ PUSHER_KEY = options[:pusher_key]
32
+ PUSHER_SECRET = options[:pusher_secret]
33
+
34
+ Pusher.app_id = PUSHER_APP_ID
35
+ Pusher.key = PUSHER_KEY
36
+ Pusher.secret = PUSHER_SECRET
24
37
 
25
38
  module Hell
26
39
  class App < Sinatra::Base
@@ -28,7 +41,7 @@ module Hell
28
41
  helpers Sinatra::Streaming
29
42
  helpers Hell::Helpers
30
43
 
31
- cap = Capistrano::CLI.parse(["-T"])
44
+ register Sinatra::AssetPack
32
45
 
33
46
  set :public_folder, File.join(File.expand_path('..', __FILE__), 'public')
34
47
  set :root, HELL_DIR
@@ -36,38 +49,67 @@ module Hell
36
49
  set :static, true
37
50
  set :views, File.join(File.expand_path('..', __FILE__), 'views')
38
51
 
52
+ assets do
53
+ css_compression :sass
54
+
55
+ js :jquery, [
56
+ '/assets/js/jquery.js',
57
+ ]
58
+
59
+ js :main, [
60
+ '/assets/js/backbone-localstorage.js',
61
+ '/assets/js/bootstrap.growl.js',
62
+ '/assets/js/timeago.js',
63
+ '/assets/js/hashchange.js',
64
+ '/assets/js/hell.js',
65
+ ]
66
+
67
+ css :bootstrap, [
68
+ '/assets/css/bootstrap.min.css',
69
+ '/assets/css/bootstrap-responsive.css',
70
+ '/assets/css/hell.css',
71
+ ]
72
+
73
+ prebuild true
74
+ end
75
+
39
76
  configure :production, :development do
40
77
  enable :logging
41
78
  end
42
79
 
43
80
  get '/' do
44
- @tasks = cap.task_index.keys
45
- @require_env = REQUIRE_ENV
46
- @www_base_dir = HELL_BASE_DIR
47
- @environments = ENVIRONMENTS
81
+ @tasks = cap.tasks.keys
82
+ @require_env = HELL_REQUIRE_ENV
83
+ @www_base_dir = HELL_BASE_PATH
84
+ @environments = cap.environments
85
+
86
+ @use_pusher = USE_PUSHER
87
+ @pusher_app_id = PUSHER_APP_ID
88
+ @pusher_key = PUSHER_KEY
89
+ @pusher_secret = PUSHER_SECRET
48
90
  erb :index
49
91
  end
50
92
 
51
93
  get '/tasks' do
52
- tasks = cap.task_index
94
+ tasks = cap.tasks
53
95
  json tasks
54
96
  end
55
97
 
56
98
  get '/tasks/search/:pattern' do
57
- tasks = cap.task_index(params[:pattern])
99
+ tasks = cap.tasks(params[:pattern])
58
100
  json tasks
59
101
  end
60
102
 
61
103
  get '/tasks/:name/exists' do
62
- tasks = cap.task_index(params[:name], {:exact => true})
104
+ tasks = cap.tasks(params[:name], {:exact => true})
63
105
  response = { :exists => !tasks.empty?, :task => params[:name]}
64
106
  json response
65
107
  end
66
108
 
67
- get '/tasks/:name/background' do
109
+ put '/tasks/:name/background' do
68
110
  tasks, original_cmd = verify_task(cap, params[:name])
69
111
  verbose = ""
70
- verbose = "LOGGING=debug" if params[:verbose] == true
112
+ verbose = "LOGGING=debug" if params[:verbose] == "on"
71
113
 
72
114
  task_id = run_in_background!("bundle exec cap -l STDOUT %s %s" % [original_cmd, verbose]) unless tasks.empty?
73
115
  response = {}
@@ -80,9 +122,9 @@ module Hell
80
122
  get '/logs/:id/tail' do
81
123
  content_type "text/event-stream"
82
124
  if valid_log params[:id]
83
- _stream_success("tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
125
+ send_success(params[:id], "tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
84
126
  else
85
- _stream_error("log file '#{params[:id]}' not found")
127
+ send_error(params[:id], "log file '#{params[:id]}' not found")
86
128
  end
87
129
  end
88
130
 
@@ -94,35 +136,69 @@ module Hell
94
136
 
95
137
  get '/tasks/:name/execute' do
96
138
  tasks, original_cmd = verify_task(cap, params[:name])
139
+ task_id = random_id
97
140
  content_type "text/event-stream"
98
141
  if tasks.empty?
99
- _stream_error("cap task '#{original_cmd}' not found")
142
+ send_error(task_id, "cap task '#{original_cmd}' not found")
100
143
  else
101
- _stream_success("bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
144
+ send_success(task_id, "bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
102
145
  end
103
146
  end
104
147
 
105
- def _stream_error(message)
148
+ def self.init_send
149
+ if USE_PUSHER
150
+ alias_method :send_error, :_pusher_error
151
+ alias_method :send_success, :_pusher_success
152
+ else
153
+ alias_method :send_error, :_stream_error
154
+ alias_method :send_success, :_stream_success
155
+ end
156
+ end
157
+
158
+ def cap
159
+ FileUtils.chdir HELL_APP_ROOT do
160
+ @cap ||= Capistrano::CLI.parse(["-T"])
161
+ end
162
+ return @cap
163
+ end
164
+
165
+ def _pusher_error(task_id, message)
166
+ Pusher[task_id].trigger('start', {:data => ''})
167
+ Pusher[task_id].trigger('message', ws_message("<p>#{message}</p>"))
168
+ Pusher[task_id].trigger('end', {:data => ''})
169
+ end
170
+
171
+ def _pusher_success(task_id, command, opts = {})
172
+ opts = {:prepend => false}.merge(opts)
173
+ Pusher[task_id].trigger('start', {:data => ''})
174
+ Pusher[task_id].trigger('message', ws_message("<p>#{command}</p>")) unless opts[:prepend] == false
175
+ IO.popen(command, 'rb') do |io|
176
+ io.each {|line| push_line(task_id, line, out, io)}
177
+ end
178
+ Pusher[task_id].trigger('end', {:data => ''})
179
+ end
180
+
181
+ def _stream_error(task_id, message)
106
182
  stream do |out|
107
183
  out << "event: start\ndata:\n\n" unless out.closed?
108
- out << "data: " + ws_message("<p>#{message}</p>") unless out.closed?
184
+ out << "data: " + ws_message("<p>#{message}</p>") + "\n\n" unless out.closed?
109
185
  out << "event: end\ndata:\n\n" unless out.closed?
110
186
  out.close
111
187
  end
112
188
  end
113
189
 
114
- def _stream_success(command, opts = {})
190
+ def _stream_success(task_id, command, opts = {})
115
191
  opts = {:prepend => false}.merge(opts)
116
192
  stream do |out|
117
193
  out << "event: start\ndata:\n\n" unless out.closed?
118
- out << "data: " + ws_message("<p>#{command}</p>") unless out.closed? or opts[:prepend] == false
194
+ out << "data: " + ws_message("<p>#{command}</p>") + "\n\n" unless out.closed? or opts[:prepend] == false
119
195
  IO.popen(command, 'rb') do |io|
120
- io.each do |line|
121
- process_line(line, out, io)
122
- end
196
+ io.each {|line| stream_line(task_id, line, out, io)}
123
197
  end
124
198
  close_stream(out)
125
199
  end
126
200
  end
201
+
202
+ init_send
127
203
  end
128
204
  end
@@ -0,0 +1,57 @@
1
+ require 'capistrano/cli'
2
+ require 'json'
3
+
4
+ module Capistrano
5
+ class CLI
6
+ module JsonTaskList
7
+
8
+ def task_list
9
+ config = instantiate_configuration(options)
10
+ config.debug = options[:debug]
11
+ config.dry_run = options[:dry_run]
12
+ config.preserve_roles = options[:preserve_roles]
13
+ config.logger.level = options[:verbose]
14
+
15
+ set_pre_vars(config)
16
+ load_recipes(config)
17
+
18
+ config.trigger(:load)
19
+ [config, options]
20
+
21
+ tasks = config.task_list(:all)
22
+ tasks.reject! {|t| HELL_BLACKLIST.include?(t.fully_qualified_name)}
23
+ tasks = Hash[tasks.map {|task| [task.fully_qualified_name, task.to_hash]}]
24
+
25
+ config.trigger(:exit)
26
+
27
+ tasks
28
+ end
29
+
30
+ def tasks(pattern = nil, opts = {})
31
+ @available_tasks ||= task_list
32
+ tasks = @available_tasks.reject {|name, task| is_environment?(task)}
33
+
34
+ if pattern.is_a?(String)
35
+ if opts.fetch(:exact, false)
36
+ tasks.select! {|name| name == pattern}
37
+ else
38
+ tasks.select! {|name| name =~ /#{pattern}/}
39
+ end
40
+ end
41
+
42
+ tasks
43
+ end
44
+
45
+ def environments
46
+ @available_tasks ||= task_list
47
+ environments = @available_tasks.select {|name, task| is_environment?(task)}
48
+ environments.keys
49
+ end
50
+
51
+ def is_environment?(task)
52
+ return false unless task[:options].include?(:hell_tags)
53
+ Array(task[:options][:hell_tags]).include?(:environment)
54
+ end
55
+ end
56
+ end
57
+ end