hell 0.0.1 → 0.1.0

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