roadworker 0.5.10 → 0.5.15

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: 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: []