record_store 5.10.0 → 6.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|