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