record_store 5.11.0 → 6.1.2

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: 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