rds-rotate-db-snapshots 0.4.3 → 0.5.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 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