rds-rotate-db-snapshots 0.5.1 → 1.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +4 -19
- data/.github/workflows/codeql.yml +2 -2
- data/Gemfile +10 -9
- data/README.md +3 -2
- data/Rakefile +15 -0
- data/VERSION +1 -1
- data/bin/rds-rotate-db-snapshots +247 -18
- data/rds-rotate-db-snapshots.gemspec +13 -33
- metadata +32 -16
- data/.rspec +0 -1
- data/lib/rds_rotate_db_snapshots/action_wrappers.rb +0 -26
- data/lib/rds_rotate_db_snapshots/actions.rb +0 -105
- data/lib/rds_rotate_db_snapshots/options_parser.rb +0 -110
- data/lib/rds_rotate_db_snapshots/rds_client.rb +0 -20
- data/lib/rds_rotate_db_snapshots.rb +0 -58
- data/spec/helper.rb +0 -81
- data/spec/lib/rds_rotate_db_snapshots/action_wrappers_spec.rb +0 -45
- data/spec/lib/rds_rotate_db_snapshots/actions_spec.rb +0 -96
- data/spec/lib/rds_rotate_db_snapshots/options_parser_spec.rb +0 -33
- data/spec/lib/rds_rotate_db_snapshots/rds_client_spec.rb +0 -32
- data/spec/lib/rds_rotate_db_snapshots_spec.rb +0 -62
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f21f380e6c4adfe878d359b84cf4842cc143225c81656e10e77a8e7f1d5efa0
|
4
|
+
data.tar.gz: e9e43b9f8fc687a263a3f5cf49f5bfecd3b5fcb69200546adbcb75856f7348ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d0a8ef6a1b168e0bac4d93b4b13ba2b7907bdbe7fd6307a596c739d36247dc252af8d665faf728a50402edb7e69940ee9c568b3601b4e3c219308861596b9c8
|
7
|
+
data.tar.gz: '09a5010edb7bad75fc94e4c64c0c523e0acf3e547224cfe33e107a0616cdc5eb8920ccf0fe91aafd25073216f1460f767996fdcc952228ca9d92daa4527491f3'
|
data/.github/workflows/ci.yml
CHANGED
@@ -2,16 +2,16 @@ name: "CI"
|
|
2
2
|
|
3
3
|
on:
|
4
4
|
push:
|
5
|
-
branches: ["
|
5
|
+
branches: ["master"]
|
6
6
|
pull_request:
|
7
|
-
branches: ["
|
7
|
+
branches: ["master"]
|
8
8
|
|
9
9
|
jobs:
|
10
10
|
test:
|
11
11
|
runs-on: ubuntu-20.04
|
12
12
|
strategy:
|
13
13
|
matrix:
|
14
|
-
ruby_version: [2.7, 3.0, 3.1]
|
14
|
+
ruby_version: [2.7, 3.0, 3.1, 'jruby']
|
15
15
|
steps:
|
16
16
|
- name: Checkout code
|
17
17
|
uses: actions/checkout@v3
|
@@ -25,19 +25,4 @@ jobs:
|
|
25
25
|
- name: Bundle Install
|
26
26
|
run: bundle install
|
27
27
|
- name: Test
|
28
|
-
run: bundle exec
|
29
|
-
- name: Coveralls Parallel
|
30
|
-
uses: coverallsapp/github-action@master
|
31
|
-
with:
|
32
|
-
github-token: ${{ secrets.github_token }}
|
33
|
-
flag-name: run-${{ matrix.ruby_version }}
|
34
|
-
parallel: true
|
35
|
-
finish:
|
36
|
-
needs: test
|
37
|
-
runs-on: ubuntu-latest
|
38
|
-
steps:
|
39
|
-
- name: Coveralls Finished
|
40
|
-
uses: coverallsapp/github-action@master
|
41
|
-
with:
|
42
|
-
github-token: ${{ secrets.github_token }}
|
43
|
-
parallel-finished: true
|
28
|
+
run: bundle exec rake test
|
@@ -13,10 +13,10 @@ name: "CodeQL"
|
|
13
13
|
|
14
14
|
on:
|
15
15
|
push:
|
16
|
-
branches: [ "
|
16
|
+
branches: [ "master" ]
|
17
17
|
pull_request:
|
18
18
|
# The branches below must be a subset of the branches above
|
19
|
-
branches: [ "
|
19
|
+
branches: [ "master" ]
|
20
20
|
schedule:
|
21
21
|
- cron: '22 4 * * 2'
|
22
22
|
|
data/Gemfile
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
3
|
gem 'aws-sdk-rds', '~> 1'
|
4
|
-
|
4
|
+
|
5
|
+
# Add dependencies to develop your gem here.
|
6
|
+
# Include everything needed to run rake, tests, features, etc.
|
7
|
+
group :development, :test do
|
8
|
+
gem 'bundler'
|
9
|
+
gem 'simplecov'
|
10
|
+
end
|
5
11
|
|
6
12
|
group :development do
|
7
13
|
gem 'juwelier'
|
8
|
-
gem "pry"
|
9
|
-
gem "pry-byebug"
|
10
14
|
end
|
11
15
|
|
12
16
|
group :test do
|
13
|
-
gem
|
14
|
-
gem
|
15
|
-
gem
|
16
|
-
gem "simplecov", ">= 0.13"
|
17
|
-
gem 'simplecov-lcov', '~> 0.8.0'
|
18
|
-
gem "webmock"
|
17
|
+
gem 'rake'
|
18
|
+
gem 'shoulda'
|
19
|
+
gem 'minitest'
|
19
20
|
end
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# rds-rotate-db-snapshots
|
2
2
|
|
3
3
|
[<img src="https://badge.fury.io/rb/rds-rotate-db-snapshots.svg" alt="Gem
|
4
|
-
Version" />](https://badge.fury.io/rb/rds-rotate-db-snapshots) [ [](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml?query=branch%3Amaster+event%3Apush) [](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml?query=branch%3Amaster+event%3Apush)
|
5
5
|
|
6
6
|
Provides a simple way to rotate db snapshots in Amazon Relational Database
|
7
7
|
Service (RDS).
|
@@ -11,6 +11,7 @@ Service (RDS).
|
|
11
11
|
- 2.7
|
12
12
|
- 3.1
|
13
13
|
- 3.2
|
14
|
+
- jruby
|
14
15
|
|
15
16
|
## Usage
|
16
17
|
|
@@ -64,7 +65,7 @@ show the messages.
|
|
64
65
|
|
65
66
|
## Contributing to rds-rotate-db-snapshots
|
66
67
|
|
67
|
-
- Check out the latest
|
68
|
+
- Check out the latest master to make sure the feature hasn't been
|
68
69
|
implemented or the bug hasn't been fixed yet
|
69
70
|
- Check out the issue tracker to make sure someone already hasn't requested
|
70
71
|
it and/or contributed it
|
data/Rakefile
CHANGED
@@ -22,6 +22,21 @@ Juwelier::Tasks.new do |gem|
|
|
22
22
|
end
|
23
23
|
Juwelier::RubygemsDotOrgTasks.new
|
24
24
|
|
25
|
+
require 'rake/testtask'
|
26
|
+
Rake::TestTask.new(:test) do |test|
|
27
|
+
test.pattern = 'test/**/test_*.rb'
|
28
|
+
test.verbose = true
|
29
|
+
end
|
30
|
+
|
31
|
+
# require 'simplecov'
|
32
|
+
# Rcov::RcovTask.new do |test|
|
33
|
+
# test.libs << 'test'
|
34
|
+
# test.pattern = 'test/**/test_*.rb'
|
35
|
+
# test.verbose = true
|
36
|
+
# end
|
37
|
+
|
38
|
+
task :default => :test
|
39
|
+
|
25
40
|
require 'rdoc/task'
|
26
41
|
Rake::RDocTask.new do |rdoc|
|
27
42
|
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.3
|
data/bin/rds-rotate-db-snapshots
CHANGED
@@ -1,25 +1,200 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
3
|
+
require 'rubygems'
|
4
|
+
require 'aws-sdk-rds'
|
5
|
+
require 'optparse'
|
4
6
|
|
5
|
-
|
7
|
+
$opts = {
|
8
|
+
:aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
|
9
|
+
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
10
|
+
:aws_session_token => ENV["AWS_SESSION_TOKEN"],
|
11
|
+
:aws_region => ENV["AWS_REGION"],
|
12
|
+
:pattern => nil,
|
13
|
+
:by_tags => nil,
|
14
|
+
:dry_run => false,
|
15
|
+
:backoff_limit => 15,
|
16
|
+
:create_snapshot => nil
|
17
|
+
}
|
6
18
|
|
7
|
-
|
8
|
-
|
9
|
-
|
19
|
+
$time_periods = {
|
20
|
+
:hourly => { :seconds => 60 * 60, :format => '%Y-%m-%d-%H', :keep => 0, :keeping => {} },
|
21
|
+
:daily => { :seconds => 24 * 60 * 60, :format => '%Y-%m-%d', :keep => 0, :keeping => {} },
|
22
|
+
:weekly => { :seconds => 7 * 24 * 60 * 60, :format => '%Y-%W', :keep => 0, :keeping => {} },
|
23
|
+
:monthly => { :seconds => 30 * 24 * 60 * 60, :format => '%Y-%m', :keep => 0, :keeping => {} },
|
24
|
+
:yearly => { :seconds => 12 * 30 * 24 * 60 * 60, :format => '%Y', :keep => 0, :keeping => {} },
|
25
|
+
}
|
26
|
+
def backoff()
|
27
|
+
$backoffed = $backoffed + 1
|
28
|
+
|
29
|
+
if $opts[:backoff_limit] > 0 && $opts[:backoff_limit] < $backoffed
|
30
|
+
puts "Too many backoff attempts. Sorry it didn't work out."
|
31
|
+
exit 2
|
32
|
+
end
|
33
|
+
|
34
|
+
naptime = rand(60) * $backoffed
|
35
|
+
puts "Backing off for #{naptime} seconds..."
|
36
|
+
sleep naptime
|
37
|
+
end
|
38
|
+
|
39
|
+
def rotate_em(snapshots)
|
40
|
+
# poor man's way to get a deep copy of our time_periods definition hash
|
41
|
+
periods = Marshal.load(Marshal.dump($time_periods))
|
42
|
+
|
43
|
+
snapshots.each do |snapshot|
|
44
|
+
time = snapshot[:snapshot_create_time]
|
45
|
+
db_id = snapshot[:db_instance_identifier]
|
46
|
+
snapshot_id = snapshot[:db_snapshot_identifier]
|
47
|
+
description = snapshot_id
|
48
|
+
keep_reason = nil
|
49
|
+
|
50
|
+
if $opts[:pattern] && description !~ /#{$opts[:pattern]}/
|
51
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Skipping snapshot with description #{description}"
|
52
|
+
next
|
53
|
+
end
|
54
|
+
|
55
|
+
periods.keys.sort { |a, b| periods[a][:seconds] <=> periods[b][:seconds] }.each do |period|
|
56
|
+
period_info = periods[period]
|
57
|
+
keep = period_info[:keep]
|
58
|
+
keeping = period_info[:keeping]
|
59
|
+
|
60
|
+
time_string = time.strftime period_info[:format]
|
61
|
+
if Time.now - time < keep * period_info[:seconds]
|
62
|
+
if !keeping.key?(time_string) && keeping.length < keep
|
63
|
+
keep_reason = period
|
64
|
+
keeping[time_string] = snapshot
|
65
|
+
end
|
66
|
+
break
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if keep_reason.nil? && snapshot == snapshots.last && $opts[:keep_last]
|
71
|
+
keep_reason = 'last snapshot'
|
72
|
+
end
|
73
|
+
|
74
|
+
if !keep_reason.nil?
|
75
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Keeping for #{keep_reason}"
|
76
|
+
else
|
77
|
+
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Deleting"
|
78
|
+
begin
|
79
|
+
$rds.delete_db_snapshot(db_snapshot_identifier: snapshot_id) unless $opts[:dry_run]
|
80
|
+
rescue Aws::RDS::Errors => e
|
81
|
+
backoff()
|
82
|
+
retry
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
10
86
|
end
|
11
87
|
|
12
|
-
|
13
|
-
|
88
|
+
def create_snapshot(name, db_indentifier_ids)
|
89
|
+
if !!name
|
90
|
+
name = name.gsub(/[^a-zA-Z0-9\-]/, '')
|
91
|
+
if name.size > 0
|
92
|
+
name = "#{name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
93
|
+
db_indentifier_ids.each do |db_id|
|
94
|
+
begin
|
95
|
+
$rds.create_db_snapshot(db_snapshot_identifier: name, db_instance_identifier: db_id) unless $opts[:dry_run]
|
96
|
+
puts " #{Time.now.strftime '%Y-%m-%d %H:%M:%S'} Creation snapshot #{name} is pending (db: #{db_id})"
|
97
|
+
rescue Aws::RDS::Errors::InvalidDBInstanceStateFault => e
|
98
|
+
backoff()
|
99
|
+
retry
|
100
|
+
end
|
101
|
+
end
|
102
|
+
else
|
103
|
+
puts "invalid snapshot name format - #{name}"
|
104
|
+
exit 1
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def split_tag(hash,v)
|
110
|
+
v.split(',').each do |pair|
|
111
|
+
tag, value = pair.split('=',2)
|
112
|
+
if value.nil?
|
113
|
+
puts "invalid tag=value format"
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
hash[tag] = value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def get_db_snapshots(options)
|
121
|
+
snapshots = []
|
122
|
+
response = $rds.describe_db_snapshots(options)
|
123
|
+
while true do
|
124
|
+
snapshots += response.db_snapshots
|
125
|
+
break unless response[:marker]
|
126
|
+
|
127
|
+
response = $rds.describe_db_snapshots(options.merge(marker: response[:marker]))
|
128
|
+
end
|
129
|
+
snapshots
|
130
|
+
end
|
131
|
+
|
132
|
+
OptionParser.new do |o|
|
133
|
+
script_name = File.basename($0)
|
134
|
+
o.banner = "Usage: #{script_name} [options] <db_indentifier>\nUsage: #{script_name} --by-tags <tag=value,...> [other options]"
|
135
|
+
o.separator ""
|
136
|
+
|
137
|
+
o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
|
138
|
+
$opts[:aws_access_key] = v
|
139
|
+
end
|
140
|
+
|
141
|
+
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
142
|
+
$opts[:aws_secret_access_key] = v
|
143
|
+
end
|
144
|
+
|
145
|
+
o.on("--aws-region REGION", "AWS Region") do |v|
|
146
|
+
$opts[:aws_region] = v
|
147
|
+
end
|
148
|
+
|
149
|
+
o.on("--aws-session-token SESSION_TOKEN", "AWS session token") do |v|
|
150
|
+
$opts[:aws_session_token] = v
|
151
|
+
end
|
152
|
+
|
153
|
+
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
154
|
+
$opts[:pattern] = v
|
155
|
+
end
|
156
|
+
|
157
|
+
o.on("--by-tags TAG=VALUE,TAG=VALUE", "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs.") do |v|
|
158
|
+
$opts[:by_tags] = {}
|
159
|
+
puts 'Hey! It\'s not implemented in RDS yet. Who knows, maybe they will add Tagging in RDS later.'
|
160
|
+
exit 0
|
161
|
+
split_tag($opts[:by_tags],v)
|
162
|
+
end
|
163
|
+
|
164
|
+
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15") do |v|
|
165
|
+
$opts[:backoff_limit] = v
|
166
|
+
end
|
167
|
+
|
168
|
+
o.on("--create-snapshot STRING", "Use this option if you want to create a snapshot") do |v|
|
169
|
+
$opts[:create_snapshot] = v
|
170
|
+
end
|
171
|
+
|
172
|
+
$time_periods.keys.sort { |a, b| $time_periods[a][:seconds] <=> $time_periods[b][:seconds] }.each do |period|
|
173
|
+
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
174
|
+
$time_periods[period][:keep] = v
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
179
|
+
$opts[:keep_last] = true
|
180
|
+
end
|
181
|
+
|
182
|
+
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
183
|
+
$opts[:dry_run] = true
|
184
|
+
end
|
185
|
+
end.parse!
|
186
|
+
|
187
|
+
if $opts[:aws_access_key].nil? || $opts[:aws_secret_access_key].nil?
|
188
|
+
puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key"
|
14
189
|
exit 1
|
15
190
|
end
|
16
191
|
|
17
|
-
if ARGV.empty? and
|
192
|
+
if ARGV.empty? and $opts[:by_tags].nil?
|
18
193
|
puts "You must provide at least one DB Indentifier when not rotating by tags"
|
19
194
|
exit 1
|
20
195
|
end
|
21
196
|
|
22
|
-
if
|
197
|
+
if $opts[:by_tags].nil?
|
23
198
|
db_indentifier_ids = ARGV
|
24
199
|
|
25
200
|
db_indentifier_ids.each do |db_id|
|
@@ -33,29 +208,83 @@ else
|
|
33
208
|
if !ARGV.empty?
|
34
209
|
puts "Ignoring supplied db_indentifier_ids because we're rotating by tags."
|
35
210
|
end
|
36
|
-
if
|
211
|
+
if $opts[:by_tags].length == 0
|
37
212
|
puts "Rotating by tags but no tags specified? Refusing to rotate all snapshots!"
|
38
213
|
exit 1
|
39
214
|
end
|
40
215
|
end
|
41
216
|
|
42
|
-
if
|
217
|
+
if $opts[:backoff_limit] < 0
|
43
218
|
puts "A negative backoff limit doesn't make much sense."
|
44
219
|
exit 1
|
45
220
|
end
|
46
221
|
|
47
|
-
|
48
|
-
|
222
|
+
$backoffed = 0
|
223
|
+
begin
|
224
|
+
Aws.config.update(
|
225
|
+
access_key_id: $opts[:aws_access_key],
|
226
|
+
secret_access_key: $opts[:aws_secret_access_key],
|
227
|
+
region: $opts[:aws_region],
|
228
|
+
session_token: $opts[:aws_session_token]
|
229
|
+
)
|
230
|
+
$rds = Aws::RDS::Client.new
|
231
|
+
rescue Aws::RDS::Errors => e
|
232
|
+
backoff()
|
233
|
+
retry
|
234
|
+
end
|
235
|
+
|
236
|
+
if $opts[:create_snapshot]
|
237
|
+
create_snapshot($opts[:create_snapshot], db_indentifier_ids)
|
49
238
|
end
|
50
239
|
|
51
240
|
all_snapshots = []
|
52
|
-
if
|
53
|
-
|
241
|
+
if $opts[:by_tags]
|
242
|
+
$opts[:by_tags].each do |tag, value|
|
243
|
+
begin
|
244
|
+
these_snapshots = $rds.describe_tags(snapshot_type: 'manual', filters: {'resource-type'=>"snapshot", 'key'=>tag, 'value'=>value}).
|
245
|
+
delete_if{ |e| e.status != 'available' }
|
246
|
+
rescue Aws::RDS::Errors => e
|
247
|
+
backoff()
|
248
|
+
retry
|
249
|
+
end
|
250
|
+
if these_snapshots.length == 0
|
251
|
+
puts "(tag,value)=(#{tag},#{value}) found no snapshots; nothing to rotate!"
|
252
|
+
exit 0
|
253
|
+
end
|
254
|
+
if all_snapshots.length == 0
|
255
|
+
remaining_snapshots = these_snapshots
|
256
|
+
else
|
257
|
+
remaining_snapshots = all_snapshots & these_snapshots
|
258
|
+
end
|
259
|
+
if remaining_snapshots.length == 0
|
260
|
+
puts "No remaining snapshots after applying (tag,value)=(#{tag},#{value}) filter; nothing to rotate!"
|
261
|
+
exit 0
|
262
|
+
end
|
263
|
+
all_snapshots = remaining_snapshots
|
264
|
+
end
|
265
|
+
|
266
|
+
begin
|
267
|
+
rotate_these = get_db_snapshots(db_instance_identifier: all_snapshots.map(&:db_instance_identifier).uniq).
|
268
|
+
delete_if{ |e| !all_snapshots.include?(e.db_snapshot_identifier) }.
|
269
|
+
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
270
|
+
rescue Aws::RDS::Errors => e
|
271
|
+
backoff()
|
272
|
+
retry
|
273
|
+
end
|
274
|
+
|
275
|
+
rotate_em(rotate_these)
|
54
276
|
else
|
55
|
-
|
277
|
+
begin
|
278
|
+
all_snapshots = get_db_snapshots(snapshot_type: 'manual').
|
279
|
+
delete_if{ |e| e[:status] != 'available' }
|
280
|
+
rescue Aws::RDS::Errors => e
|
281
|
+
backoff()
|
282
|
+
retry
|
283
|
+
end
|
284
|
+
|
56
285
|
db_indentifier_ids.each do |db_id|
|
57
|
-
|
58
|
-
|
286
|
+
rotate_em(
|
287
|
+
all_snapshots.select {|ss| ss[:db_instance_identifier] == db_id }.
|
59
288
|
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
60
289
|
)
|
61
290
|
end
|
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: rds-rotate-db-snapshots 0.
|
5
|
+
# stub: rds-rotate-db-snapshots 1.0.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "rds-rotate-db-snapshots".freeze
|
9
|
-
s.version = "0.
|
9
|
+
s.version = "1.0.0".freeze
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
13
13
|
s.authors = ["Siarhei Kavaliou".freeze]
|
14
|
-
s.date = "
|
14
|
+
s.date = "2024-01-02"
|
15
15
|
s.description = "Provides a simple way to rotate RDS DB snapshots with configurable retention periods.".freeze
|
16
16
|
s.email = "kovserg@gmail.com".freeze
|
17
17
|
s.executables = ["rds-rotate-db-snapshots".freeze]
|
@@ -24,47 +24,27 @@ Gem::Specification.new do |s|
|
|
24
24
|
".github/dependabot.yml",
|
25
25
|
".github/workflows/ci.yml",
|
26
26
|
".github/workflows/codeql.yml",
|
27
|
-
".rspec",
|
28
27
|
"Gemfile",
|
29
28
|
"LICENSE.txt",
|
30
29
|
"README.md",
|
31
30
|
"Rakefile",
|
32
31
|
"VERSION",
|
33
32
|
"bin/rds-rotate-db-snapshots",
|
34
|
-
"
|
35
|
-
"lib/rds_rotate_db_snapshots/action_wrappers.rb",
|
36
|
-
"lib/rds_rotate_db_snapshots/actions.rb",
|
37
|
-
"lib/rds_rotate_db_snapshots/options_parser.rb",
|
38
|
-
"lib/rds_rotate_db_snapshots/rds_client.rb",
|
39
|
-
"rds-rotate-db-snapshots.gemspec",
|
40
|
-
"spec/helper.rb",
|
41
|
-
"spec/lib/rds_rotate_db_snapshots/action_wrappers_spec.rb",
|
42
|
-
"spec/lib/rds_rotate_db_snapshots/actions_spec.rb",
|
43
|
-
"spec/lib/rds_rotate_db_snapshots/options_parser_spec.rb",
|
44
|
-
"spec/lib/rds_rotate_db_snapshots/rds_client_spec.rb",
|
45
|
-
"spec/lib/rds_rotate_db_snapshots_spec.rb"
|
33
|
+
"rds-rotate-db-snapshots.gemspec"
|
46
34
|
]
|
47
35
|
s.homepage = "http://github.com/serg-kovalev/rds-rotate-db-snapshots".freeze
|
48
36
|
s.licenses = ["MIT".freeze]
|
49
|
-
s.rubygems_version = "3.3
|
37
|
+
s.rubygems_version = "3.5.3".freeze
|
50
38
|
s.summary = "Amazon RDS DB snapshot rotator".freeze
|
51
39
|
|
52
|
-
|
53
|
-
s.specification_version = 4
|
54
|
-
end
|
40
|
+
s.specification_version = 4
|
55
41
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
s.add_dependency(%q<aws-sdk-rds>.freeze, ["~> 1"])
|
64
|
-
s.add_dependency(%q<rake>.freeze, [">= 0"])
|
65
|
-
s.add_dependency(%q<juwelier>.freeze, [">= 0"])
|
66
|
-
s.add_dependency(%q<pry>.freeze, [">= 0"])
|
67
|
-
s.add_dependency(%q<pry-byebug>.freeze, [">= 0"])
|
68
|
-
end
|
42
|
+
s.add_runtime_dependency(%q<aws-sdk-rds>.freeze, ["~> 1".freeze])
|
43
|
+
s.add_development_dependency(%q<rake>.freeze, [">= 0".freeze])
|
44
|
+
s.add_development_dependency(%q<juwelier>.freeze, [">= 0".freeze])
|
45
|
+
s.add_development_dependency(%q<pry>.freeze, [">= 0".freeze])
|
46
|
+
s.add_development_dependency(%q<pry-byebug>.freeze, [">= 0".freeze])
|
47
|
+
s.add_development_dependency(%q<rubocop>.freeze, [">= 0".freeze])
|
48
|
+
s.add_development_dependency(%q<rubocop-rspec>.freeze, [">= 0".freeze])
|
69
49
|
end
|
70
50
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rds-rotate-db-snapshots
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Siarhei Kavaliou
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-01-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-rds
|
@@ -31,7 +31,7 @@ dependencies:
|
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
|
-
type: :
|
34
|
+
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description: Provides a simple way to rotate RDS DB snapshots with configurable retention
|
84
112
|
periods.
|
85
113
|
email: kovserg@gmail.com
|
@@ -94,25 +122,13 @@ files:
|
|
94
122
|
- ".github/dependabot.yml"
|
95
123
|
- ".github/workflows/ci.yml"
|
96
124
|
- ".github/workflows/codeql.yml"
|
97
|
-
- ".rspec"
|
98
125
|
- Gemfile
|
99
126
|
- LICENSE.txt
|
100
127
|
- README.md
|
101
128
|
- Rakefile
|
102
129
|
- VERSION
|
103
130
|
- bin/rds-rotate-db-snapshots
|
104
|
-
- lib/rds_rotate_db_snapshots.rb
|
105
|
-
- lib/rds_rotate_db_snapshots/action_wrappers.rb
|
106
|
-
- lib/rds_rotate_db_snapshots/actions.rb
|
107
|
-
- lib/rds_rotate_db_snapshots/options_parser.rb
|
108
|
-
- lib/rds_rotate_db_snapshots/rds_client.rb
|
109
131
|
- rds-rotate-db-snapshots.gemspec
|
110
|
-
- spec/helper.rb
|
111
|
-
- spec/lib/rds_rotate_db_snapshots/action_wrappers_spec.rb
|
112
|
-
- spec/lib/rds_rotate_db_snapshots/actions_spec.rb
|
113
|
-
- spec/lib/rds_rotate_db_snapshots/options_parser_spec.rb
|
114
|
-
- spec/lib/rds_rotate_db_snapshots/rds_client_spec.rb
|
115
|
-
- spec/lib/rds_rotate_db_snapshots_spec.rb
|
116
132
|
homepage: http://github.com/serg-kovalev/rds-rotate-db-snapshots
|
117
133
|
licenses:
|
118
134
|
- MIT
|
@@ -132,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
148
|
- !ruby/object:Gem::Version
|
133
149
|
version: '0'
|
134
150
|
requirements: []
|
135
|
-
rubygems_version: 3.3
|
151
|
+
rubygems_version: 3.5.3
|
136
152
|
signing_key:
|
137
153
|
specification_version: 4
|
138
154
|
summary: Amazon RDS DB snapshot rotator
|
data/.rspec
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
-w --color
|
@@ -1,26 +0,0 @@
|
|
1
|
-
require 'aws-sdk-rds'
|
2
|
-
|
3
|
-
class RdsRotateDbSnapshots
|
4
|
-
module ActionWrappers
|
5
|
-
def with_backoff(*method_names)
|
6
|
-
method_names.each do |m|
|
7
|
-
wrapper = Module.new do
|
8
|
-
define_method(m) do |*args|
|
9
|
-
reset_backoff
|
10
|
-
begin
|
11
|
-
super *args
|
12
|
-
rescue Aws::RDS::Errors::ServiceError => e
|
13
|
-
raise if e.is_a? Aws::RDS::Errors::ExpiredToken
|
14
|
-
# TODO: re-work
|
15
|
-
puts "Error: #{e}"
|
16
|
-
backoff
|
17
|
-
retry
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
22
|
-
self.prepend wrapper
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,105 +0,0 @@
|
|
1
|
-
class RdsRotateDbSnapshots
|
2
|
-
module Actions
|
3
|
-
def rotate_em(snapshots)
|
4
|
-
# poor man's way to get a deep copy of our time_periods definition hash
|
5
|
-
periods = Marshal.load(Marshal.dump(time_periods))
|
6
|
-
|
7
|
-
snapshots.each do |snapshot|
|
8
|
-
time = snapshot[:snapshot_create_time]
|
9
|
-
db_id = snapshot[:db_instance_identifier]
|
10
|
-
snapshot_id = snapshot[:db_snapshot_identifier]
|
11
|
-
description = snapshot_id
|
12
|
-
keep_reason = nil
|
13
|
-
|
14
|
-
if options[:pattern] && description !~ /#{options[:pattern]}/
|
15
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Skipping snapshot with description #{description}"
|
16
|
-
next
|
17
|
-
end
|
18
|
-
|
19
|
-
periods.keys.sort { |a, b| periods[a][:seconds] <=> periods[b][:seconds] }.each do |period|
|
20
|
-
period_info = periods[period]
|
21
|
-
keep = period_info[:keep]
|
22
|
-
keeping = period_info[:keeping]
|
23
|
-
|
24
|
-
time_string = time.strftime period_info[:format]
|
25
|
-
if Time.now - time < keep * period_info[:seconds]
|
26
|
-
if !keeping.key?(time_string) && keeping.length < keep
|
27
|
-
keep_reason = period
|
28
|
-
keeping[time_string] = snapshot
|
29
|
-
end
|
30
|
-
break
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
if keep_reason.nil? && snapshot == snapshots.last && options[:keep_last]
|
35
|
-
keep_reason = 'last snapshot'
|
36
|
-
end
|
37
|
-
|
38
|
-
if !keep_reason.nil?
|
39
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Keeping for #{keep_reason}"
|
40
|
-
else
|
41
|
-
puts " #{time.strftime '%Y-%m-%d %H:%M:%S'} #{snapshot_id} Deleting"
|
42
|
-
begin
|
43
|
-
client.delete_db_snapshot(db_snapshot_identifier: snapshot_id) unless options[:dry_run]
|
44
|
-
rescue Aws::RDS::Errors => e
|
45
|
-
backoff
|
46
|
-
retry
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def create_snapshot(name, db_indentifier_ids)
|
53
|
-
if !!name
|
54
|
-
name = name.gsub(/[^a-zA-Z0-9\-]/, '')
|
55
|
-
if name.size > 0
|
56
|
-
name = "#{name}-#{Time.now.strftime('%Y%m%d%H%M%S')}"
|
57
|
-
db_indentifier_ids.each do |db_id|
|
58
|
-
begin
|
59
|
-
client.create_db_snapshot(db_snapshot_identifier: name, db_instance_identifier: db_id) unless options[:dry_run]
|
60
|
-
puts " #{Time.now.strftime '%Y-%m-%d %H:%M:%S'} Creation snapshot #{name} is pending (db: #{db_id})"
|
61
|
-
rescue Aws::RDS::Errors::InvalidDBInstanceStateFault => e
|
62
|
-
backoff
|
63
|
-
retry
|
64
|
-
end
|
65
|
-
end
|
66
|
-
else
|
67
|
-
puts "invalid snapshot name format - #{name}"
|
68
|
-
exit 1
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def get_db_snapshots(options)
|
74
|
-
snapshots = []
|
75
|
-
response = client.describe_db_snapshots(options)
|
76
|
-
while true do
|
77
|
-
snapshots += response.db_snapshots
|
78
|
-
break unless response[:marker]
|
79
|
-
|
80
|
-
response = client.describe_db_snapshots(options.merge(marker: response[:marker]))
|
81
|
-
end
|
82
|
-
snapshots
|
83
|
-
end
|
84
|
-
|
85
|
-
def rotate_by_tags
|
86
|
-
snapshots = []
|
87
|
-
options[:by_tags].each do |tag, value|
|
88
|
-
snapshots = rrds.client.describe_tags(
|
89
|
-
snapshot_type: 'manual', filters: {'resource-type'=>"snapshot", 'key'=>tag, 'value'=>value}
|
90
|
-
).delete_if{ |e| e.status != 'available' }
|
91
|
-
# TODO: re-work
|
92
|
-
if snapshots.length == 0
|
93
|
-
puts "(tag,value)=(#{tag},#{value}) found no snapshots; nothing to rotate!"
|
94
|
-
exit 0
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
snapshots = get_db_snapshots(db_instance_identifier: snapshots.map(&:db_instance_identifier).uniq).
|
99
|
-
delete_if{ |e| !snapshots.include?(e.db_snapshot_identifier) }.
|
100
|
-
sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
|
101
|
-
|
102
|
-
rotate_em snapshots
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
require 'optparse'
|
2
|
-
|
3
|
-
class RdsRotateDbSnapshots
|
4
|
-
class OptionsParser
|
5
|
-
class NotImplementedError < StandardError; end
|
6
|
-
class InvalidArgument < StandardError; end
|
7
|
-
|
8
|
-
attr_reader :options, :script_name, :time_periods
|
9
|
-
|
10
|
-
def initialize(script_name: nil, cli: false)
|
11
|
-
@script_name = script_name
|
12
|
-
@options = {
|
13
|
-
:aws_access_key => ENV["AWS_ACCESS_KEY_ID"],
|
14
|
-
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
15
|
-
:aws_session_token => ENV["AWS_SESSION_TOKEN"],
|
16
|
-
:aws_region => ENV["AWS_REGION"],
|
17
|
-
:pattern => nil,
|
18
|
-
:by_tags => nil,
|
19
|
-
:dry_run => false,
|
20
|
-
:backoff_limit => 15,
|
21
|
-
:create_snapshot => nil
|
22
|
-
}
|
23
|
-
@time_periods = {
|
24
|
-
:hourly => { :seconds => 60 * 60, :format => '%Y-%m-%d-%H', :keep => 0, :keeping => {} },
|
25
|
-
:daily => { :seconds => 24 * 60 * 60, :format => '%Y-%m-%d', :keep => 0, :keeping => {} },
|
26
|
-
:weekly => { :seconds => 7 * 24 * 60 * 60, :format => '%Y-%W', :keep => 0, :keeping => {} },
|
27
|
-
:monthly => { :seconds => 30 * 24 * 60 * 60, :format => '%Y-%m', :keep => 0, :keeping => {} },
|
28
|
-
:yearly => { :seconds => 12 * 30 * 24 * 60 * 60, :format => '%Y', :keep => 0, :keeping => {} },
|
29
|
-
}
|
30
|
-
@cli = cli
|
31
|
-
init_cli_parser if cli?
|
32
|
-
end
|
33
|
-
|
34
|
-
def parse!
|
35
|
-
@parser.parse!
|
36
|
-
@options.merge(time_periods: @time_periods)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def cli?
|
42
|
-
!!@cli
|
43
|
-
end
|
44
|
-
|
45
|
-
def init_cli_parser
|
46
|
-
@parser ||= OptionParser.new do |o|
|
47
|
-
o.banner = "Usage: #{script_name} [options] <db_indentifier>\nUsage: #{script_name} --by-tags <tag=value,...> [other options]"
|
48
|
-
o.separator ""
|
49
|
-
|
50
|
-
o.on("--aws-access-key ACCESS_KEY", "AWS Access Key") do |v|
|
51
|
-
@options[:aws_access_key] = v
|
52
|
-
end
|
53
|
-
|
54
|
-
o.on("--aws-secret-access-key SECRET_KEY", "AWS Secret Access Key") do |v|
|
55
|
-
@options[:aws_secret_access_key] = v
|
56
|
-
end
|
57
|
-
|
58
|
-
o.on("--aws-region REGION", "AWS Region") do |v|
|
59
|
-
@options[:aws_region] = v
|
60
|
-
end
|
61
|
-
|
62
|
-
o.on("--aws-session-token SESSION_TOKEN", "AWS session token") do |v|
|
63
|
-
@options[:aws_session_token] = v
|
64
|
-
end
|
65
|
-
|
66
|
-
o.on("--pattern STRING", "Snapshots without this string in the description will be ignored") do |v|
|
67
|
-
@options[:pattern] = v
|
68
|
-
end
|
69
|
-
|
70
|
-
o.on("--by-tags TAG=VALUE,TAG=VALUE", "Instead of rotating specific snapshots, rotate over all the snapshots having the intersection of all given TAG=VALUE pairs.") do |v|
|
71
|
-
@options[:by_tags] = {}
|
72
|
-
raise NotImplementedError, 'Hey! It\'s not implemented in RDS yet. Who knows, maybe they will add Tagging in RDS later.'
|
73
|
-
split_tag(@options[:by_tags],v)
|
74
|
-
end
|
75
|
-
|
76
|
-
o.on("--backoff-limit LIMIT", "Backoff and retry when hitting RDS Error exceptions no more than this many times. Default is 15") do |v|
|
77
|
-
@options[:backoff_limit] = v
|
78
|
-
end
|
79
|
-
|
80
|
-
o.on("--create-snapshot STRING", "Use this option if you want to create a snapshot") do |v|
|
81
|
-
@options[:create_snapshot] = v
|
82
|
-
end
|
83
|
-
|
84
|
-
@time_periods.keys.sort { |a, b| @time_periods[a][:seconds] <=> @time_periods[b][:seconds] }.each do |period|
|
85
|
-
o.on("--keep-#{period} NUMBER", Integer, "Number of #{period} snapshots to keep") do |v|
|
86
|
-
@time_periods[period][:keep] = v
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
o.on("--keep-last", "Keep the most recent snapshot, regardless of time-based policy") do |v|
|
91
|
-
@options[:keep_last] = true
|
92
|
-
end
|
93
|
-
|
94
|
-
o.on("--dry-run", "Shows what would happen without doing anything") do |v|
|
95
|
-
@options[:dry_run] = true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def split_tag(hash,v)
|
101
|
-
v.split(',').each do |pair|
|
102
|
-
tag, value = pair.split('=',2)
|
103
|
-
if value.nil?
|
104
|
-
raise InvalidArgument, "invalid tag=value format"
|
105
|
-
end
|
106
|
-
hash[tag] = value
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
require 'forwardable'
|
2
|
-
require 'aws-sdk-rds'
|
3
|
-
|
4
|
-
class RdsRotateDbSnapshots
|
5
|
-
class RdsClient
|
6
|
-
extend Forwardable
|
7
|
-
|
8
|
-
def_delegators :@client, :describe_db_snapshots, :create_db_snapshot, :delete_db_snapshot
|
9
|
-
|
10
|
-
def initialize(options)
|
11
|
-
Aws.config.update(
|
12
|
-
access_key_id: options[:aws_access_key],
|
13
|
-
secret_access_key: options[:aws_secret_access_key],
|
14
|
-
region: options[:aws_region],
|
15
|
-
session_token: options[:aws_session_token]
|
16
|
-
)
|
17
|
-
@client = Aws::RDS::Client.new
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
@@ -1,58 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require_relative 'rds_rotate_db_snapshots/actions'
|
3
|
-
require_relative 'rds_rotate_db_snapshots/action_wrappers'
|
4
|
-
require_relative 'rds_rotate_db_snapshots/options_parser'
|
5
|
-
require_relative 'rds_rotate_db_snapshots/rds_client'
|
6
|
-
|
7
|
-
class RdsRotateDbSnapshots
|
8
|
-
extend RdsRotateDbSnapshots::ActionWrappers
|
9
|
-
include RdsRotateDbSnapshots::Actions
|
10
|
-
|
11
|
-
attr_reader :options
|
12
|
-
with_backoff :get_db_snapshots, :create_snapshot, :rotate_em
|
13
|
-
|
14
|
-
def initialize(script_name: nil, cli: false, options: {})
|
15
|
-
@script_name = script_name
|
16
|
-
@options = options
|
17
|
-
@cli = cli
|
18
|
-
parse_options if cli?
|
19
|
-
@backoff_counter = 0
|
20
|
-
end
|
21
|
-
|
22
|
-
def rds_client
|
23
|
-
@rds_client ||= RdsRotateDbSnapshots::RdsClient.new(@options)
|
24
|
-
end
|
25
|
-
alias_method :client, :rds_client
|
26
|
-
|
27
|
-
def reset_backoff
|
28
|
-
@backoff_counter = 0
|
29
|
-
end
|
30
|
-
|
31
|
-
def time_periods
|
32
|
-
@options[:time_periods]
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def cli?
|
38
|
-
!!@cli
|
39
|
-
end
|
40
|
-
|
41
|
-
def parse_options
|
42
|
-
@options = RdsRotateDbSnapshots::OptionsParser.new(script_name: @script_name, cli: @cli).parse!
|
43
|
-
end
|
44
|
-
|
45
|
-
def backoff
|
46
|
-
@backoff_counter = @backoff_counter + 1
|
47
|
-
|
48
|
-
# TODO: re-work
|
49
|
-
if options && options[:backoff_limit] > 0 && options[:backoff_limit] < @backoff_counter
|
50
|
-
puts "Too many backoff attempts. Sorry it didn't work out."
|
51
|
-
exit 2
|
52
|
-
end
|
53
|
-
|
54
|
-
naptime = rand(60) * @backoff_counter
|
55
|
-
puts "Backing off for #{naptime} seconds..."
|
56
|
-
sleep naptime
|
57
|
-
end
|
58
|
-
end
|
data/spec/helper.rb
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
$TESTING = true
|
2
|
-
|
3
|
-
require "simplecov"
|
4
|
-
|
5
|
-
SimpleCov.start do
|
6
|
-
add_filter "/spec"
|
7
|
-
minimum_coverage(75)
|
8
|
-
|
9
|
-
if ENV['CI']
|
10
|
-
require 'simplecov-lcov'
|
11
|
-
|
12
|
-
SimpleCov::Formatter::LcovFormatter.config do |c|
|
13
|
-
c.report_with_single_file = true
|
14
|
-
c.single_report_path = 'coverage/lcov.info'
|
15
|
-
end
|
16
|
-
|
17
|
-
formatter SimpleCov::Formatter::LcovFormatter
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
22
|
-
require "rds_rotate_db_snapshots"
|
23
|
-
|
24
|
-
require "rdoc"
|
25
|
-
require "rspec"
|
26
|
-
require "diff/lcs" # You need diff/lcs installed to run specs.
|
27
|
-
# require 'stringio'
|
28
|
-
require "webmock/rspec"
|
29
|
-
|
30
|
-
WebMock.disable_net_connect!(:allow => "coveralls.io")
|
31
|
-
|
32
|
-
$0 = "rds_rotate_db_snapshots"
|
33
|
-
ARGV.clear
|
34
|
-
|
35
|
-
RSpec.configure do |config|
|
36
|
-
config.before do
|
37
|
-
ARGV.replace []
|
38
|
-
end
|
39
|
-
|
40
|
-
config.expect_with :rspec do |c|
|
41
|
-
c.syntax = :expect
|
42
|
-
end
|
43
|
-
|
44
|
-
# def capture(stream)
|
45
|
-
# begin
|
46
|
-
# stream = stream.to_s
|
47
|
-
# eval "$#{stream} = StringIO.new"
|
48
|
-
# yield
|
49
|
-
# result = eval("$#{stream}").string
|
50
|
-
# ensure
|
51
|
-
# eval("$#{stream} = #{stream.upcase}")
|
52
|
-
# end
|
53
|
-
|
54
|
-
# result
|
55
|
-
# end
|
56
|
-
|
57
|
-
def source_root
|
58
|
-
File.join(File.dirname(__FILE__), "fixtures")
|
59
|
-
end
|
60
|
-
|
61
|
-
def destination_root
|
62
|
-
File.join(File.dirname(__FILE__), "sandbox")
|
63
|
-
end
|
64
|
-
|
65
|
-
def silence_warnings
|
66
|
-
old_verbose = $VERBOSE
|
67
|
-
$VERBOSE = nil
|
68
|
-
yield
|
69
|
-
ensure
|
70
|
-
$VERBOSE = old_verbose
|
71
|
-
end
|
72
|
-
|
73
|
-
# true if running on windows, used for conditional spec skips
|
74
|
-
#
|
75
|
-
# @return [TrueClass/FalseClass]
|
76
|
-
def windows?
|
77
|
-
Gem.win_platform?
|
78
|
-
end
|
79
|
-
|
80
|
-
# alias silence capture
|
81
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
class TestClass
|
4
|
-
def initialize
|
5
|
-
@backoff_counter = 0
|
6
|
-
@options = { backoff_limit: 5 }
|
7
|
-
end
|
8
|
-
|
9
|
-
def test_method
|
10
|
-
raise Aws::RDS::Errors::ServiceError.new(nil, 'service error')
|
11
|
-
end
|
12
|
-
|
13
|
-
def reset_backoff
|
14
|
-
@backoff_counter = 0
|
15
|
-
end
|
16
|
-
|
17
|
-
def backoff
|
18
|
-
@backoff_counter = @backoff_counter + 1
|
19
|
-
|
20
|
-
raise StandardError, 'gave up' if @options[:backoff_limit] > 0 && @options[:backoff_limit] < @backoff_counter
|
21
|
-
end
|
22
|
-
|
23
|
-
extend RdsRotateDbSnapshots::ActionWrappers
|
24
|
-
|
25
|
-
with_backoff :test_method
|
26
|
-
end
|
27
|
-
|
28
|
-
describe RdsRotateDbSnapshots::ActionWrappers do
|
29
|
-
subject { TestClass.new }
|
30
|
-
|
31
|
-
describe "#with_backoff" do
|
32
|
-
it "does not retry if the exception raised is Aws::RDS::Errors::ExpiredToken" do
|
33
|
-
allow(subject).to receive(:test_method).and_raise(Aws::RDS::Errors::ExpiredToken.new(nil, 'token expired'))
|
34
|
-
expect(subject).not_to receive(:reset_backoff)
|
35
|
-
expect(subject).not_to receive(:backoff)
|
36
|
-
expect{subject.test_method}.to raise_error(Aws::RDS::Errors::ExpiredToken)
|
37
|
-
end
|
38
|
-
|
39
|
-
it "retries if the exception raised is Aws::RDS::Errors::ServiceError" do
|
40
|
-
expect(subject).to receive(:backoff).exactly(6).and_call_original
|
41
|
-
|
42
|
-
expect{ subject.test_method }.to raise_error(StandardError, 'gave up')
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,96 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
RSpec.shared_examples 'rds_rotate_db_snapshots actions' do
|
4
|
-
let(:rds_client) { instance_double(RdsRotateDbSnapshots::RdsClient) }
|
5
|
-
let(:client) { rds_client }
|
6
|
-
let(:options) do
|
7
|
-
{
|
8
|
-
aws_access_key: 'ACCESS_KEY',
|
9
|
-
aws_secret_access_key: 'SECRET_KEY',
|
10
|
-
aws_region: 'REGION',
|
11
|
-
pattern: 'test',
|
12
|
-
dry_run: true,
|
13
|
-
backoff_limit: 15
|
14
|
-
}
|
15
|
-
end
|
16
|
-
let(:rds_snapshots) do
|
17
|
-
[
|
18
|
-
{ snapshot_create_time: Time.now, db_instance_identifier: 'test_db', db_snapshot_identifier: 'test_snapshot' }
|
19
|
-
]
|
20
|
-
end
|
21
|
-
|
22
|
-
before do
|
23
|
-
allow(subject).to receive(:client).and_return(rds_client)
|
24
|
-
allow(rds_client).to receive(:describe_db_snapshots).and_return(rds_snapshots)
|
25
|
-
allow(rds_client).to receive(:create_db_snapshot)
|
26
|
-
allow(rds_client).to receive(:delete_db_snapshot)
|
27
|
-
end
|
28
|
-
|
29
|
-
describe "#rotate_em" do
|
30
|
-
it "deletes the snapshots that are not part of the specified time periods" do
|
31
|
-
expect(rds_client).to receive(:delete_db_snapshot)
|
32
|
-
subject.rotate_em(rds_snapshots)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "#create_snapshot" do
|
37
|
-
it "creates a snapshot with the specified name" do
|
38
|
-
expect(rds_client).to receive(:create_db_snapshot)
|
39
|
-
subject.create_snapshot('test', ['test_db'])
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe "#get_db_snapshots" do
|
44
|
-
let(:snapshots) { double('snapshots', db_snapshots: rds_snapshots) }
|
45
|
-
|
46
|
-
it "returns the list of snapshots from the client" do
|
47
|
-
allow(snapshots).to receive(:[]).with(:marker).and_return(nil)
|
48
|
-
expect(rds_client).to receive(:describe_db_snapshots).and_return(snapshots)
|
49
|
-
snapshots = subject.get_db_snapshots(options)
|
50
|
-
expect(snapshots).to eq(rds_snapshots)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
end
|
55
|
-
|
56
|
-
class Test
|
57
|
-
include RdsRotateDbSnapshots::Actions
|
58
|
-
|
59
|
-
attr_reader :options
|
60
|
-
|
61
|
-
def initialize(script_name: nil, cli: false, options: {})
|
62
|
-
@script_name = script_name
|
63
|
-
@options = options
|
64
|
-
@cli = cli
|
65
|
-
parse_options if cli?
|
66
|
-
@backoff_counter = 0
|
67
|
-
end
|
68
|
-
|
69
|
-
def reset_backoff
|
70
|
-
@backoff_counter = 0
|
71
|
-
end
|
72
|
-
|
73
|
-
def time_periods
|
74
|
-
@options[:time_periods] || {}
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
def cli?
|
80
|
-
!!@cli
|
81
|
-
end
|
82
|
-
|
83
|
-
def parse_options
|
84
|
-
@options = RdsRotateDbSnapshots::OptionsParser.new(script_name: @script_name, cli: @cli).parse!
|
85
|
-
end
|
86
|
-
|
87
|
-
def backoff
|
88
|
-
@backoff_counter = @backoff_counter + 1
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
describe RdsRotateDbSnapshots::Actions do
|
93
|
-
subject { Test.new }
|
94
|
-
|
95
|
-
it_behaves_like 'rds_rotate_db_snapshots actions'
|
96
|
-
end
|
@@ -1,33 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe RdsRotateDbSnapshots::OptionsParser do
|
4
|
-
let(:script_name) { "rds_rotate_snapshots.rb" }
|
5
|
-
subject { RdsRotateDbSnapshots::OptionsParser.new(script_name: script_name, cli: true).parse! }
|
6
|
-
|
7
|
-
describe "#parse!" do
|
8
|
-
before { ARGV.clear }
|
9
|
-
|
10
|
-
it "parses options correctly" do
|
11
|
-
ARGV.concat(["--aws-access-key", "ACCESS_KEY",
|
12
|
-
"--aws-secret-access-key", "SECRET_KEY",
|
13
|
-
"--aws-region", "REGION",
|
14
|
-
"--pattern", "PATTERN",
|
15
|
-
"--backoff-limit", "20",
|
16
|
-
"--create-snapshot", "snapshot"])
|
17
|
-
options = subject
|
18
|
-
|
19
|
-
expect(options[:aws_access_key]).to eq("ACCESS_KEY")
|
20
|
-
expect(options[:aws_secret_access_key]).to eq("SECRET_KEY")
|
21
|
-
expect(options[:aws_region]).to eq("REGION")
|
22
|
-
expect(options[:pattern]).to eq("PATTERN")
|
23
|
-
expect(options[:backoff_limit]).to eq("20")
|
24
|
-
expect(options[:create_snapshot]).to eq("snapshot")
|
25
|
-
end
|
26
|
-
|
27
|
-
it "raises NotImplementedError when by-tags option is passed and it is not implemented" do
|
28
|
-
ARGV.concat(["--by-tags", "tag=value,tag2=value"])
|
29
|
-
|
30
|
-
expect { subject }.to raise_error(RdsRotateDbSnapshots::OptionsParser::NotImplementedError)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
require 'helper'
|
2
|
-
|
3
|
-
describe RdsRotateDbSnapshots::RdsClient do
|
4
|
-
let(:options) { {
|
5
|
-
:aws_access_key => "ACCESS_KEY",
|
6
|
-
:aws_secret_access_key => "SECRET_KEY",
|
7
|
-
:aws_session_token => "SESSION_TOKEN",
|
8
|
-
:aws_region => "REGION"
|
9
|
-
} }
|
10
|
-
let(:rds_client) { RdsRotateDbSnapshots::RdsClient.new(options) }
|
11
|
-
|
12
|
-
it 'configures the client with the correct credentials and region' do
|
13
|
-
expect(rds_client.instance_variable_get(:@client).config.credentials).
|
14
|
-
to have_attributes(access_key_id: "ACCESS_KEY", secret_access_key: "SECRET_KEY", session_token: "SESSION_TOKEN")
|
15
|
-
expect(rds_client.instance_variable_get(:@client).config.region).to eq("REGION")
|
16
|
-
end
|
17
|
-
|
18
|
-
it 'delegates describe_db_snapshots method to the @client object' do
|
19
|
-
expect(rds_client.instance_variable_get(:@client)).to receive(:describe_db_snapshots)
|
20
|
-
rds_client.describe_db_snapshots
|
21
|
-
end
|
22
|
-
|
23
|
-
it 'delegates create_db_snapshot method to the @client object' do
|
24
|
-
expect(rds_client.instance_variable_get(:@client)).to receive(:create_db_snapshot)
|
25
|
-
rds_client.create_db_snapshot("test-snapshot")
|
26
|
-
end
|
27
|
-
|
28
|
-
it 'delegates delete_db_snapshot method to the @client object' do
|
29
|
-
expect(rds_client.instance_variable_get(:@client)).to receive(:delete_db_snapshot)
|
30
|
-
rds_client.delete_db_snapshot("test-snapshot")
|
31
|
-
end
|
32
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
require "helper"
|
2
|
-
|
3
|
-
describe RdsRotateDbSnapshots do
|
4
|
-
subject { described_class.new(script_name: script_name, cli: cli) }
|
5
|
-
|
6
|
-
let(:script_name) { "test" }
|
7
|
-
let(:cli) { true }
|
8
|
-
before do
|
9
|
-
allow(Aws::RDS::Client).to receive(:new)
|
10
|
-
end
|
11
|
-
|
12
|
-
describe "on include" do
|
13
|
-
it "adds action methods to the base class" do
|
14
|
-
expect(described_class.instance_methods).to include(:rotate_em)
|
15
|
-
expect(described_class.instance_methods).to include(:create_snapshot)
|
16
|
-
expect(described_class.instance_methods).to include(:get_db_snapshots)
|
17
|
-
expect(described_class.instance_methods).to include(:rotate_by_tags)
|
18
|
-
expect(described_class.instance_methods).to include(:client)
|
19
|
-
expect(described_class.instance_methods).to include(:time_periods)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
describe "#client" do
|
24
|
-
it "returns an RdsClient" do
|
25
|
-
expect(subject.client).to be_a(RdsRotateDbSnapshots::RdsClient)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
describe "#rds_client" do
|
30
|
-
it "returns an RdsClient" do
|
31
|
-
expect(subject.rds_client).to be_a(RdsRotateDbSnapshots::RdsClient)
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
describe "#reset_backoff" do
|
36
|
-
it "resets backoff counter" do
|
37
|
-
subject.instance_variable_set(:@backoff_counter, 1)
|
38
|
-
subject.reset_backoff
|
39
|
-
expect(subject.instance_variable_get(:@backoff_counter)).to eq(0)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe "#time_periods" do
|
44
|
-
it "returns time periods" do
|
45
|
-
expect(subject.time_periods).to eq(
|
46
|
-
:daily=>{:format=>"%Y-%m-%d", :keep=>0, :keeping=>{}, :seconds=>86400},
|
47
|
-
:hourly => {:format=>"%Y-%m-%d-%H", :keep=>0, :keeping=>{}, :seconds=>3600},
|
48
|
-
:monthly => {:format=>"%Y-%m", :keep=>0, :keeping=>{}, :seconds=>2592000},
|
49
|
-
:weekly => {:format=>"%Y-%W", :keep=>0, :keeping=>{}, :seconds=>604800},
|
50
|
-
:yearly=>{:format=>"%Y", :keep=>0, :keeping=>{}, :seconds=>31104000}
|
51
|
-
)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
describe "#backoff" do
|
56
|
-
it "backs off" do
|
57
|
-
subject.instance_variable_set(:@backoff_counter, 1)
|
58
|
-
expect(subject).to receive(:sleep)
|
59
|
-
subject.send(:backoff)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|