pogo 2.31.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +73 -0
- data/bin/pogo +22 -0
- data/data/cacert.pem +3988 -0
- data/lib/heroku.rb +22 -0
- data/lib/heroku/auth.rb +320 -0
- data/lib/heroku/cli.rb +38 -0
- data/lib/heroku/client.rb +764 -0
- data/lib/heroku/client/heroku_postgresql.rb +111 -0
- data/lib/heroku/client/pgbackups.rb +113 -0
- data/lib/heroku/client/rendezvous.rb +105 -0
- data/lib/heroku/client/ssl_endpoint.rb +25 -0
- data/lib/heroku/command.rb +273 -0
- data/lib/heroku/command/account.rb +23 -0
- data/lib/heroku/command/accounts.rb +34 -0
- data/lib/heroku/command/addons.rb +305 -0
- data/lib/heroku/command/apps.rb +311 -0
- data/lib/heroku/command/auth.rb +86 -0
- data/lib/heroku/command/base.rb +230 -0
- data/lib/heroku/command/certs.rb +148 -0
- data/lib/heroku/command/config.rb +137 -0
- data/lib/heroku/command/db.rb +218 -0
- data/lib/heroku/command/domains.rb +85 -0
- data/lib/heroku/command/drains.rb +46 -0
- data/lib/heroku/command/git.rb +65 -0
- data/lib/heroku/command/help.rb +163 -0
- data/lib/heroku/command/keys.rb +115 -0
- data/lib/heroku/command/labs.rb +161 -0
- data/lib/heroku/command/logs.rb +98 -0
- data/lib/heroku/command/maintenance.rb +61 -0
- data/lib/heroku/command/pg.rb +277 -0
- data/lib/heroku/command/pgbackups.rb +289 -0
- data/lib/heroku/command/plugins.rb +110 -0
- data/lib/heroku/command/ps.rb +232 -0
- data/lib/heroku/command/releases.rb +124 -0
- data/lib/heroku/command/run.rb +179 -0
- data/lib/heroku/command/sharing.rb +89 -0
- data/lib/heroku/command/ssl.rb +61 -0
- data/lib/heroku/command/stack.rb +62 -0
- data/lib/heroku/command/status.rb +51 -0
- data/lib/heroku/command/update.rb +47 -0
- data/lib/heroku/command/version.rb +23 -0
- data/lib/heroku/deprecated.rb +5 -0
- data/lib/heroku/deprecated/help.rb +38 -0
- data/lib/heroku/distribution.rb +9 -0
- data/lib/heroku/helpers.rb +517 -0
- data/lib/heroku/helpers/heroku_postgresql.rb +104 -0
- data/lib/heroku/plugin.rb +161 -0
- data/lib/heroku/updater.rb +158 -0
- data/lib/heroku/version.rb +3 -0
- data/lib/vendor/heroku/okjson.rb +598 -0
- data/spec/helper/legacy_help.rb +16 -0
- data/spec/heroku/auth_spec.rb +246 -0
- data/spec/heroku/client/heroku_postgresql_spec.rb +34 -0
- data/spec/heroku/client/pgbackups_spec.rb +43 -0
- data/spec/heroku/client/rendezvous_spec.rb +62 -0
- data/spec/heroku/client/ssl_endpoint_spec.rb +48 -0
- data/spec/heroku/client_spec.rb +564 -0
- data/spec/heroku/command/addons_spec.rb +585 -0
- data/spec/heroku/command/apps_spec.rb +351 -0
- data/spec/heroku/command/auth_spec.rb +38 -0
- data/spec/heroku/command/base_spec.rb +109 -0
- data/spec/heroku/command/certs_spec.rb +178 -0
- data/spec/heroku/command/config_spec.rb +144 -0
- data/spec/heroku/command/db_spec.rb +110 -0
- data/spec/heroku/command/domains_spec.rb +87 -0
- data/spec/heroku/command/drains_spec.rb +34 -0
- data/spec/heroku/command/git_spec.rb +116 -0
- data/spec/heroku/command/help_spec.rb +93 -0
- data/spec/heroku/command/keys_spec.rb +120 -0
- data/spec/heroku/command/labs_spec.rb +99 -0
- data/spec/heroku/command/logs_spec.rb +60 -0
- data/spec/heroku/command/maintenance_spec.rb +51 -0
- data/spec/heroku/command/pg_spec.rb +223 -0
- data/spec/heroku/command/pgbackups_spec.rb +280 -0
- data/spec/heroku/command/plugins_spec.rb +104 -0
- data/spec/heroku/command/ps_spec.rb +195 -0
- data/spec/heroku/command/releases_spec.rb +130 -0
- data/spec/heroku/command/run_spec.rb +86 -0
- data/spec/heroku/command/sharing_spec.rb +59 -0
- data/spec/heroku/command/ssl_spec.rb +32 -0
- data/spec/heroku/command/stack_spec.rb +46 -0
- data/spec/heroku/command/status_spec.rb +48 -0
- data/spec/heroku/command/version_spec.rb +16 -0
- data/spec/heroku/command_spec.rb +211 -0
- data/spec/heroku/helpers/heroku_postgresql_spec.rb +109 -0
- data/spec/heroku/helpers_spec.rb +48 -0
- data/spec/heroku/plugin_spec.rb +172 -0
- data/spec/heroku/updater_spec.rb +44 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +209 -0
- data/spec/support/display_message_matcher.rb +49 -0
- data/spec/support/openssl_mock_helper.rb +8 -0
- 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
|