rds-rotate-db-snapshots 0.4.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a666ea612ccbf5f055bebe3df9ac6f8c303e8cd6c5e56f2b389828aa2ed08e9a
4
- data.tar.gz: a75dbd3e2c5d9652700de2d1ec00d68ab93eda719a8c634f6edcf6843b431b74
3
+ metadata.gz: 7ddfaa42b87e8ad4f9cabafcea086ce82d0f351068d05db7da97b53fa5080a0f
4
+ data.tar.gz: dac01f0570c16a5a5a205253f0ca3e6cd26d7f7ed22377da3f9f1bad2e99d87b
5
5
  SHA512:
6
- metadata.gz: 5c0f1b569236f25233329e26b5de932fd2ff876695db1f413e04cfd8833c9743cb2d3eb38115da783be95388c441252a691a10f2082e50df9f5686d4a770e1ae
7
- data.tar.gz: d545f978528f5f176b6ce440fc42aec92d9ff07bb41a510cae1aae4834897e7a1c016f37645d35ae317fbbbaae168def57e18661bfe865b5f65801e790080d41
6
+ metadata.gz: 4d5d586b5ff033df7c7a4d131259d7f47b1f30e2592b616fc0e6644fcbd4999d0e24815dba96c9d335e8d6e35446293bee2b59f83ab286eda4a32a3f39cbaf38
7
+ data.tar.gz: 5f9339752c7a963981f1d6be97ed321a0897f8028d52f632b6dae0fb81e2e495327e127f7dec3d4040b35610ad3a7c56d9206b8220a376485c87f4372fe88470
@@ -2,16 +2,16 @@ name: "CI"
2
2
 
3
3
  on:
4
4
  push:
5
- branches: ["master"]
5
+ branches: ["main"]
6
6
  pull_request:
7
- branches: ["master"]
7
+ branches: ["main"]
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, 'jruby']
14
+ ruby_version: [2.7, 3.0, 3.1]
15
15
  steps:
16
16
  - name: Checkout code
17
17
  uses: actions/checkout@v3
@@ -25,4 +25,19 @@ jobs:
25
25
  - name: Bundle Install
26
26
  run: bundle install
27
27
  - name: Test
28
- run: bundle exec rake test
28
+ run: bundle exec rspec
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
@@ -13,10 +13,10 @@ name: "CodeQL"
13
13
 
14
14
  on:
15
15
  push:
16
- branches: [ "master" ]
16
+ branches: [ "main" ]
17
17
  pull_request:
18
18
  # The branches below must be a subset of the branches above
19
- branches: [ "master" ]
19
+ branches: [ "main" ]
20
20
  schedule:
21
21
  - cron: '22 4 * * 2'
22
22
 
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -w --color
data/Gemfile CHANGED
@@ -1,20 +1,19 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gem 'aws-sdk-rds', '~> 1'
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
4
+ gem "rake"
11
5
 
12
6
  group :development do
13
7
  gem 'juwelier'
8
+ gem "pry"
9
+ gem "pry-byebug"
14
10
  end
15
11
 
16
12
  group :test do
17
- gem 'rake'
18
- gem 'shoulda'
19
- gem 'minitest'
13
+ gem "rspec", ">= 3.2"
14
+ gem "rspec-mocks", ">= 3"
15
+ gem "rubocop", "~> 0.50.0"
16
+ gem "simplecov", ">= 0.13"
17
+ gem 'simplecov-lcov', '~> 0.8.0'
18
+ gem "webmock"
20
19
  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) [![CI](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml/badge.svg?query=branch%3Amaster+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml?query=branch%3Amaster+event%3Apush) [![CodeQL](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml/badge.svg?query=branch%3Amaster+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml?query=branch%3Amaster+event%3Apush)
4
+ Version" />](https://badge.fury.io/rb/rds-rotate-db-snapshots) [![CI](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml/badge.svg?query=branch%3Amain+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/ci.yml?query=branch%3Amain+event%3Apush) [![CodeQL](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml/badge.svg?query=branch%3Amain+event%3Apush)](https://github.com/serg-kovalev/rds-rotate-db-snapshots/actions/workflows/codeql.yml?query=branch%3Amain+event%3Apush)
5
5
 
6
6
  Provides a simple way to rotate db snapshots in Amazon Relational Database
7
7
  Service (RDS).
@@ -11,7 +11,6 @@ Service (RDS).
11
11
  - 2.7
12
12
  - 3.1
13
13
  - 3.2
14
- - jruby
15
14
 
16
15
  ## Usage
17
16
 
@@ -65,7 +64,7 @@ show the messages.
65
64
 
66
65
  ## Contributing to rds-rotate-db-snapshots
67
66
 
68
- - Check out the latest master to make sure the feature hasn't been
67
+ - Check out the latest main to make sure the feature hasn't been
69
68
  implemented or the bug hasn't been fixed yet
70
69
  - Check out the issue tracker to make sure someone already hasn't requested
71
70
  it and/or contributed it
data/Rakefile CHANGED
@@ -22,21 +22,6 @@ 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
-
40
25
  require 'rdoc/task'
41
26
  Rake::RDocTask.new do |rdoc|
42
27
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.3
1
+ 0.5.0
@@ -1,200 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'rubygems'
4
- require 'aws-sdk-rds'
5
- require 'optparse'
3
+ require_relative '../lib/rds_rotate_db_snapshots'
6
4
 
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
- }
5
+ rrds = RdsRotateDbSnapshots.new(script_name: File.basename($0), cli: true)
18
6
 
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
86
- end
87
-
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
7
+ if rrds.options[:aws_access_key].nil? || rrds.options[:aws_secret_access_key].nil?
8
+ puts "You must specify your Amazon credentials via --aws-access-key and --aws-secret_access-key and --aws-session-token"
9
+ exit 1
130
10
  end
131
11
 
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"
12
+ if rrds.options[:aws_region].nil?
13
+ puts "You must specify your AWS Region via --aws-region"
189
14
  exit 1
190
15
  end
191
16
 
192
- if ARGV.empty? and $opts[:by_tags].nil?
17
+ if ARGV.empty? and rrds.options[:by_tags].nil?
193
18
  puts "You must provide at least one DB Indentifier when not rotating by tags"
194
19
  exit 1
195
20
  end
196
21
 
197
- if $opts[:by_tags].nil?
22
+ if rrds.options[:by_tags].nil?
198
23
  db_indentifier_ids = ARGV
199
24
 
200
25
  db_indentifier_ids.each do |db_id|
@@ -208,83 +33,29 @@ else
208
33
  if !ARGV.empty?
209
34
  puts "Ignoring supplied db_indentifier_ids because we're rotating by tags."
210
35
  end
211
- if $opts[:by_tags].length == 0
36
+ if rrds.options[:by_tags].length == 0
212
37
  puts "Rotating by tags but no tags specified? Refusing to rotate all snapshots!"
213
38
  exit 1
214
39
  end
215
40
  end
216
41
 
217
- if $opts[:backoff_limit] < 0
42
+ if rrds.options[:backoff_limit] < 0
218
43
  puts "A negative backoff limit doesn't make much sense."
219
44
  exit 1
220
45
  end
221
46
 
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)
47
+ if rrds.options[:create_snapshot]
48
+ rrds.create_snapshot(rrds.options[:create_snapshot], db_indentifier_ids)
238
49
  end
239
50
 
240
51
  all_snapshots = []
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)
52
+ if rrds.options[:by_tags]
53
+ rrds.rotate_by_tags
276
54
  else
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
-
55
+ snapshots = rrds.get_db_snapshots(snapshot_type: 'manual').delete_if{ |e| e[:status] != 'available' }
285
56
  db_indentifier_ids.each do |db_id|
286
- rotate_em(
287
- all_snapshots.select {|ss| ss[:db_instance_identifier] == db_id }.
57
+ rrds.rotate_em(
58
+ snapshots.select {|ss| ss[:db_instance_identifier] == db_id }.
288
59
  sort {|a,b| a[:snapshot_create_time] <=> b[:snapshot_create_time] }
289
60
  )
290
61
  end
@@ -0,0 +1,23 @@
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 => e
13
+ backoff
14
+ retry
15
+ end
16
+
17
+ end
18
+ end
19
+ self.prepend wrapper
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,105 @@
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
@@ -0,0 +1,110 @@
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
+
99
+ def split_tag(hash,v)
100
+ v.split(',').each do |pair|
101
+ tag, value = pair.split('=',2)
102
+ if value.nil?
103
+ raise InvalidArgument, "invalid tag=value format"
104
+ end
105
+ hash[tag] = value
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,58 @@
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
@@ -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.4.3 ruby lib
5
+ # stub: rds-rotate-db-snapshots 0.5.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.4.3"
9
+ s.version = "0.5.0"
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 = "2022-12-26"
14
+ s.date = "2023-01-10"
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,16 +24,21 @@ Gem::Specification.new do |s|
24
24
  ".github/dependabot.yml",
25
25
  ".github/workflows/ci.yml",
26
26
  ".github/workflows/codeql.yml",
27
+ ".rspec",
27
28
  "Gemfile",
28
29
  "LICENSE.txt",
29
30
  "README.md",
30
31
  "Rakefile",
31
32
  "VERSION",
32
33
  "bin/rds-rotate-db-snapshots",
33
- "lib/.empty",
34
+ "lib/rds_rotate_db_snapshots.rb",
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",
34
39
  "rds-rotate-db-snapshots.gemspec",
35
- "test/helper.rb",
36
- "test/test_rds-rotate-db-snapshots.rb"
40
+ "spec/helper.rb",
41
+ "spec/rds_rotate_db_snapshots_spec.rb"
37
42
  ]
38
43
  s.homepage = "http://github.com/serg-kovalev/rds-rotate-db-snapshots".freeze
39
44
  s.licenses = ["MIT".freeze]
@@ -46,14 +51,16 @@ Gem::Specification.new do |s|
46
51
 
47
52
  if s.respond_to? :add_runtime_dependency then
48
53
  s.add_runtime_dependency(%q<aws-sdk-rds>.freeze, ["~> 1"])
49
- s.add_development_dependency(%q<bundler>.freeze, [">= 0"])
50
- s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
54
+ s.add_runtime_dependency(%q<rake>.freeze, [">= 0"])
51
55
  s.add_development_dependency(%q<juwelier>.freeze, [">= 0"])
56
+ s.add_development_dependency(%q<pry>.freeze, [">= 0"])
57
+ s.add_development_dependency(%q<pry-byebug>.freeze, [">= 0"])
52
58
  else
53
59
  s.add_dependency(%q<aws-sdk-rds>.freeze, ["~> 1"])
54
- s.add_dependency(%q<bundler>.freeze, [">= 0"])
55
- s.add_dependency(%q<simplecov>.freeze, [">= 0"])
60
+ s.add_dependency(%q<rake>.freeze, [">= 0"])
56
61
  s.add_dependency(%q<juwelier>.freeze, [">= 0"])
62
+ s.add_dependency(%q<pry>.freeze, [">= 0"])
63
+ s.add_dependency(%q<pry-byebug>.freeze, [">= 0"])
57
64
  end
58
65
  end
59
66
 
data/spec/helper.rb ADDED
@@ -0,0 +1,81 @@
1
+ $TESTING = true
2
+
3
+ require "simplecov"
4
+
5
+ SimpleCov.start do
6
+ add_filter "/spec"
7
+ # minimum_coverage(70)
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
@@ -0,0 +1,62 @@
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
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.3
4
+ version: 0.5.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: 2022-12-26 00:00:00.000000000 Z
11
+ date: 2023-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-rds
@@ -25,7 +25,21 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: juwelier
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
@@ -39,7 +53,7 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: simplecov
56
+ name: pry
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
@@ -53,7 +67,7 @@ dependencies:
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
- name: juwelier
70
+ name: pry-byebug
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - ">="
@@ -80,16 +94,21 @@ files:
80
94
  - ".github/dependabot.yml"
81
95
  - ".github/workflows/ci.yml"
82
96
  - ".github/workflows/codeql.yml"
97
+ - ".rspec"
83
98
  - Gemfile
84
99
  - LICENSE.txt
85
100
  - README.md
86
101
  - Rakefile
87
102
  - VERSION
88
103
  - bin/rds-rotate-db-snapshots
89
- - lib/.empty
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
90
109
  - rds-rotate-db-snapshots.gemspec
91
- - test/helper.rb
92
- - test/test_rds-rotate-db-snapshots.rb
110
+ - spec/helper.rb
111
+ - spec/rds_rotate_db_snapshots_spec.rb
93
112
  homepage: http://github.com/serg-kovalev/rds-rotate-db-snapshots
94
113
  licenses:
95
114
  - MIT
data/lib/.empty DELETED
@@ -1 +0,0 @@
1
- Juwelier doesn't like me not having a lib/ directory.
data/test/helper.rb DELETED
@@ -1,18 +0,0 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- require 'minitest'
4
- require 'shoulda'
5
-
6
- begin
7
- Bundler.setup(:default, :development)
8
- rescue Bundler::BundlerError => e
9
- $stderr.puts e.message
10
- $stderr.puts "Run `bundle install` to install missing gems"
11
- exit e.status_code
12
- end
13
-
14
- $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
- $LOAD_PATH.unshift(File.dirname(__FILE__))
16
-
17
- class Minitest::Test
18
- end
@@ -1,7 +0,0 @@
1
- require_relative 'helper'
2
-
3
- class TestRdsRotateDbSnapshots < Minitest::Test
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
6
- end
7
- end