heroku-rds 0.4.0
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/.gemspec +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +73 -0
- data/init.rb +1 -0
- data/lib/heroku/commands/rds.rb +283 -0
- data/lib/heroku/rds.rb +8 -0
- metadata +83 -0
data/.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'rubygems' unless Object.const_defined?(:Gem)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "heroku-rds"
|
6
|
+
s.version = '0.4.0'
|
7
|
+
s.authors = ["Jonathan Dance"]
|
8
|
+
s.email = "rubygems@wuputah.com"
|
9
|
+
s.homepage = "http://github.com/wegowise/heroku-rds"
|
10
|
+
s.summary = "Heroku plugin to aid working with RDS databases"
|
11
|
+
s.description = "Heroku plugin to aid working with RDS databases"
|
12
|
+
s.required_rubygems_version = ">= 1.3.6"
|
13
|
+
s.add_dependency 'heroku', '~> 2.0'
|
14
|
+
s.add_dependency 'fog', '>= 0.7.0'
|
15
|
+
s.files = Dir.glob('lib/**/*.rb') + %w[README.md .gemspec init.rb]
|
16
|
+
s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
|
17
|
+
s.license = 'MIT'
|
18
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT LICENSE
|
2
|
+
|
3
|
+
Copyright (c) 2011 Jonathan Dance
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
## Installation
|
2
|
+
|
3
|
+
Forewarning: Heroku RDS is not designed to work on Windows.
|
4
|
+
|
5
|
+
### Gem-based install (recommended)
|
6
|
+
|
7
|
+
Gem-based plugins are new to the Heroku ecosystem, and require first
|
8
|
+
installing the [Herogems](https://github.com/hone/herogems) plugin:
|
9
|
+
|
10
|
+
heroku plugins:install http://github.com/hone/herogems.git
|
11
|
+
|
12
|
+
Now, simply install the gem and enable the plugin:
|
13
|
+
|
14
|
+
gem install heroku-rds
|
15
|
+
heroku herogems:enable heroku-rds
|
16
|
+
|
17
|
+
You can update to new releases of heroku-rds by running `gem update
|
18
|
+
heroku-rds`.
|
19
|
+
|
20
|
+
### Traditional install
|
21
|
+
|
22
|
+
heroku plugins:install http://github.com/wegowise/heroku-rds.git
|
23
|
+
gem install fog
|
24
|
+
|
25
|
+
To update, you must re-install the plugin using `heroku
|
26
|
+
plugins:install`.
|
27
|
+
|
28
|
+
### Optional Packages
|
29
|
+
|
30
|
+
Commands involving data transfer support a progress bar using `pv`.
|
31
|
+
Install `pv` to see the awesome. Most package managers have a pv
|
32
|
+
package:
|
33
|
+
|
34
|
+
brew install pv # OS X
|
35
|
+
apt-get install pv # linux/fink
|
36
|
+
port install pv # BSD/macports
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
Access the command list at any time by typing `heroku help rds`:
|
41
|
+
|
42
|
+
Usage: heroku rds
|
43
|
+
|
44
|
+
Opens a MySQL console connected to the current database. Ingress access
|
45
|
+
is required to run this command (use rds:ingress to grant access).
|
46
|
+
|
47
|
+
Additional commands, type "heroku help COMMAND" for more details:
|
48
|
+
|
49
|
+
rds:access # displays current ingress access settings
|
50
|
+
rds:dump [FILE] # Download a database dump, bzipped and saved locally
|
51
|
+
rds:ingress [IP] [SECURITY GROUP] # Authorize ingress access to a particular IP
|
52
|
+
rds:pull [RAILS_ENV or DATABASE_URL] # downloads the remote database into a local database
|
53
|
+
rds:revoke [IP] [SECURITY GROUP] # Revokes previously-granted ingress access from a particular IP
|
54
|
+
|
55
|
+
## Planned features
|
56
|
+
|
57
|
+
### Short term
|
58
|
+
|
59
|
+
* rds:import - load a local database dump into the remote database
|
60
|
+
* rds:push - export a local database into the remote database
|
61
|
+
|
62
|
+
### Lower priority
|
63
|
+
|
64
|
+
* rds:snapshot - capture a snapshot
|
65
|
+
* rds:restore - restore from a snapshot
|
66
|
+
* rds:reboot - reboot instance
|
67
|
+
* rds:describe - show all RDS instances you have access to
|
68
|
+
* rds:scale - change instance size
|
69
|
+
|
70
|
+
These commands are not ingress related so the target of the command
|
71
|
+
cannot be inferred from DATABASE\_URL. This functionality is also
|
72
|
+
readily available from the RDS dashboard, so implementing them is not a
|
73
|
+
priority at this time.
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'heroku/rds'
|
@@ -0,0 +1,283 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Heroku::Command
|
5
|
+
|
6
|
+
# manage Amazon RDS instances
|
7
|
+
#
|
8
|
+
class Rds < BaseWithApp
|
9
|
+
|
10
|
+
# rds
|
11
|
+
#
|
12
|
+
# Opens a MySQL console connected to the current database. Ingress access
|
13
|
+
# is required to run this command (use rds:ingress to grant access).
|
14
|
+
#
|
15
|
+
def index
|
16
|
+
check_dependencies('mysql')
|
17
|
+
exec *(['mysql', '--compress'] + mysql_args(database_uri))
|
18
|
+
end
|
19
|
+
|
20
|
+
# rds:dump [FILE]
|
21
|
+
#
|
22
|
+
# Download a database dump, bzipped and saved locally
|
23
|
+
#
|
24
|
+
# -f, --force # allow overwriting existing file
|
25
|
+
#
|
26
|
+
# if no FILE is specified, appname-date.sql.bz2 is used by default.
|
27
|
+
# if name of FILE does not end in .sql.bz2, it will be added automatically.
|
28
|
+
#
|
29
|
+
def dump
|
30
|
+
check_dependencies('mysqldump', 'bzip2', '/bin/sh')
|
31
|
+
options = {}
|
32
|
+
while arg = args.shift
|
33
|
+
case arg
|
34
|
+
when '-f', '--force'
|
35
|
+
options[:force] = true
|
36
|
+
when /^[^-]/
|
37
|
+
raise CommandFailed, "too many arguments passed" if options[:filename]
|
38
|
+
options[:filename] = arg
|
39
|
+
else
|
40
|
+
raise CommandFailed, "unsupported option: #{arg}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
options[:filename] ||= "#{app}-#{Time.now.strftime('%Y-%m-%d')}.sql.bz2"
|
45
|
+
options[:filename] += '.sql.bz2' unless options[:filename] =~ /\.sql(\.bz2)?$/
|
46
|
+
options[:filename] += '.bz2' unless options[:filename] =~ /\.bz2$/
|
47
|
+
|
48
|
+
if File.exists?(options[:filename]) && !options[:force]
|
49
|
+
raise CommandFailed, "file already exists. use --force to override."
|
50
|
+
end
|
51
|
+
|
52
|
+
exec('/bin/sh', '-c',
|
53
|
+
"mysqldump --compress --single-transaction #{args_to_s(mysql_args(database_uri))}" +
|
54
|
+
pv_pipe +
|
55
|
+
%{| bzip2 > '#{options[:filename]}'})
|
56
|
+
end
|
57
|
+
|
58
|
+
# rds:ingress [IP] [SECURITY GROUP]
|
59
|
+
#
|
60
|
+
# Authorize ingress access to a particular IP
|
61
|
+
#
|
62
|
+
# * if IP is not specified, your current IP will be used
|
63
|
+
# * if SECURITY GROUP is not specified, 'default' will be used
|
64
|
+
# * IP can also be a CIDR range
|
65
|
+
#
|
66
|
+
def ingress
|
67
|
+
ip, security_group = parse_security_group_and_ip_from_args
|
68
|
+
rds.authorize_db_security_group_ingress(security_group, 'CIDRIP' => ip)
|
69
|
+
self.access
|
70
|
+
end
|
71
|
+
|
72
|
+
# rds:revoke [IP] [SECURITY GROUP]
|
73
|
+
#
|
74
|
+
# Revokes previously-granted ingress access from a particular IP
|
75
|
+
#
|
76
|
+
# * if IP is not specified, your current IP will be used
|
77
|
+
# * if SECURITY GROUP is not specified, 'default' will be used
|
78
|
+
# * IP can also be a CIDR range
|
79
|
+
#
|
80
|
+
def revoke
|
81
|
+
ip, security_group = parse_security_group_and_ip_from_args
|
82
|
+
rds.revoke_db_security_group_ingress(security_group, 'CIDRIP' => ip)
|
83
|
+
self.access
|
84
|
+
end
|
85
|
+
|
86
|
+
# rds:access
|
87
|
+
#
|
88
|
+
# displays current ingress access settings
|
89
|
+
#
|
90
|
+
def access
|
91
|
+
data = rds.security_groups.all.map do |group|
|
92
|
+
group.ec2_security_groups.map do |group_access|
|
93
|
+
[group.id, group_access['EC2SecurityGroupName'] + ' @ ' + group_access['EC2SecurityGroupOwnerId'], group_access['Status']]
|
94
|
+
end +
|
95
|
+
group.ip_ranges.map do |ip_range|
|
96
|
+
[group.id, ip_range['CIDRIP'], ip_range['Status']]
|
97
|
+
end
|
98
|
+
end.flatten(1)
|
99
|
+
data.unshift ['SECURITY GROUP', 'IP RANGE / SECURITY GROUP', 'STATUS']
|
100
|
+
lengths = (0..2).map { |i| data.map { |d| d[i].length }.max }
|
101
|
+
puts data.map { |d| '%-*s %-*s %-*s' % [lengths[0], d[0], lengths[1], d[1], lengths[2], d[2]] }.join("\n")
|
102
|
+
end
|
103
|
+
|
104
|
+
# rds:pull [RAILS_ENV or DATABASE_URL]
|
105
|
+
#
|
106
|
+
# downloads the remote database into a local database
|
107
|
+
#
|
108
|
+
# If a RAILS_ENV or DATABASE_URL is not specified, the current development environment
|
109
|
+
# is used (as read from config/database.yml). This command will confirm before executing
|
110
|
+
# the transfer.
|
111
|
+
#
|
112
|
+
def pull
|
113
|
+
check_dependencies('mysqldump', 'mysql')
|
114
|
+
|
115
|
+
target = args.shift || 'development'
|
116
|
+
if target =~ %r{://}
|
117
|
+
target = uri_to_hash(validate_db(URI.parse(target)))
|
118
|
+
else
|
119
|
+
raise CommandFailed, "config/database.yml not found" unless File.readable?("config/database.yml")
|
120
|
+
db_config = YAML.load(File.open("config/database.yml"))
|
121
|
+
raise CommandFailed, "environment #{target.inspect} not found in config/database.yml" unless
|
122
|
+
db_config.has_key?(target)
|
123
|
+
target = validate_db(db_config[target], target)
|
124
|
+
end
|
125
|
+
|
126
|
+
display "This will erase all data in the #{target['database'].inspect} database" +
|
127
|
+
(target['host'].empty? ? '' : " on #{target['host']}") + "!"
|
128
|
+
exit unless ask("Are you sure you wish to continue? [yN] ").downcase == 'y'
|
129
|
+
|
130
|
+
exec('/bin/sh', '-c',
|
131
|
+
'mysqldump --compress --single-transaction ' + args_to_s(mysql_args(database_uri)) +
|
132
|
+
pv_pipe +
|
133
|
+
%{| mysql --compress } + args_to_s(mysql_args(target)))
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def current_ip
|
139
|
+
# simple rack app which returns your external IP
|
140
|
+
RestClient::Resource.new("http://ip4.heroku.com")['/'].get.strip
|
141
|
+
end
|
142
|
+
|
143
|
+
def ask(prompt = nil)
|
144
|
+
Readline.readline(prompt)
|
145
|
+
end
|
146
|
+
|
147
|
+
def parse_database_uri
|
148
|
+
URI.parse(heroku.config_vars(app)['DATABASE_URL'])
|
149
|
+
end
|
150
|
+
|
151
|
+
def mysql_args(creds)
|
152
|
+
creds = uri_to_hash(creds) if creds.is_a?(URI)
|
153
|
+
args = []
|
154
|
+
args.concat(['-u', creds['username']]) if creds['username'] && !creds['username'].empty?
|
155
|
+
args << "-p#{creds['password']}" if creds['password'] && !creds['password'].empty?
|
156
|
+
args.concat(['-h', creds['host']]) if creds['host'] && !creds['host'].empty?
|
157
|
+
args.concat(['-P', creds['port']]) if creds['port'] && !creds['port'].empty?
|
158
|
+
args.concat(['-S', creds['socket']]) if creds['socket'] && !creds['socket'].empty?
|
159
|
+
args << creds['database']
|
160
|
+
args
|
161
|
+
end
|
162
|
+
|
163
|
+
def args_to_s(args)
|
164
|
+
"'" + args.collect { |s| s.gsub("'", "\\'") }.join("' '") + "'"
|
165
|
+
end
|
166
|
+
|
167
|
+
def exec(*args)
|
168
|
+
ENV['DEBUG'] ? puts("exec(): #{args.inspect}") && exit : super
|
169
|
+
end
|
170
|
+
|
171
|
+
def system(*args)
|
172
|
+
exec = ENV['DEBUG'].nil?
|
173
|
+
unless exec
|
174
|
+
puts("system(): #{args.inspect}")
|
175
|
+
exec = ask("execute? [yN] ") == 'y'
|
176
|
+
end
|
177
|
+
if exec
|
178
|
+
super or raise CommandFailed, "command failed [code=#{$?.exitstatus}]: " + args.join(' ')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def validate_db(creds, name = nil)
|
183
|
+
if creds.is_a?(URI)
|
184
|
+
raise CommandFailed, "#{name || creds.to_s} is not a MySQL server" unless creds.scheme =~ /^mysql/
|
185
|
+
else
|
186
|
+
raise CommandFailed, "#{name || creds.inspect} is not a MySQL server" unless creds['adapter'] =~ /^mysql/
|
187
|
+
end
|
188
|
+
creds
|
189
|
+
end
|
190
|
+
|
191
|
+
def check_dependencies(*commands)
|
192
|
+
options = commands.last.is_a?(Hash) ? commands.pop : {}
|
193
|
+
results = commands.collect do |cmd|
|
194
|
+
path = `which #{cmd}`.strip
|
195
|
+
if !options[:optional]
|
196
|
+
raise CommandFailed, "#{cmd}: not found in path" if path.empty?
|
197
|
+
raise CommandFailed, "#{cmd}: not executable" unless File.executable?(path)
|
198
|
+
else
|
199
|
+
!path.empty? && File.executable?(path)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
results.inject { |a, b| a && b }
|
203
|
+
end
|
204
|
+
|
205
|
+
def uri_to_hash(uri)
|
206
|
+
{ 'username' => uri.user,
|
207
|
+
'password' => uri.password,
|
208
|
+
'host' => uri.host,
|
209
|
+
'port' => uri.port,
|
210
|
+
'database' => uri.path.sub('/', '') }
|
211
|
+
end
|
212
|
+
|
213
|
+
def pv_installed?
|
214
|
+
check_dependencies('pv', :optional => true)
|
215
|
+
end
|
216
|
+
|
217
|
+
def pv_pipe
|
218
|
+
pv_installed? ? '| pv ' : ''
|
219
|
+
end
|
220
|
+
|
221
|
+
def database_uri
|
222
|
+
@database_uri ||= validate_db(parse_database_uri)
|
223
|
+
end
|
224
|
+
|
225
|
+
def aws_access_key_id
|
226
|
+
@aws_access_key_id ||= read_or_prompt_git_config('herokurds.accessKeyID', 'Please enter your AWS Access Key ID: ', 20)
|
227
|
+
end
|
228
|
+
|
229
|
+
def aws_secret_access_key
|
230
|
+
@aws_secret_access_key ||= read_or_prompt_git_config('herokurds.secretAccessKey', 'Please enter your AWS Secret Access Key: ', 40)
|
231
|
+
end
|
232
|
+
|
233
|
+
def read_or_prompt_git_config(config_var, prompt, length)
|
234
|
+
value = `git config #{config_var}`.strip
|
235
|
+
if value.empty?
|
236
|
+
value = ask(prompt)
|
237
|
+
unless value.length == length
|
238
|
+
puts "That is not valid; the value should be #{length} characters long."
|
239
|
+
return read_or_prompt_git_config(config_var, prompt, length)
|
240
|
+
end
|
241
|
+
system('git', 'config', '--add', config_var, value)
|
242
|
+
end
|
243
|
+
value
|
244
|
+
end
|
245
|
+
|
246
|
+
def rds
|
247
|
+
@rds ||= RdsProxy.new(:aws_access_key_id => aws_access_key_id, :aws_secret_access_key => aws_secret_access_key)
|
248
|
+
end
|
249
|
+
|
250
|
+
def parse_security_group_and_ip_from_args
|
251
|
+
ip = security_group = nil
|
252
|
+
while arg = args.shift
|
253
|
+
if arg =~ /^(?:\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/
|
254
|
+
raise CommandFailed, "too many arguments passed" if ip
|
255
|
+
ip = arg
|
256
|
+
else
|
257
|
+
raise CommandFailed, "IP not in correct format or too many arguments passed" if security_group
|
258
|
+
security_group = arg
|
259
|
+
end
|
260
|
+
end
|
261
|
+
ip ||= current_ip + '/32'
|
262
|
+
ip += '/32' unless ip =~ /\/\d{1,2}$/
|
263
|
+
security_group ||= 'default'
|
264
|
+
[ip, security_group]
|
265
|
+
end
|
266
|
+
|
267
|
+
class RdsProxy
|
268
|
+
def initialize(*args)
|
269
|
+
@instance = Fog::AWS::RDS.new(*args)
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
def method_missing(sym, *args, &block)
|
274
|
+
begin
|
275
|
+
@instance.send(sym, *args, &block)
|
276
|
+
rescue Excon::Errors::HTTPStatusError => error
|
277
|
+
raise CommandFailed, Nokogiri::XML.parse(error.response.body).css('Message').first.content
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
data/lib/heroku/rds.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: heroku-rds
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.4.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Dance
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-06-01 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: heroku
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "2.0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fog
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.7.0
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
description: Heroku plugin to aid working with RDS databases
|
39
|
+
email: rubygems@wuputah.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- README.md
|
46
|
+
- LICENSE.txt
|
47
|
+
files:
|
48
|
+
- lib/heroku/commands/rds.rb
|
49
|
+
- lib/heroku/rds.rb
|
50
|
+
- README.md
|
51
|
+
- .gemspec
|
52
|
+
- init.rb
|
53
|
+
- LICENSE.txt
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/wegowise/heroku-rds
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 1.3.6
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project:
|
78
|
+
rubygems_version: 1.6.2
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: Heroku plugin to aid working with RDS databases
|
82
|
+
test_files: []
|
83
|
+
|