roadworker 0.5.12 → 0.5.13

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: 954087eeb0c2336152bcc1b2e5508ab1ac508e33df52c02feffb8cdb2973cd7d
4
- data.tar.gz: 8d767b2ff8558de226cddfb43c662ce52952a75b7b5fabae89dd14638b555fb3
3
+ metadata.gz: 9e6ddf47c1d608d00d7daad9af18efe458565157e671d85b097985f1a2120c05
4
+ data.tar.gz: 0fa8d9cad469f73eb385e95771b3717bd7f513c62d8c9e143c0dab84d3719afd
5
5
  SHA512:
6
- metadata.gz: 67903de0d8b95bd63d4dd0fe4d47a6a882574f10614e95824d30640f208619081a6ff2f21932c70720b9aeb9340527bf786e336d778f6c6fc057e055c62b71dc
7
- data.tar.gz: ecce73635cdd3465a21070c16205f7e6df4f87ffd37e08c1c3bf3d9e126cbb85397ad58941f416eb912cf2a4475ef0ec011b88fc6e86e697ac5fea9da3ee3e2c
6
+ metadata.gz: 0e09c4d9fdec40a48da7ff63cc43cd4a50933d2cf324d6b22dfa449a4f886ae3eb131169351c25412936e42a1e5de159e767e636d893d12c1c7f86c69c91c957
7
+ data.tar.gz: eb3fbbf02edf2cc3686f0cc16c05ccc1bd6879179fb980e200e5eae4487f4c90eb18b0122587c8c9a9c3411bd2ac5dd1c6087d9b3e5481018861feec1c8cbaa2
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
 
@@ -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,362 @@
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 += 1
89
+ if total_value_size > 32000 || total_ops > 1000
90
+ total_value_size = op.value_size
91
+ total_ops = 1
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
+ # See also: Batch#slice_operations
154
+ # @return [Integer]
155
+ def value_size
156
+ changes.map do |change|
157
+ upsert_multiplier = change[:action] == 'UPSERT' ? 2 : 1
158
+ rrset = change[:resource_record_set]
159
+ next 0 unless rrset
160
+ rrs = rrset[:resource_records]
161
+ next 0 unless rrs
162
+ (rrs.map { |_| _[:value]&.size || 0 }.sum) * upsert_multiplier
163
+ end.sum || 0
164
+ end
165
+
166
+ # @return [Array<Hash>]
167
+ def changes
168
+ raise NotImplementedError
169
+ end
170
+
171
+ # @return [Hash]
172
+ def desired_rrset
173
+ raise NotImplementedError
174
+ end
175
+
176
+ # @return [Roadworker::Route53Wrapper::ResourceRecordSetWrapper]
177
+ def present_rrset
178
+ hosted_zone.find_resource_record_set(rrset.name, rrset.type, rrset.set_identifier) or raise "record not present"
179
+ end
180
+
181
+ def diff!(dry_run: false)
182
+ raise NotImplementedError
183
+ end
184
+
185
+ def inspect
186
+ "#<#{self.class.name} @changes=#{changes.inspect}>"
187
+ end
188
+
189
+ def to_s
190
+ inspect
191
+ end
192
+
193
+ private
194
+
195
+ # @param [String] dns_name
196
+ # @param [Hash] options
197
+ # @return [?]
198
+ def get_alias_target(dns_name, options)
199
+ Aws::Route53.dns_name_to_alias_target(dns_name, options, hosted_zone.id, hosted_zone.name)
200
+ end
201
+
202
+ # @param [?] health_check
203
+ # @return [?]
204
+ def get_health_check(check)
205
+ check ? health_checks.find_or_create(check) : nil
206
+ end
207
+
208
+ end
209
+
210
+ class Create < Operation
211
+ # @return [Hash]
212
+ def desired_rrset
213
+ return @new_rrset if defined? @new_rrset
214
+ @new_rrset = {
215
+ name: rrset.name,
216
+ type: rrset.type,
217
+ }
218
+
219
+ Route53Wrapper::RRSET_ATTRS.each do |attribute|
220
+ value = rrset.send(attribute)
221
+ next unless value
222
+
223
+ case attribute
224
+ when :dns_name
225
+ attribute = :alias_target
226
+ dns_name, dns_name_opts = value
227
+ value = get_alias_target(dns_name, dns_name_opts)
228
+ when :health_check
229
+ attribute = :health_check_id
230
+ value = get_health_check(value)
231
+ end
232
+
233
+ @new_rrset[attribute] = value
234
+ end
235
+
236
+ @new_rrset
237
+ end
238
+
239
+ def changes
240
+ [
241
+ {
242
+ action: 'CREATE',
243
+ resource_record_set: desired_rrset.to_h,
244
+ },
245
+ ]
246
+ end
247
+
248
+ def diff!
249
+ log(:info, 'Create ResourceRecordSet', :cyan) do
250
+ "#{desired_rrset[:name]} #{desired_rrset[:type]}#{ desired_rrset[:set_identifier] && " (#{desired_rrset[:set_identifier]})" }"
251
+ end
252
+ end
253
+ end
254
+
255
+ class Delete < Operation
256
+ # CNAME should always be deleted first, as CNAME doesn't permit other records
257
+ def cname_first?
258
+ true
259
+ end
260
+
261
+ def hosted_zone_soa_or_ns?
262
+ (present_rrset.type == 'SOA' || present_rrset.type == 'NS') && hosted_zone.name == present_rrset.name
263
+ end
264
+
265
+ def changes
266
+ # Avoid deleting hosted zone SOA/NS
267
+ if hosted_zone_soa_or_ns?
268
+ return []
269
+ end
270
+
271
+ [
272
+ {
273
+ action: 'DELETE',
274
+ resource_record_set: present_rrset.to_h,
275
+ }
276
+ ]
277
+ end
278
+
279
+ def diff!
280
+ return if changes.empty?
281
+ log(:info, 'Delete ResourceRecordSet', :red) do
282
+ "#{present_rrset.name} #{present_rrset.type}#{ present_rrset.set_identifier && " (#{present_rrset.set_identifier})" }"
283
+ end
284
+ end
285
+ end
286
+
287
+
288
+ class Update < Operation
289
+ def desired_rrset
290
+ return @desired_rrset if defined? @desired_rrset
291
+ @desired_rrset = {name: rrset[:name]}
292
+
293
+ Route53Wrapper::RRSET_ATTRS_WITH_TYPE.each do |attribute|
294
+ value = rrset[attribute]
295
+ next unless value
296
+
297
+ case attribute
298
+ when :dns_name
299
+ dns_name, dns_name_opts = value
300
+ @desired_rrset[:alias_target] = get_alias_target(dns_name, dns_name_opts)
301
+ when :health_check
302
+ @desired_rrset[:health_check_id] = get_health_check(value)
303
+ else
304
+ @desired_rrset[attribute] = value
305
+ end
306
+ end
307
+
308
+ @desired_rrset
309
+ end
310
+
311
+ def changes
312
+ [
313
+ {
314
+ action: 'DELETE',
315
+ resource_record_set: present_rrset.to_h,
316
+ },
317
+ {
318
+ action: 'CREATE',
319
+ resource_record_set: desired_rrset.to_h,
320
+ },
321
+ ]
322
+ end
323
+
324
+ def diff!
325
+ log(:info, 'Update ResourceRecordSet', :green) do
326
+ "#{present_rrset.name} #{present_rrset.type}#{ present_rrset.set_identifier && " (#{present_rrset.set_identifier})" }"
327
+ end
328
+
329
+ # Note that desired_rrset is directly for Route 53, and present_record is also from Route 53
330
+ # Only given +rrset+ is brought from DSL, and dns_name & health_check is only valid in our DSL
331
+ Route53Wrapper::RRSET_ATTRS_WITH_TYPE.each do |attribute|
332
+ case attribute
333
+ when :dns_name
334
+ present = normalize_attribute_for_diff(attribute, present_rrset[:alias_target] && present_rrset[:alias_target][:dns_name])
335
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[:alias_target] && desired_rrset[:alias_target][:dns_name])
336
+ when :health_check
337
+ present = normalize_attribute_for_diff(attribute, present_rrset[:health_check_id])
338
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[:health_check_id])
339
+ else
340
+ present = normalize_attribute_for_diff(attribute, present_rrset[attribute])
341
+ desired = normalize_attribute_for_diff(attribute, desired_rrset[attribute])
342
+ end
343
+
344
+ if desired != present
345
+ color = String.colorize # XXX:
346
+ log(:info, " #{attribute}:\n".green + Roadworker::Utils.diff(present, desired, color: color, indent: ' '), false)
347
+ end
348
+ end
349
+ end
350
+
351
+ private
352
+
353
+ def normalize_attribute_for_diff(attribute, value)
354
+ if value.is_a?(Array)
355
+ value = Aws::Route53.sort_rrset_values(attribute, value)
356
+ value = nil if value.empty?
357
+ end
358
+ value
359
+ end
360
+ end
361
+ end
362
+ 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
@@ -155,11 +155,28 @@ module Aws
155
155
  elsif name =~ /\.([^.]+)\.vpce\.amazonaws\.com\z/i
156
156
  region = $1.downcase
157
157
  vpce_dns_name_to_alias_target(name, region, hosted_zone_id)
158
+ elsif name =~ /\.awsglobalaccelerator\.com\z/i
159
+ globalaccelerator_dns_name_to_alias_target(name)
158
160
  else
159
161
  raise "Invalid DNS Name: #{name}"
160
162
  end
161
163
  end
162
164
 
165
+ def sort_rrset_values(attribute, values)
166
+ sort_lambda =
167
+ case attribute
168
+ when :resource_records
169
+ # After aws-sdk-core v3.44.1, Aws::Route53::Types::ResourceRecord#to_s returns filtered string
170
+ # like "{:value=>\"[FILTERED]\"}" (cf. https://github.com/aws/aws-sdk-ruby/pull/1941).
171
+ # To keep backward compatibility, sort by the value of resource record explicitly.
172
+ lambda { |i| i[:value] }
173
+ else
174
+ lambda { |i| i.to_s }
175
+ end
176
+
177
+ values.sort_by(&sort_lambda)
178
+ end
179
+
163
180
  private
164
181
 
165
182
  def elb_dns_name_to_alias_target(name, region, options)
@@ -250,6 +267,15 @@ module Aws
250
267
  :evaluate_target_health => false, # XXX:
251
268
  }
252
269
  end
270
+
271
+ def globalaccelerator_dns_name_to_alias_target(name)
272
+ # https://docs.aws.amazon.com/Route53/latest/APIReference/API_AliasTarget.html
273
+ {
274
+ :hosted_zone_id => 'Z2BJ6XQ5FK7U4H',
275
+ :dns_name => name,
276
+ :evaluate_target_health => false, # XXX:
277
+ }
278
+ end
253
279
  end # of class method
254
280
 
255
281
  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 = sort_rrset_values(attribute, expected) 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 = sort_rrset_values(attribute, actual) 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,41 +242,12 @@ 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
- def sort_rrset_values(attribute, values)
388
- sort_lambda =
389
- case attribute
390
- when :resource_records
391
- # After aws-sdk-core v3.44.1, Aws::Route53::Types::ResourceRecord#to_s returns filtered string
392
- # like "{:value=>\"[FILTERED]\"}" (cf. https://github.com/aws/aws-sdk-ruby/pull/1941).
393
- # To keep backward compatibility, sort by the value of resource record explicitly.
394
- lambda { |i| i[:value] }
395
- else
396
- lambda { |i| i.to_s }
397
- end
398
-
399
- values.sort_by(&sort_lambda)
400
- end
401
-
402
251
  def method_missing(method_name, *args)
403
252
  @resource_record_set.send(method_name, *args)
404
253
  end
@@ -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.12"
2
+ VERSION = "0.5.13"
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.12
4
+ version: 0.5.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - winebarrel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-26 00:00:00.000000000 Z
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-route53
@@ -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