record_store 5.11.0 → 6.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f7a361748d4f6dd8179730ba18eadc60890c4317df58730ffd23dbb15bef6230
4
- data.tar.gz: b4a3c7ffaeceedde2621a580cd52534fbba8cd4b4052a0f8561008f8718e6d64
3
+ metadata.gz: 4d05ecb06f048975b797baa10df042222f03ee4f0b1b5e268afa87e97b16ede6
4
+ data.tar.gz: 4ca96a597c88f5d17df9f85ea50e8743b0cab6fa4d57196447d3808cc22711a6
5
5
  SHA512:
6
- metadata.gz: 95c9e88b0721f53f0212e5b97c852523a0864a6a191045a45083541a4ff3f3d25c5a179814519c651bcba4b4b857a4ab9b34e435ecffadb4f727af9c2f115c32
7
- data.tar.gz: 6f418902506b12e6a5e58640ddc3efc21a2c790a2d0a315a97b284fa5ae2443bb9d76a6f623d641eae1b24eec2bd6971c3dafb0be5da6454b01a43faef50439c
6
+ metadata.gz: 98581b53f61499cb2f83d92ae2de4cf132ce2784cf56f8dc81531228f4090995f8e7a5a2c70f0a8fcac0cc5a3bdea29dd5c1d7b6996eef61e416c0c65c6a252d
7
+ data.tar.gz: 8e753593bc358e83f82a0191f07528e14aa793305570afae5fcc865e1bdd97fffb48319fec43642e662c7b62e1070dd3b384464b9ca55a39e2a590fcf0f6738b
@@ -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,20 @@
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
+
15
+ ## 6.0.0
16
+ - add `--all` option for `record-store diff` to compare ignored records too [FEATURE]
17
+
3
18
  ## 5.11.0
4
19
  - support PTR record type [FEATURE]
5
20
 
@@ -36,12 +36,15 @@ module RecordStore
36
36
 
37
37
  attr_reader :current_records, :desired_records, :removals, :additions, :updates, :provider, :zone
38
38
 
39
- def self.build_from(provider:, zone:)
39
+ def self.build_from(provider:, zone:, all: false)
40
40
  current_zone = provider.build_zone(zone_name: zone.unrooted_name, config: zone.config)
41
41
 
42
+ current_records = all ? current_zone.all : current_zone.records
43
+ desired_records = all ? zone.all : zone.records
44
+
42
45
  new(
43
- current_records: current_zone.records,
44
- desired_records: zone.records,
46
+ current_records: current_records,
47
+ desired_records: desired_records,
45
48
  provider: provider,
46
49
  zone: zone.unrooted_name
47
50
  )
@@ -59,13 +59,18 @@ module RecordStore
59
59
  end
60
60
  end
61
61
 
62
- desc 'diff', 'Displays the DNS differences between the zone files in this repo and production'
62
+ desc 'diff [ZONE ...]', 'Displays the DNS differences between the zone files in this repo and production'
63
+ option :all, desc: 'Include all records', aliases: '-a', type: :boolean, default: false
63
64
  option :verbose, desc: 'Print records that haven\'t diverged', aliases: '-v', type: :boolean, default: false
64
- def diff
65
- puts "Diffing #{Zone.defined.count} zones"
65
+ def diff(*zones)
66
+ puts "Diffing #{zones.any? ? zones.count : Zone.defined.count} zone(s)"
67
+
68
+ all = options.fetch('all')
66
69
 
67
70
  Zone.each do |name, zone|
68
- changesets = zone.build_changesets
71
+ next unless zones.empty? || zones.include?(name)
72
+
73
+ changesets = zone.build_changesets(all: all)
69
74
 
70
75
  if !options.fetch('verbose') && changesets.all?(&:empty?)
71
76
  print_and_flush('.')
@@ -81,21 +86,21 @@ module RecordStore
81
86
  puts '-' * 20
82
87
  puts "Provider: #{changeset.provider}"
83
88
 
84
- if !changeset.additions.empty? || options.fetch('verbose')
89
+ if changeset.additions.any?
85
90
  puts "Add:"
86
91
  changeset.additions.map(&:record).each do |record|
87
92
  puts " - #{record}"
88
93
  end
89
94
  end
90
95
 
91
- if !changeset.removals.empty? || options.fetch('verbose')
96
+ if changeset.removals.any?
92
97
  puts "Remove:"
93
98
  changeset.removals.map(&:record).each do |record|
94
99
  puts " - #{record}"
95
100
  end
96
101
  end
97
102
 
98
- if !changeset.updates.empty? || options.fetch('verbose')
103
+ if changeset.updates.any?
99
104
  puts "Update:"
100
105
  changeset.updates.map(&:record).each do |record|
101
106
  puts " - #{record}"
@@ -215,12 +220,14 @@ module RecordStore
215
220
  end
216
221
  end
217
222
 
218
- desc 'validate_authority', 'Validates that authoritative nameservers match the providers'
223
+ desc 'validate_authority [ZONE ...]', 'Validates that authoritative nameservers match the providers'
219
224
  option :verbose, desc: 'Include valid zones in output', aliases: '-v', type: :boolean, default: false
220
- def validate_authority
225
+ def validate_authority(*zones)
221
226
  verbose = options.fetch('verbose')
222
227
 
223
228
  Zone.each do |name, zone|
229
+ next unless zones.empty? || zones.include?(name)
230
+
224
231
  authority = zone.fetch_authority
225
232
 
226
233
  delegation = Hash.new { |h, k| h[k] = [] }
@@ -128,6 +128,36 @@ module RecordStore
128
128
 
129
129
  dns.getresource(zone_name, Resolv::DNS::Resource::IN::SOA).mname.to_s
130
130
  end
131
+
132
+ def retry_on_connection_errors(
133
+ max_timeouts: 5,
134
+ max_conn_resets: 5,
135
+ delay: 1,
136
+ backoff_multiplier: 2,
137
+ max_backoff: 10
138
+ )
139
+ loop do
140
+ begin
141
+ return yield
142
+ rescue Net::OpenTimeout
143
+ raise if max_timeouts <= 0
144
+ max_timeouts -= 1
145
+
146
+ $stderr.puts("Retrying after a connection timeout")
147
+ rescue Errno::ECONNRESET
148
+ raise if max_conn_resets <= 0
149
+ max_conn_resets -= 1
150
+
151
+ $stderr.puts("Retrying in #{delay}s after a connection reset")
152
+ backoff_sleep(delay)
153
+ delay = [delay * backoff_multiplier, max_backoff].min
154
+ end
155
+ end
156
+ end
157
+
158
+ def backoff_sleep(delay)
159
+ sleep(delay)
160
+ end
131
161
  end
132
162
  end
133
163
  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
@@ -13,19 +14,23 @@ module RecordStore
13
14
 
14
15
  # returns an array of Record objects that match the records which exist in the provider
15
16
  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
17
+ retry_on_connection_errors do
18
+ session.zones.all_records(account_id, zone).data.map do |record|
19
+ begin
20
+ build_from_api(record, zone)
21
+ rescue StandardError
22
+ stdout.puts "Cannot build record: #{record}"
23
+ raise
24
+ end
25
+ end.compact
26
+ end
24
27
  end
25
28
 
26
29
  # Returns an array of the zones managed by provider as strings
27
30
  def zones
28
- session.zones.all_zones(account_id).data.map(&:name)
31
+ retry_on_connection_errors do
32
+ session.zones.all_zones(account_id).data.map(&:name)
33
+ end
29
34
  end
30
35
 
31
36
  private
@@ -0,0 +1,33 @@
1
+ require_relative '../provider_utils/rate_limit'
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 = RateLimit.new('DNSimple')
30
+ rate_limit.sleep_for(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/rate_limit'
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 = RateLimit.new('NS1')
18
+ rate_limit.sleep_for(sleep_time)
19
+ end
9
20
 
10
21
  body = JSON.parse(response.body)
11
22
  case response
@@ -0,0 +1,14 @@
1
+ class RateLimit
2
+ def initialize(provider)
3
+ @provider = provider
4
+ end
5
+
6
+ def sleep_for(sleep_time)
7
+ while sleep_time > 0
8
+ wait = [10, sleep_time].min
9
+ puts "Waiting on #{@provider} rate-limit" if wait > 1
10
+ sleep(wait)
11
+ sleep_time -= wait
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '5.11.0'.freeze
2
+ VERSION = '6.1.2'.freeze
3
3
  end
@@ -70,10 +70,10 @@ module RecordStore
70
70
  @records = build_records(records)
71
71
  end
72
72
 
73
- def build_changesets
73
+ def build_changesets(all: false)
74
74
  @changesets ||= begin
75
75
  providers.map do |provider|
76
- Changeset.build_from(provider: provider, zone: self)
76
+ Changeset.build_from(provider: provider, zone: self, all: all)
77
77
  end
78
78
  end
79
79
  end
@@ -129,14 +129,12 @@ module RecordStore
129
129
  )
130
130
 
131
131
  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?
132
+ authority = fetch_soa(nameserver) do |reply, _name|
133
+ break if reply.answer.any?
135
134
 
136
- raise "No authority found (#{name})" unless reply.authority.any?
135
+ raise "No authority found (#{name})" unless reply.authority.any?
137
136
 
138
- break extract_authority(reply)
139
- end
137
+ break extract_authority(reply.authority)
140
138
  end
141
139
 
142
140
  # candidate DNS name is returned instead when NXDomain or other error
@@ -147,18 +145,39 @@ module RecordStore
147
145
 
148
146
  private
149
147
 
150
- def extract_authority(reply)
151
- authority = reply.authority.sample
148
+ def fetch_soa(nameserver)
149
+ Resolv::DNS.open(nameserver: nameserver) do |resolv|
150
+ resolv.fetch_resource(name, Resolv::DNS::Resource::IN::SOA) do |reply, name|
151
+ yield reply, name
152
+ end
153
+ end
154
+ end
152
155
 
153
- if unrooted_name.casecmp?(authority.first.to_s)
154
- build_authority(reply.authority)
156
+ def resolve_authority(authority)
157
+ nameservers = authority.map { |a| a.last.name.to_s }
158
+
159
+ begin
160
+ nameserver = nameservers.shift
161
+ fetch_authority(nameserver)
162
+ rescue Errno::EHOSTUNREACH => e
163
+ $stderr.puts "Warning: #{e} [host=#{nameserver}]"
164
+ raise if nameservers.empty?
165
+ retry
166
+ end
167
+ end
168
+
169
+ def extract_authority(authority)
170
+ if unrooted_name.casecmp?(authority.first.first.to_s)
171
+ build_authority(authority)
155
172
  else
156
- fetch_authority(authority.last.name.to_s) || build_authority(reply.authority)
173
+ resolve_authority(authority) || build_authority(authority)
157
174
  end
158
175
  end
159
176
 
160
177
  def build_authority(authority)
161
- authority.map.with_index do |(name, ttl, data), index|
178
+ ns = authority.select { |_name, _ttl, data| data.is_a?(Resolv::DNS::Resource::IN::NS) }
179
+
180
+ ns.map.with_index do |(name, ttl, data), index|
162
181
  Record::NS.new(ttl: ttl, fqdn: name.to_s, nsdname: data.name.to_s, record_id: index)
163
182
  end
164
183
  end
@@ -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
 
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_runtime_dependency 'fog-xml'
37
37
  spec.add_runtime_dependency 'fog-dynect', '~> 0.4.0'
38
38
  spec.add_runtime_dependency 'dnsimple', '~> 4.4.0'
39
- spec.add_runtime_dependency 'google-cloud-dns'
39
+ spec.add_runtime_dependency 'google-cloud-dns', '~> 0.31.0'
40
40
  spec.add_runtime_dependency 'ruby-limiter', '~> 1.0', '>= 1.0.1'
41
41
  spec.add_runtime_dependency 'ns1'
42
42
  spec.add_runtime_dependency 'oci', '~> 2.6.0'
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: 5.11.0
4
+ version: 6.1.2
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-16 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
@@ -141,16 +141,16 @@ dependencies:
141
141
  name: google-cloud-dns
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
- - - ">="
144
+ - - "~>"
145
145
  - !ruby/object:Gem::Version
146
- version: '0'
146
+ version: 0.31.0
147
147
  type: :runtime
148
148
  prerelease: false
149
149
  version_requirements: !ruby/object:Gem::Requirement
150
150
  requirements:
151
- - - ">="
151
+ - - "~>"
152
152
  - !ruby/object:Gem::Version
153
- version: '0'
153
+ version: 0.31.0
154
154
  - !ruby/object:Gem::Dependency
155
155
  name: ruby-limiter
156
156
  requirement: !ruby/object:Gem::Requirement
@@ -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/rate_limit.rb
364
366
  - lib/record_store/record.rb
365
367
  - lib/record_store/record/a.rb
366
368
  - lib/record_store/record/aaaa.rb
@@ -412,7 +414,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
412
414
  - !ruby/object:Gem::Version
413
415
  version: '0'
414
416
  requirements: []
415
- rubygems_version: 3.0.2
417
+ rubygems_version: 3.0.3
416
418
  signing_key:
417
419
  specification_version: 4
418
420
  summary: Manage DNS using git