record_store 5.10.0 → 6.1.1

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: 305f971c2df40732e6ed0a53798af1ef1ffe4bda959454beea2c9f2d4da09ee7
4
- data.tar.gz: 4233420dd7d25ac624d52d643a4326762133f6531784de8174c1de15ca890a57
3
+ metadata.gz: f7a5da9483eaf8ce4eb82e8d974fefe82f7f481cde945a45ec6ae204b02524dc
4
+ data.tar.gz: 51e681688b6cc80c851f801ad00bb07ccf473ea2fb0b1c8a8e722d3cbc2ec196
5
5
  SHA512:
6
- metadata.gz: 99034b1c2625066dd7c4d32ee33582ef58ad2203197c52650f2a810365799604969b49575699f70ad8a009ee443838cd2de8989e75724dc4dc8967fecfbc6e63
7
- data.tar.gz: 150539a073dc355c83288f0356ba8db15d28287e17977dd7cce882487a439f19cfa614305cd67b3f1ffe1aa21f7359c170a168803248af9d8eb914f5fd844496
6
+ metadata.gz: 502acb53fcef150d5cb5442414c47552fc199efe66012e85d30745ae25c8af326064283c855ba12f4cead5b6f7b09f5a97c0b3bd6a4de1a010bba15664954335
7
+ data.tar.gz: 491ee9ff80ccb35ceb7ffbe70776948cd459467df8c213917c08ac6830e249585dbd993849a44e413bd6fc8e38589601eab27601cf3061c9b51abd09a94898a0
@@ -803,7 +803,7 @@ Layout/SpaceInsideRangeLiteral:
803
803
  Style/SymbolLiteral:
804
804
  Enabled: true
805
805
 
806
- Layout/Tab:
806
+ Layout/IndentationStyle:
807
807
  Enabled: true
808
808
 
809
809
  Layout/TrailingWhitespace:
@@ -1,5 +1,23 @@
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
+
18
+ ## 5.11.0
19
+ - support PTR record type [FEATURE]
20
+
3
21
  ## 5.10.0
4
22
  - add `record-store validate_authority` command to sanity check delegation [FEATURE]
5
23
  - fix handling of NXDOMAIN, etc. when fetching authoritative nameservers [BUGFIX]
@@ -19,6 +19,7 @@ require 'record_store/record/caa'
19
19
  require 'record_store/record/cname'
20
20
  require 'record_store/record/mx'
21
21
  require 'record_store/record/ns'
22
+ require 'record_store/record/ptr'
22
23
  require 'record_store/record/sshfp'
23
24
  require 'record_store/record/txt'
24
25
  require 'record_store/record/spf'
@@ -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] = [] }
@@ -1,10 +1,11 @@
1
1
  require 'dnsimple'
2
+ require_relative 'dnsimple/patch_api_header'
2
3
 
3
4
  module RecordStore
4
5
  class Provider::DNSimple < Provider
5
6
  class << self
6
7
  def record_types
7
- super | Set.new(['SSHFP'])
8
+ super | Set.new(%w(PTR SSHFP))
8
9
  end
9
10
 
10
11
  def supports_alias?
@@ -99,6 +100,8 @@ module RecordStore
99
100
  record.merge!(preference: api_record.priority, exchange: api_record.content)
100
101
  when 'NS'
101
102
  record.merge!(nsdname: api_record.content)
103
+ when 'PTR'
104
+ record.merge!(ptrdname: api_record.content)
102
105
  when 'SSHFP'
103
106
  algorithm, fptype, fingerprint = api_record.content.split(' ')
104
107
  record.merge!(
@@ -143,6 +146,8 @@ module RecordStore
143
146
  record_hash[:content] = record.exchange.chomp('.')
144
147
  when 'NS'
145
148
  record_hash[:content] = record.nsdname.chomp('.')
149
+ when 'PTR'
150
+ record_hash[:content] = record.ptrdname.chomp('.')
146
151
  when 'SSHFP'
147
152
  record_hash[:content] = record.rdata_txt
148
153
  when 'SPF', 'TXT'
@@ -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
@@ -41,6 +41,10 @@ module RecordStore
41
41
  end
42
42
 
43
43
  class << self
44
+ def record_types
45
+ super | Set.new(%w(PTR))
46
+ end
47
+
44
48
  def client
45
49
  Provider::NS1::Client.new(api_key: secrets['api_key'])
46
50
  end
@@ -229,6 +233,8 @@ module RecordStore
229
233
  )
230
234
  when 'NS'
231
235
  record.merge!(nsdname: answer.rrdata_string)
236
+ when 'PTR'
237
+ record.merge!(ptrdname: answer.rrdata_string)
232
238
  when 'SPF', 'TXT'
233
239
  record.merge!(txtdata: answer.rrdata_string.gsub(';', '\;'))
234
240
  when 'SRV'
@@ -1,11 +1,15 @@
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
6
7
  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)
8
+ sleep_time = response.to_hash['x-ratelimit-period'].first.to_i /
9
+ [1, response.to_hash['x-ratelimit-remaining'].first.to_i].max.to_f
10
+
11
+ rate_limit = RateLimit.new('NS1')
12
+ rate_limit.sleep_for(sleep_time)
9
13
 
10
14
  body = JSON.parse(response.body)
11
15
  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
@@ -0,0 +1,42 @@
1
+ module RecordStore
2
+ class Record::PTR < Record
3
+ attr_accessor :ptrdname
4
+
5
+ OCTET_LABEL_SEQUENCE_REGEX = /\A(?:([0-9]|[1-9][0-9]|[1-9][0-9][0-9])\.){1,4}/
6
+ IN_ADDR_ARPA_SUFFIX_REGEX = /in-addr\.arpa\.\z/
7
+ FQDN_FORMAT_REGEX = Regexp.new(OCTET_LABEL_SEQUENCE_REGEX.source + IN_ADDR_ARPA_SUFFIX_REGEX.source)
8
+
9
+ validates_format_of :fqdn, with: FQDN_FORMAT_REGEX
10
+
11
+ validate :validate_fqdn_octets_in_range
12
+ validate :validate_fqdn_is_in_addr_arpa_subzone
13
+
14
+ def initialize(record)
15
+ super
16
+
17
+ @ptrdname = Record.ensure_ends_with_dot(record.fetch(:ptrdname))
18
+ end
19
+
20
+ def rdata
21
+ { ptrdname: ptrdname }
22
+ end
23
+
24
+ def rdata_txt
25
+ ptrdname.to_s
26
+ end
27
+
28
+ def validate_fqdn_octets_in_range
29
+ OCTET_LABEL_SEQUENCE_REGEX.match(fqdn) do |m|
30
+ unless m.captures.all? { |o| o.to_d.between?(0, 255) }
31
+ errors.add(:fqdn, 'octet labels must be within the range 0-255')
32
+ end
33
+ end
34
+ end
35
+
36
+ def validate_fqdn_is_in_addr_arpa_subzone
37
+ unless IN_ADDR_ARPA_SUFFIX_REGEX.match?(fqdn)
38
+ errors.add(:fqdn, 'PTR records may only exist in the in-addr.arpa zone')
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '5.10.0'.freeze
2
+ VERSION = '6.1.1'.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.10.0
4
+ version: 6.1.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-04-15 00:00:00.000000000 Z
12
+ date: 2020-06-11 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
@@ -369,6 +371,7 @@ files:
369
371
  - lib/record_store/record/cname.rb
370
372
  - lib/record_store/record/mx.rb
371
373
  - lib/record_store/record/ns.rb
374
+ - lib/record_store/record/ptr.rb
372
375
  - lib/record_store/record/spf.rb
373
376
  - lib/record_store/record/srv.rb
374
377
  - lib/record_store/record/sshfp.rb