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 +4 -4
- data/README.md +1 -16
- data/lib/roadworker.rb +1 -0
- data/lib/roadworker/batch.rb +362 -0
- data/lib/roadworker/client.rb +40 -29
- data/lib/roadworker/dsl.rb +13 -4
- data/lib/roadworker/log.rb +3 -3
- data/lib/roadworker/route53-ext.rb +26 -0
- data/lib/roadworker/route53-wrapper.rb +24 -175
- data/lib/roadworker/utils.rb +6 -2
- data/lib/roadworker/version.rb +1 -1
- metadata +3 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e6ddf47c1d608d00d7daad9af18efe458565157e671d85b097985f1a2120c05
|
4
|
+
data.tar.gz: 0fa8d9cad469f73eb385e95771b3717bd7f513c62d8c9e143c0dab84d3719afd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
@@ -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
|
data/lib/roadworker/client.rb
CHANGED
@@ -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
|
-
|
67
|
-
|
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
|
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 =
|
138
|
+
actual = actual_zone.rrsets.to_h.dup
|
122
139
|
|
123
140
|
expected.each do |keys, expected_record|
|
124
|
-
name = keys
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
156
|
+
change_batch.update(expected_record)
|
145
157
|
end
|
146
158
|
else
|
147
|
-
|
159
|
+
change_batch.create(expected_record)
|
148
160
|
end
|
149
161
|
end
|
150
162
|
|
151
|
-
actual.each do |
|
152
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/roadworker/dsl.rb
CHANGED
@@ -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
|
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
|
data/lib/roadworker/log.rb
CHANGED
@@ -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
|
7
|
+
message << ' (dry-run)' if dry_run
|
8
8
|
message = message.send(color) if color
|
9
|
-
|
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
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|
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
|
data/lib/roadworker/utils.rb
CHANGED
@@ -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 &&=
|
12
|
+
result &&= name_for_patterns !~ @options.exclude_zone
|
9
13
|
end
|
10
14
|
|
11
15
|
if @options.target_zone
|
12
|
-
result &&=
|
16
|
+
result &&= name_for_patterns =~ @options.target_zone
|
13
17
|
end
|
14
18
|
|
15
19
|
result
|
data/lib/roadworker/version.rb
CHANGED
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.
|
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:
|
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
|