pogo 2.31.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/README.md +73 -0
  2. data/bin/pogo +22 -0
  3. data/data/cacert.pem +3988 -0
  4. data/lib/heroku.rb +22 -0
  5. data/lib/heroku/auth.rb +320 -0
  6. data/lib/heroku/cli.rb +38 -0
  7. data/lib/heroku/client.rb +764 -0
  8. data/lib/heroku/client/heroku_postgresql.rb +111 -0
  9. data/lib/heroku/client/pgbackups.rb +113 -0
  10. data/lib/heroku/client/rendezvous.rb +105 -0
  11. data/lib/heroku/client/ssl_endpoint.rb +25 -0
  12. data/lib/heroku/command.rb +273 -0
  13. data/lib/heroku/command/account.rb +23 -0
  14. data/lib/heroku/command/accounts.rb +34 -0
  15. data/lib/heroku/command/addons.rb +305 -0
  16. data/lib/heroku/command/apps.rb +311 -0
  17. data/lib/heroku/command/auth.rb +86 -0
  18. data/lib/heroku/command/base.rb +230 -0
  19. data/lib/heroku/command/certs.rb +148 -0
  20. data/lib/heroku/command/config.rb +137 -0
  21. data/lib/heroku/command/db.rb +218 -0
  22. data/lib/heroku/command/domains.rb +85 -0
  23. data/lib/heroku/command/drains.rb +46 -0
  24. data/lib/heroku/command/git.rb +65 -0
  25. data/lib/heroku/command/help.rb +163 -0
  26. data/lib/heroku/command/keys.rb +115 -0
  27. data/lib/heroku/command/labs.rb +161 -0
  28. data/lib/heroku/command/logs.rb +98 -0
  29. data/lib/heroku/command/maintenance.rb +61 -0
  30. data/lib/heroku/command/pg.rb +277 -0
  31. data/lib/heroku/command/pgbackups.rb +289 -0
  32. data/lib/heroku/command/plugins.rb +110 -0
  33. data/lib/heroku/command/ps.rb +232 -0
  34. data/lib/heroku/command/releases.rb +124 -0
  35. data/lib/heroku/command/run.rb +179 -0
  36. data/lib/heroku/command/sharing.rb +89 -0
  37. data/lib/heroku/command/ssl.rb +61 -0
  38. data/lib/heroku/command/stack.rb +62 -0
  39. data/lib/heroku/command/status.rb +51 -0
  40. data/lib/heroku/command/update.rb +47 -0
  41. data/lib/heroku/command/version.rb +23 -0
  42. data/lib/heroku/deprecated.rb +5 -0
  43. data/lib/heroku/deprecated/help.rb +38 -0
  44. data/lib/heroku/distribution.rb +9 -0
  45. data/lib/heroku/helpers.rb +517 -0
  46. data/lib/heroku/helpers/heroku_postgresql.rb +104 -0
  47. data/lib/heroku/plugin.rb +161 -0
  48. data/lib/heroku/updater.rb +158 -0
  49. data/lib/heroku/version.rb +3 -0
  50. data/lib/vendor/heroku/okjson.rb +598 -0
  51. data/spec/helper/legacy_help.rb +16 -0
  52. data/spec/heroku/auth_spec.rb +246 -0
  53. data/spec/heroku/client/heroku_postgresql_spec.rb +34 -0
  54. data/spec/heroku/client/pgbackups_spec.rb +43 -0
  55. data/spec/heroku/client/rendezvous_spec.rb +62 -0
  56. data/spec/heroku/client/ssl_endpoint_spec.rb +48 -0
  57. data/spec/heroku/client_spec.rb +564 -0
  58. data/spec/heroku/command/addons_spec.rb +585 -0
  59. data/spec/heroku/command/apps_spec.rb +351 -0
  60. data/spec/heroku/command/auth_spec.rb +38 -0
  61. data/spec/heroku/command/base_spec.rb +109 -0
  62. data/spec/heroku/command/certs_spec.rb +178 -0
  63. data/spec/heroku/command/config_spec.rb +144 -0
  64. data/spec/heroku/command/db_spec.rb +110 -0
  65. data/spec/heroku/command/domains_spec.rb +87 -0
  66. data/spec/heroku/command/drains_spec.rb +34 -0
  67. data/spec/heroku/command/git_spec.rb +116 -0
  68. data/spec/heroku/command/help_spec.rb +93 -0
  69. data/spec/heroku/command/keys_spec.rb +120 -0
  70. data/spec/heroku/command/labs_spec.rb +99 -0
  71. data/spec/heroku/command/logs_spec.rb +60 -0
  72. data/spec/heroku/command/maintenance_spec.rb +51 -0
  73. data/spec/heroku/command/pg_spec.rb +223 -0
  74. data/spec/heroku/command/pgbackups_spec.rb +280 -0
  75. data/spec/heroku/command/plugins_spec.rb +104 -0
  76. data/spec/heroku/command/ps_spec.rb +195 -0
  77. data/spec/heroku/command/releases_spec.rb +130 -0
  78. data/spec/heroku/command/run_spec.rb +86 -0
  79. data/spec/heroku/command/sharing_spec.rb +59 -0
  80. data/spec/heroku/command/ssl_spec.rb +32 -0
  81. data/spec/heroku/command/stack_spec.rb +46 -0
  82. data/spec/heroku/command/status_spec.rb +48 -0
  83. data/spec/heroku/command/version_spec.rb +16 -0
  84. data/spec/heroku/command_spec.rb +211 -0
  85. data/spec/heroku/helpers/heroku_postgresql_spec.rb +109 -0
  86. data/spec/heroku/helpers_spec.rb +48 -0
  87. data/spec/heroku/plugin_spec.rb +172 -0
  88. data/spec/heroku/updater_spec.rb +44 -0
  89. data/spec/spec.opts +1 -0
  90. data/spec/spec_helper.rb +209 -0
  91. data/spec/support/display_message_matcher.rb +49 -0
  92. data/spec/support/openssl_mock_helper.rb +8 -0
  93. metadata +220 -0
@@ -0,0 +1,98 @@
1
+ require "heroku/command/base"
2
+
3
+ # display logs for an app
4
+ #
5
+ class Heroku::Command::Logs < Heroku::Command::Base
6
+
7
+ # logs
8
+ #
9
+ # display recent log output
10
+ #
11
+ # -n, --num NUM # the number of lines to display
12
+ # -p, --ps PS # only display logs from the given process
13
+ # -s, --source SOURCE # only display logs from the given source
14
+ # -t, --tail # continually stream logs
15
+ #
16
+ #Example:
17
+ #
18
+ # $ heroku logs
19
+ # 2012-01-01T12:00:00+00:00 heroku[api]: Config add EXAMPLE by email@example.com
20
+ # 2012-01-01T12:00:01+00:00 heroku[api]: Release v1 created by email@example.com
21
+ #
22
+ def index
23
+ validate_arguments!
24
+
25
+ opts = []
26
+ opts << "tail=1" if options[:tail]
27
+ opts << "num=#{options[:num]}" if options[:num]
28
+ opts << "ps=#{URI.encode(options[:ps])}" if options[:ps]
29
+ opts << "source=#{URI.encode(options[:source])}" if options[:source]
30
+
31
+ @assigned_colors = {}
32
+ @line_start = true
33
+ @token = nil
34
+
35
+ heroku.read_logs(app, opts) do |chunk|
36
+ unless chunk.empty?
37
+ if STDOUT.isatty && ENV.has_key?("TERM")
38
+ display(colorize(chunk))
39
+ else
40
+ display(chunk)
41
+ end
42
+ end
43
+ end
44
+ rescue Errno::EPIPE
45
+ rescue Interrupt => interrupt
46
+ if STDOUT.isatty && ENV.has_key?("TERM")
47
+ display("\e[0m")
48
+ end
49
+ raise(interrupt)
50
+ end
51
+
52
+ # logs:drains
53
+ #
54
+ # DEPRECATED: use `heroku drains`
55
+ #
56
+ def drains
57
+ # deprecation notice added 09/30/2011
58
+ display("~ `heroku logs:drains` has been deprecated and replaced with `heroku drains`")
59
+ Heroku::Command::Drains.new.index
60
+ end
61
+
62
+ protected
63
+
64
+ COLORS = %w( cyan yellow green magenta red )
65
+ COLOR_CODES = {
66
+ "red" => 31,
67
+ "green" => 32,
68
+ "yellow" => 33,
69
+ "magenta" => 35,
70
+ "cyan" => 36,
71
+ }
72
+
73
+ def colorize(chunk)
74
+ lines = []
75
+ chunk.split("\n").map do |line|
76
+ if parsed_line = parse_log(line)
77
+ header, identifier, body = parsed_line
78
+ @assigned_colors[identifier] ||= COLORS[@assigned_colors.size % COLORS.size]
79
+ lines << [
80
+ "\e[#{COLOR_CODES[@assigned_colors[identifier]]}m",
81
+ header,
82
+ "\e[0m",
83
+ body,
84
+ ].join("")
85
+ elsif not line.empty?
86
+ lines << line
87
+ end
88
+ end
89
+ lines.join("\n")
90
+ end
91
+
92
+ def parse_log(log)
93
+ return unless parsed = log.match(/^(.*\[(\w+)([\d\.]+)?\]:)(.*)?$/)
94
+ [1, 2, 4].map { |i| parsed[i] }
95
+ end
96
+
97
+ end
98
+
@@ -0,0 +1,61 @@
1
+ require "heroku/command/base"
2
+
3
+ # manage maintenance mode for an app
4
+ #
5
+ class Heroku::Command::Maintenance < Heroku::Command::Base
6
+
7
+ # maintenance
8
+ #
9
+ # display the current maintenance status of app
10
+ #
11
+ #Example:
12
+ #
13
+ # $ heroku maintenance
14
+ # off
15
+ #
16
+ def index
17
+ validate_arguments!
18
+
19
+ case api.get_app_maintenance(app).body['maintenance']
20
+ when true
21
+ display('on')
22
+ when false
23
+ display('off')
24
+ end
25
+ end
26
+
27
+ # maintenance:on
28
+ #
29
+ # put the app into maintenance mode
30
+ #
31
+ #Example:
32
+ #
33
+ # $ heroku maintenance:on
34
+ # Enabling maintenance mode for myapp
35
+ #
36
+ def on
37
+ validate_arguments!
38
+
39
+ action("Enabling maintenance mode for #{app}") do
40
+ api.post_app_maintenance(app, '1')
41
+ end
42
+ end
43
+
44
+ # maintenance:off
45
+ #
46
+ # take the app out of maintenance mode
47
+ #
48
+ #Example:
49
+ #
50
+ # $ heroku maintenance:off
51
+ # Disabling maintenance mode for myapp
52
+ #
53
+ def off
54
+ validate_arguments!
55
+
56
+ action("Disabling maintenance mode for #{app}") do
57
+ api.post_app_maintenance(app, '0')
58
+ end
59
+ end
60
+
61
+ end
@@ -0,0 +1,277 @@
1
+ require "heroku/client/heroku_postgresql"
2
+ require "heroku/command/base"
3
+ require "heroku/helpers/heroku_postgresql"
4
+
5
+ # manage heroku-postgresql databases
6
+ #
7
+ class Heroku::Command::Pg < Heroku::Command::Base
8
+
9
+ include Heroku::Helpers::HerokuPostgresql
10
+
11
+ # pg
12
+ #
13
+ # List databases for an app
14
+ #
15
+ def index
16
+ validate_arguments!
17
+
18
+ if hpg_databases_with_info.empty?
19
+ display("#{app} has no heroku-postgresql databases.")
20
+ else
21
+ hpg_databases_with_info.keys.sort.each do |name|
22
+ display_db name, hpg_databases_with_info[name]
23
+ end
24
+ end
25
+ end
26
+
27
+ # pg:info [DATABASE]
28
+ #
29
+ # Display database information
30
+ #
31
+ # If DATABASE is not specified, displays all databases
32
+ #
33
+ def info
34
+ db = shift_argument
35
+ validate_arguments!
36
+
37
+ if db
38
+ name, url = hpg_resolve(db)
39
+ display_db name, hpg_info(url)
40
+ else
41
+ index
42
+ end
43
+ end
44
+
45
+ # pg:promote DATABASE
46
+ #
47
+ # Sets DATABASE as your DATABASE_URL
48
+ #
49
+ def promote
50
+ unless db = shift_argument
51
+ error("Usage: heroku pg:promote DATABASE\nMust specify DATABASE to promote.")
52
+ end
53
+ validate_arguments!
54
+
55
+ name, url = hpg_resolve(db)
56
+ name ||= 'Custom URL'
57
+
58
+ action "Promoting #{name} to DATABASE_URL" do
59
+ hpg_promote(url)
60
+ end
61
+ end
62
+
63
+ # pg:psql [DATABASE]
64
+ #
65
+ # Open a psql shell to the database
66
+ #
67
+ # defaults to DATABASE_URL databases if no DATABASE is specified
68
+ #
69
+ def psql
70
+ name, url = hpg_resolve(shift_argument, "DATABASE_URL")
71
+ validate_arguments!
72
+
73
+ uri = URI.parse(url)
74
+ begin
75
+ ENV["PGPASSWORD"] = uri.password
76
+ ENV["PGSSLMODE"] = 'require'
77
+ exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}"
78
+ rescue Errno::ENOENT
79
+ output_with_bang "The local psql command could not be located"
80
+ output_with_bang "For help installing psql, see http://devcenter.heroku.com/articles/local-postgresql"
81
+ abort
82
+ end
83
+ end
84
+
85
+ # pg:reset DATABASE
86
+ #
87
+ # Delete all data in DATABASE
88
+ #
89
+ def reset
90
+ unless db = shift_argument
91
+ error("Usage: heroku pg:reset DATABASE\nMust specify DATABASE to reset.")
92
+ end
93
+ validate_arguments!
94
+
95
+ name, url = hpg_resolve(db)
96
+ return unless confirm_command
97
+
98
+ action("Resetting #{name}") do
99
+ if name =~ /^SHARED_DATABASE/i
100
+ heroku.database_reset(app)
101
+ else
102
+ hpg_client(url).reset
103
+ end
104
+ end
105
+ end
106
+
107
+ # pg:unfollow REPLICA
108
+ #
109
+ # stop a replica from following and make it a read/write database
110
+ #
111
+ def unfollow
112
+ unless db = shift_argument
113
+ error("Usage: heroku pg:unfollow REPLICA\nMust specify REPLICA to unfollow.")
114
+ end
115
+ validate_arguments!
116
+
117
+ replica_name, replica_url = hpg_resolve(db)
118
+ replica = hpg_info(replica_url)
119
+
120
+ unless replica[:following]
121
+ error("#{replica_name} is not following another database.")
122
+ end
123
+ origin_url = replica[:following]
124
+ origin_name = database_name_from_url(origin_url)
125
+
126
+ output_with_bang "#{replica_name} will become writable and no longer"
127
+ output_with_bang "follow #{origin_name}. This cannot be undone."
128
+ return unless confirm_command
129
+
130
+ action "Unfollowing #{db}" do
131
+ hpg_client(replica_url).unfollow
132
+ end
133
+ end
134
+
135
+ # pg:wait [DATABASE]
136
+ #
137
+ # monitor database creation, exit when complete
138
+ #
139
+ # defaults to all databases if no DATABASE is specified
140
+ #
141
+ def wait
142
+ db = shift_argument
143
+ validate_arguments!
144
+
145
+ if db
146
+ wait_for hpg_info(hpg_resolve(db).last)
147
+ else
148
+ hpg_databases_with_info.keys.sort.each do |name|
149
+ unless name =~ /^SHARED_DATABASE/i
150
+ wait_for(hpg_databases_with_info[name])
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ # pg:credentials DATABASE
157
+ #
158
+ # Display the DATABASE credentials.
159
+ #
160
+ # --reset # Reset credentials on the specified database.
161
+ #
162
+ def credentials
163
+ unless db = shift_argument
164
+ error("Usage: heroku pg:credentials DATABASE\nMust specify DATABASE to display credentials.")
165
+ end
166
+ validate_arguments!
167
+
168
+ name, url = hpg_resolve(db)
169
+
170
+ url_is_database_url = (url == app_config_vars["DATABASE_URL"])
171
+
172
+ if options[:reset]
173
+ action "Resetting credentials for #{name}" do
174
+ hpg_client(url).rotate_credentials
175
+ end
176
+ if url_is_database_url
177
+ forget_config!
178
+ name, new_url = hpg_resolve(db)
179
+ action "Promoting #{name}" do
180
+ hpg_promote(new_url)
181
+ end
182
+ end
183
+ else
184
+ uri = URI.parse(url)
185
+ display "Connection info string:"
186
+ display " \"dbname=#{uri.path[1..-1]} host=#{uri.host} port=#{uri.port || 5432} user=#{uri.user} password=#{uri.password} sslmode=require\""
187
+ end
188
+ end
189
+
190
+ private
191
+
192
+ def database_name_from_url(url)
193
+ vars = app_config_vars.reject {|key,value| key == 'DATABASE_URL'}
194
+ if var = vars.invert[url]
195
+ var.gsub(/_URL$/, '')
196
+ else
197
+ uri = URI.parse(url)
198
+ "Database on #{uri.host}:#{uri.port || 5432}#{uri.path}"
199
+ end
200
+ end
201
+
202
+ def display_db(name, db)
203
+ pretty_name = name
204
+ if !pretty_name.include?(' (DATABASE_URL)') && app_config_vars["#{name}_URL"] == app_config_vars["DATABASE_URL"]
205
+ pretty_name += " (DATABASE_URL)"
206
+ end
207
+
208
+ styled_header(pretty_name)
209
+ styled_hash(db[:info].inject({}) do |hash, item|
210
+ hash.update(item["name"] => hpg_info_display(item))
211
+ end, db[:info].map {|item| item['name']})
212
+
213
+ display
214
+ end
215
+
216
+ def hpg_client(url)
217
+ Heroku::Client::HerokuPostgresql.new(url)
218
+ end
219
+
220
+ def hpg_databases_with_info
221
+ @hpg_databases_with_info ||= hpg_databases.inject({}) do |hash, (name, url)|
222
+ if name =~ /^SHARED_DATABASE/i
223
+ data = api.get_app(app).body
224
+ hash.update(name => {
225
+ :info => [{
226
+ 'name' => 'Data Size',
227
+ 'values' => [format_bytes(data['database_size'])]
228
+ }],
229
+ :url => url
230
+ })
231
+ else
232
+ hash.update(name => hpg_info(url))
233
+ end
234
+ end
235
+ end
236
+
237
+ def hpg_info(url)
238
+ info = hpg_client(url).get_database
239
+ info[:url] = url
240
+ info
241
+ end
242
+
243
+ def hpg_info_display(item)
244
+ item["values"] = [item["value"]] if item["value"]
245
+ item["values"].map do |value|
246
+ if item["resolve_db_name"]
247
+ database_name_from_url(value)
248
+ else
249
+ value
250
+ end
251
+ end
252
+ end
253
+
254
+ def ticking
255
+ ticks = 0
256
+ loop do
257
+ yield(ticks)
258
+ ticks +=1
259
+ sleep 1
260
+ end
261
+ end
262
+
263
+ def wait_for(db)
264
+ ticking do |ticks|
265
+ status = hpg_client(db[:url]).get_wait_status
266
+ error status[:message] if status[:error?]
267
+ break if !status[:waiting?] && ticks.zero?
268
+ redisplay("Waiting for database %s... %s%s" % [
269
+ db[:pretty_name],
270
+ status[:waiting?] ? "#{spinner(ticks)} " : "",
271
+ status[:message]],
272
+ !status[:waiting?]) # only display a newline on the last tick
273
+ break unless status[:waiting?]
274
+ end
275
+ end
276
+
277
+ end
@@ -0,0 +1,289 @@
1
+ require "heroku/client/pgbackups"
2
+ require "heroku/command/base"
3
+ require "heroku/helpers/heroku_postgresql"
4
+
5
+ module Heroku::Command
6
+
7
+ # manage backups of heroku postgresql databases
8
+ class Pgbackups < Base
9
+
10
+ include Heroku::Helpers::HerokuPostgresql
11
+
12
+ # pgbackups
13
+ #
14
+ # list captured backups
15
+ #
16
+ def index
17
+ validate_arguments!
18
+
19
+ backups = []
20
+ pgbackup_client.get_transfers.each { |t|
21
+ next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at']
22
+ backups << {
23
+ 'id' => backup_name(t['to_url']),
24
+ 'created_at' => t['created_at'],
25
+ 'size' => t['size'],
26
+ 'database' => t['from_name']
27
+ }
28
+ }
29
+
30
+ if backups.empty?
31
+ no_backups_error!
32
+ else
33
+ display_table(
34
+ backups,
35
+ %w{ id created_at size database },
36
+ ["ID", "Backup Time", "Size", "Database"]
37
+ )
38
+ end
39
+ end
40
+
41
+ # pgbackups:url [BACKUP_ID]
42
+ #
43
+ # get a temporary URL for a backup
44
+ #
45
+ def url
46
+ name = shift_argument
47
+ validate_arguments!
48
+
49
+ if name
50
+ b = pgbackup_client.get_backup(name)
51
+ else
52
+ b = pgbackup_client.get_latest_backup
53
+ end
54
+ unless b['public_url']
55
+ error("No backup found.")
56
+ end
57
+ if $stdout.isatty
58
+ display '"'+b['public_url']+'"'
59
+ else
60
+ display b['public_url']
61
+ end
62
+ end
63
+
64
+ # pgbackups:capture [DATABASE]
65
+ #
66
+ # capture a backup from a database id
67
+ #
68
+ # if no DATABASE is specified, defaults to DATABASE_URL
69
+ #
70
+ # -e, --expire # if no slots are available to capture, destroy the oldest backup to make room
71
+ #
72
+ def capture
73
+ from_name, from_url = hpg_resolve(shift_argument, "DATABASE_URL")
74
+ validate_arguments!
75
+
76
+ to_url = nil # server will assign
77
+ to_name = "BACKUP"
78
+
79
+ opts = {:expire => options[:expire]}
80
+
81
+ backup = transfer!(from_url, from_name, to_url, to_name, opts)
82
+
83
+ to_uri = URI.parse backup["to_url"]
84
+ backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*')
85
+ display "\n#{from_name} ----backup---> #{backup_id}"
86
+
87
+ backup = poll_transfer!(backup)
88
+
89
+ if backup["error_at"]
90
+ message = "An error occurred and your backup did not finish."
91
+ message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/
92
+ message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/
93
+ error(message)
94
+ end
95
+ end
96
+
97
+ # pgbackups:restore [<DATABASE> [BACKUP_ID|BACKUP_URL]]
98
+ #
99
+ # restore a backup to a database
100
+ #
101
+ # if no DATABASE is specified, defaults to DATABASE_URL and latest backup
102
+ # if DATABASE is specified, but no BACKUP_ID, defaults to latest backup
103
+ #
104
+ def restore
105
+ if 0 == args.size
106
+ to_name, to_url = hpg_resolve(nil, "DATABASE_URL")
107
+ backup_id = :latest
108
+ elsif 1 == args.size
109
+ to_name, to_url = hpg_resolve(shift_argument)
110
+ backup_id = :latest
111
+ else
112
+ to_name, to_url = hpg_resolve(shift_argument)
113
+ backup_id = shift_argument
114
+ end
115
+
116
+ if :latest == backup_id
117
+ backup = pgbackup_client.get_latest_backup
118
+ no_backups_error! if {} == backup
119
+ to_uri = URI.parse backup["to_url"]
120
+ backup_id = File.basename(to_uri.path, '.*')
121
+ backup_id = "#{backup_id} (most recent)"
122
+ from_url = backup["to_url"]
123
+ from_name = "BACKUP"
124
+ elsif backup_id =~ /^http(s?):\/\//
125
+ from_url = backup_id
126
+ from_name = "EXTERNAL_BACKUP"
127
+ from_uri = URI.parse backup_id
128
+ backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path)
129
+ else
130
+ backup = pgbackup_client.get_backup(backup_id)
131
+ abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"]
132
+
133
+ from_url = backup["to_url"]
134
+ from_name = "BACKUP"
135
+ end
136
+
137
+ message = "#{to_name} <---restore--- "
138
+ padding = " " * message.length
139
+ display "\n#{message}#{backup_id}"
140
+ if backup
141
+ display padding + "#{backup['from_name']}"
142
+ display padding + "#{backup['created_at']}"
143
+ display padding + "#{backup['size']}"
144
+ end
145
+
146
+ if confirm_command
147
+ restore = transfer!(from_url, from_name, to_url, to_name)
148
+ restore = poll_transfer!(restore)
149
+
150
+ if restore["error_at"]
151
+ message = "An error occurred and your restore did not finish."
152
+ message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL." if restore['log'] =~ /Invalid dump format: .*: XML document text/
153
+ error(message)
154
+ end
155
+ end
156
+ end
157
+
158
+ # pgbackups:destroy BACKUP_ID
159
+ #
160
+ # destroys a backup
161
+ #
162
+ def destroy
163
+ unless name = shift_argument
164
+ error("Usage: heroku pgbackups:destroy BACKUP_ID\nMust specify BACKUP_ID to destroy.")
165
+ end
166
+ backup = pgbackup_client.get_backup(name)
167
+ if backup["destroyed_at"]
168
+ error("Backup #{name} already destroyed.")
169
+ end
170
+
171
+ action("Destroying #{name}") do
172
+ pgbackup_client.delete_backup(name)
173
+ end
174
+ end
175
+
176
+ protected
177
+
178
+ def config_vars
179
+ @config_vars ||= api.get_config_vars(app).body
180
+ end
181
+
182
+ def pgbackup_client
183
+ pgbackups_url = ENV["PGBACKUPS_URL"] || config_vars["PGBACKUPS_URL"]
184
+ error("Please add the pgbackups addon first via:\nheroku addons:add pgbackups") unless pgbackups_url
185
+ @pgbackup_client ||= Heroku::Client::Pgbackups.new(pgbackups_url)
186
+ end
187
+
188
+ def backup_name(to_url)
189
+ # translate s3://bucket/email/foo/bar.dump => foo/bar
190
+ parts = to_url.split('/')
191
+ parts.slice(4..-1).join('/').gsub(/\.dump$/, '')
192
+ end
193
+
194
+ def transfer!(from_url, from_name, to_url, to_name, opts={})
195
+ pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts)
196
+ end
197
+
198
+ def poll_transfer!(transfer)
199
+ display "\n"
200
+
201
+ if transfer["errors"]
202
+ transfer["errors"].values.flatten.each { |e|
203
+ output_with_bang "#{e}"
204
+ }
205
+ abort
206
+ end
207
+
208
+ while true
209
+ update_display(transfer)
210
+ break if transfer["finished_at"]
211
+
212
+ attempts = 0
213
+ begin
214
+ sleep 1
215
+ transfer = pgbackup_client.get_transfer(transfer["id"])
216
+ rescue RestClient::ServiceUnavailable
217
+ (attempts += 1) <= 5 ? retry : raise
218
+ end
219
+ end
220
+
221
+ display "\n"
222
+
223
+ return transfer
224
+ end
225
+
226
+ def update_display(transfer)
227
+ @ticks ||= 0
228
+ @last_updated_at ||= 0
229
+ @last_logs ||= []
230
+ @last_progress ||= ["", 0]
231
+
232
+ @ticks += 1
233
+
234
+ step_map = {
235
+ "dump" => "Capturing",
236
+ "upload" => "Storing",
237
+ "download" => "Retrieving",
238
+ "restore" => "Restoring",
239
+ "gunzip" => "Uncompressing",
240
+ "load" => "Restoring",
241
+ }
242
+
243
+ if !transfer["log"]
244
+ @last_progress = ['pending', nil]
245
+ redisplay "Pending... #{spinner(@ticks)}"
246
+ else
247
+ logs = transfer["log"].split("\n")
248
+ new_logs = logs - @last_logs
249
+ @last_logs = logs
250
+
251
+ new_logs.each do |line|
252
+ matches = line.scan /^([a-z_]+)_progress:\s+([^ ]+)/
253
+ next if matches.empty?
254
+
255
+ step, amount = matches[0]
256
+
257
+ if ['done', 'error'].include? amount
258
+ # step is done, explicitly print result and newline
259
+ redisplay "#{@last_progress[0].capitalize}... #{amount}\n"
260
+ end
261
+
262
+ # store progress, last one in the logs will get displayed
263
+ step = step_map[step] || step
264
+ @last_progress = [step, amount]
265
+ end
266
+
267
+ step, amount = @last_progress
268
+ unless ['done', 'error'].include? amount
269
+ redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}"
270
+ end
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ def no_backups_error!
277
+ error("No backups. Capture one with `heroku pgbackups:capture`.")
278
+ end
279
+
280
+ # lists all types of backups ('to_name' attribute)
281
+ #
282
+ # Useful when one doesn't care if a backup is of a particular
283
+ # kind, but wants to know what backups of any kind exist.
284
+ #
285
+ def backup_types
286
+ %w[BACKUP DAILY_SCHEDULED_BACKUP HOURLY_SCHEDULED_BACKUP AUTO_SCHEDULED_BACKUP]
287
+ end
288
+ end
289
+ end