roadworker 0.5.10 → 0.5.15

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: 2287d1ae8a74127c9f8f71d4e4a0275dc7c6173d8ab1c2b2eb3616cecf6fd810
4
- data.tar.gz: 306329875040ef860bbbc3617cd338351a45b01a979711fd879fef1ebb69c93d
3
+ metadata.gz: 163707e7a36317de25bda9f1c0af701729fbf6cbb67bcabf51f7fed86e5f97a9
4
+ data.tar.gz: 044bbcd3a3c89be69d86be7b16f183588865770122001023ad11576409408267
5
5
  SHA512:
6
- metadata.gz: ac2d71629ae6793a17262eecddce58b7a0a72663d3a2395a28cfc114e903b275c749ae4695470f30eb827a7ca7a92b31b0aa7eda73dcb6c1664465d9da3cbc04
7
- data.tar.gz: 5029bf6a42d78e37025aad78cfcfdec63bde9ab100e11da2969114529bd6b5e0c0a54047945da99ca9cae93ae11228f3fd0049ede8d6202e4e6e26b3c6e3e43b
6
+ metadata.gz: 7049cb4fb2254a360eafe3f54a53d923faa1d042e7533aab9c54d61a06899d7bc55805263ae672bf01d17c880af32ebe8a5afe2c7e2fa1ec2efd653942473696
7
+ data.tar.gz: c323c0eef4acebe24f9dc04e261e4434a0c814913d678ba563aa93a47a0d0c5f741bca039767314ef023cb140abeae9e623e0c47ec5356aec0736d828849b797
data/README.md CHANGED
@@ -10,22 +10,7 @@ It defines the state of Route53 using DSL, and updates Route53 according to DSL.
10
10
 
11
11
  **Notice**
12
12
 
13
- * Roadworker cannot update TTL of two or more same weighted A records (with different SetIdentifier) after creation.
14
- * `>= 0.4.3` compare resource records ignoring the order.
15
- * `>= 0.5.5`
16
- * **Disable Divided HostedZone**
17
- * **Use aws-sdk v2** [PR#20](https://github.com/winebarrel/roadworker/pull/20)
18
- * Support Cross Account ELB Alias [PR#21](https://github.com/winebarrel/roadworker/pull/21)
19
- * `>= 0.5.6`
20
- * Disable HealthCheck GC (pass `--health-check-gc` option if enable)
21
- * Support Calculated Health Checks
22
- * Support New Health Check attributes
23
- * Add template feature
24
- * `>= 0.5.7`
25
- * Fix for `dualstack` prefix
26
- * Use constant for CanonicalHostedZoneNameID
27
- * `>= 0.5.9`
28
- * Support CloudWatch Metrics Health Check
13
+ Roadworker cannot update TTL of two or more same weighted A records (with different SetIdentifier) after creation.
29
14
 
30
15
  ## Installation
31
16
 
data/lib/roadworker.rb CHANGED
@@ -17,6 +17,7 @@ require 'roadworker/log'
17
17
  require 'roadworker/utils'
18
18
  require 'roadworker/template-helper'
19
19
 
20
+ require 'roadworker/batch'
20
21
  require 'roadworker/client'
21
22
  require 'roadworker/collection'
22
23
  require 'roadworker/dsl'
@@ -0,0 +1,375 @@
1
+ module Roadworker
2
+ class Batch
3
+ include Log
4
+
5
+ # @param [Roadworker::Route53Wrapper::HostedzoneWrapper] hosted_zone
6
+ # @param [Roadworker::HealthCheck] health_checks
7
+ def initialize(hosted_zone, dry_run:, logger:, health_checks:)
8
+ @hosted_zone = hosted_zone
9
+ @dry_run = dry_run
10
+ @logger = logger
11
+ @health_checks = health_checks
12
+
13
+ @operations = []
14
+ end
15
+
16
+ attr_reader :hosted_zone, :health_checks
17
+ attr_reader :dry_run, :logger
18
+ attr_reader :operations
19
+
20
+ # @param [OpenStruct] rrset Roadworker::DSL::ResourceRecordSet#result
21
+ def create(rrset)
22
+ add_operation Create, rrset
23
+ end
24
+
25
+ # @param [OpenStruct] rrset Roadworker::DSL::ResourceRecordSet#result
26
+ def update(rrset)
27
+ add_operation Update, rrset
28
+ end
29
+
30
+ # @param [Roadworker::Route53Wrapper::ResourceRecordSetWrapper] rrset
31
+ def delete(rrset)
32
+ add_operation Delete, rrset
33
+ end
34
+
35
+ # @param [Aws::Route53::Client] route53
36
+ # @return [Boolean] updated
37
+ def request!(route53)
38
+ sorted_operations = operations.sort_by(&:sort_key)
39
+
40
+ batches = slice_operations(sorted_operations)
41
+ batches.each_with_index do |batch, i|
42
+ dispatch_batch!(route53, batch, i, batches.size)
43
+ end
44
+
45
+ sorted_operations.any? { |op| !op.changes.empty? }
46
+ end
47
+
48
+ def inspect
49
+ "#<#{self.class.name}: #{operations.size} operations>"
50
+ end
51
+
52
+ def to_s
53
+ inspect
54
+ end
55
+
56
+ private
57
+
58
+ def dispatch_batch!(route53, batch, i, total)
59
+ changes = batch.flat_map(&:changes)
60
+ return if changes.empty?
61
+
62
+ page = total > 1 ? " | #{i+1}/#{total}" : nil
63
+ log(:info, "=== Change batch: #{hosted_zone.name} | #{hosted_zone.id}#{hosted_zone.vpcs.empty? ? '' : ' - private'}#{page}", :bold)
64
+ batch.each do |operation|
65
+ operation.diff!()
66
+ end
67
+
68
+ if dry_run
69
+ log(:info, "---", :bold, dry_run: false)
70
+ else
71
+ change = route53.change_resource_record_sets(
72
+ hosted_zone_id: hosted_zone.id,
73
+ change_batch: {
74
+ changes: changes,
75
+ },
76
+ )
77
+ log(:info, "--> Change submitted: #{change.change_info.id}", :bold)
78
+ end
79
+ log(:info, "", :bold, dry_run: false)
80
+ end
81
+
82
+ # Slice operations to batches, per 32,000 characters in "Value" or per 1,000 operations.
83
+ def slice_operations(ops)
84
+ total_value_size = 0
85
+ total_ops = 0
86
+ ops.slice_before do |op|
87
+ total_value_size += op.value_size
88
+ total_ops += op.op_size
89
+ if total_value_size > 32000 || total_ops > 1000
90
+ total_value_size = op.value_size
91
+ total_ops = op.op_size
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end.to_a
97
+ end
98
+
99
+ def add_operation(klass, rrset)
100
+ assert_record_name rrset
101
+ operations << klass.new(hosted_zone, rrset, health_checks: health_checks, dry_run: dry_run, logger: logger)
102
+ self
103
+ end
104
+
105
+ def assert_record_name(record)
106
+ unless record.name.downcase.sub(/\.$/,'').end_with?(hosted_zone.name.sub(/\.$/,''))
107
+ raise ArgumentError, "#{record.name.inspect} isn't under hosted zone name #{hosted_zone.name.inspect}"
108
+ end
109
+ end
110
+
111
+ class Operation
112
+ include Log
113
+
114
+ # @param [Roadworker::Route53Wrapper::HostedzoneWrapper] hosted_zone
115
+ # @param [Roadworker::DSL::ResourceRecordSet] rrset
116
+ # @param [Roadworker::HealthCheck] health_checks
117
+ # @param [Logger] logger
118
+ def initialize(hosted_zone, rrset, health_checks:, dry_run:, logger:)
119
+ @hosted_zone = hosted_zone
120
+ @rrset = rrset
121
+ @health_checks = health_checks
122
+ @dry_run = dry_run
123
+ @logger = logger
124
+ end
125
+
126
+ attr_reader :hosted_zone, :rrset
127
+ attr_reader :health_checks
128
+ attr_reader :dry_run, :logger
129
+
130
+ def sort_key
131
+ # See Operation#cname_first?
132
+ cname_precedence = if rrset.type == 'CNAME'
133
+ cname_first? ? 0 : 2
134
+ else
135
+ 1
136
+ end
137
+ # Alias target may be created in the same change batch. Let's do operations for non-alias records first.
138
+ alias_precedence = if rrset.dns_name
139
+ 1
140
+ else
141
+ 0
142
+ end
143
+ [rrset.name, cname_precedence, alias_precedence, rrset.type, rrset.set_identifier]
144
+ end
145
+
146
+ # CNAME should always be created/updated later, as CNAME doesn't permit other records
147
+ # See also Roadworker::Batch::Delete#cname_first?
148
+ def cname_first?
149
+ false
150
+ end
151
+
152
+ # Count total length of RR "Value" included in changes
153
+ # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests-changeresourcerecordsets
154
+ #
155
+ # See also: Batch#slice_operations
156
+ # @return [Integer]
157
+ def value_size
158
+ changes.map do |change|
159
+ upsert_multiplier = change[:action] == 'UPSERT' ? 2 : 1
160
+ rrset = change[:resource_record_set]
161
+ next 0 unless rrset
162
+ rrs = rrset[:resource_records]
163
+ next 0 unless rrs
164
+ (rrs.map { |_| _[:value]&.size || 0 }.sum) * upsert_multiplier
165
+ end.sum || 0
166
+ end
167
+
168
+ # Count of operational size
169
+ # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests-changeresourcerecordsets
170
+ #
171
+ # See also: Batch#slice_operations
172
+ # @return [Integer]
173
+ def op_size
174
+ changes.map do |change|
175
+ change[:action] == 'UPSERT' ? 2 : 1
176
+ end.sum || 0
177
+ end
178
+
179
+ # @return [Array<Hash>]
180
+ def changes
181
+ raise NotImplementedError
182
+ end
183
+
184
+ # @return [Hash]
185
+ def desired_rrset
186
+ raise NotImplementedError
187
+ end
188
+
189
+ # @return [Roadworker::Route53Wrapper::ResourceRecordSetWrapper]
190
+ def present_rrset
191
+ hosted_zone.find_resource_record_set(rrset.name, rrset.type, rrset.set_identifier) or raise "record not present"
192
+ end
193
+
194
+ def diff!(dry_run: false)
195
+ raise NotImplementedError
196
+ end
197
+
198
+ def inspect
199
+ "#<#{self.class.name} @changes=#{changes.inspect}>"
200
+ end
201
+
202
+ def to_s
203
+ inspect
204
+ end
205
+
206
+ private
207
+
208
+ # @param [String] dns_name
209
+ # @param [Hash] options
210
+ # @return [?]
211
+ def get_alias_target(dns_name, options)
212
+ Aws::Route53.dns_name_to_alias_target(dns_name, options, hosted_zone.id, hosted_zone.name)
213
+ end
214
+
215
+ # @param [?] health_check
216
+ # @return [?]
217
+ def get_health_check(check)
218
+ check ? health_checks.find_or_create(check) : nil
219
+ end
220
+
221
+ end
222
+
223
+ class Create < Operation
224
+ # @return [Hash]
225
+ def desired_rrset
226
+ return @new_rrset if defined? @new_rrset
227
+ @new_rrset = {
228
+ name: rrset.name,
229
+ type: rrset.type,
230
+ }
231
+
232
+ Route53Wrapper::RRSET_ATTRS.each do |attribute|
233
+ value = rrset.send(attribute)
234
+ next unless value
235
+
236
+ case attribute
237
+ when :dns_name
238
+ attribute = :alias_target
239
+ dns_name, dns_name_opts = value
240
+ value = get_alias_target(dns_name, dns_name_opts)
241
+ when :health_check
242
+ attribute = :health_check_id
243
+ value = get_health_check(value)
244
+ end
245
+
246
+ @new_rrset[attribute] = value
247
+ end
248
+
249
+ @new_rrset
250
+ end
251
+
252
+ def changes
253
+ [
254
+ {
255
+ action: 'CREATE',
256
+ resource_record_set: desired_rrset.to_h,
257
+ },
258
+ ]
259
+ end
260
+
261
+ def diff!
262
+ log(:info, 'Create ResourceRecordSet', :cyan) do
263
+ "#{desired_rrset[:name]} #{desired_rrset[:type]}#{ desired_rrset[:set_identifier] && " (#{desired_rrset[:set_identifier]})" }"
264
+ end
265
+ end
266
+ end
267
+
268
+ class Delete < Operation
269
+ # CNAME should always be deleted first, as CNAME doesn't permit other records
270
+ def cname_first?
271
+ true
272
+ end
273
+
274
+ def hosted_zone_soa_or_ns?
275
+ (present_rrset.type == 'SOA' || present_rrset.type == 'NS') && hosted_zone.name == present_rrset.name
276
+ end
277
+
278
+ def changes
279
+ # Avoid deleting hosted zone SOA/NS
280
+ if hosted_zone_soa_or_ns?
281
+ return []
282
+ end
283
+
284
+ [
285
+ {
286
+ action: 'DELETE',
287
+ resource_record_set: present_rrset.to_h,
288
+ }
289
+ ]
290
+ end
291
+
292
+ def diff!
293
+ return if changes.empty?
294
+ log(:info, 'Delete ResourceRecordSet', :red) do
295
+ "#{present_rrset.name} #{present_rrset.type}#{ present_rrset.set_identifier && " (#{present_rrset.set_identifier})" }"
296
+ end
297
+ end
298
+ end
299
+
300
+
301
+ class Update < Operation
302
+ def desired_rrset
303
+ return @desired_rrset if defined? @desired_rrset
304
+ @desired_rrset = {name: rrset[:name]}
305
+
306
+ Route53Wrapper::RRSET_ATTRS_WITH_TYPE.each do |attribute|
307
+ value = rrset[attribute]
308
+ next unless value
309
+
310
+ case attribute
311
+ when :dns_name
312
+ dns_name, dns_name_opts = value
313
+ @desired_rrset[:alias_target] = get_alias_target(dns_name, dns_name_opts)
314
+ when :health_check
315
+ @desired_rrset[:health_check_id] = get_health_check(value)
316
+ else
317
+ @desired_rrset[attribute] = value
318
+ end
319
+ end
320
+
321
+ @desired_rrset
322
+ end
323
+
324
+ def changes
325
+ [
326
+ {
327
+ action: 'DELETE',
328
+ resource_record_set: present_rrset.to_h,
329
+ },
330
+ {
331
+ action: 'CREATE',
332
+ resource_record_set: desired_rrset.to_h,
333
+ },
334
+ ]
335
+ end
336
+
337
+ def diff!
338
+ log(:info, 'Update ResourceRecordSet', :green) do
339
+ "#{present_rrset.name} #{present_rrset.type}#{ present_rrset.set_identifier && " (#{present_rrset.set_identifier})" }"
340
+ end
341
+
342
+ # Note that desired_rrset is directly for Route 53, and present_record is also from Route 53
343
+ # Only given +rrset+ is brought from DSL, and dns_name & health_check is only valid in our DSL
344
+ Route53Wrapper::RRSET_ATTRS_WITH_TYPE.each do |attribute|
345
+ case attribute
346
+ when :dns_name
347
+ present = normalize_attribute_for_diff(attribute, present_rrset[:alias_target] && present_rrset[:alias_target][:dns_name])
348
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[:alias_target] && desired_rrset[:alias_target][:dns_name])
349
+ when :health_check
350
+ present = normalize_attribute_for_diff(attribute, present_rrset[:health_check_id])
351
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[:health_check_id])
352
+ else
353
+ present = normalize_attribute_for_diff(attribute, present_rrset[attribute])
354
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[attribute])
355
+ end
356
+
357
+ if desired != present
358
+ color = String.colorize # XXX:
359
+ log(:info, " #{attribute}:\n".green + Roadworker::Utils.diff(present, desired, color: color, indent: ' '), false)
360
+ end
361
+ end
362
+ end
363
+
364
+ private
365
+
366
+ def normalize_attribute_for_diff(attribute, value)
367
+ if value.is_a?(Array)
368
+ value = Aws::Route53.sort_rrset_values(attribute, value)
369
+ value = nil if value.empty?
370
+ end
371
+ value
372
+ end
373
+ end
374
+ end
375
+ end
@@ -20,8 +20,7 @@ module Roadworker
20
20
  if dsl.hosted_zones.empty? and not @options.force
21
21
  log(:warn, "Nothing is defined (pass `--force` if you want to remove)", :yellow)
22
22
  else
23
- walk_hosted_zones(dsl)
24
- updated = @options.updated
23
+ updated = walk_hosted_zones(dsl)
25
24
  end
26
25
 
27
26
  if updated and @options.health_check_gc
@@ -63,8 +62,10 @@ module Roadworker
63
62
  end
64
63
 
65
64
  def walk_hosted_zones(dsl)
66
- expected = collection_to_hash(dsl.hosted_zones) {|i| [normalize_name(i.name), i.vpcs.empty?, normalize_id(i.id)] }
67
- actual = collection_to_hash(@route53.hosted_zones) {|i| [normalize_name(i.name), i.vpcs.empty?, normalize_id(i.id)] }
65
+ updated = false
66
+
67
+ expected = collection_to_hash(dsl.hosted_zones) {|i| [i.name, i.vpcs.empty?, normalize_id(i.id)] }
68
+ actual = collection_to_hash(@route53.hosted_zones) {|i| [i.name, i.vpcs.empty?, normalize_id(i.id)] }
68
69
 
69
70
  expected.each do |keys, expected_zone|
70
71
  name, private_zone, id = keys
@@ -80,20 +81,28 @@ module Roadworker
80
81
  actual.delete(actual_keys) if actual_keys
81
82
  end
82
83
 
83
- actual_zone ||= @route53.hosted_zones.create(name, :vpc => expected_zone.vpcs.first)
84
+ unless actual_zone
85
+ updated = true
86
+ actual_zone = @route53.hosted_zones.create(name, :vpc => expected_zone.vpcs.first)
87
+ end
84
88
 
85
- walk_vpcs(expected_zone, actual_zone)
86
- walk_rrsets(expected_zone, actual_zone)
89
+ updated = true if walk_vpcs(expected_zone, actual_zone)
90
+ updated = true if walk_rrsets(expected_zone, actual_zone)
87
91
  end
88
92
 
89
93
  actual.each do |keys, zone|
90
94
  name = keys[0]
91
95
  next unless matched_zone?(name)
92
96
  zone.delete
97
+ updated = true
93
98
  end
99
+
100
+ updated
94
101
  end
95
102
 
96
103
  def walk_vpcs(expected_zone, actual_zone)
104
+ updated = false
105
+
97
106
  expected_vpcs = expected_zone.vpcs || []
98
107
  actual_vpcs = actual_zone.vpcs || []
99
108
 
@@ -102,6 +111,7 @@ module Roadworker
102
111
  else
103
112
  (expected_vpcs - actual_vpcs).each do |vpc|
104
113
  actual_zone.associate_vpc(vpc)
114
+ updated = true
105
115
  end
106
116
 
107
117
  unexpected_vpcs = actual_vpcs - expected_vpcs
@@ -111,28 +121,30 @@ module Roadworker
111
121
  else
112
122
  unexpected_vpcs.each do |vpc|
113
123
  actual_zone.disassociate_vpc(vpc)
124
+ updated = true
114
125
  end
115
126
  end
116
127
  end
128
+
129
+ updated
117
130
  end
118
131
 
132
+ # @param [OpenStruct] expected_zone Roadworker::DSL::Hostedzone#result
133
+ # @param [Roadworker::Route53Wrapper::HostedzoneWrapper] actual_zone
119
134
  def walk_rrsets(expected_zone, actual_zone)
135
+ change_batch = Batch.new(actual_zone, health_checks: @options.health_checks, logger: @options.logger, dry_run: @options.dry_run)
136
+
120
137
  expected = collection_to_hash(expected_zone.rrsets, :name, :type, :set_identifier)
121
- actual = collection_to_hash(actual_zone.rrsets, :name, :type, :set_identifier)
138
+ actual = actual_zone.rrsets.to_h.dup
122
139
 
123
140
  expected.each do |keys, expected_record|
124
- name = keys[0]
125
- type = keys[1]
126
- set_identifier = keys[2]
127
-
141
+ name, type, set_identifier = keys
128
142
  actual_record = actual.delete(keys)
129
143
 
130
- if not actual_record and %w(A CNAME).include?(type)
131
- actual_type = (type == 'A' ? 'CNAME' : 'A')
132
- actual_record = actual.delete([name, actual_type, set_identifier])
133
- end
134
-
135
- if expected_zone.ignore_patterns.any? { |pattern| pattern === name }
144
+ # XXX: normalization should be happen on DSL as much as possible, but ignore_patterns expect no trailing dot
145
+ # and to keep backward compatibility, removing then dot when checking ignored_patterns.
146
+ name_for_ignore_patterns = name.sub(/\.\z/, '')
147
+ if expected_zone.ignore_patterns.any? { |pattern| pattern === name_for_ignore_patterns }
136
148
  log(:warn, "Ignoring defined record in DSL, because it is ignored record", :yellow) do
137
149
  "#{name} #{type}" + (set_identifier ? " (#{set_identifier})" : '')
138
150
  end
@@ -141,21 +153,25 @@ module Roadworker
141
153
 
142
154
  if actual_record
143
155
  unless actual_record.eql?(expected_record)
144
- actual_record.update(expected_record)
156
+ change_batch.update(expected_record)
145
157
  end
146
158
  else
147
- actual_record = actual_zone.rrsets.create(name, type, expected_record)
159
+ change_batch.create(expected_record)
148
160
  end
149
161
  end
150
162
 
151
- actual.each do |keys, record|
152
- name = keys[0]
163
+ actual.each do |(name, _type, _set_identifier), record|
164
+ # XXX: normalization should be happen on DSL as much as possible, but ignore_patterns expect no trailing dot
165
+ # and to keep backward compatibility, removing then dot when checking ignored_patterns.
166
+ name = name.sub(/\.\z/, '')
153
167
  if expected_zone.ignore_patterns.any? { |pattern| pattern === name }
154
168
  next
155
169
  end
156
170
 
157
- record.delete
171
+ change_batch.delete(record)
158
172
  end
173
+
174
+ change_batch.request!(@options.route53)
159
175
  end
160
176
 
161
177
  def collection_to_hash(collection, *keys)
@@ -166,8 +182,7 @@ module Roadworker
166
182
  key_list = yield(item)
167
183
  else
168
184
  key_list = keys.map do |k|
169
- value = item.send(k)
170
- (k == :name && value) ? normalize_name(value) : value
185
+ item.send(k)
171
186
  end
172
187
  end
173
188
 
@@ -177,10 +192,6 @@ module Roadworker
177
192
  return hash
178
193
  end
179
194
 
180
- def normalize_name(name)
181
- name.downcase.sub(/\.\z/, '')
182
- end
183
-
184
195
  def normalize_id(id)
185
196
  id.sub(%r!^/hostedzone/!, '') if id
186
197
  end
@@ -16,6 +16,12 @@ module Roadworker
16
16
  def test(dsl, options)
17
17
  Tester.test(dsl, options)
18
18
  end
19
+
20
+ def normalize_dns_name(name)
21
+ # Normalize name. AWS always returns name with trailing dot, and stores name always lowercase.
22
+ # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DomainNameFormat.html
23
+ "#{name.downcase}.".sub(/\.+\z/, '.')
24
+ end
19
25
  end # of class method
20
26
 
21
27
  attr_reader :result
@@ -60,12 +66,12 @@ module Roadworker
60
66
  attr_reader :result
61
67
 
62
68
  def initialize(context, name, id, rrsets = [], &block)
63
- @name = name
64
- @context = context.merge(:hosted_zone_name => name)
69
+ @name = DSL.normalize_dns_name(name)
70
+ @context = context.merge(:hosted_zone_name => @name)
65
71
 
66
72
  @result = OpenStruct.new({
67
73
  :id => id,
68
- :name => name,
74
+ :name => @name,
69
75
  :vpcs => [],
70
76
  :resource_record_sets => rrsets,
71
77
  :rrsets => rrsets,
@@ -105,7 +111,7 @@ module Roadworker
105
111
  end
106
112
 
107
113
  def ignore_under(rrset_name)
108
- ignore /(\A|\.)#{Regexp.escape(rrset_name.to_s.sub(/\.\z/, ''))}\z/
114
+ ignore(/(\A|\.)#{Regexp.escape(rrset_name.to_s.sub(/\.\z/, ''))}\z/)
109
115
  end
110
116
 
111
117
  def resource_record_set(rrset_name, type, &block)
@@ -123,6 +129,9 @@ module Roadworker
123
129
  attr_reader :result
124
130
 
125
131
  def initialize(context, name, type, &block)
132
+ name = DSL.normalize_dns_name(name)
133
+ type = type.upcase
134
+
126
135
  @context = context.merge(
127
136
  :rrset_name => name,
128
137
  :rrset_type => type
@@ -1,12 +1,12 @@
1
1
  module Roadworker
2
2
  module Log
3
3
 
4
- def log(level, message, color, log_id = nil)
4
+ def log(level, message, color, log_id = nil, dry_run: @dry_run || (@options && @options.dry_run), logger: @logger || @options.logger)
5
5
  log_id = yield if block_given?
6
6
  message = "#{message}: #{log_id}" if log_id
7
- message << ' (dry-run)' if @options.dry_run
7
+ message << ' (dry-run)' if dry_run
8
8
  message = message.send(color) if color
9
- @options.logger.send(level, message)
9
+ logger.send(level, message)
10
10
  end
11
11
 
12
12
  end # Log
@@ -14,6 +14,7 @@ module Aws
14
14
  's3-website-eu-west-1.amazonaws.com' => 'Z1BKCTXD74EZPE',
15
15
  's3-website-sa-east-1.amazonaws.com' => 'Z7KQH4QJS55SO',
16
16
  's3-website-us-east-1.amazonaws.com' => 'Z3AQBSTGFYJSTF',
17
+ 's3-website-us-east-2.amazonaws.com' => 'Z2O1EMRO9K5GLX',
17
18
  's3-website-us-gov-west-1.amazonaws.com' => 'Z31GFT0UA1I2HV',
18
19
  's3-website-us-west-1.amazonaws.com' => 'Z2F56UZL2M1ACD',
19
20
  's3-website-us-west-2.amazonaws.com' => 'Z3BJ6K6RIION7M',
@@ -30,6 +31,7 @@ module Aws
30
31
  'eu-west-1' => 'Z3NF1Z3NOM5OY2',
31
32
  'sa-east-1' => 'Z2ES78Y61JGQKS',
32
33
  'us-east-1' => 'Z3DZXE0Q79N41H',
34
+ 'us-east-2' => 'Z3AADJGX6KTTL2',
33
35
  'us-west-1' => 'Z1M58G0W56PQJA',
34
36
  'us-west-2' => 'Z33MTJ483KN6FU',
35
37
  }
@@ -44,6 +46,7 @@ module Aws
44
46
  'eu-west-1' => 'Z32O12XQLNTSW2',
45
47
  'sa-east-1' => 'Z2P70J7HTTTPLU',
46
48
  'us-east-1' => 'Z35SXDOTRQ7X7K',
49
+ 'us-east-2' => 'Z3AADJGX6KTTL2',
47
50
  'us-west-1' => 'Z368ELLRRE2KJ0',
48
51
  'us-west-2' => 'Z1H1FL5HABSF5',
49
52
  }
@@ -155,11 +158,28 @@ module Aws
155
158
  elsif name =~ /\.([^.]+)\.vpce\.amazonaws\.com\z/i
156
159
  region = $1.downcase
157
160
  vpce_dns_name_to_alias_target(name, region, hosted_zone_id)
161
+ elsif name =~ /\.awsglobalaccelerator\.com\z/i
162
+ globalaccelerator_dns_name_to_alias_target(name)
158
163
  else
159
164
  raise "Invalid DNS Name: #{name}"
160
165
  end
161
166
  end
162
167
 
168
+ def sort_rrset_values(attribute, values)
169
+ sort_lambda =
170
+ case attribute
171
+ when :resource_records
172
+ # After aws-sdk-core v3.44.1, Aws::Route53::Types::ResourceRecord#to_s returns filtered string
173
+ # like "{:value=>\"[FILTERED]\"}" (cf. https://github.com/aws/aws-sdk-ruby/pull/1941).
174
+ # To keep backward compatibility, sort by the value of resource record explicitly.
175
+ lambda { |i| i[:value] }
176
+ else
177
+ lambda { |i| i.to_s }
178
+ end
179
+
180
+ values.sort_by(&sort_lambda)
181
+ end
182
+
163
183
  private
164
184
 
165
185
  def elb_dns_name_to_alias_target(name, region, options)
@@ -250,6 +270,15 @@ module Aws
250
270
  :evaluate_target_health => false, # XXX:
251
271
  }
252
272
  end
273
+
274
+ def globalaccelerator_dns_name_to_alias_target(name)
275
+ # https://docs.aws.amazon.com/Route53/latest/APIReference/API_AliasTarget.html
276
+ {
277
+ :hosted_zone_id => 'Z2BJ6XQ5FK7U4H',
278
+ :dns_name => name,
279
+ :evaluate_target_health => false, # XXX:
280
+ }
281
+ end
253
282
  end # of class method
254
283
 
255
284
  end # Route53
@@ -86,18 +86,26 @@ module Roadworker
86
86
  attr_reader :vpcs
87
87
 
88
88
  def resource_record_sets
89
- ResourceRecordSetCollectionWrapper.new(@hosted_zone, @options)
89
+ @resource_record_sets ||= ResourceRecordSetCollectionWrapper.new(@hosted_zone, @options)
90
90
  end
91
91
  alias rrsets resource_record_sets
92
92
 
93
+ # @return [Roadworker::Route53Wrapper::ResourceRecordSetWrapper]
94
+ def find_resource_record_set(name, type, set_identifier)
95
+ resource_record_sets.to_h[[name, type, set_identifier]]
96
+ end
97
+
93
98
  def delete
94
99
  if @options.force
95
100
  log(:info, 'Delete Hostedzone', :red, @hosted_zone.name)
96
101
 
102
+ change_batch = Batch.new(self, health_checks: @options.health_checks, logger: @options.logger, dry_run: @options.dry_run)
97
103
  self.rrsets.each do |record|
98
- record.delete
104
+ change_batch.delete(record)
99
105
  end
100
106
 
107
+ change_batch.request!(@options.route53)
108
+
101
109
  unless @options.dry_run
102
110
  @options.route53.delete_hosted_zone(id: @hosted_zone.id)
103
111
  @options.updated = true
@@ -142,61 +150,24 @@ module Roadworker
142
150
  @options = options
143
151
  end
144
152
 
145
- def each
146
- if @hosted_zone.id
147
- Collection.batch(@options.route53.list_resource_record_sets(hosted_zone_id: @hosted_zone.id), :resource_record_sets) do |record|
148
- yield(ResourceRecordSetWrapper.new(record, @hosted_zone, @options))
149
- end
150
- end
151
- end
153
+ # @return [Hash<Array<(String,String,String)>, Roadworker::Route53Wrapper::ResourceRecordSetWrapper>]
154
+ def to_h
155
+ return @hash if defined? @hash
156
+ @hash = {}
152
157
 
153
- def create(name, type, expected_record)
154
- log(:info, 'Create ResourceRecordSet', :cyan) do
155
- log_id = [name, type].join(' ')
156
- rrset_setid = expected_record.set_identifier
157
- rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
158
+ self.each do |item|
159
+ @hash[[item.name, item.type, item.set_identifier]] = item
158
160
  end
159
161
 
160
- if @options.dry_run
161
- record = expected_record
162
- else
163
- resource_record_set_params = {
164
- name: name,
165
- type: type,
166
- }
167
-
168
- Route53Wrapper::RRSET_ATTRS.each do |attribute|
169
- value = expected_record.send(attribute)
170
- next unless value
171
-
172
- case attribute
173
- when :dns_name
174
- attribute = :alias_target
175
- dns_name, dns_name_opts = value
176
- value = Aws::Route53.dns_name_to_alias_target(dns_name, dns_name_opts, @hosted_zone.id, @hosted_zone.name || @options.hosted_zone_name)
177
- when :health_check
178
- attribute = :health_check_id
179
- value = @options.health_checks.find_or_create(value)
180
- end
162
+ @hash
163
+ end
181
164
 
182
- resource_record_set_params[attribute] = value
165
+ def each
166
+ if @hosted_zone.id
167
+ Collection.batch(@options.route53.list_resource_record_sets(hosted_zone_id: @hosted_zone.id), :resource_record_sets) do |record|
168
+ yield(ResourceRecordSetWrapper.new(record, @hosted_zone, @options))
183
169
  end
184
-
185
- @options.route53.change_resource_record_sets(
186
- hosted_zone_id: @hosted_zone.id,
187
- change_batch: {
188
- changes: [
189
- {
190
- action: 'CREATE',
191
- resource_record_set: resource_record_set_params,
192
- },
193
- ],
194
- },
195
- )
196
- @options.updated = true
197
170
  end
198
-
199
- ResourceRecordSetWrapper.new(expected_record, @hosted_zone, @options)
200
171
  end
201
172
  end # ResourceRecordSetCollectionWrapper
202
173
 
@@ -212,10 +183,10 @@ module Roadworker
212
183
  def eql?(expected_record)
213
184
  Route53Wrapper::RRSET_ATTRS_WITH_TYPE.all? do |attribute|
214
185
  expected = expected_record.public_send(attribute)
215
- expected = expected.sort_by {|i| i.to_s } if expected.kind_of?(Array)
186
+ expected = Aws::Route53.sort_rrset_values(attribute, expected) if expected.kind_of?(Array)
216
187
  expected = nil if expected.kind_of?(Array) && expected.empty?
217
188
  actual = self.public_send(attribute)
218
- actual = actual.sort_by {|i| i.to_s } if actual.kind_of?(Array)
189
+ actual = Aws::Route53.sort_rrset_values(attribute, actual) if actual.kind_of?(Array)
219
190
  actual = nil if actual.kind_of?(Array) && actual.empty?
220
191
 
221
192
  if attribute == :geo_location and actual
@@ -252,99 +223,6 @@ module Roadworker
252
223
  end
253
224
  end
254
225
 
255
- def update(expected_record)
256
- log_id_proc = proc do
257
- log_id = [self.name, self.type].join(' ')
258
- rrset_setid = self.set_identifier
259
- rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
260
- end
261
-
262
- log(:info, 'Update ResourceRecordSet', :green, &log_id_proc)
263
-
264
- resource_record_set_prev = @resource_record_set.dup
265
- Route53Wrapper::RRSET_ATTRS_WITH_TYPE.each do |attribute|
266
- expected = expected_record.send(attribute)
267
- expected = expected.sort_by {|i| i.to_s } if expected.kind_of?(Array)
268
- expected = nil if expected.kind_of?(Array) && expected.empty?
269
- actual = self.send(attribute)
270
- actual = actual.sort_by {|i| i.to_s } if actual.kind_of?(Array)
271
- actual = nil if actual.kind_of?(Array) && actual.empty?
272
-
273
- # XXX: Fix for diff
274
- if attribute == :health_check and actual
275
- if (actual[:child_health_checks] || []).empty?
276
- actual[:child_health_checks] = []
277
- end
278
-
279
- if (actual[:regions] || []).empty?
280
- actual[:regions] = []
281
- end
282
- end
283
-
284
- if (expected and !actual) or (!expected and actual)
285
- log(:info, " #{attribute}:\n".green + Roadworker::Utils.diff(actual, expected, :color => @options.color, :indent => ' '), false)
286
- unless @options.dry_run
287
- self.send(:"#{attribute}=", expected)
288
- end
289
- elsif expected and actual
290
- if expected != actual
291
- log(:info, " #{attribute}:\n".green + Roadworker::Utils.diff(actual, expected, :color => @options.color, :indent => ' '), false)
292
- unless @options.dry_run
293
- self.send(:"#{attribute}=", expected)
294
- end
295
- end
296
- end
297
- end
298
-
299
- unless @options.dry_run
300
- @options.route53.change_resource_record_sets(
301
- hosted_zone_id: @hosted_zone.id,
302
- change_batch: {
303
- changes: [
304
- {
305
- action: 'DELETE',
306
- resource_record_set: resource_record_set_prev,
307
- },
308
- {
309
- action: 'CREATE',
310
- resource_record_set: @resource_record_set,
311
- },
312
- ],
313
- },
314
- )
315
- @options.updated = true
316
- end
317
- end
318
-
319
- def delete
320
- if self.type =~ /\A(SOA|NS)\z/i
321
- hz_name = (@hosted_zone.name || @options.hosted_zone_name).downcase.sub(/\.\z/, '')
322
- rrs_name = @resource_record_set.name.downcase.sub(/\.\z/, '')
323
- return if hz_name == rrs_name
324
- end
325
-
326
- log(:info, 'Delete ResourceRecordSet', :red) do
327
- log_id = [self.name, self.type].join(' ')
328
- rrset_setid = self.set_identifier
329
- rrset_setid ? (log_id + " (#{rrset_setid})") : log_id
330
- end
331
-
332
- unless @options.dry_run
333
- @options.route53.change_resource_record_sets(
334
- hosted_zone_id: @hosted_zone.id,
335
- change_batch: {
336
- changes: [
337
- {
338
- action: 'DELETE',
339
- resource_record_set: @resource_record_set,
340
- },
341
- ],
342
- },
343
- )
344
- @options.updated = true
345
- end
346
- end
347
-
348
226
  def name
349
227
  value = @resource_record_set.name
350
228
  value ? value.gsub("\\052", '*') : value
@@ -364,24 +242,10 @@ module Roadworker
364
242
  end
365
243
  end
366
244
 
367
- def dns_name=(value)
368
- if value
369
- dns_name, dns_name_opts = value
370
- @resource_record_set.alias_target = Aws::Route53.dns_name_to_alias_target(dns_name, dns_name_opts, @hosted_zone.id, @hosted_zone.name || @options.hosted_zone_name)
371
- else
372
- @resource_record_set.alias_target = nil
373
- end
374
- end
375
-
376
245
  def health_check
377
246
  @options.health_checks[@resource_record_set.health_check_id]
378
247
  end
379
248
 
380
- def health_check=(check)
381
- health_check_id = check ? @options.health_checks.find_or_create(check) : nil
382
- @resource_record_set.health_check_id = health_check_id
383
- end
384
-
385
249
  private
386
250
 
387
251
  def method_missing(method_name, *args)
@@ -4,12 +4,16 @@ module Roadworker
4
4
  def matched_zone?(name)
5
5
  result = true
6
6
 
7
+ # XXX: normalization should be happen on DSL as much as possible, but patterns expect no trailing dot
8
+ # and to keep backward compatibility, removing then dot when checking patterns.
9
+ name_for_patterns = name.sub(/\.\z/, '')
10
+
7
11
  if @options.exclude_zone
8
- result &&= name !~ @options.exclude_zone
12
+ result &&= name_for_patterns !~ @options.exclude_zone
9
13
  end
10
14
 
11
15
  if @options.target_zone
12
- result &&= name =~ @options.target_zone
16
+ result &&= name_for_patterns =~ @options.target_zone
13
17
  end
14
18
 
15
19
  result
@@ -1,3 +1,3 @@
1
1
  module Roadworker
2
- VERSION = "0.5.10"
2
+ VERSION = "0.5.15"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roadworker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.10
4
+ version: 0.5.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - winebarrel
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-11 00:00:00.000000000 Z
11
+ date: 2021-06-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-route53
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.13.0
19
+ version: 1.22.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.13.0
26
+ version: 1.22.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: term-ansicolor
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -192,20 +192,6 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
- - !ruby/object:Gem::Dependency
196
- name: transpec
197
- requirement: !ruby/object:Gem::Requirement
198
- requirements:
199
- - - ">="
200
- - !ruby/object:Gem::Version
201
- version: '0'
202
- type: :development
203
- prerelease: false
204
- version_requirements: !ruby/object:Gem::Requirement
205
- requirements:
206
- - - ">="
207
- - !ruby/object:Gem::Version
208
- version: '0'
209
195
  description: Roadworker is a tool to manage Route53. It defines the state of Route53
210
196
  using DSL, and updates Route53 according to DSL.
211
197
  email: sgwr_dts@yahoo.co.jp
@@ -217,6 +203,7 @@ files:
217
203
  - README.md
218
204
  - bin/roadwork
219
205
  - lib/roadworker.rb
206
+ - lib/roadworker/batch.rb
220
207
  - lib/roadworker/client.rb
221
208
  - lib/roadworker/collection.rb
222
209
  - lib/roadworker/dsl-converter.rb
@@ -236,7 +223,7 @@ homepage: https://github.com/winebarrel/roadworker
236
223
  licenses:
237
224
  - MIT
238
225
  metadata: {}
239
- post_install_message:
226
+ post_install_message:
240
227
  rdoc_options: []
241
228
  require_paths:
242
229
  - lib
@@ -251,9 +238,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
251
238
  - !ruby/object:Gem::Version
252
239
  version: '0'
253
240
  requirements: []
254
- rubyforge_project:
255
- rubygems_version: 2.7.6
256
- signing_key:
241
+ rubygems_version: 3.1.4
242
+ signing_key:
257
243
  specification_version: 4
258
244
  summary: Roadworker is a tool to manage Route53.
259
245
  test_files: []