pogo 2.31.2 → 2.32.14
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/pogo +5 -4
- data/lib/heroku.rb +6 -11
- data/lib/heroku/auth.rb +22 -5
- data/lib/heroku/client/heroku_postgresql.rb +52 -22
- data/lib/heroku/client/rendezvous.rb +5 -2
- data/lib/heroku/command.rb +42 -21
- data/lib/heroku/command/apps.rb +2 -2
- data/lib/heroku/command/labs.rb +65 -79
- data/lib/heroku/command/pg.rb +55 -66
- data/lib/heroku/command/pgbackups.rb +29 -9
- data/lib/heroku/command/plugins.rb +1 -1
- data/lib/heroku/command/run.rb +3 -1
- data/lib/heroku/command/status.rb +1 -1
- data/lib/heroku/excon.rb +9 -0
- data/lib/heroku/helpers.rb +4 -0
- data/lib/heroku/helpers/heroku_postgresql.rb +110 -56
- data/lib/heroku/updater.rb +70 -66
- data/lib/heroku/version.rb +1 -1
- data/spec/heroku/client/heroku_postgresql_spec.rb +55 -18
- data/spec/heroku/command/addons_spec.rb +18 -2
- data/spec/heroku/command/labs_spec.rb +10 -9
- data/spec/heroku/command/pg_spec.rb +56 -43
- data/spec/heroku/command/pgbackups_spec.rb +28 -6
- data/spec/heroku/command_spec.rb +2 -2
- data/spec/heroku/helpers/heroku_postgresql_spec.rb +65 -47
- metadata +6 -5
data/lib/heroku/command/pg.rb
CHANGED
@@ -26,6 +26,8 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
26
26
|
|
27
27
|
# pg:info [DATABASE]
|
28
28
|
#
|
29
|
+
# -x, --extended # Show extended information
|
30
|
+
#
|
29
31
|
# Display database information
|
30
32
|
#
|
31
33
|
# If DATABASE is not specified, displays all databases
|
@@ -35,8 +37,8 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
35
37
|
validate_arguments!
|
36
38
|
|
37
39
|
if db
|
38
|
-
|
39
|
-
display_db
|
40
|
+
attachment = hpg_resolve(db)
|
41
|
+
display_db attachment.display_name, hpg_info(attachment, options[:extended])
|
40
42
|
else
|
41
43
|
index
|
42
44
|
end
|
@@ -52,11 +54,10 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
52
54
|
end
|
53
55
|
validate_arguments!
|
54
56
|
|
55
|
-
|
56
|
-
name ||= 'Custom URL'
|
57
|
+
attachment = hpg_resolve(db)
|
57
58
|
|
58
|
-
action "Promoting #{
|
59
|
-
hpg_promote(url)
|
59
|
+
action "Promoting #{attachment.display_name} to DATABASE_URL" do
|
60
|
+
hpg_promote(attachment.url)
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
@@ -67,10 +68,10 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
67
68
|
# defaults to DATABASE_URL databases if no DATABASE is specified
|
68
69
|
#
|
69
70
|
def psql
|
70
|
-
|
71
|
+
attachment = hpg_resolve(shift_argument, "DATABASE_URL")
|
71
72
|
validate_arguments!
|
72
73
|
|
73
|
-
uri = URI.parse(url)
|
74
|
+
uri = URI.parse( attachment.url )
|
74
75
|
begin
|
75
76
|
ENV["PGPASSWORD"] = uri.password
|
76
77
|
ENV["PGSSLMODE"] = 'require'
|
@@ -92,14 +93,14 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
92
93
|
end
|
93
94
|
validate_arguments!
|
94
95
|
|
95
|
-
|
96
|
+
attachment = hpg_resolve(db) unless db == "SHARED_DATABASE"
|
96
97
|
return unless confirm_command
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
hpg_client(
|
99
|
+
if db == "SHARED_DATABASE"
|
100
|
+
action("Resetting SHARED_DATABASE") { heroku.database_reset(app) }
|
101
|
+
else
|
102
|
+
action("Resetting #{attachment.display_name}") do
|
103
|
+
hpg_client(attachment).reset
|
103
104
|
end
|
104
105
|
end
|
105
106
|
end
|
@@ -114,21 +115,21 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
114
115
|
end
|
115
116
|
validate_arguments!
|
116
117
|
|
117
|
-
|
118
|
-
|
118
|
+
replica = hpg_resolve(db)
|
119
|
+
replica_info = hpg_info(replica)
|
119
120
|
|
120
|
-
unless
|
121
|
-
error("#{
|
121
|
+
unless replica_info[:following]
|
122
|
+
error("#{replica.display_name} is not following another database.")
|
122
123
|
end
|
123
|
-
origin_url =
|
124
|
+
origin_url = replica_info[:following]
|
124
125
|
origin_name = database_name_from_url(origin_url)
|
125
126
|
|
126
|
-
output_with_bang "#{
|
127
|
+
output_with_bang "#{replica.display_name} will become writable and no longer"
|
127
128
|
output_with_bang "follow #{origin_name}. This cannot be undone."
|
128
129
|
return unless confirm_command
|
129
130
|
|
130
|
-
action "Unfollowing #{
|
131
|
-
hpg_client(
|
131
|
+
action "Unfollowing #{replica.display_name}" do
|
132
|
+
hpg_client(replica).unfollow
|
132
133
|
end
|
133
134
|
end
|
134
135
|
|
@@ -143,12 +144,10 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
143
144
|
validate_arguments!
|
144
145
|
|
145
146
|
if db
|
146
|
-
wait_for
|
147
|
+
wait_for hpg_resolve(db)
|
147
148
|
else
|
148
|
-
|
149
|
-
|
150
|
-
wait_for(hpg_databases_with_info[name])
|
151
|
-
end
|
149
|
+
hpg_databases.values.each do |attach|
|
150
|
+
wait_for(attach)
|
152
151
|
end
|
153
152
|
end
|
154
153
|
end
|
@@ -165,23 +164,21 @@ class Heroku::Command::Pg < Heroku::Command::Base
|
|
165
164
|
end
|
166
165
|
validate_arguments!
|
167
166
|
|
168
|
-
|
169
|
-
|
170
|
-
url_is_database_url = (url == app_config_vars["DATABASE_URL"])
|
167
|
+
attachment = hpg_resolve(db)
|
171
168
|
|
172
169
|
if options[:reset]
|
173
|
-
action "Resetting credentials for #{
|
174
|
-
hpg_client(
|
170
|
+
action "Resetting credentials for #{attachment.display_name}" do
|
171
|
+
hpg_client(attachment).rotate_credentials
|
175
172
|
end
|
176
|
-
if
|
173
|
+
if attachment.primary_attachment?
|
177
174
|
forget_config!
|
178
|
-
|
179
|
-
action "Promoting #{
|
180
|
-
hpg_promote(
|
175
|
+
attachment = hpg_resolve(db)
|
176
|
+
action "Promoting #{attachment.display_name}" do
|
177
|
+
hpg_promote(attachment.url)
|
181
178
|
end
|
182
179
|
end
|
183
180
|
else
|
184
|
-
uri = URI.parse(url)
|
181
|
+
uri = URI.parse( attachment.url )
|
185
182
|
display "Connection info string:"
|
186
183
|
display " \"dbname=#{uri.path[1..-1]} host=#{uri.host} port=#{uri.port || 5432} user=#{uri.user} password=#{uri.password} sslmode=require\""
|
187
184
|
end
|
@@ -200,12 +197,7 @@ private
|
|
200
197
|
end
|
201
198
|
|
202
199
|
def display_db(name, db)
|
203
|
-
|
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)
|
200
|
+
styled_header(name)
|
209
201
|
styled_hash(db[:info].inject({}) do |hash, item|
|
210
202
|
hash.update(item["name"] => hpg_info_display(item))
|
211
203
|
end, db[:info].map {|item| item['name']})
|
@@ -213,31 +205,28 @@ private
|
|
213
205
|
display
|
214
206
|
end
|
215
207
|
|
216
|
-
def hpg_client(
|
217
|
-
Heroku::Client::HerokuPostgresql.new(
|
208
|
+
def hpg_client(attachment)
|
209
|
+
Heroku::Client::HerokuPostgresql.new(attachment)
|
218
210
|
end
|
219
211
|
|
220
212
|
def hpg_databases_with_info
|
221
|
-
@hpg_databases_with_info
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
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
|
213
|
+
return @hpg_databases_with_info if @hpg_databases_with_info
|
214
|
+
|
215
|
+
@hpg_databases_with_info = Hash[ hpg_databases.map { |config, att| [att.display_name, hpg_info(att, options[:extended])] } ]
|
216
|
+
|
217
|
+
return @hpg_databases_with_info
|
235
218
|
end
|
236
219
|
|
237
|
-
def hpg_info(
|
238
|
-
|
239
|
-
|
240
|
-
|
220
|
+
def hpg_info(attachment, extended=false)
|
221
|
+
if attachment.resource_name == "SHARED_DATABASE"
|
222
|
+
data = api.get_app(app).body
|
223
|
+
{:info => [{
|
224
|
+
'name' => 'Data Size',
|
225
|
+
'values' => [format_bytes(data['database_size'])]
|
226
|
+
}]}
|
227
|
+
else
|
228
|
+
hpg_client(attachment).get_database(extended)
|
229
|
+
end
|
241
230
|
end
|
242
231
|
|
243
232
|
def hpg_info_display(item)
|
@@ -260,13 +249,13 @@ private
|
|
260
249
|
end
|
261
250
|
end
|
262
251
|
|
263
|
-
def wait_for(
|
252
|
+
def wait_for(attach)
|
264
253
|
ticking do |ticks|
|
265
|
-
status = hpg_client(
|
254
|
+
status = hpg_client(attach).get_wait_status
|
266
255
|
error status[:message] if status[:error?]
|
267
256
|
break if !status[:waiting?] && ticks.zero?
|
268
257
|
redisplay("Waiting for database %s... %s%s" % [
|
269
|
-
|
258
|
+
attach.display_name,
|
270
259
|
status[:waiting?] ? "#{spinner(ticks)} " : "",
|
271
260
|
status[:message]],
|
272
261
|
!status[:waiting?]) # only display a newline on the last tick
|
@@ -67,12 +67,14 @@ module Heroku::Command
|
|
67
67
|
#
|
68
68
|
# if no DATABASE is specified, defaults to DATABASE_URL
|
69
69
|
#
|
70
|
-
# -e, --expire # if no slots are available
|
70
|
+
# -e, --expire # if no slots are available, destroy the oldest manual backup to make room
|
71
71
|
#
|
72
72
|
def capture
|
73
|
-
|
73
|
+
attachment = hpg_resolve(shift_argument, "DATABASE_URL")
|
74
74
|
validate_arguments!
|
75
75
|
|
76
|
+
from_name = attachment.display_name
|
77
|
+
from_url = attachment.url
|
76
78
|
to_url = nil # server will assign
|
77
79
|
to_name = "BACKUP"
|
78
80
|
|
@@ -103,13 +105,19 @@ module Heroku::Command
|
|
103
105
|
#
|
104
106
|
def restore
|
105
107
|
if 0 == args.size
|
106
|
-
|
108
|
+
attachment = hpg_resolve(nil, "DATABASE_URL")
|
109
|
+
to_name = attachment.display_name
|
110
|
+
to_url = attachment.url
|
107
111
|
backup_id = :latest
|
108
112
|
elsif 1 == args.size
|
109
|
-
|
113
|
+
attachment = hpg_resolve(shift_argument)
|
114
|
+
to_name = attachment.display_name
|
115
|
+
to_url = attachment.url
|
110
116
|
backup_id = :latest
|
111
117
|
else
|
112
|
-
|
118
|
+
attachment = hpg_resolve(shift_argument)
|
119
|
+
to_name = attachment.display_name
|
120
|
+
to_url = attachment.url
|
113
121
|
backup_id = shift_argument
|
114
122
|
end
|
115
123
|
|
@@ -195,6 +203,13 @@ module Heroku::Command
|
|
195
203
|
pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts)
|
196
204
|
end
|
197
205
|
|
206
|
+
def poll_error(app)
|
207
|
+
error <<-EOM
|
208
|
+
Failed to query the PGBackups status API. Your backup may still be running.
|
209
|
+
Verify the status of your backup with `heroku pgbackups -a #{app}`
|
210
|
+
EOM
|
211
|
+
end
|
212
|
+
|
198
213
|
def poll_transfer!(transfer)
|
199
214
|
display "\n"
|
200
215
|
|
@@ -209,12 +224,17 @@ module Heroku::Command
|
|
209
224
|
update_display(transfer)
|
210
225
|
break if transfer["finished_at"]
|
211
226
|
|
212
|
-
|
227
|
+
sleep_time = 1
|
213
228
|
begin
|
214
|
-
sleep
|
229
|
+
sleep(sleep_time)
|
215
230
|
transfer = pgbackup_client.get_transfer(transfer["id"])
|
216
|
-
rescue RestClient::ServiceUnavailable
|
217
|
-
|
231
|
+
rescue RestClient::ServiceUnavailable, RestClient::ServerBrokeConnection
|
232
|
+
if sleep_time > 300
|
233
|
+
poll_error(app)
|
234
|
+
else
|
235
|
+
sleep_time *= 2
|
236
|
+
retry
|
237
|
+
end
|
218
238
|
end
|
219
239
|
end
|
220
240
|
|
data/lib/heroku/command/run.rb
CHANGED
@@ -125,7 +125,9 @@ protected
|
|
125
125
|
rendezvous.start
|
126
126
|
rescue Timeout::Error
|
127
127
|
error "\nTimeout awaiting process"
|
128
|
-
rescue
|
128
|
+
rescue OpenSSL::SSL::SSLError
|
129
|
+
error "Authentication error"
|
130
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
|
129
131
|
error "\nError connecting to process"
|
130
132
|
rescue Interrupt
|
131
133
|
ensure
|
@@ -38,7 +38,7 @@ class Heroku::Command::Status < Heroku::Command::Base
|
|
38
38
|
styled_header("#{issue['title']} #{duration}")
|
39
39
|
changes = issue['updates'].map do |issue|
|
40
40
|
[
|
41
|
-
time_ago(
|
41
|
+
time_ago(issue['created_at']),
|
42
42
|
issue['update_type'],
|
43
43
|
issue['contents']
|
44
44
|
]
|
data/lib/heroku/excon.rb
ADDED
data/lib/heroku/helpers.rb
CHANGED
@@ -5,27 +5,78 @@ module Heroku::Helpers::HerokuPostgresql
|
|
5
5
|
extend self
|
6
6
|
extend Heroku::Helpers
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
class Attachment
|
9
|
+
attr_reader :config_var, :resource_name, :url, :addon, :plan
|
10
|
+
def initialize(raw)
|
11
|
+
@raw = raw
|
12
|
+
@config_var = raw['config_var']
|
13
|
+
@resource_name = raw['resource']['name']
|
14
|
+
@url = raw['resource']['value']
|
15
|
+
@addon, @plan = raw['resource']['type'].split(':')
|
16
|
+
end
|
17
|
+
|
18
|
+
def starter_plan?
|
19
|
+
plan =~ /dev|basic/
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_name
|
23
|
+
config_var + (primary_attachment? ? " (DATABASE_URL)" : '')
|
24
|
+
end
|
25
|
+
|
26
|
+
def primary_attachment!
|
27
|
+
@primary_attachment = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_attachment?
|
31
|
+
@primary_attachment
|
32
|
+
end
|
10
33
|
end
|
11
34
|
|
12
35
|
def hpg_addon_name
|
13
|
-
ENV['
|
36
|
+
if ENV['SHOGUN']
|
37
|
+
"shogun-#{ENV['SHOGUN']}"
|
38
|
+
else
|
39
|
+
ENV['HEROKU_POSTGRESQL_ADDON_NAME'] || 'heroku-postgresql'
|
40
|
+
end
|
14
41
|
end
|
15
42
|
|
16
|
-
def
|
17
|
-
|
43
|
+
def app_config_vars
|
44
|
+
@app_config_vars ||= api.get_config_vars(app).body
|
45
|
+
end
|
46
|
+
|
47
|
+
def app_attachments
|
48
|
+
@app_attachments ||= api.get_attachments(app).body.map { |raw| Attachment.new(raw) }
|
18
49
|
end
|
19
50
|
|
20
51
|
def hpg_databases
|
21
|
-
@hpg_databases
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
52
|
+
return @hpg_databases if @hpg_databases
|
53
|
+
pairs = app_attachments.select {|att|
|
54
|
+
att.addon == hpg_addon_name
|
55
|
+
}.map { |att|
|
56
|
+
[att.config_var, att]
|
57
|
+
}
|
58
|
+
@hpg_databases = Hash[ pairs ]
|
59
|
+
|
60
|
+
if find_database_url_real_attachment
|
61
|
+
@hpg_databases['DATABASE_URL'] = find_database_url_real_attachment
|
28
62
|
end
|
63
|
+
|
64
|
+
if app_config_vars['SHARED_DATABASE_URL']
|
65
|
+
@hpg_databases['SHARED_DATABASE'] = Attachment.new({
|
66
|
+
'config_var' => 'SHARED_DATABASE',
|
67
|
+
'resource' => {
|
68
|
+
'name' => 'SHARED_DATABASE',
|
69
|
+
'value' => app_config_vars['SHARED_DATABASE_URL'],
|
70
|
+
'type' => 'shared:database'
|
71
|
+
}
|
72
|
+
})
|
73
|
+
end
|
74
|
+
|
75
|
+
return @hpg_databases
|
76
|
+
end
|
77
|
+
|
78
|
+
def resource_url(resource)
|
79
|
+
api.get_resource(resource).body['value']
|
29
80
|
end
|
30
81
|
|
31
82
|
def forget_config!
|
@@ -33,32 +84,47 @@ module Heroku::Helpers::HerokuPostgresql
|
|
33
84
|
@app_config_vars = nil
|
34
85
|
end
|
35
86
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
87
|
+
def find_database_url_real_attachment
|
88
|
+
primary_db_url = app_config_vars['DATABASE_URL']
|
89
|
+
return unless primary_db_url
|
90
|
+
|
91
|
+
real_config = app_config_vars.detect {|k,v| k != 'DATABASE_URL' && v == primary_db_url }
|
92
|
+
if real_config
|
93
|
+
real = hpg_databases[real_config.first]
|
94
|
+
real.primary_attachment! if real
|
95
|
+
return real
|
40
96
|
else
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
if hpg_databases.empty?
|
45
|
-
error("Your app has no databases.")
|
46
|
-
end
|
97
|
+
return nil
|
98
|
+
end
|
99
|
+
end
|
47
100
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
101
|
+
def match_attachments_by_name(name)
|
102
|
+
return [] if name.empty?
|
103
|
+
return [name] if hpg_databases[name]
|
104
|
+
hpg_databases.keys.grep(%r{#{ name }}i)
|
105
|
+
end
|
106
|
+
|
107
|
+
def hpg_resolve(name, default=nil)
|
108
|
+
name = '' if name.nil?
|
109
|
+
name = 'DATABASE_URL' if name == 'DATABASE'
|
110
|
+
|
111
|
+
if hpg_databases.empty?
|
112
|
+
error("Your app has no databases.")
|
61
113
|
end
|
114
|
+
|
115
|
+
found_attachment = nil
|
116
|
+
canidates = match_attachments_by_name(name)
|
117
|
+
if default && name.empty? && app_config_vars[default]
|
118
|
+
found_attachment = hpg_databases[default]
|
119
|
+
elsif canidates.size == 1
|
120
|
+
found_attachment = hpg_databases[canidates.first]
|
121
|
+
end
|
122
|
+
|
123
|
+
if found_attachment.nil?
|
124
|
+
error("Unknown database#{': ' + name unless name.empty?}. Valid options are: #{hpg_databases.keys.sort.join(", ")}")
|
125
|
+
end
|
126
|
+
|
127
|
+
return found_attachment
|
62
128
|
end
|
63
129
|
|
64
130
|
def hpg_translate_fork_and_follow(addon, config)
|
@@ -68,35 +134,23 @@ module Heroku::Helpers::HerokuPostgresql
|
|
68
134
|
unless val.is_a?(String)
|
69
135
|
error("--#{opt} requires a database argument")
|
70
136
|
end
|
71
|
-
name, url = hpg_resolve(val)
|
72
|
-
config[opt] = url
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
137
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
if ['DATABASE', 'DATABASE_URL'].include?(name)
|
82
|
-
if key = app_config_vars.keys.detect do |key|
|
83
|
-
if key == 'DATABASE_URL'
|
84
|
-
next
|
138
|
+
uri = URI.parse(val) rescue nil
|
139
|
+
if uri && uri.scheme
|
140
|
+
argument_url = uri.to_s
|
85
141
|
else
|
86
|
-
|
142
|
+
attachment = hpg_resolve(val)
|
143
|
+
argument_url = attachment.url
|
87
144
|
end
|
145
|
+
|
146
|
+
config[opt] = argument_url
|
88
147
|
end
|
89
|
-
"#{key.gsub(/_URL$/, '')} (DATABASE_URL)"
|
90
|
-
else
|
91
|
-
name.gsub(/_URL$/, '')
|
92
148
|
end
|
93
|
-
elsif hpg_databases[name] == hpg_databases['DATABASE']
|
94
|
-
"#{name} (DATABASE_URL)"
|
95
|
-
else
|
96
|
-
name
|
97
149
|
end
|
98
150
|
end
|
99
151
|
|
152
|
+
private
|
153
|
+
|
100
154
|
def hpg_promote(url)
|
101
155
|
api.put_config_vars(app, "DATABASE_URL" => url)
|
102
156
|
end
|