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