artofmission-heroku 1.6.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
Heroku API - deploy apps to Heroku from the command line
|
2
|
+
========================================================
|
3
|
+
|
4
|
+
This library wraps the REST API for managing and deploying Rails apps to the
|
5
|
+
Heroku platform. It can be called as a Ruby library, or invoked from the
|
6
|
+
command line. Code push and pull is done through Git.
|
7
|
+
|
8
|
+
For more about Heroku see <http://heroku.com>.
|
9
|
+
|
10
|
+
For full documentation see <http://heroku.com/docs>.
|
11
|
+
|
12
|
+
|
13
|
+
Sample Workflow
|
14
|
+
---------------
|
15
|
+
|
16
|
+
Create a new Rails app and deploy it:
|
17
|
+
|
18
|
+
rails myapp && cd myapp # Create an app
|
19
|
+
git init # Init git repository
|
20
|
+
git add . # Add everything
|
21
|
+
git commit -m Initial # Commit everything
|
22
|
+
heroku create # Create your app on Heroku
|
23
|
+
git push heroku master # Deploy your app on Heroku
|
24
|
+
|
25
|
+
|
26
|
+
Setup
|
27
|
+
-----
|
28
|
+
|
29
|
+
gem install heroku
|
30
|
+
|
31
|
+
If you wish to push or pull code, you must also have a working install of Git
|
32
|
+
("apt-get install git-core" on Ubuntu or "port install git-core" on OS X), and
|
33
|
+
an ssh public key ("ssh-keygen -t rsa").
|
34
|
+
|
35
|
+
The first time you run a command, such as "heroku list," you will be prompted
|
36
|
+
for your Heroku username and password. If you're on a Mac, these are saved to
|
37
|
+
your Keychain. Otherwise they are stored in plain text in ~/heroku/credentials
|
38
|
+
for future requests.
|
39
|
+
|
40
|
+
Your public key (~/.ssh/id_[rd]sa.pub) will be uploaded to Heroku after you
|
41
|
+
enter your credentials. Use heroku keys:add if you wish to upload additional
|
42
|
+
keys or specify a key in a non-standard location.
|
43
|
+
|
44
|
+
Meta
|
45
|
+
----
|
46
|
+
|
47
|
+
Created by Adam Wiggins
|
48
|
+
|
49
|
+
Maintained by Pedro Belo
|
50
|
+
|
51
|
+
Patches contributed by:
|
52
|
+
|
53
|
+
* Chris O'Sullivan
|
54
|
+
* Blake Mizerany
|
55
|
+
* Ricardo Chimal
|
56
|
+
* Les Hill
|
57
|
+
* Ryan Tomayko
|
58
|
+
* Sarah Mei
|
59
|
+
* Nick Quaranto
|
60
|
+
* Matt Buck
|
61
|
+
* Terence Lee
|
62
|
+
* Caio Chassot
|
63
|
+
|
64
|
+
|
65
|
+
Released under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
|
66
|
+
<http://github.com/heroku/heroku>
|
data/Rakefile
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
|
4
|
+
desc "Run all specs"
|
5
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
6
|
+
t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
|
7
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Print specdocs"
|
11
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
12
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
13
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Generate RCov code coverage report"
|
17
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
18
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
19
|
+
t.rcov = true
|
20
|
+
t.rcov_opts = ['--exclude', 'examples']
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :spec
|
24
|
+
|
25
|
+
######################################################
|
26
|
+
|
27
|
+
require 'rake'
|
28
|
+
require 'rake/testtask'
|
29
|
+
require 'rake/clean'
|
30
|
+
require 'rake/gempackagetask'
|
31
|
+
require 'rake/rdoctask'
|
32
|
+
require 'fileutils'
|
33
|
+
include FileUtils
|
34
|
+
|
35
|
+
begin
|
36
|
+
require 'lib/heroku'
|
37
|
+
version = Heroku::Client.version
|
38
|
+
rescue LoadError
|
39
|
+
version = ""
|
40
|
+
|
41
|
+
puts "ERROR: Missing one or more dependencies. Make sure jeweler is installed and run: rake check_dependencies"
|
42
|
+
puts
|
43
|
+
end
|
44
|
+
|
45
|
+
name = "heroku"
|
46
|
+
|
47
|
+
spec = Gem::Specification.new do |s|
|
48
|
+
s.name = name
|
49
|
+
s.version = version
|
50
|
+
s.summary = "Client library and CLI to deploy Rails apps on Heroku."
|
51
|
+
s.description = "Client library and command-line tool to manage and deploy Rails apps on Heroku."
|
52
|
+
s.author = "Heroku"
|
53
|
+
s.email = "support@heroku.com"
|
54
|
+
s.homepage = "http://heroku.com/"
|
55
|
+
s.executables = [ "heroku" ]
|
56
|
+
s.default_executable = "heroku"
|
57
|
+
s.rubyforge_project = "heroku"
|
58
|
+
|
59
|
+
s.platform = Gem::Platform::RUBY
|
60
|
+
s.has_rdoc = false
|
61
|
+
|
62
|
+
s.files = %w(Rakefile) +
|
63
|
+
Dir.glob("{bin,lib,spec}/**/*")
|
64
|
+
|
65
|
+
s.require_path = "lib"
|
66
|
+
s.bindir = "bin"
|
67
|
+
|
68
|
+
s.add_development_dependency 'rake'
|
69
|
+
s.add_development_dependency 'rspec', '~> 1.2.0'
|
70
|
+
s.add_development_dependency 'taps', '~> 0.2.23'
|
71
|
+
|
72
|
+
s.add_dependency 'rest-client', '~> 1.2'
|
73
|
+
s.add_dependency 'launchy', '~> 0.3.2'
|
74
|
+
s.add_dependency 'json', '~> 1.2.0'
|
75
|
+
end
|
76
|
+
|
77
|
+
Rake::GemPackageTask.new(spec) do |p|
|
78
|
+
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Install #{name} gem (#{version})"
|
82
|
+
task :install => [ :test, :package ] do
|
83
|
+
sh %{sudo gem install pkg/#{name}-#{version}.gem}
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "Uninstall #{name} gem"
|
87
|
+
task :uninstall => [ :clean ] do
|
88
|
+
sh %{sudo gem uninstall #{name}}
|
89
|
+
end
|
90
|
+
|
91
|
+
Rake::TestTask.new do |t|
|
92
|
+
t.libs << "spec"
|
93
|
+
t.test_files = FileList['spec/*_spec.rb']
|
94
|
+
t.verbose = true
|
95
|
+
end
|
96
|
+
|
97
|
+
CLEAN.include [ 'build/*', '**/*.o', '**/*.so', '**/*.a', 'lib/*-*', '**/*.log', 'pkg', 'lib/*.bundle', '*.gem', '.config' ]
|
98
|
+
|
99
|
+
begin
|
100
|
+
require 'jeweler'
|
101
|
+
Jeweler::Tasks.new(spec) do |s|
|
102
|
+
s.version = version
|
103
|
+
end
|
104
|
+
Jeweler::RubyforgeTasks.new
|
105
|
+
rescue LoadError
|
106
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
107
|
+
end
|
data/bin/heroku
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
6
|
+
|
7
|
+
require 'heroku'
|
8
|
+
require 'heroku/command'
|
9
|
+
|
10
|
+
args = ARGV.dup
|
11
|
+
ARGV.clear
|
12
|
+
command = args.shift.strip rescue 'help'
|
13
|
+
|
14
|
+
Heroku::Command.run(command, args)
|
15
|
+
|
data/lib/heroku.rb
ADDED
@@ -0,0 +1,487 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'rest_client'
|
3
|
+
require 'uri'
|
4
|
+
require 'time'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# A Ruby class to call the Heroku REST API. You might use this if you want to
|
8
|
+
# manage your Heroku apps from within a Ruby program, such as Capistrano.
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
#
|
12
|
+
# require 'heroku'
|
13
|
+
# heroku = Heroku::Client.new('me@example.com', 'mypass')
|
14
|
+
# heroku.create('myapp')
|
15
|
+
#
|
16
|
+
class Heroku::Client
|
17
|
+
def self.version
|
18
|
+
'1.6.3'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.gem_version_string
|
22
|
+
"heroku-gem/#{version}"
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :host, :user, :password
|
26
|
+
|
27
|
+
def initialize(user, password, host='heroku.com')
|
28
|
+
@user = user
|
29
|
+
@password = password
|
30
|
+
@host = host
|
31
|
+
end
|
32
|
+
|
33
|
+
# Show a list of apps which you are a collaborator on.
|
34
|
+
def list
|
35
|
+
doc = xml(get('/apps'))
|
36
|
+
doc.elements.to_a("//apps/app").map do |a|
|
37
|
+
name = a.elements.to_a("name").first
|
38
|
+
owner = a.elements.to_a("owner").first
|
39
|
+
[name.text, owner.text]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Show info such as mode, custom domain, and collaborators on an app.
|
44
|
+
def info(name_or_domain)
|
45
|
+
name_or_domain = name_or_domain.gsub(/^(http:\/\/)?(www\.)?/, '')
|
46
|
+
doc = xml(get("/apps/#{name_or_domain}"))
|
47
|
+
attrs = doc.elements.to_a('//app/*').inject({}) do |hash, element|
|
48
|
+
hash[element.name.gsub(/-/, '_').to_sym] = element.text; hash
|
49
|
+
end
|
50
|
+
attrs.merge!(:collaborators => list_collaborators(attrs[:name]))
|
51
|
+
attrs.merge!(:addons => installed_addons(attrs[:name]))
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create a new app, with an optional name.
|
55
|
+
def create(name=nil, options={})
|
56
|
+
options[:name] = name if name
|
57
|
+
xml(post('/apps', :app => options)).elements["//app/name"].text
|
58
|
+
end
|
59
|
+
|
60
|
+
# Update an app. Available attributes:
|
61
|
+
# :name => rename the app (changes http and git urls)
|
62
|
+
def update(name, attributes)
|
63
|
+
put("/apps/#{name}", :app => attributes)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Destroy the app permanently.
|
67
|
+
def destroy(name)
|
68
|
+
delete("/apps/#{name}")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get a list of collaborators on the app, returns an array of hashes each with :email
|
72
|
+
def list_collaborators(app_name)
|
73
|
+
doc = xml(get("/apps/#{app_name}/collaborators"))
|
74
|
+
doc.elements.to_a("//collaborators/collaborator").map do |a|
|
75
|
+
{ :email => a.elements['email'].text }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Invite a person by email address to collaborate on the app.
|
80
|
+
def add_collaborator(app_name, email)
|
81
|
+
xml(post("/apps/#{app_name}/collaborators", { 'collaborator[email]' => email }))
|
82
|
+
rescue RestClient::RequestFailed => e
|
83
|
+
raise e unless e.http_code == 422
|
84
|
+
e.response.body
|
85
|
+
end
|
86
|
+
|
87
|
+
# Remove a collaborator.
|
88
|
+
def remove_collaborator(app_name, email)
|
89
|
+
delete("/apps/#{app_name}/collaborators/#{escape(email)}")
|
90
|
+
end
|
91
|
+
|
92
|
+
def list_domains(app_name)
|
93
|
+
doc = xml(get("/apps/#{app_name}/domains"))
|
94
|
+
doc.elements.to_a("//domain-names/*").map do |d|
|
95
|
+
attrs = { :domain => d.elements['domain'].text }
|
96
|
+
if cert = d.elements['cert']
|
97
|
+
attrs[:cert] = {
|
98
|
+
:expires_at => Time.parse(cert.elements['expires-at'].text),
|
99
|
+
:subject => cert.elements['subject'].text,
|
100
|
+
:issuer => cert.elements['issuer'].text,
|
101
|
+
}
|
102
|
+
end
|
103
|
+
attrs
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_domain(app_name, domain)
|
108
|
+
post("/apps/#{app_name}/domains", domain)
|
109
|
+
end
|
110
|
+
|
111
|
+
def remove_domain(app_name, domain)
|
112
|
+
delete("/apps/#{app_name}/domains/#{domain}")
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_domains(app_name)
|
116
|
+
delete("/apps/#{app_name}/domains")
|
117
|
+
end
|
118
|
+
|
119
|
+
def add_ssl(app_name, pem, key)
|
120
|
+
JSON.parse(post("/apps/#{app_name}/ssl", :pem => pem, :key => key))
|
121
|
+
end
|
122
|
+
|
123
|
+
def remove_ssl(app_name, domain)
|
124
|
+
delete("/apps/#{app_name}/domains/#{domain}/ssl")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get the list of ssh public keys for the current user.
|
128
|
+
def keys
|
129
|
+
doc = xml get('/user/keys')
|
130
|
+
doc.elements.to_a('//keys/key').map do |key|
|
131
|
+
key.elements['contents'].text
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Add an ssh public key to the current user.
|
136
|
+
def add_key(key)
|
137
|
+
post("/user/keys", key, { 'Content-Type' => 'text/ssh-authkey' })
|
138
|
+
end
|
139
|
+
|
140
|
+
# Remove an existing ssh public key from the current user.
|
141
|
+
def remove_key(key)
|
142
|
+
delete("/user/keys/#{escape(key)}")
|
143
|
+
end
|
144
|
+
|
145
|
+
# Clear all keys on the current user.
|
146
|
+
def remove_all_keys
|
147
|
+
delete("/user/keys")
|
148
|
+
end
|
149
|
+
|
150
|
+
class AppCrashed < RuntimeError; end
|
151
|
+
|
152
|
+
# Run a rake command on the Heroku app and return all output as
|
153
|
+
# a string.
|
154
|
+
def rake(app_name, cmd)
|
155
|
+
start(app_name, "rake #{cmd}", attached=true).to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
# support for console sessions
|
159
|
+
class ConsoleSession
|
160
|
+
def initialize(id, app, client)
|
161
|
+
@id = id; @app = app; @client = client
|
162
|
+
end
|
163
|
+
def run(cmd)
|
164
|
+
@client.run_console_command("/apps/#{@app}/consoles/#{@id}/command", cmd, "=> ")
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Execute a one-off console command, or start a new console tty session if
|
169
|
+
# cmd is nil.
|
170
|
+
def console(app_name, cmd=nil)
|
171
|
+
if block_given?
|
172
|
+
id = post("/apps/#{app_name}/consoles")
|
173
|
+
yield ConsoleSession.new(id, app_name, self)
|
174
|
+
delete("/apps/#{app_name}/consoles/#{id}")
|
175
|
+
else
|
176
|
+
run_console_command("/apps/#{app_name}/console", cmd)
|
177
|
+
end
|
178
|
+
rescue RestClient::RequestFailed => e
|
179
|
+
raise(AppCrashed, e.response.body) if e.response.code.to_i == 502
|
180
|
+
raise e
|
181
|
+
end
|
182
|
+
|
183
|
+
# internal method to run console commands formatting the output
|
184
|
+
def run_console_command(url, command, prefix=nil)
|
185
|
+
output = post(url, command)
|
186
|
+
return output unless prefix
|
187
|
+
if output.include?("\n")
|
188
|
+
lines = output.split("\n")
|
189
|
+
(lines[0..-2] << "#{prefix}#{lines.last}").join("\n")
|
190
|
+
else
|
191
|
+
prefix + output
|
192
|
+
end
|
193
|
+
rescue RestClient::RequestFailed => e
|
194
|
+
raise e unless e.http_code == 422
|
195
|
+
e.http_body
|
196
|
+
end
|
197
|
+
|
198
|
+
class Service
|
199
|
+
attr_accessor :attached, :upid
|
200
|
+
|
201
|
+
def initialize(client, app, upid=nil)
|
202
|
+
@client = client
|
203
|
+
@app = app
|
204
|
+
@upid = upid
|
205
|
+
end
|
206
|
+
|
207
|
+
# start the service
|
208
|
+
def start(command, attached=false)
|
209
|
+
@attached = attached
|
210
|
+
@response = @client.post(
|
211
|
+
"/apps/#{@app}/services",
|
212
|
+
command,
|
213
|
+
:content_type => 'text/plain'
|
214
|
+
)
|
215
|
+
@next_chunk = @response
|
216
|
+
@interval = 0
|
217
|
+
self
|
218
|
+
rescue RestClient::RequestFailed => e
|
219
|
+
raise AppCrashed, e.http_body if e.http_code == 502
|
220
|
+
raise
|
221
|
+
end
|
222
|
+
|
223
|
+
def transition(action)
|
224
|
+
@response = @client.put(
|
225
|
+
"/apps/#{@app}/services/#{@upid}",
|
226
|
+
action,
|
227
|
+
:content_type => 'text/plain'
|
228
|
+
)
|
229
|
+
self
|
230
|
+
rescue RestClient::RequestFailed => e
|
231
|
+
raise AppCrashed, e.http_body if e.http_code == 502
|
232
|
+
raise
|
233
|
+
end
|
234
|
+
|
235
|
+
def down ; transition('down') ; end
|
236
|
+
def up ; transition('up') ; end
|
237
|
+
def bounce ; transition('bounce') ; end
|
238
|
+
|
239
|
+
# Does the service have any remaining output?
|
240
|
+
def end_of_stream?
|
241
|
+
@next_chunk.nil?
|
242
|
+
end
|
243
|
+
|
244
|
+
# Read the next chunk of output.
|
245
|
+
def read
|
246
|
+
chunk = @client.get(@next_chunk)
|
247
|
+
if chunk.nil? or chunk == ''
|
248
|
+
# assume no content and back off
|
249
|
+
@interval = 2
|
250
|
+
''
|
251
|
+
elsif location = chunk.headers[:location]
|
252
|
+
# some data read and next chunk available
|
253
|
+
@next_chunk = location
|
254
|
+
@interval = 0
|
255
|
+
chunk
|
256
|
+
else
|
257
|
+
# no more chunks
|
258
|
+
@next_chunk = nil
|
259
|
+
chunk
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Iterate over all output chunks until EOF is reached.
|
264
|
+
def each
|
265
|
+
until end_of_stream?
|
266
|
+
sleep(@interval)
|
267
|
+
output = read
|
268
|
+
yield output unless output.empty?
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# All output as a string
|
273
|
+
def to_s
|
274
|
+
buf = []
|
275
|
+
each { |part| buf << part }
|
276
|
+
buf.join
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Retreive ps list for the given app name.
|
281
|
+
def ps(app_name)
|
282
|
+
JSON.parse resource("/apps/#{app_name}/ps").get(:accept => 'application/json')
|
283
|
+
end
|
284
|
+
|
285
|
+
# Run a service. If Responds to #each and yields output as it's received.
|
286
|
+
def start(app_name, command, attached=false)
|
287
|
+
service = Service.new(self, app_name)
|
288
|
+
service.start(command, attached)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Get a Service instance to execute commands against.
|
292
|
+
def service(app_name, upid)
|
293
|
+
Service.new(self, app_name, upid)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Bring a service up.
|
297
|
+
def up(app_name, upid)
|
298
|
+
service(app_name, upid).up
|
299
|
+
end
|
300
|
+
|
301
|
+
# Bring a service down.
|
302
|
+
def down(app_name, upid)
|
303
|
+
service(app_name, upid).down
|
304
|
+
end
|
305
|
+
|
306
|
+
# Bounce a service.
|
307
|
+
def bounce(app_name, upid)
|
308
|
+
service(app_name, upid).bounce
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
# Restart the app servers.
|
313
|
+
def restart(app_name)
|
314
|
+
delete("/apps/#{app_name}/server")
|
315
|
+
end
|
316
|
+
|
317
|
+
# Fetch recent logs from the app server.
|
318
|
+
def logs(app_name)
|
319
|
+
get("/apps/#{app_name}/logs")
|
320
|
+
end
|
321
|
+
|
322
|
+
# Fetch recent cron logs from the app server.
|
323
|
+
def cron_logs(app_name)
|
324
|
+
get("/apps/#{app_name}/cron_logs")
|
325
|
+
end
|
326
|
+
|
327
|
+
# Scales the web processes.
|
328
|
+
def set_dynos(app_name, qty)
|
329
|
+
put("/apps/#{app_name}/dynos", :dynos => qty).to_i
|
330
|
+
end
|
331
|
+
|
332
|
+
# Scales the background processes.
|
333
|
+
def set_workers(app_name, qty)
|
334
|
+
put("/apps/#{app_name}/workers", :workers => qty).to_i
|
335
|
+
end
|
336
|
+
|
337
|
+
# Capture a bundle from the given app, as a backup or for download.
|
338
|
+
def bundle_capture(app_name, bundle_name=nil)
|
339
|
+
xml(post("/apps/#{app_name}/bundles", :bundle => { :name => bundle_name })).elements["//bundle/name"].text
|
340
|
+
end
|
341
|
+
|
342
|
+
def bundle_destroy(app_name, bundle_name)
|
343
|
+
delete("/apps/#{app_name}/bundles/#{bundle_name}")
|
344
|
+
end
|
345
|
+
|
346
|
+
# Get a temporary URL where the bundle can be downloaded.
|
347
|
+
# If bundle_name is nil it will use the most recently captured bundle for the app
|
348
|
+
def bundle_url(app_name, bundle_name=nil)
|
349
|
+
bundle = JSON.parse(get("/apps/#{app_name}/bundles/#{bundle_name || 'latest'}", { :accept => 'application/json' }))
|
350
|
+
bundle['temporary_url']
|
351
|
+
end
|
352
|
+
|
353
|
+
def bundle_download(app_name, fname, bundle_name=nil)
|
354
|
+
warn "[DEPRECATION] `bundle_download` is deprecated. Please use `bundle_url` instead"
|
355
|
+
data = RestClient.get(bundle_url(app_name, bundle_name))
|
356
|
+
File.open(fname, "wb") { |f| f.write data }
|
357
|
+
end
|
358
|
+
|
359
|
+
# Get a list of bundles of the app.
|
360
|
+
def bundles(app_name)
|
361
|
+
doc = xml(get("/apps/#{app_name}/bundles"))
|
362
|
+
doc.elements.to_a("//bundles/bundle").map do |a|
|
363
|
+
{
|
364
|
+
:name => a.elements['name'].text,
|
365
|
+
:state => a.elements['state'].text,
|
366
|
+
:created_at => Time.parse(a.elements['created-at'].text),
|
367
|
+
}
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def config_vars(app_name)
|
372
|
+
JSON.parse get("/apps/#{app_name}/config_vars")
|
373
|
+
end
|
374
|
+
|
375
|
+
def add_config_vars(app_name, new_vars)
|
376
|
+
put("/apps/#{app_name}/config_vars", new_vars.to_json)
|
377
|
+
end
|
378
|
+
|
379
|
+
def remove_config_var(app_name, key)
|
380
|
+
delete("/apps/#{app_name}/config_vars/#{key}")
|
381
|
+
end
|
382
|
+
|
383
|
+
def clear_config_vars(app_name)
|
384
|
+
delete("/apps/#{app_name}/config_vars")
|
385
|
+
end
|
386
|
+
|
387
|
+
def addons
|
388
|
+
JSON.parse get("/addons", :accept => 'application/json')
|
389
|
+
end
|
390
|
+
|
391
|
+
def installed_addons(app_name)
|
392
|
+
JSON.parse get("/apps/#{app_name}/addons", :accept => 'application/json')
|
393
|
+
end
|
394
|
+
|
395
|
+
def install_addon(app_name, addon, config={})
|
396
|
+
post("/apps/#{app_name}/addons/#{escape(addon)}", { :config => config }, :accept => 'application/json')
|
397
|
+
end
|
398
|
+
|
399
|
+
def uninstall_addon(app_name, addon)
|
400
|
+
delete("/apps/#{app_name}/addons/#{escape(addon)}", :accept => 'application/json')
|
401
|
+
end
|
402
|
+
|
403
|
+
def confirm_billing
|
404
|
+
post("/user/#{escape(@user)}/confirm_billing")
|
405
|
+
end
|
406
|
+
|
407
|
+
def on_warning(&blk)
|
408
|
+
@warning_callback = blk
|
409
|
+
end
|
410
|
+
|
411
|
+
##################
|
412
|
+
|
413
|
+
def resource(uri)
|
414
|
+
RestClient.proxy = ENV['HTTP_PROXY']
|
415
|
+
if uri =~ /^https?/
|
416
|
+
RestClient::Resource.new(uri, user, password)
|
417
|
+
else
|
418
|
+
RestClient::Resource.new("https://api.#{host}", user, password)[uri]
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def get(uri, extra_headers={}) # :nodoc:
|
423
|
+
process(:get, uri, extra_headers)
|
424
|
+
end
|
425
|
+
|
426
|
+
def post(uri, payload="", extra_headers={}) # :nodoc:
|
427
|
+
process(:post, uri, extra_headers, payload)
|
428
|
+
end
|
429
|
+
|
430
|
+
def put(uri, payload, extra_headers={}) # :nodoc:
|
431
|
+
process(:put, uri, extra_headers, payload)
|
432
|
+
end
|
433
|
+
|
434
|
+
def delete(uri, extra_headers={}) # :nodoc:
|
435
|
+
process(:delete, uri, extra_headers)
|
436
|
+
end
|
437
|
+
|
438
|
+
def process(method, uri, extra_headers={}, payload=nil)
|
439
|
+
headers = heroku_headers.merge(extra_headers)
|
440
|
+
args = [method, payload, headers].compact
|
441
|
+
response = resource(uri).send(*args)
|
442
|
+
|
443
|
+
extract_warning(response)
|
444
|
+
response
|
445
|
+
end
|
446
|
+
|
447
|
+
def extract_warning(response)
|
448
|
+
return unless response
|
449
|
+
if response.headers[:x_heroku_warning] && @warning_callback
|
450
|
+
warning = response.headers[:x_heroku_warning]
|
451
|
+
@displayed_warnings ||= {}
|
452
|
+
unless @displayed_warnings[warning]
|
453
|
+
@warning_callback.call(warning)
|
454
|
+
@displayed_warnings[warning] = true
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def heroku_headers # :nodoc:
|
460
|
+
{
|
461
|
+
'X-Heroku-API-Version' => '2',
|
462
|
+
'User-Agent' => self.class.gem_version_string,
|
463
|
+
}
|
464
|
+
end
|
465
|
+
|
466
|
+
def xml(raw) # :nodoc:
|
467
|
+
REXML::Document.new(raw)
|
468
|
+
end
|
469
|
+
|
470
|
+
def escape(value) # :nodoc:
|
471
|
+
escaped = URI.escape(value.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
472
|
+
escaped.gsub('.', '%2E') # not covered by the previous URI.escape
|
473
|
+
end
|
474
|
+
|
475
|
+
def database_session(app_name)
|
476
|
+
post("/apps/#{app_name}/database/session", '')
|
477
|
+
end
|
478
|
+
|
479
|
+
def database_reset(app_name)
|
480
|
+
post("/apps/#{app_name}/database/reset", '')
|
481
|
+
end
|
482
|
+
|
483
|
+
def maintenance(app_name, mode)
|
484
|
+
mode = mode == :on ? '1' : '0'
|
485
|
+
post("/apps/#{app_name}/server/maintenance", :maintenance_mode => mode)
|
486
|
+
end
|
487
|
+
end
|