hell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.document +5 -0
  2. data/Gemfile +14 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.markdown +108 -0
  5. data/Rakefile +38 -0
  6. data/VERSION +1 -0
  7. data/bin/hell +12 -0
  8. data/lib/hell.rb +0 -0
  9. data/lib/hell/app.rb +128 -0
  10. data/lib/hell/config.ru +4 -0
  11. data/lib/hell/hell.rb +0 -0
  12. data/lib/hell/lib/helpers.rb +119 -0
  13. data/lib/hell/lib/monkey_patch.rb +79 -0
  14. data/lib/hell/public/assets/css/bootstrap-responsive.css +1088 -0
  15. data/lib/hell/public/assets/css/bootstrap-responsive.min.css +9 -0
  16. data/lib/hell/public/assets/css/bootstrap.css +5893 -0
  17. data/lib/hell/public/assets/css/bootstrap.min.css +9 -0
  18. data/lib/hell/public/assets/css/hell.css +158 -0
  19. data/lib/hell/public/assets/ico/favicon.ico +0 -0
  20. data/lib/hell/public/assets/ico/favicon.png +0 -0
  21. data/lib/hell/public/assets/img/glyphicons-halflings-white.png +0 -0
  22. data/lib/hell/public/assets/img/glyphicons-halflings.png +0 -0
  23. data/lib/hell/public/assets/js/backbone-localstorage.js +84 -0
  24. data/lib/hell/public/assets/js/backbone.js +1431 -0
  25. data/lib/hell/public/assets/js/backbone.min.js +38 -0
  26. data/lib/hell/public/assets/js/bankersbox.js +768 -0
  27. data/lib/hell/public/assets/js/bootstrap.growl.js +2 -0
  28. data/lib/hell/public/assets/js/bootstrap.js +2025 -0
  29. data/lib/hell/public/assets/js/bootstrap.min.js +6 -0
  30. data/lib/hell/public/assets/js/hashchange.min.js +9 -0
  31. data/lib/hell/public/assets/js/hell.js +444 -0
  32. data/lib/hell/public/assets/js/jquery.min.js +4 -0
  33. data/lib/hell/public/assets/js/timeago.js +152 -0
  34. data/lib/hell/public/assets/js/underscore.min.js +1 -0
  35. data/lib/hell/views/index.erb +146 -0
  36. data/test/helper.rb +18 -0
  37. data/test/test_hell.rb +7 -0
  38. metadata +201 -0
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "jeweler"
5
+ end
6
+
7
+
8
+ gem 'capistrano'
9
+ gem 'json'
10
+
11
+ gem 'sinatra'
12
+ gem 'sinatra-contrib'
13
+ gem 'thin'
14
+ gem 'websocket'
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Jose Diaz-Gonzalez
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Hell
2
+
3
+ An interactive web ui for Capistrano that also provides a json api
4
+
5
+ ![http://cl.ly/image/0R2H1J0t0c35](http://cl.ly/image/0R2H1J0t0c35/s10e11_480.jpg)
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 1.9.2
10
+ - Bundler
11
+
12
+ ## Installation
13
+
14
+ Simply add `hell` as a dependency in your Gemfile:
15
+
16
+ source 'http://rubygems.org'
17
+ source 'http://gems.github.com'
18
+
19
+ gem 'hell'
20
+
21
+ And then run the following in your app directory:
22
+
23
+ bundle install
24
+
25
+ ## Usage
26
+
27
+ Once installed, you'll want to run the `hell` binary:
28
+
29
+ hell
30
+
31
+ Note that if you have multiple capistrano versions installed for multiple repositories, you may need to instead use:
32
+
33
+ bundle exec hell
34
+
35
+ Then open `localhost:4567` in your browser to get an interactive interface around your capistrano recipes.
36
+
37
+ ### Running commands
38
+
39
+ Hell provides an autocompleted list of all capistrano tasks you have setup, and you can run this against a specific environment as necessary. By default, hell will start the command in the background, and then use an `EventSource` to read the generated logfile.
40
+
41
+ When you run a task, that task is added to your history so that you may re-read the logs at a later date, as well as re-run the tasks as necessary. This history is done using `localstorage`, and as such is not shared amongst different users of your application.
42
+
43
+ Note that Hell does not provide persistence of generated tasks, nor does it perform locking of individual tasks. For now, suggest using tools such as [Logstash](http://logstash.net/) to persist the logs, and using locking techniques within your capistrano tooling instead.
44
+
45
+
46
+ ### Capistrano as an API
47
+
48
+ Inspired by Github's internal [Heaven](https://github.com/blog/1241-deploying-at-github) app, Hell provides a simple, rest-like json api around Capistrano. XML-RPC implementation to come.
49
+
50
+ Available endpoints:
51
+
52
+ - `/tasks`: List all available tasks
53
+ - `/tasks/search/:pattern`: Search for a given, non-regex pattern
54
+ - `/tasks/:name/background`: Kicks off the execution of a task in the background and writes the output to a log file. Will respond with an id for future log file recovery.
55
+ - `/tasks/:name/exists`: Checks if a task exists
56
+ - `/tasks/:name/execute`: Kicks off the execution of a task and responds with the results using the sinatra streaming api. EventSource-compatible.
57
+ - `/logs/:id/tail`: Using the id provided by `/tasks/:name/background`, will start a sinatra stream on the log file in question.
58
+ - `/logs/:id/view`: Using the id provided by `/tasks/:name/background`, will output the current contents of a logfile. Useful for later recovery of the logs through the web api.
59
+
60
+ Note that the current response is subject to change, and as such is not documented, though we will attempt to augment rather than change them.
61
+
62
+ ### Configuration
63
+
64
+ The following environment variables are available for your use:
65
+
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
+ - `HELL_REQUIRE_ENV`: Whether or not to require specifying an environment. Default: `1`.
69
+ - `HELL_LOG_PATH`: Path to which logs should be written to. Defaults to `Dir.pwd + '/log'`.
70
+ - `HELL_BASE_DIR`: Base directory to use in web ui. Useful for subdirectories. Defaults to `/`.
71
+ - `HELL_SENTINEL_STRINGS`: Sentinel string used to denote the end of a task run. Defaults to `Hellish Task Completed`.
72
+
73
+ ## TODO
74
+
75
+ * ~~Finish the execute task so that it sends output to the browser~~
76
+ * ~~Figure out where/how to store deploy logs on disk~~
77
+ * ~~Blacklist tasks from being displayed~~
78
+ * ~~Add support for environment variables~~
79
+ * ~~Add support for deployment environments~~
80
+ * ~~Add support for ad-hoc deploy callbacks~~
81
+ * Save favorite commands as buttons
82
+ * Save defaults in a cookie
83
+ * Use a slide-up element for task output
84
+ * Add optional task locking so that deploys cannot interfere with one-another
85
+ * Add ability to use pusher instead of sinatra streaming
86
+
87
+ ## License
88
+
89
+ Copyright (c) 2012 Jose Diaz-Gonzalez
90
+
91
+ Permission is hereby granted, free of charge, to any person obtaining
92
+ a copy of this software and associated documentation files (the
93
+ "Software"), to deal in the Software without restriction, including
94
+ without limitation the rights to use, copy, modify, merge, publish,
95
+ distribute, sublicense, and/or sell copies of the Software, and to
96
+ permit persons to whom the Software is furnished to do so, subject to
97
+ the following conditions:
98
+
99
+ The above copyright notice and this permission notice shall be
100
+ included in all copies or substantial portions of the Software.
101
+
102
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
103
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
104
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
105
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
106
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
107
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
108
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'rake'
15
+ require 'jeweler'
16
+
17
+ Jeweler::Tasks.new do |gem|
18
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
19
+ gem.name = "hell"
20
+ gem.homepage = "http://github.com/seatgeek/hell"
21
+ gem.license = "MIT"
22
+ gem.summary = %Q{A web interface and api wrapper around Capistrano}
23
+ gem.description = %Q{Hell is an open source web interface that exposes a set of capistrano recipes as a json api, for usage within large teams}
24
+ gem.email = "jose@seatgeek.com"
25
+ gem.authors = ["Jose Diaz-Gonzalez"]
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rdoc/task'
31
+ Rake::RDocTask.new do |rdoc|
32
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
33
+
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "hell #{version}"
36
+ rdoc.rdoc_files.include('README*')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,12 @@
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'
10
+ end
11
+
12
+ Hell::App.run!
File without changes
@@ -0,0 +1,128 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'sinatra'
4
+ require 'sinatra/base'
5
+ require 'sinatra/json'
6
+ require 'sinatra/streaming'
7
+
8
+ require 'json'
9
+ 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
+
21
+ require 'hell/lib/monkey_patch'
22
+ require 'hell/lib/helpers'
23
+
24
+
25
+ module Hell
26
+ class App < Sinatra::Base
27
+ helpers Sinatra::JSON
28
+ helpers Sinatra::Streaming
29
+ helpers Hell::Helpers
30
+
31
+ cap = Capistrano::CLI.parse(["-T"])
32
+
33
+ set :public_folder, File.join(File.expand_path('..', __FILE__), 'public')
34
+ set :root, HELL_DIR
35
+ set :server, :thin
36
+ set :static, true
37
+ set :views, File.join(File.expand_path('..', __FILE__), 'views')
38
+
39
+ configure :production, :development do
40
+ enable :logging
41
+ end
42
+
43
+ get '/' do
44
+ @tasks = cap.task_index.keys
45
+ @require_env = REQUIRE_ENV
46
+ @www_base_dir = HELL_BASE_DIR
47
+ @environments = ENVIRONMENTS
48
+ erb :index
49
+ end
50
+
51
+ get '/tasks' do
52
+ tasks = cap.task_index
53
+ json tasks
54
+ end
55
+
56
+ get '/tasks/search/:pattern' do
57
+ tasks = cap.task_index(params[:pattern])
58
+ json tasks
59
+ end
60
+
61
+ get '/tasks/:name/exists' do
62
+ tasks = cap.task_index(params[:name], {:exact => true})
63
+ response = { :exists => !tasks.empty?, :task => params[:name]}
64
+ json response
65
+ end
66
+
67
+ get '/tasks/:name/background' do
68
+ tasks, original_cmd = verify_task(cap, params[:name])
69
+ verbose = ""
70
+ verbose = "LOGGING=debug" if params[:verbose] == true
71
+
72
+ task_id = run_in_background!("bundle exec cap -l STDOUT %s %s" % [original_cmd, verbose]) unless tasks.empty?
73
+ response = {}
74
+ response[:status] = tasks.empty? ? 404 : 200,
75
+ response[:message] = tasks.empty? ? "Task not found" : "Running task in background",
76
+ response[:task_id] = task_id unless tasks.empty?
77
+ json response
78
+ end
79
+
80
+ get '/logs/:id/tail' do
81
+ content_type "text/event-stream"
82
+ if valid_log params[:id]
83
+ _stream_success("tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
84
+ else
85
+ _stream_error("log file '#{params[:id]}' not found")
86
+ end
87
+ end
88
+
89
+ get '/logs/:id/view' do
90
+ log_path = File.join(HELL_LOG_PATH, params[:id] + ".log")
91
+ logger.info log_path
92
+ ansi_escape(File.read(log_path))
93
+ end
94
+
95
+ get '/tasks/:name/execute' do
96
+ tasks, original_cmd = verify_task(cap, params[:name])
97
+ content_type "text/event-stream"
98
+ if tasks.empty?
99
+ _stream_error("cap task '#{original_cmd}' not found")
100
+ else
101
+ _stream_success("bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
102
+ end
103
+ end
104
+
105
+ def _stream_error(message)
106
+ stream do |out|
107
+ out << "event: start\ndata:\n\n" unless out.closed?
108
+ out << "data: " + ws_message("<p>#{message}</p>") unless out.closed?
109
+ out << "event: end\ndata:\n\n" unless out.closed?
110
+ out.close
111
+ end
112
+ end
113
+
114
+ def _stream_success(command, opts = {})
115
+ opts = {:prepend => false}.merge(opts)
116
+ stream do |out|
117
+ out << "event: start\ndata:\n\n" unless out.closed?
118
+ out << "data: " + ws_message("<p>#{command}</p>") unless out.closed? or opts[:prepend] == false
119
+ IO.popen(command, 'rb') do |io|
120
+ io.each do |line|
121
+ process_line(line, out, io)
122
+ end
123
+ end
124
+ close_stream(out)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/ruby
2
+ require './app'
3
+
4
+ run Hell::App
File without changes
@@ -0,0 +1,119 @@
1
+ module Hell
2
+ class TailDone < StandardError; end
3
+
4
+ module Helpers
5
+ def escape_to_html(data)
6
+ {
7
+ 1 => :nothing,
8
+ 2 => :nothing,
9
+ 4 => :nothing,
10
+ 5 => :nothing,
11
+ 7 => :nothing,
12
+ 8 => :backspace,
13
+ 30 => "#303030",
14
+ 31 => "#D10915",
15
+ 32 => "#53A948",
16
+ 33 => "#CD7D3D",
17
+ 34 => "#3582E0",
18
+ 35 => :magenta,
19
+ 36 => "#30EFEF",
20
+ 37 => :white,
21
+ 40 => :nothing,
22
+ 41 => :nothing,
23
+ 43 => :nothing,
24
+ 44 => :nothing,
25
+ 45 => :nothing,
26
+ 46 => :nothing,
27
+ 47 => :nothing,
28
+ }.each do |key, value|
29
+ if value == :nothing
30
+ data.gsub!(/\e\[#{key}m/,"<span>")
31
+ elsif value == :backspace
32
+ data.gsub!(/.[\b]/, '')
33
+ else
34
+ data.gsub!(/\e\[#{key}m/,"<span style=\"color:#{value}\">")
35
+ end
36
+ end
37
+ data.gsub!(/\e\[0m/, '</span>')
38
+ data.gsub!(/\e\[0/, '</span>')
39
+ # data.gsub!(' ', '&nbsp;')
40
+ data
41
+ end
42
+
43
+ def ansi_escape(message)
44
+ escape_to_html(utf8_dammit(message))
45
+ end
46
+
47
+ def ws_message(message)
48
+ message = {:message => ansi_escape(message)}.to_json + "\n\n"
49
+ end
50
+
51
+ def process_line(line, out, io)
52
+ begin
53
+ out << "data: " + ws_message(line) unless out.closed?
54
+ raise TailDone if SENTINEL_STRINGS.any? { |w| line =~ /#{w}/ }
55
+ rescue
56
+ Process.kill("KILL", io.pid)
57
+ end
58
+ end
59
+
60
+ def close_stream(out)
61
+ out << "event: end\ndata:\n\n" unless out.closed?
62
+ out.close
63
+ end
64
+
65
+ def run_in_background!(background_cmd)
66
+ log_file = Time.now.to_i.to_s + '.' + SecureRandom.hex(2)
67
+ cmd = [
68
+ "cd #{APP_ROOT}",
69
+ "echo '#{background_cmd}' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
70
+ "#{background_cmd} >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
71
+ "echo 'Hellish Task Completed' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
72
+ ].join(" && ")
73
+ system("sh -c \"#{cmd}\" &")
74
+
75
+ # Wait up to three seconds in case of server load
76
+ i = 0
77
+ while i < 3
78
+ i += 1
79
+ break if File.exists?(File.join(HELL_LOG_PATH, log_file + ".log"))
80
+ sleep 1
81
+ end
82
+
83
+ log_file
84
+ end
85
+
86
+ def verify_task(cap, name)
87
+ original_cmd = name.gsub('+', ' ')
88
+ cmd = original_cmd.split(' ')
89
+ cmd.shift if ENVIRONMENTS.include?(cmd.first)
90
+ cmd = cmd.join(' ')
91
+
92
+ tasks = cap.task_index(cmd, {:exact => true})
93
+ return tasks, original_cmd
94
+ end
95
+
96
+ def valid_log(id)
97
+ File.exists?(File.join(HELL_LOG_PATH, id + ".log"))
98
+ end
99
+
100
+ def utf8_dammit(s)
101
+ # Converting ASCII-8BIT to UTF-8 based domain-specific guesses
102
+ if s.is_a? String
103
+ begin
104
+ # Try it as UTF-8 directly
105
+ cleaned = s.dup.force_encoding('UTF-8')
106
+ unless cleaned.valid_encoding?
107
+ # Some of it might be old Windows code page
108
+ cleaned = s.encode( 'UTF-8', 'Windows-1252' )
109
+ end
110
+ s = cleaned
111
+ rescue EncodingError
112
+ # Force it to UTF-8, throwing out invalid bits
113
+ s.encode!('UTF-8', :invalid => :replace, :undef => :replace)
114
+ end
115
+ s
116
+ end
117
+ end
118
+ end
119
+ end