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.
- data/README.md +66 -0
- data/Rakefile +107 -0
- data/bin/heroku +15 -0
- data/lib/heroku.rb +5 -0
- data/lib/heroku/client.rb +487 -0
- data/lib/heroku/command.rb +96 -0
- data/lib/heroku/commands/account.rb +13 -0
- data/lib/heroku/commands/addons.rb +109 -0
- data/lib/heroku/commands/app.rb +239 -0
- data/lib/heroku/commands/auth.rb +137 -0
- data/lib/heroku/commands/base.rb +133 -0
- data/lib/heroku/commands/bundles.rb +51 -0
- data/lib/heroku/commands/config.rb +55 -0
- data/lib/heroku/commands/db.rb +129 -0
- data/lib/heroku/commands/domains.rb +31 -0
- data/lib/heroku/commands/help.rb +148 -0
- data/lib/heroku/commands/keys.rb +49 -0
- data/lib/heroku/commands/logs.rb +11 -0
- data/lib/heroku/commands/maintenance.rb +13 -0
- data/lib/heroku/commands/plugins.rb +25 -0
- data/lib/heroku/commands/ps.rb +37 -0
- data/lib/heroku/commands/service.rb +23 -0
- data/lib/heroku/commands/sharing.rb +29 -0
- data/lib/heroku/commands/ssl.rb +33 -0
- data/lib/heroku/commands/version.rb +7 -0
- data/lib/heroku/helpers.rb +23 -0
- data/lib/heroku/plugin.rb +65 -0
- data/spec/base.rb +23 -0
- data/spec/client_spec.rb +366 -0
- data/spec/command_spec.rb +15 -0
- data/spec/commands/addons_spec.rb +47 -0
- data/spec/commands/app_spec.rb +175 -0
- data/spec/commands/auth_spec.rb +104 -0
- data/spec/commands/base_spec.rb +114 -0
- data/spec/commands/bundles_spec.rb +48 -0
- data/spec/commands/config_spec.rb +45 -0
- data/spec/commands/db_spec.rb +53 -0
- data/spec/commands/domains_spec.rb +31 -0
- data/spec/commands/keys_spec.rb +60 -0
- data/spec/commands/logs_spec.rb +21 -0
- data/spec/commands/maintenance_spec.rb +21 -0
- data/spec/commands/plugins_spec.rb +26 -0
- data/spec/commands/ps_spec.rb +16 -0
- data/spec/commands/sharing_spec.rb +32 -0
- data/spec/commands/ssl_spec.rb +25 -0
- data/spec/plugin_spec.rb +64 -0
- 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
|