record_store 9.0.0 → 9.1.0
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/CHANGELOG.md +4 -0
- data/lib/record_store/provider/cloudflare.rb +80 -30
- data/lib/record_store/record.rb +8 -1
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone/config/implicit_record_template.rb +32 -6
- data/lib/record_store/zone.rb +18 -3
- data/lib/record_store.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: effd0589fd20b2602042b321bd4f82d00b1f9fe08aa67f02b88e7b798d217559
|
|
4
|
+
data.tar.gz: 051e18aa4b65ac9ff67824027c1fcf83ebc66222dbd9b244ed7ae8f5914f8fce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 237530be7cc7ee854275117be9d21d3e039eaf17fd45b73bb7cc56ecc7723bff2c531712021c263db2c87ef2eeca3d7a84fae18341a33615c4cf70af0ce66f77
|
|
7
|
+
data.tar.gz: 9ed803f7f7aad03e8a45bd46e0e0a8e1f398fdf7f70479b5154cb06b2efebd36cbc77cbeb2d150ed0374610dc995c14afd7ee057d9d28ce7bff3e96a71240623
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 9.1.0
|
|
4
|
+
- Load zones faster. Implicit record templates no longer rescan every record for each templated record, the duplicate record check no longer compares every pair of records, and `Record#type` is computed once per class. Zones, records and validation errors are unchanged. Loading and validating a repository with several hundred zones is several times faster.
|
|
5
|
+
- Cloudflare: look up zone ids from one listing of all zones, fetched once per run and shared by all threads, instead of one request per zone each time a zone is read or written. A zone that is not in the listing is looked up on its own. A zone that does not exist now fails with `Zone not found for <name>`; before, it failed with `undefined method 'size' for nil`.
|
|
6
|
+
|
|
3
7
|
## 9.0.0
|
|
4
8
|
- Only load implicit record templates by plain file name from directly inside `implicit_records_templates_path`. Template names containing path separators, `.` or `..`, and symlinks that resolve outside that directory now raise an error. Zone definitions that referenced templates this way must move them into the templates directory and use the plain file name.
|
|
5
9
|
|
|
@@ -2,6 +2,12 @@ require_relative 'cloudflare/client'
|
|
|
2
2
|
|
|
3
3
|
module RecordStore
|
|
4
4
|
class Provider::Cloudflare < Provider
|
|
5
|
+
# Cloudflare returns at most 50 zones per page.
|
|
6
|
+
ZONE_LIST_PER_PAGE = 50
|
|
7
|
+
|
|
8
|
+
ZONE_IDS_MUTEX = Mutex.new
|
|
9
|
+
private_constant :ZONE_IDS_MUTEX
|
|
10
|
+
|
|
5
11
|
class << self
|
|
6
12
|
def record_types
|
|
7
13
|
super | Set.new(%w(PTR))
|
|
@@ -45,25 +51,13 @@ module RecordStore
|
|
|
45
51
|
# Cloudflare returns zones across all accounts accessible by the API token
|
|
46
52
|
# Can implement filtering in request if needed
|
|
47
53
|
def zones
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
per_page = 50
|
|
51
|
-
|
|
52
|
-
loop do
|
|
53
|
-
response = nil
|
|
54
|
-
retry_on_connection_errors do
|
|
55
|
-
response = client.get('/client/v4/zones', page: page, per_page: per_page)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
zones = response.result_raw || []
|
|
59
|
-
all_zones.concat(zones)
|
|
60
|
-
|
|
61
|
-
break if page * per_page >= response.result_info_raw['total_count']
|
|
62
|
-
|
|
63
|
-
page += 1
|
|
64
|
-
end
|
|
54
|
+
list_zones.map { |zone| zone['name'] }
|
|
55
|
+
end
|
|
65
56
|
|
|
66
|
-
|
|
57
|
+
# Forgets the zone name => id mapping, so the next lookup lists the zones again. The mapping is kept for the
|
|
58
|
+
# life of the process, which is a single run.
|
|
59
|
+
def clear_zone_id_cache
|
|
60
|
+
ZONE_IDS_MUTEX.synchronize { @zone_ids = nil }
|
|
67
61
|
end
|
|
68
62
|
|
|
69
63
|
def apply_changeset(changeset, stdout = $stdout)
|
|
@@ -204,20 +198,76 @@ module RecordStore
|
|
|
204
198
|
)
|
|
205
199
|
end
|
|
206
200
|
|
|
201
|
+
# Returns every zone the token can see, as the raw API hashes.
|
|
202
|
+
def list_zones
|
|
203
|
+
all_zones = []
|
|
204
|
+
page = 1
|
|
205
|
+
|
|
206
|
+
loop do
|
|
207
|
+
response = nil
|
|
208
|
+
retry_on_connection_errors do
|
|
209
|
+
response = client.get('/client/v4/zones', page: page, per_page: ZONE_LIST_PER_PAGE)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
zones = response.result_raw || []
|
|
213
|
+
all_zones.concat(zones)
|
|
214
|
+
|
|
215
|
+
break if page * ZONE_LIST_PER_PAGE >= response.result_info_raw['total_count']
|
|
216
|
+
|
|
217
|
+
page += 1
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
all_zones
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Zone ids are looked up from one listing of all zones, which is fetched on first use and shared by every
|
|
224
|
+
# thread. A zone that is not in that listing (for example one created after it was fetched) is looked up on its
|
|
225
|
+
# own, and the answer is remembered.
|
|
207
226
|
def zone_name_to_id(zone_name)
|
|
227
|
+
key = zone_id_cache_key(zone_name)
|
|
228
|
+
ids = zone_ids_by_name[key]
|
|
229
|
+
|
|
230
|
+
if ids.nil?
|
|
231
|
+
ids = zone_ids_from_lookup(zone_name)
|
|
232
|
+
remember_zone_ids(key, ids) if ids.size == 1
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
case ids.size
|
|
236
|
+
when 0
|
|
237
|
+
raise "Zone not found for #{zone_name}"
|
|
238
|
+
when 1
|
|
239
|
+
ids.first
|
|
240
|
+
else
|
|
241
|
+
raise "Multiple zones found for #{zone_name}. " \
|
|
242
|
+
"API key must only return the zone on which records are to be managed."
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def zone_id_cache_key(zone_name)
|
|
247
|
+
zone_name.to_s.chomp('.').downcase
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# name => [id, ...] for every zone in the account(s). Several ids under one name means the name is ambiguous.
|
|
251
|
+
# A zone created while the listing is being paged can show up on two pages, hence the uniq.
|
|
252
|
+
def zone_ids_by_name
|
|
253
|
+
@zone_ids || ZONE_IDS_MUTEX.synchronize do
|
|
254
|
+
@zone_ids ||= list_zones
|
|
255
|
+
.group_by { |zone| zone_id_cache_key(zone['name']) }
|
|
256
|
+
.transform_values { |zones| zones.map { |zone| zone['id'] }.uniq }
|
|
257
|
+
.freeze
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def remember_zone_ids(key, ids)
|
|
262
|
+
ZONE_IDS_MUTEX.synchronize do
|
|
263
|
+
@zone_ids = @zone_ids.merge(key => ids.freeze).freeze if @zone_ids
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def zone_ids_from_lookup(zone_name)
|
|
208
268
|
retry_on_connection_errors do
|
|
209
|
-
|
|
210
|
-
matching_zones
|
|
211
|
-
|
|
212
|
-
case matching_zones.size
|
|
213
|
-
when 0
|
|
214
|
-
raise "Zone not found for #{zone_name}"
|
|
215
|
-
when 1
|
|
216
|
-
matching_zones.first['id']
|
|
217
|
-
else
|
|
218
|
-
raise "Multiple zones found for #{zone_name}. " \
|
|
219
|
-
"API key must only return the zone on which records are to be managed."
|
|
220
|
-
end
|
|
269
|
+
matching_zones = client.get("/client/v4/zones", name: zone_name).result_raw || []
|
|
270
|
+
matching_zones.map { |zone| zone['id'] }
|
|
221
271
|
end
|
|
222
272
|
end
|
|
223
273
|
end
|
data/lib/record_store/record.rb
CHANGED
|
@@ -42,6 +42,12 @@ module RecordStore
|
|
|
42
42
|
unescape(value.sub(/\A"(.*)"\z/, '\1'))
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# The record type, e.g. "A" for Record::A. Called millions of times when loading zones, so it is computed once
|
|
46
|
+
# per class.
|
|
47
|
+
def type_name
|
|
48
|
+
@type_name ||= name.demodulize.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
def ensure_ends_with_dot(fqdn)
|
|
46
52
|
fqdn.end_with?(".") ? fqdn : "#{fqdn}."
|
|
47
53
|
end
|
|
@@ -80,8 +86,9 @@ module RecordStore
|
|
|
80
86
|
}.merge(rdata)
|
|
81
87
|
end
|
|
82
88
|
|
|
89
|
+
# Returns a new string each time, as it always has: callers such as fog-dynect modify it in place.
|
|
83
90
|
def type
|
|
84
|
-
self.class.
|
|
91
|
+
self.class.type_name.dup
|
|
85
92
|
end
|
|
86
93
|
|
|
87
94
|
def ==(other)
|
data/lib/record_store/version.rb
CHANGED
|
@@ -75,18 +75,26 @@ module RecordStore
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def generate_records_to_inject(current_records:)
|
|
78
|
+
# Every [type, fqdn] pair currently in the zone, including what has been injected so far. Conflict filters
|
|
79
|
+
# that only name a type and an fqdn are answered from this set instead of scanning every record.
|
|
80
|
+
present = Set.new(current_records.map { |record| [record.type, record.fqdn] })
|
|
81
|
+
|
|
78
82
|
current_records
|
|
79
83
|
.select { |record| should_template?(record: record) }
|
|
80
84
|
.map { |record| template_record_for(record: record, current_records: current_records) }
|
|
81
85
|
.each_with_object([]) do |template_records, records_to_inject|
|
|
82
86
|
next unless should_inject?(
|
|
83
87
|
template_records: template_records,
|
|
84
|
-
current_records: current_records
|
|
88
|
+
current_records: current_records,
|
|
89
|
+
records_to_inject: records_to_inject,
|
|
90
|
+
present: present,
|
|
85
91
|
)
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
new_records = template_records.fetch(:injected_records, []).map do |r_yml|
|
|
94
|
+
Record.build_from_yaml_definition(r_yml)
|
|
95
|
+
end
|
|
96
|
+
new_records.each { |record| present << [record.type, record.fqdn] }
|
|
97
|
+
records_to_inject.push(*new_records)
|
|
90
98
|
end
|
|
91
99
|
end
|
|
92
100
|
|
|
@@ -94,15 +102,33 @@ module RecordStore
|
|
|
94
102
|
|
|
95
103
|
attr_reader :template, :filters_for_records_to_template, :filters_for_records_to_exclude
|
|
96
104
|
|
|
97
|
-
def should_inject?(template_records:, current_records:)
|
|
105
|
+
def should_inject?(template_records:, current_records:, records_to_inject:, present:)
|
|
98
106
|
conflict_with = template_records[:conflict_with] || []
|
|
99
|
-
|
|
107
|
+
|
|
108
|
+
if conflict_with.all? { |filter| type_and_fqdn_filter?(filter) }
|
|
109
|
+
conflict_with.none? { |filter| present.include?([filter[:type], filter[:fqdn]]) }
|
|
110
|
+
else
|
|
111
|
+
!conflicts_with_any?(conflict_with: conflict_with, records: current_records + records_to_inject)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The general check: any record matching any filter is a conflict. Used for filters that match on anything
|
|
116
|
+
# other than an exact type and fqdn (regexps, other attributes).
|
|
117
|
+
def conflicts_with_any?(conflict_with:, records:)
|
|
118
|
+
records.any? do |record|
|
|
100
119
|
conflict_with.any? do |filter|
|
|
101
120
|
record_match?(record: record, filter: filter)
|
|
102
121
|
end
|
|
103
122
|
end
|
|
104
123
|
end
|
|
105
124
|
|
|
125
|
+
def type_and_fqdn_filter?(filter)
|
|
126
|
+
filter.is_a?(Hash) &&
|
|
127
|
+
filter.keys.sort == [:fqdn, :type] &&
|
|
128
|
+
filter[:type].is_a?(String) &&
|
|
129
|
+
filter[:fqdn].is_a?(String)
|
|
130
|
+
end
|
|
131
|
+
|
|
106
132
|
def should_template?(record:)
|
|
107
133
|
filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) } &&
|
|
108
134
|
filters_for_records_to_exclude.none? { |filter| record_match?(record: record, filter: filter) }
|
data/lib/record_store/zone.rb
CHANGED
|
@@ -259,9 +259,24 @@ module RecordStore
|
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
def validate_all_records_are_unique
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
262
|
+
if records.all? { |record| comparable_by_hash?(record) }
|
|
263
|
+
# Hash keys keep first-seen order, so each duplicated record is reported once, in the order it first appears.
|
|
264
|
+
records.tally.each do |record, count|
|
|
265
|
+
errors.add(:records, "Duplicate record: #{record}") if count > 1
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
duplicates = records.select { |r| records.count(r) > 1 }
|
|
269
|
+
duplicates.uniq.each do |record|
|
|
270
|
+
errors.add(:records, "Duplicate record: #{record}")
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Record#== compares to_hash with ==, but tally and uniq use Record#hash and eql?, which tell 1 from 1.0. The two
|
|
276
|
+
# agree when every value is a string, an Integer, a boolean or nil, so only then is tallying safe.
|
|
277
|
+
def comparable_by_hash?(record)
|
|
278
|
+
record.to_hash.each_value.all? do |value|
|
|
279
|
+
value.is_a?(String) || value.is_a?(Integer) || value == true || value == false || value.nil?
|
|
265
280
|
end
|
|
266
281
|
end
|
|
267
282
|
|
data/lib/record_store.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: record_store
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.
|
|
4
|
+
version: 9.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Willem van Bergen
|
|
@@ -461,7 +461,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
461
461
|
- !ruby/object:Gem::Version
|
|
462
462
|
version: '0'
|
|
463
463
|
requirements: []
|
|
464
|
-
rubygems_version: 4.0.
|
|
464
|
+
rubygems_version: 4.0.23
|
|
465
465
|
specification_version: 4
|
|
466
466
|
summary: Manage DNS using git
|
|
467
467
|
test_files: []
|