roadworker 0.5.12 → 0.5.13

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