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 +4 -4
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1 -1
- data/CHANGELOG.md +18 -0
- data/lib/record_store.rb +1 -0
- data/lib/record_store/changeset.rb +6 -3
- data/lib/record_store/cli.rb +16 -9
- data/lib/record_store/provider/dnsimple.rb +6 -1
- data/lib/record_store/provider/dnsimple/patch_api_header.rb +33 -0
- data/lib/record_store/provider/ns1.rb +6 -0
- data/lib/record_store/provider/ns1/patch_api_header.rb +6 -2
- data/lib/record_store/provider/provider_utils/rate_limit.rb +14 -0
- data/lib/record_store/record/ptr.rb +42 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +33 -14
- data/lib/record_store/zone/yaml_definitions.rb +1 -0
- data/record_store.gemspec +1 -1
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f7a5da9483eaf8ce4eb82e8d974fefe82f7f481cde945a45ec6ae204b02524dc
|
4
|
+
data.tar.gz: 51e681688b6cc80c851f801ad00bb07ccf473ea2fb0b1c8a8e722d3cbc2ec196
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 502acb53fcef150d5cb5442414c47552fc199efe66012e85d30745ae25c8af326064283c855ba12f4cead5b6f7b09f5a97c0b3bd6a4de1a010bba15664954335
|
7
|
+
data.tar.gz: 491ee9ff80ccb35ceb7ffbe70776948cd459467df8c213917c08ac6830e249585dbd993849a44e413bd6fc8e38589601eab27601cf3061c9b51abd09a94898a0
|
data/CHANGELOG.md
CHANGED
@@ -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]
|
data/lib/record_store.rb
CHANGED
@@ -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:
|
44
|
-
desired_records:
|
46
|
+
current_records: current_records,
|
47
|
+
desired_records: desired_records,
|
45
48
|
provider: provider,
|
46
49
|
zone: zone.unrooted_name
|
47
50
|
)
|
data/lib/record_store/cli.rb
CHANGED
@@ -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}
|
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
|
-
|
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
|
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
|
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
|
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(
|
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
|
-
|
8
|
-
|
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
|
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -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 =
|
133
|
-
|
134
|
-
break if reply.answer.any?
|
132
|
+
authority = fetch_soa(nameserver) do |reply, _name|
|
133
|
+
break if reply.answer.any?
|
135
134
|
|
136
|
-
|
135
|
+
raise "No authority found (#{name})" unless reply.authority.any?
|
137
136
|
|
138
|
-
|
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
|
151
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
173
|
+
resolve_authority(authority) || build_authority(authority)
|
157
174
|
end
|
158
175
|
end
|
159
176
|
|
160
177
|
def build_authority(authority)
|
161
|
-
authority.
|
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
|
data/record_store.gemspec
CHANGED
@@ -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:
|
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-
|
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:
|
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:
|
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
|