pogo 2.31.2

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