record_store 6.0.0 → 6.2.0

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: 6f450801e1929b93095645ce0975dba493e5acc0eb3dd8582fe1e480eeccb285
4
- data.tar.gz: 9e6418b616eccdacf8b164a51627fc2ddb2d9c8eb8e233f67b03862642eeaf4a
3
+ metadata.gz: 7c401c0dafc01955edd90912d793cdd0f27bd698df86f18ed4132e3ae0a8d1bc
4
+ data.tar.gz: 261f5a6a0c768d417e247746fe95955393075c643322fb7a5c701d39edef218d
5
5
  SHA512:
6
- metadata.gz: 76bccf5594a613fc29f9052596add31a8622f5b0bbdb3cf32a1a46ee6ff91cd1312fd576b8108d86534397ac253bcc61b7fa630be0d1ffb69d5e179b424a4dad
7
- data.tar.gz: dc01e786afb99b508b1cd8b2fbb612b2a52741a9c9f12146a0b08189cf2d0d3f9366cefc05f4ba4e1fd190e55612445fc80e2484aaa91bd1f5712a51c13864e2
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
- 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,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
 
@@ -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
- session.zones.all_records(account_id, zone).data.map do |record|
17
- begin
18
- build_from_api(record, zone)
19
- rescue StandardError
20
- stdout.puts "Cannot build record: #{record}"
21
- raise
22
- end
23
- 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
24
31
  end
25
32
 
26
33
  # Returns an array of the zones managed by provider as strings
27
34
  def zones
28
- 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
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
- 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.0'.freeze
2
+ VERSION = '6.2.0'.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.0
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-04-24 00:00:00.000000000 Z
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