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