record_store 6.0.0 → 6.2.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/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +2 -2
- data/CHANGELOG.md +12 -0
- data/lib/record_store/cli.rb +4 -2
- data/lib/record_store/provider.rb +35 -0
- data/lib/record_store/provider/dnsimple.rb +18 -9
- data/lib/record_store/provider/dnsimple/patch_api_header.rb +33 -0
- data/lib/record_store/provider/ns1.rb +6 -2
- data/lib/record_store/provider/ns1/patch_api_header.rb +13 -2
- data/lib/record_store/provider/provider_utils/waiter.rb +41 -0
- data/lib/record_store/record.rb +4 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +54 -12
- data/lib/record_store/zone/config.rb +4 -0
- data/lib/record_store/zone/yaml_definitions.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c401c0dafc01955edd90912d793cdd0f27bd698df86f18ed4132e3ae0a8d1bc
|
4
|
+
data.tar.gz: 261f5a6a0c768d417e247746fe95955393075c643322fb7a5c701d39edef218d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7160e11eec16c8ecea7fa7cf02ae7437a5093ec58f6f932a7e9e63feb85413ff5d9c28e0a90f8960ff76a94dd0bc7992e0712a0a6f40a2158062755079a16bac
|
7
|
+
data.tar.gz: 7d81f1268810c177bd785b8ab5610175d2e9b7f20db743d12996df2ab6f145c522a354a1f7e94e86e91b90c7a44b1b63bcbe1a92a1e93e0f1f5eefb13d76a659
|
@@ -671,7 +671,7 @@ Style/LineEndConcatenation:
|
|
671
671
|
Style/MethodCallWithoutArgsParentheses:
|
672
672
|
Enabled: true
|
673
673
|
|
674
|
-
|
674
|
+
Lint/MissingSuper:
|
675
675
|
Enabled: true
|
676
676
|
|
677
677
|
Style/MissingRespondToMissing:
|
@@ -964,7 +964,7 @@ Lint/UselessAccessModifier:
|
|
964
964
|
Lint/UselessAssignment:
|
965
965
|
Enabled: true
|
966
966
|
|
967
|
-
Lint/
|
967
|
+
Lint/BinaryOperatorWithIdenticalOperands:
|
968
968
|
Enabled: true
|
969
969
|
|
970
970
|
Lint/UselessElseWithoutRescue:
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 6.1.1
|
4
|
+
- Emit messages when waiting for rate-limit to elapse for DNSimple and NS1 providers, so deployment does not timeout [BUGFIX]
|
5
|
+
|
6
|
+
## 6.1.0
|
7
|
+
- sort zone files [FEATURE]
|
8
|
+
- CLI support for specifying zones for validate_authority [FEATURE]
|
9
|
+
- retry failed lookup using another nameserver if unreachable [BUGFIX]
|
10
|
+
- ignore records other than NS in authority section [BUGFIX]
|
11
|
+
|
12
|
+
## 6.0.1
|
13
|
+
- add API rate limiting to DNSimple provider [FEATURE]
|
14
|
+
|
3
15
|
## 6.0.0
|
4
16
|
- add `--all` option for `record-store diff` to compare ignored records too [FEATURE]
|
5
17
|
|
data/lib/record_store/cli.rb
CHANGED
@@ -220,12 +220,14 @@ module RecordStore
|
|
220
220
|
end
|
221
221
|
end
|
222
222
|
|
223
|
-
desc 'validate_authority', 'Validates that authoritative nameservers match the providers'
|
223
|
+
desc 'validate_authority [ZONE ...]', 'Validates that authoritative nameservers match the providers'
|
224
224
|
option :verbose, desc: 'Include valid zones in output', aliases: '-v', type: :boolean, default: false
|
225
|
-
def validate_authority
|
225
|
+
def validate_authority(*zones)
|
226
226
|
verbose = options.fetch('verbose')
|
227
227
|
|
228
228
|
Zone.each do |name, zone|
|
229
|
+
next unless zones.empty? || zones.include?(name)
|
230
|
+
|
229
231
|
authority = zone.fetch_authority
|
230
232
|
|
231
233
|
delegation = Hash.new { |h, k| h[k] = [] }
|
@@ -52,6 +52,10 @@ module RecordStore
|
|
52
52
|
false
|
53
53
|
end
|
54
54
|
|
55
|
+
def empty_non_terminal_over_wildcard?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
55
59
|
def build_zone(zone_name:, config:)
|
56
60
|
zone = Zone.new(name: zone_name)
|
57
61
|
zone.records = retrieve_current_records(zone: zone_name)
|
@@ -128,6 +132,37 @@ module RecordStore
|
|
128
132
|
|
129
133
|
dns.getresource(zone_name, Resolv::DNS::Resource::IN::SOA).mname.to_s
|
130
134
|
end
|
135
|
+
|
136
|
+
def retry_on_connection_errors(
|
137
|
+
max_timeouts: 5,
|
138
|
+
max_conn_resets: 5,
|
139
|
+
delay: 1,
|
140
|
+
backoff_multiplier: 2,
|
141
|
+
max_backoff: 10
|
142
|
+
)
|
143
|
+
waiter = BackoffWaiter.new(
|
144
|
+
"Waiting to retry after a connection reset",
|
145
|
+
initial_delay: delay,
|
146
|
+
multiplier: backoff_multiplier,
|
147
|
+
max_delay: max_backoff,
|
148
|
+
)
|
149
|
+
|
150
|
+
loop do
|
151
|
+
begin
|
152
|
+
return yield
|
153
|
+
rescue Net::OpenTimeout
|
154
|
+
raise if max_timeouts <= 0
|
155
|
+
max_timeouts -= 1
|
156
|
+
|
157
|
+
$stderr.puts("Retrying after a connection timeout")
|
158
|
+
rescue Errno::ECONNRESET
|
159
|
+
raise if max_conn_resets <= 0
|
160
|
+
max_conn_resets -= 1
|
161
|
+
|
162
|
+
waiter.wait
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
131
166
|
end
|
132
167
|
end
|
133
168
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'dnsimple'
|
2
|
+
require_relative 'dnsimple/patch_api_header'
|
2
3
|
|
3
4
|
module RecordStore
|
4
5
|
class Provider::DNSimple < Provider
|
@@ -11,21 +12,29 @@ module RecordStore
|
|
11
12
|
true
|
12
13
|
end
|
13
14
|
|
15
|
+
def empty_non_terminal_over_wildcard?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
14
19
|
# returns an array of Record objects that match the records which exist in the provider
|
15
20
|
def retrieve_current_records(zone:, stdout: $stdout)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
retry_on_connection_errors do
|
22
|
+
session.zones.all_records(account_id, zone).data.map do |record|
|
23
|
+
begin
|
24
|
+
build_from_api(record, zone)
|
25
|
+
rescue StandardError
|
26
|
+
stdout.puts "Cannot build record: #{record}"
|
27
|
+
raise
|
28
|
+
end
|
29
|
+
end.compact
|
30
|
+
end
|
24
31
|
end
|
25
32
|
|
26
33
|
# Returns an array of the zones managed by provider as strings
|
27
34
|
def zones
|
28
|
-
|
35
|
+
retry_on_connection_errors do
|
36
|
+
session.zones.all_zones(account_id).data.map(&:name)
|
37
|
+
end
|
29
38
|
end
|
30
39
|
|
31
40
|
private
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative '../provider_utils/waiter'
|
2
|
+
|
3
|
+
# Patch Dnsimple client method which retrieves headers for API rate limit dynamically
|
4
|
+
module Dnsimple
|
5
|
+
class Client
|
6
|
+
def execute(method, path, data = nil, options = {})
|
7
|
+
response = request(method, path, data, options)
|
8
|
+
rate_limit_sleep(response.headers['x-ratelimit-reset'].to_i, response.headers['x-ratelimit-remaining'].to_i)
|
9
|
+
|
10
|
+
case response.code
|
11
|
+
when 200..299
|
12
|
+
response
|
13
|
+
when 401
|
14
|
+
raise AuthenticationFailed, response['message']
|
15
|
+
when 404
|
16
|
+
raise NotFoundError, response
|
17
|
+
else
|
18
|
+
raise RequestError, response
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def rate_limit_sleep(rate_limit_reset, rate_limit_remaining)
|
25
|
+
rate_limit_reset_in = [0, rate_limit_reset - Time.now.to_i].max
|
26
|
+
rate_limit_periods = rate_limit_remaining + 1
|
27
|
+
sleep_time = rate_limit_reset_in / rate_limit_periods.to_f
|
28
|
+
|
29
|
+
rate_limit = RateLimitWaiter.new('DNSimple')
|
30
|
+
rate_limit.wait(sleep_time)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -60,14 +60,18 @@ module RecordStore
|
|
60
60
|
|
61
61
|
# Returns an array of the zones managed by provider as strings
|
62
62
|
def zones
|
63
|
-
|
63
|
+
retry_on_connection_errors do
|
64
|
+
client.zones.map { |zone| zone['zone'] }
|
65
|
+
end
|
64
66
|
end
|
65
67
|
|
66
68
|
private
|
67
69
|
|
68
70
|
# Fetches simplified records for the provided zone
|
69
71
|
def records_for_zone(zone)
|
70
|
-
|
72
|
+
retry_on_connection_errors do
|
73
|
+
client.zone(zone)['records']
|
74
|
+
end
|
71
75
|
end
|
72
76
|
|
73
77
|
# Creates a new record to the zone. It is expected this call modifies external state.
|
@@ -1,11 +1,22 @@
|
|
1
1
|
require 'net/http'
|
2
|
+
require_relative '../provider_utils/waiter'
|
2
3
|
|
3
4
|
# Patch the method which retrieves headers for API rate limit dynamically
|
4
5
|
module NS1::Transport
|
5
6
|
class NetHttp
|
7
|
+
X_RATELIMIT_PERIOD = 'x-ratelimit-period'.freeze
|
8
|
+
X_RATELIMIT_REMAINING = 'x-ratelimit-remaining'.freeze
|
9
|
+
|
6
10
|
def process_response(response)
|
7
|
-
|
8
|
-
|
11
|
+
response_hash = response.to_hash
|
12
|
+
|
13
|
+
if response_hash.key?(X_RATELIMIT_PERIOD) && response_hash.key?(X_RATELIMIT_REMAINING)
|
14
|
+
sleep_time = response_hash[X_RATELIMIT_PERIOD].first.to_i /
|
15
|
+
[1, response_hash[X_RATELIMIT_REMAINING].first.to_i].max.to_f
|
16
|
+
|
17
|
+
rate_limit = RateLimitWaiter.new('NS1')
|
18
|
+
rate_limit.wait(sleep_time)
|
19
|
+
end
|
9
20
|
|
10
21
|
body = JSON.parse(response.body)
|
11
22
|
case response
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class Waiter
|
2
|
+
def initialize(message = nil)
|
3
|
+
@message = message || 'Waiting'
|
4
|
+
end
|
5
|
+
|
6
|
+
attr_accessor :message
|
7
|
+
|
8
|
+
def wait(sleep_time)
|
9
|
+
while sleep_time > 0
|
10
|
+
wait_time = [10, sleep_time].min
|
11
|
+
puts "#{message} (#{sleep_time}s left)" if wait_time > 1
|
12
|
+
sleep(wait_time)
|
13
|
+
sleep_time -= wait_time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RateLimitWaiter < Waiter
|
19
|
+
def initialize(provider)
|
20
|
+
super("Waiting on #{provider} rate-limit")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class BackoffWaiter < Waiter
|
25
|
+
def initialize(message, initial_delay:, multiplier:, max_delay: nil)
|
26
|
+
super(message)
|
27
|
+
|
28
|
+
@initial_delay = @current_delay = initial_delay
|
29
|
+
@multiplier = multiplier
|
30
|
+
@max_delay = max_delay
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset
|
34
|
+
@current_delay = @initial_delay
|
35
|
+
end
|
36
|
+
|
37
|
+
def wait
|
38
|
+
super(@current_delay)
|
39
|
+
@current_delay = [@current_delay * @multiplier, @max_delay].compact.min
|
40
|
+
end
|
41
|
+
end
|
data/lib/record_store/record.rb
CHANGED
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -18,6 +18,7 @@ module RecordStore
|
|
18
18
|
validate :validate_cname_records_dont_point_to_root
|
19
19
|
validate :validate_same_ttl_for_records_sharing_fqdn_and_type
|
20
20
|
validate :validate_provider_can_handle_zone_records
|
21
|
+
validate :validate_no_empty_non_terminal
|
21
22
|
validate :validate_can_handle_alias_records
|
22
23
|
|
23
24
|
class << self
|
@@ -129,14 +130,12 @@ module RecordStore
|
|
129
130
|
)
|
130
131
|
|
131
132
|
def fetch_authority(nameserver = ROOT_SERVERS.sample)
|
132
|
-
authority =
|
133
|
-
|
134
|
-
break if reply.answer.any?
|
133
|
+
authority = fetch_soa(nameserver) do |reply, _name|
|
134
|
+
break if reply.answer.any?
|
135
135
|
|
136
|
-
|
136
|
+
raise "No authority found (#{name})" unless reply.authority.any?
|
137
137
|
|
138
|
-
|
139
|
-
end
|
138
|
+
break extract_authority(reply.authority)
|
140
139
|
end
|
141
140
|
|
142
141
|
# candidate DNS name is returned instead when NXDomain or other error
|
@@ -147,18 +146,39 @@ module RecordStore
|
|
147
146
|
|
148
147
|
private
|
149
148
|
|
150
|
-
def
|
151
|
-
|
149
|
+
def fetch_soa(nameserver)
|
150
|
+
Resolv::DNS.open(nameserver: nameserver) do |resolv|
|
151
|
+
resolv.fetch_resource(name, Resolv::DNS::Resource::IN::SOA) do |reply, name|
|
152
|
+
yield reply, name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def resolve_authority(authority)
|
158
|
+
nameservers = authority.map { |a| a.last.name.to_s }
|
159
|
+
|
160
|
+
begin
|
161
|
+
nameserver = nameservers.shift
|
162
|
+
fetch_authority(nameserver)
|
163
|
+
rescue Errno::EHOSTUNREACH => e
|
164
|
+
$stderr.puts "Warning: #{e} [host=#{nameserver}]"
|
165
|
+
raise if nameservers.empty?
|
166
|
+
retry
|
167
|
+
end
|
168
|
+
end
|
152
169
|
|
153
|
-
|
154
|
-
|
170
|
+
def extract_authority(authority)
|
171
|
+
if unrooted_name.casecmp?(authority.first.first.to_s)
|
172
|
+
build_authority(authority)
|
155
173
|
else
|
156
|
-
|
174
|
+
resolve_authority(authority) || build_authority(authority)
|
157
175
|
end
|
158
176
|
end
|
159
177
|
|
160
178
|
def build_authority(authority)
|
161
|
-
authority.
|
179
|
+
ns = authority.select { |_name, _ttl, data| data.is_a?(Resolv::DNS::Resource::IN::NS) }
|
180
|
+
|
181
|
+
ns.map.with_index do |(name, ttl, data), index|
|
162
182
|
Record::NS.new(ttl: ttl, fqdn: name.to_s, nsdname: data.name.to_s, record_id: index)
|
163
183
|
end
|
164
184
|
end
|
@@ -239,6 +259,28 @@ module RecordStore
|
|
239
259
|
end
|
240
260
|
end
|
241
261
|
|
262
|
+
def validate_no_empty_non_terminal
|
263
|
+
return unless config.empty_non_terminal_over_wildcard?
|
264
|
+
|
265
|
+
wildcards = records.select(&:wildcard?).map(&:fqdn).uniq
|
266
|
+
wildcards.each do |wildcard|
|
267
|
+
suffix = wildcard[1..-1]
|
268
|
+
|
269
|
+
terminal_records = records.map(&:fqdn)
|
270
|
+
.select { |record| record.match?(/^([a-zA-Z0-9-_]+\.[a-zA-Z0-9-_])#{Regexp.escape(suffix)}$/) }
|
271
|
+
next unless terminal_records.any?
|
272
|
+
|
273
|
+
intermediate_records = records.map(&:fqdn)
|
274
|
+
.select { |record| record.match?(/^([a-zA-Z0-9-_]+)#{Regexp.escape(suffix)}$/) }
|
275
|
+
terminal_records.each do |terminal_record|
|
276
|
+
non_terminal = terminal_record.partition('.').last
|
277
|
+
errors.add(:records, "found empty non-terminal #{non_terminal} "\
|
278
|
+
"(caused by existing records #{wildcard} and #{terminal_record})")\
|
279
|
+
unless intermediate_records.include?(non_terminal)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
242
284
|
def validate_can_handle_alias_records
|
243
285
|
return unless records.any? { |record| record.is_a?(Record::ALIAS) }
|
244
286
|
return if config.supports_alias?
|
@@ -24,6 +24,10 @@ module RecordStore
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
def empty_non_terminal_over_wildcard?
|
28
|
+
valid_providers? && providers.any? { |provider| Provider.const_get(provider).empty_non_terminal_over_wildcard? }
|
29
|
+
end
|
30
|
+
|
27
31
|
def to_hash
|
28
32
|
config_hash = {
|
29
33
|
providers: providers,
|
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: 6.
|
4
|
+
version: 6.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-08-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -355,12 +355,14 @@ files:
|
|
355
355
|
- lib/record_store/cli.rb
|
356
356
|
- lib/record_store/provider.rb
|
357
357
|
- lib/record_store/provider/dnsimple.rb
|
358
|
+
- lib/record_store/provider/dnsimple/patch_api_header.rb
|
358
359
|
- lib/record_store/provider/dynect.rb
|
359
360
|
- lib/record_store/provider/google_cloud_dns.rb
|
360
361
|
- lib/record_store/provider/ns1.rb
|
361
362
|
- lib/record_store/provider/ns1/client.rb
|
362
363
|
- lib/record_store/provider/ns1/patch_api_header.rb
|
363
364
|
- lib/record_store/provider/oracle_cloud_dns.rb
|
365
|
+
- lib/record_store/provider/provider_utils/waiter.rb
|
364
366
|
- lib/record_store/record.rb
|
365
367
|
- lib/record_store/record/a.rb
|
366
368
|
- lib/record_store/record/aaaa.rb
|