artofmission-heroku 1.6.3

Sign up to get free protection for your applications and to get access to all the features.
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,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>
@@ -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
@@ -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
+
@@ -0,0 +1,5 @@
1
+ module Heroku; end
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/heroku')
4
+
5
+ require 'client'
@@ -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