record_store 6.0.1 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c526e771b65c9da7f83836311bfc8c7b2af502a6801256ccdaa540b54df4138
4
- data.tar.gz: f5e34d123f4dbef6d2206c4703310af91784deb0d0d2681de05f4aea78b83985
3
+ metadata.gz: f2933700da7288ff5085d9865364776031896ea9efada8cf75fea37e5edb27b1
4
+ data.tar.gz: 5d1420e6df2c086d7d98ce655f8b8512bd65467ff4825a6f00b628b16b5fa180
5
5
  SHA512:
6
- metadata.gz: d5c507ba11cdefb048f6ceb74f30c1edef6a0f4bd0f0558126f7be02de3b9b38bab5c9d02bf8c8fec9f07ac59207dd24115fac7d0819141029f2691da989ada2
7
- data.tar.gz: d688116ba3470d5ff1a4ab4d74b8d38d8d274d1180988839679d1d9b8aeb2e813a20b3fbc0634077e9a216b74d28ca1b0e3da125c43240488f5626a9c6fbd265
6
+ metadata.gz: eee575d27faa042e9db956aa842b5d0f8a35aac56784cff7369a56970554f0430cdec4dbd67d904364a97bf5f53d5769dbf3ff8f46db4c5ce3822a8d1c39412d
7
+ data.tar.gz: 796fcd383e7df7621f21b63356c7634afb0a8a71447f0775801ec93297bd5754f48250fd28390cbfde412fe1a51d85329d8f0fd0f5358073810a98fa1ad08e2b
@@ -671,7 +671,7 @@ Style/LineEndConcatenation:
671
671
  Style/MethodCallWithoutArgsParentheses:
672
672
  Enabled: true
673
673
 
674
- Style/MethodMissingSuper:
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/UselessComparison:
967
+ Lint/BinaryOperatorWithIdenticalOperands:
968
968
  Enabled: true
969
969
 
970
970
  Lint/UselessElseWithoutRescue:
@@ -1,6 +1,16 @@
1
1
  # CHANGELOG
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
+
2
12
  ## 6.0.1
3
- - add API rate limiting to DNSimple provider
13
+ - add API rate limiting to DNSimple provider [FEATURE]
4
14
 
5
15
  ## 6.0.0
6
16
  - add `--all` option for `record-store diff` to compare ignored records too [FEATURE]
@@ -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, Errno::ETIMEDOUT
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
@@ -12,21 +12,29 @@ module RecordStore
12
12
  true
13
13
  end
14
14
 
15
+ def empty_non_terminal_over_wildcard?
16
+ false
17
+ end
18
+
15
19
  # returns an array of Record objects that match the records which exist in the provider
16
20
  def retrieve_current_records(zone:, stdout: $stdout)
17
- session.zones.all_records(account_id, zone).data.map do |record|
18
- begin
19
- build_from_api(record, zone)
20
- rescue StandardError
21
- stdout.puts "Cannot build record: #{record}"
22
- raise
23
- end
24
- end.compact
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
25
31
  end
26
32
 
27
33
  # Returns an array of the zones managed by provider as strings
28
34
  def zones
29
- session.zones.all_zones(account_id).data.map(&:name)
35
+ retry_on_connection_errors do
36
+ session.zones.all_zones(account_id).data.map(&:name)
37
+ end
30
38
  end
31
39
 
32
40
  private
@@ -1,15 +1,17 @@
1
+ require_relative '../provider_utils/waiter'
2
+
1
3
  # Patch Dnsimple client method which retrieves headers for API rate limit dynamically
2
4
  module Dnsimple
3
5
  class Client
4
6
  def execute(method, path, data = nil, options = {})
5
7
  response = request(method, path, data, options)
6
- rate_limit_sleep(response.headers["x-ratelimit-reset"].to_i, response.headers["x-ratelimit-remaining"].to_i)
8
+ rate_limit_sleep(response.headers['x-ratelimit-reset'].to_i, response.headers['x-ratelimit-remaining'].to_i)
7
9
 
8
10
  case response.code
9
11
  when 200..299
10
12
  response
11
13
  when 401
12
- raise AuthenticationFailed, response["message"]
14
+ raise AuthenticationFailed, response['message']
13
15
  when 404
14
16
  raise NotFoundError, response
15
17
  else
@@ -22,9 +24,10 @@ module Dnsimple
22
24
  def rate_limit_sleep(rate_limit_reset, rate_limit_remaining)
23
25
  rate_limit_reset_in = [0, rate_limit_reset - Time.now.to_i].max
24
26
  rate_limit_periods = rate_limit_remaining + 1
25
- wait_time = rate_limit_reset_in / rate_limit_periods.to_f
27
+ sleep_time = rate_limit_reset_in / rate_limit_periods.to_f
26
28
 
27
- sleep(wait_time) if wait_time > 0
29
+ rate_limit = RateLimitWaiter.new('DNSimple')
30
+ rate_limit.wait(sleep_time)
28
31
  end
29
32
  end
30
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
- client.zones.map { |zone| zone['zone'] }
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
- client.zone(zone)['records']
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
- sleep(response.to_hash["x-ratelimit-period"].first.to_i /
8
- [1, response.to_hash["x-ratelimit-remaining"].first.to_i].max.to_f)
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
@@ -112,6 +112,10 @@ module RecordStore
112
112
  "[#{type}Record] #{fqdn} #{ttl} IN #{type} #{rdata_txt}"
113
113
  end
114
114
 
115
+ def wildcard?
116
+ fqdn.match?(/^\*\./)
117
+ end
118
+
115
119
  protected
116
120
 
117
121
  def validate_label_length
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '6.0.1'.freeze
2
+ VERSION = '6.2.1'.freeze
3
3
  end
@@ -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 = Resolv::DNS.open(nameserver: nameserver) do |resolv|
133
- resolv.fetch_resource(name, Resolv::DNS::Resource::IN::SOA) do |reply, name|
134
- break if reply.answer.any?
133
+ authority = fetch_soa(nameserver) do |reply, _name|
134
+ break if reply.answer.any?
135
135
 
136
- raise "No authority found (#{name})" unless reply.authority.any?
136
+ raise "No authority found (#{name})" unless reply.authority.any?
137
137
 
138
- break extract_authority(reply)
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 extract_authority(reply)
151
- authority = reply.authority.sample
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
- if unrooted_name.casecmp?(authority.first.to_s)
154
- build_authority(reply.authority)
170
+ def extract_authority(authority)
171
+ if unrooted_name.casecmp?(authority.first.first.to_s)
172
+ build_authority(authority)
155
173
  else
156
- fetch_authority(authority.last.name.to_s) || build_authority(reply.authority)
174
+ resolve_authority(authority) || build_authority(authority)
157
175
  end
158
176
  end
159
177
 
160
178
  def build_authority(authority)
161
- authority.map.with_index do |(name, ttl, data), index|
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,
@@ -10,6 +10,7 @@ module RecordStore
10
10
  def defined
11
11
  @defined ||= yaml_files
12
12
  .map { |file| load_yml_zone_definition(file) }
13
+ .sort_by(&:unrooted_name)
13
14
  .index_by(&:unrooted_name)
14
15
  end
15
16
 
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.0.1
4
+ version: 6.2.1
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-05-13 00:00:00.000000000 Z
12
+ date: 2020-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -362,6 +362,7 @@ files:
362
362
  - lib/record_store/provider/ns1/client.rb
363
363
  - lib/record_store/provider/ns1/patch_api_header.rb
364
364
  - lib/record_store/provider/oracle_cloud_dns.rb
365
+ - lib/record_store/provider/provider_utils/waiter.rb
365
366
  - lib/record_store/record.rb
366
367
  - lib/record_store/record/a.rb
367
368
  - lib/record_store/record/aaaa.rb