artofmission-heroku 1.6.3

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 (47) hide show
  1. data/README.md +66 -0
  2. data/Rakefile +107 -0
  3. data/bin/heroku +15 -0
  4. data/lib/heroku.rb +5 -0
  5. data/lib/heroku/client.rb +487 -0
  6. data/lib/heroku/command.rb +96 -0
  7. data/lib/heroku/commands/account.rb +13 -0
  8. data/lib/heroku/commands/addons.rb +109 -0
  9. data/lib/heroku/commands/app.rb +239 -0
  10. data/lib/heroku/commands/auth.rb +137 -0
  11. data/lib/heroku/commands/base.rb +133 -0
  12. data/lib/heroku/commands/bundles.rb +51 -0
  13. data/lib/heroku/commands/config.rb +55 -0
  14. data/lib/heroku/commands/db.rb +129 -0
  15. data/lib/heroku/commands/domains.rb +31 -0
  16. data/lib/heroku/commands/help.rb +148 -0
  17. data/lib/heroku/commands/keys.rb +49 -0
  18. data/lib/heroku/commands/logs.rb +11 -0
  19. data/lib/heroku/commands/maintenance.rb +13 -0
  20. data/lib/heroku/commands/plugins.rb +25 -0
  21. data/lib/heroku/commands/ps.rb +37 -0
  22. data/lib/heroku/commands/service.rb +23 -0
  23. data/lib/heroku/commands/sharing.rb +29 -0
  24. data/lib/heroku/commands/ssl.rb +33 -0
  25. data/lib/heroku/commands/version.rb +7 -0
  26. data/lib/heroku/helpers.rb +23 -0
  27. data/lib/heroku/plugin.rb +65 -0
  28. data/spec/base.rb +23 -0
  29. data/spec/client_spec.rb +366 -0
  30. data/spec/command_spec.rb +15 -0
  31. data/spec/commands/addons_spec.rb +47 -0
  32. data/spec/commands/app_spec.rb +175 -0
  33. data/spec/commands/auth_spec.rb +104 -0
  34. data/spec/commands/base_spec.rb +114 -0
  35. data/spec/commands/bundles_spec.rb +48 -0
  36. data/spec/commands/config_spec.rb +45 -0
  37. data/spec/commands/db_spec.rb +53 -0
  38. data/spec/commands/domains_spec.rb +31 -0
  39. data/spec/commands/keys_spec.rb +60 -0
  40. data/spec/commands/logs_spec.rb +21 -0
  41. data/spec/commands/maintenance_spec.rb +21 -0
  42. data/spec/commands/plugins_spec.rb +26 -0
  43. data/spec/commands/ps_spec.rb +16 -0
  44. data/spec/commands/sharing_spec.rb +32 -0
  45. data/spec/commands/ssl_spec.rb +25 -0
  46. data/spec/plugin_spec.rb +64 -0
  47. metadata +150 -0
@@ -0,0 +1,96 @@
1
+ require 'helpers'
2
+ require 'plugin'
3
+ require 'commands/base'
4
+
5
+ Dir["#{File.dirname(__FILE__)}/commands/*"].each { |c| require c }
6
+
7
+ module Heroku
8
+ module Command
9
+ class InvalidCommand < RuntimeError; end
10
+ class CommandFailed < RuntimeError; end
11
+
12
+ class << self
13
+ def run(command, args, retries=0)
14
+ Heroku::Plugin.load!
15
+ begin
16
+ run_internal 'auth:reauthorize', args.dup if retries > 0
17
+ run_internal(command, args.dup)
18
+ rescue InvalidCommand
19
+ error "Unknown command. Run 'heroku help' for usage information."
20
+ rescue RestClient::Unauthorized
21
+ if retries < 3
22
+ STDERR.puts "Authentication failure"
23
+ run(command, args, retries+1)
24
+ else
25
+ error "Authentication failure"
26
+ end
27
+ rescue RestClient::ResourceNotFound => e
28
+ error extract_not_found(e.http_body)
29
+ rescue RestClient::RequestFailed => e
30
+ error extract_error(e.http_body) unless e.http_code == 402
31
+ retry if run_internal('account:confirm_billing', args.dup)
32
+ rescue RestClient::RequestTimeout
33
+ error "API request timed out. Please try again, or contact support@heroku.com if this issue persists."
34
+ rescue CommandFailed => e
35
+ error e.message
36
+ rescue Interrupt => e
37
+ error "\n[canceled]"
38
+ end
39
+ end
40
+
41
+ def run_internal(command, args, heroku=nil)
42
+ klass, method = parse(command)
43
+ runner = klass.new(args, heroku)
44
+ raise InvalidCommand unless runner.respond_to?(method)
45
+ runner.send(method)
46
+ end
47
+
48
+ def error(msg)
49
+ STDERR.puts(msg)
50
+ exit 1
51
+ end
52
+
53
+ def parse(command)
54
+ parts = command.split(':')
55
+ case parts.size
56
+ when 1
57
+ begin
58
+ return eval("Heroku::Command::#{command.capitalize}"), :index
59
+ rescue NameError, NoMethodError
60
+ return Heroku::Command::App, command
61
+ end
62
+ when 2
63
+ begin
64
+ return Heroku::Command.const_get(parts[0].capitalize), parts[1]
65
+ rescue NameError
66
+ raise InvalidCommand
67
+ end
68
+ else
69
+ raise InvalidCommand
70
+ end
71
+ end
72
+
73
+ def extract_not_found(body)
74
+ body =~ /^[\w\s]+ not found$/ ? body : "Resource not found"
75
+ end
76
+
77
+ def extract_error(body)
78
+ msg = parse_error_xml(body) || parse_error_json(body) || 'Internal server error'
79
+ msg.split("\n").map { |line| ' ! ' + line }.join("\n")
80
+ end
81
+
82
+ def parse_error_xml(body)
83
+ xml_errors = REXML::Document.new(body).elements.to_a("//errors/error")
84
+ msg = xml_errors.map { |a| a.text }.join(" / ")
85
+ return msg unless msg.empty?
86
+ rescue Exception
87
+ end
88
+
89
+ def parse_error_json(body)
90
+ json = JSON.parse(body)
91
+ json['error']
92
+ rescue JSON::ParserError
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ module Heroku::Command
2
+ class Account < Base
3
+ def confirm_billing
4
+ display(" This action will cause your account to be billed at the end of the month")
5
+ display(" For more information, see http://docs.heroku.com/billing")
6
+ display(" Are you sure you want to do this? (y/n) ", false)
7
+ if ask.downcase == 'y'
8
+ heroku.confirm_billing
9
+ return true
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,109 @@
1
+ module Heroku::Command
2
+ class Addons < BaseWithApp
3
+ def index
4
+ installed = heroku.installed_addons(app)
5
+ if installed.empty?
6
+ display "No addons installed"
7
+ else
8
+ available, pending = installed.partition { |a| a['configured'] }
9
+ available.map { |a| a['name'] }.sort.each do |addon|
10
+ display addon
11
+ end
12
+ unless pending.empty?
13
+ display "\n--- not configured ---"
14
+ pending.map { |a| a['name'] }.sort.each do |addon|
15
+ display addon.ljust(24) + "http://#{heroku.host}/myapps/#{app}/addons/#{addon}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def info
22
+ addons = heroku.addons
23
+ if addons.empty?
24
+ display "No addons available currently"
25
+ else
26
+ available, beta = addons.partition { |a| !a['beta'] }
27
+ display_addons(available)
28
+ if !beta.empty?
29
+ display "\n--- beta ---"
30
+ display_addons(beta)
31
+ end
32
+ end
33
+ end
34
+
35
+ def add
36
+ addon = args.shift
37
+ config = {}
38
+ args.each do |arg|
39
+ key, value = arg.strip.split('=', 2)
40
+ if value.nil?
41
+ error("Non-config value \"#{arg}\".\nEverything after the addon name should be a key=value pair")
42
+ else
43
+ config[key] = value
44
+ end
45
+ end
46
+
47
+ display "Adding #{addon} to #{app}... ", false
48
+ display addon_run { heroku.install_addon(app, addon, config) }
49
+ end
50
+
51
+ def remove
52
+ args.each do |name|
53
+ display "Removing #{name} from #{app}... ", false
54
+ display addon_run { heroku.uninstall_addon(app, name) }
55
+ end
56
+ end
57
+
58
+ def clear
59
+ heroku.installed_addons(app).each do |addon|
60
+ display "Removing #{addon['description']} from #{app}... ", false
61
+ display addon_run { heroku.uninstall_addon(app, addon['name']) }
62
+ end
63
+ end
64
+
65
+ def confirm_billing
66
+ Heroku::Command.run_internal 'account:confirm_billing', []
67
+ end
68
+
69
+ private
70
+ def display_addons(addons)
71
+ grouped = addons.inject({}) do |base, addon|
72
+ group, short = addon['name'].split(':')
73
+ base[group] ||= []
74
+ base[group] << addon.merge('short' => short)
75
+ base
76
+ end
77
+ grouped.keys.sort.each do |name|
78
+ addons = grouped[name]
79
+ row = name.dup
80
+ if addons.any? { |a| a['short'] }
81
+ row << ':'
82
+ size = row.size
83
+ stop = false
84
+ row << addons.map { |a| a['short'] }.sort.map do |short|
85
+ size += short.size
86
+ if size < 31
87
+ short
88
+ else
89
+ stop = true
90
+ nil
91
+ end
92
+ end.compact.join(', ')
93
+ row << '...' if stop
94
+ end
95
+ display row.ljust(34) + (addons.first['url'] || '')
96
+ end
97
+ end
98
+
99
+ def addon_run
100
+ yield
101
+ 'done'
102
+ rescue RestClient::ResourceNotFound => e
103
+ "FAILED\nno addon by that name"
104
+ rescue RestClient::RequestFailed => e
105
+ retry if e.http_code == 402 && confirm_billing
106
+ "FAILED\n" + Heroku::Command.extract_error(e.http_body)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,239 @@
1
+ require 'readline'
2
+ require 'launchy'
3
+
4
+ module Heroku::Command
5
+ class App < Base
6
+ def list
7
+ list = heroku.list
8
+ if list.size > 0
9
+ display list.map {|name, owner|
10
+ if heroku.user == owner
11
+ name
12
+ else
13
+ "#{name.ljust(25)} #{owner}"
14
+ end
15
+ }.join("\n")
16
+ else
17
+ display "You have no apps."
18
+ end
19
+ end
20
+
21
+ def create
22
+ remote = extract_option('--remote', 'heroku')
23
+ name = args.shift.downcase.strip rescue nil
24
+ name = heroku.create(name, {})
25
+ display "Created #{app_urls(name)}"
26
+ if remote || File.exists?(Dir.pwd + '/.git')
27
+ remote ||= 'heroku'
28
+ return if shell('git remote').split("\n").include?(remote)
29
+ shell "git remote add #{remote} git@#{heroku.host}:#{name}.git"
30
+ display "Git remote #{remote} added"
31
+ end
32
+ end
33
+
34
+ def rename
35
+ name = extract_app
36
+ newname = args.shift.downcase.strip rescue ''
37
+ raise(CommandFailed, "Invalid name.") if newname == ''
38
+
39
+ heroku.update(name, :name => newname)
40
+ display app_urls(newname)
41
+
42
+ if remotes = git_remotes(Dir.pwd)
43
+ remotes.each do |remote_name, remote_app|
44
+ next if remote_app != name
45
+ shell "git remote rm #{remote_name}"
46
+ shell "git remote add #{remote_name} git@#{heroku.host}:#{newname}.git"
47
+ display "Git remote #{remote_name} updated"
48
+ end
49
+ else
50
+ display "Don't forget to update your Git remotes on any local checkouts."
51
+ end
52
+ end
53
+
54
+ def info
55
+ name = (args.first && !args.first =~ /^\-\-/) ? args.first : extract_app
56
+ attrs = heroku.info(name)
57
+
58
+ attrs[:web_url] ||= "http://#{attrs[:name]}.#{heroku.host}/"
59
+ attrs[:git_url] ||= "git@#{heroku.host}:#{attrs[:name]}.git"
60
+
61
+ display "=== #{attrs[:name]}"
62
+ display "Web URL: #{attrs[:web_url]}"
63
+ display "Domain name: http://#{attrs[:domain_name]}/" if attrs[:domain_name]
64
+ display "Git Repo: #{attrs[:git_url]}"
65
+ display "Dynos: #{attrs[:dynos]}"
66
+ display "Workers: #{attrs[:workers]}"
67
+ display "Repo size: #{format_bytes(attrs[:repo_size])}" if attrs[:repo_size]
68
+ display "Slug size: #{format_bytes(attrs[:slug_size])}" if attrs[:slug_size]
69
+ if attrs[:database_size]
70
+ data = format_bytes(attrs[:database_size])
71
+ if tables = attrs[:database_tables]
72
+ data = data.gsub('(empty)', '0K') + " in #{tables} table#{'s' if tables.to_i > 1}"
73
+ end
74
+ display "Data size: #{data}"
75
+ end
76
+
77
+ if attrs[:cron_next_run]
78
+ display "Next cron: #{format_date(attrs[:cron_next_run])} (scheduled)"
79
+ end
80
+ if attrs[:cron_finished_at]
81
+ display "Last cron: #{format_date(attrs[:cron_finished_at])} (finished)"
82
+ end
83
+
84
+ unless attrs[:addons].empty?
85
+ display "Addons: " + attrs[:addons].map { |a| a['description'] }.join(', ')
86
+ end
87
+
88
+ display "Owner: #{attrs[:owner]}"
89
+ collaborators = attrs[:collaborators].delete_if { |c| c[:email] == attrs[:owner] }
90
+ unless collaborators.empty?
91
+ first = true
92
+ lead = "Collaborators:"
93
+ attrs[:collaborators].each do |collaborator|
94
+ display "#{first ? lead : ' ' * lead.length} #{collaborator[:email]}"
95
+ first = false
96
+ end
97
+ end
98
+ end
99
+
100
+ def open
101
+ app = extract_app
102
+
103
+ url = web_url(app)
104
+ puts "Opening #{url}"
105
+ Launchy.open url
106
+ end
107
+
108
+ def rake
109
+ app = extract_app
110
+ cmd = args.join(' ')
111
+ if cmd.length == 0
112
+ display "Usage: heroku rake <command>"
113
+ else
114
+ heroku.start(app, "rake #{cmd}", attached=true).each do |chunk|
115
+ display chunk, false
116
+ end
117
+ end
118
+ rescue Heroku::Client::AppCrashed => e
119
+ error "Couldn't run rake\n#{e.message}"
120
+ end
121
+
122
+ def console
123
+ app = extract_app
124
+ cmd = args.join(' ').strip
125
+ if cmd.empty?
126
+ console_session(app)
127
+ else
128
+ display heroku.console(app, cmd)
129
+ end
130
+ rescue RestClient::RequestTimeout
131
+ error "Timed out. Long running requests are not supported on the console.\nPlease consider creating a rake task instead."
132
+ rescue Heroku::Client::AppCrashed => e
133
+ error "Couldn't run console command\n#{e.message}"
134
+ end
135
+
136
+ def console_session(app)
137
+ heroku.console(app) do |console|
138
+ console_history_read(app)
139
+
140
+ display "Ruby console for #{app}.#{heroku.host}"
141
+ while cmd = Readline.readline('>> ')
142
+ unless cmd.nil? || cmd.strip.empty?
143
+ console_history_add(app, cmd)
144
+ break if cmd.downcase.strip == 'exit'
145
+ display console.run(cmd)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def restart
152
+ app_name = extract_app
153
+ heroku.restart(app_name)
154
+ display "Servers restarted"
155
+ end
156
+
157
+ def dynos
158
+ app = extract_app
159
+ if dynos = args.shift
160
+ current = heroku.set_dynos(app, dynos)
161
+ display "#{app} now running on #{current} dyno#{'s' if current > 1}"
162
+ else
163
+ info = heroku.info(app)
164
+ display "#{app} is running on #{info[:dynos]} dyno#{'s' if info[:dynos].to_i > 1}"
165
+ end
166
+ end
167
+
168
+ def workers
169
+ app = extract_app
170
+ if workers = args.shift
171
+ current = heroku.set_workers(app, workers)
172
+ display "#{app} now running #{current} worker#{'s' if current != 1}"
173
+ else
174
+ info = heroku.info(app)
175
+ display "#{app} is running #{info[:workers]} worker#{'s' if info[:workers].to_i != 1}"
176
+ end
177
+ end
178
+
179
+ def destroy
180
+ if name = extract_option('--app')
181
+ info = heroku.info(name)
182
+ url = info[:domain_name] || "http://#{info[:name]}.#{heroku.host}/"
183
+ conf = nil
184
+
185
+ display("Permanently destroy #{url} (y/n)? ", false)
186
+ if ask.downcase == 'y'
187
+ heroku.destroy(name)
188
+ if remotes = git_remotes(Dir.pwd)
189
+ remotes.each do |remote_name, remote_app|
190
+ next if name != remote_app
191
+ shell "git remote rm #{remote_name}"
192
+ end
193
+ end
194
+ display "Destroyed #{name}"
195
+ end
196
+ else
197
+ display "Set the app you want to destroy adding --app <app name> to this command"
198
+ end
199
+ end
200
+
201
+ protected
202
+ @@kb = 1024
203
+ @@mb = 1024 * @@kb
204
+ @@gb = 1024 * @@mb
205
+ def format_bytes(amount)
206
+ amount = amount.to_i
207
+ return '(empty)' if amount == 0
208
+ return amount if amount < @@kb
209
+ return "#{(amount / @@kb).round}k" if amount < @@mb
210
+ return "#{(amount / @@mb).round}M" if amount < @@gb
211
+ return "#{(amount / @@gb).round}G"
212
+ end
213
+
214
+ def console_history_dir
215
+ FileUtils.mkdir_p(path = "#{home_directory}/.heroku/console_history")
216
+ path
217
+ end
218
+
219
+ def console_history_file(app)
220
+ "#{console_history_dir}/#{app}"
221
+ end
222
+
223
+ def console_history_read(app)
224
+ history = File.read(console_history_file(app)).split("\n")
225
+ if history.size > 50
226
+ history = history[(history.size - 51),(history.size - 1)]
227
+ File.open(console_history_file(app), "w") { |f| f.puts history.join("\n") }
228
+ end
229
+ history.each { |cmd| Readline::HISTORY.push(cmd) }
230
+ rescue Errno::ENOENT
231
+ end
232
+
233
+ def console_history_add(app, cmd)
234
+ Readline::HISTORY.push(cmd)
235
+ File.open(console_history_file(app), "a") { |f| f.puts cmd + "\n" }
236
+ end
237
+
238
+ end
239
+ end