record_store 5.5.3 → 5.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +1 -0
- data/bin/console +2 -2
- data/bin/record-store +1 -1
- data/bin/rubocop +29 -0
- data/dev.yml +4 -0
- data/lib/record_store.rb +4 -1
- data/lib/record_store/changeset.rb +8 -4
- data/lib/record_store/cli.rb +50 -48
- data/lib/record_store/provider.rb +11 -13
- data/lib/record_store/provider/dnsimple.rb +1 -2
- data/lib/record_store/provider/dynect.rb +6 -6
- data/lib/record_store/provider/google_cloud_dns.rb +5 -6
- data/lib/record_store/provider/ns1.rb +13 -9
- data/lib/record_store/provider/ns1/client.rb +3 -4
- data/lib/record_store/record.rb +27 -9
- data/lib/record_store/record/a.rb +4 -6
- data/lib/record_store/record/aaaa.rb +4 -6
- data/lib/record_store/record/alias.rb +5 -1
- data/lib/record_store/record/caa.rb +7 -3
- data/lib/record_store/record/cname.rb +5 -1
- data/lib/record_store/record/mx.rb +11 -3
- data/lib/record_store/record/ns.rb +5 -1
- data/lib/record_store/record/srv.rb +20 -4
- data/lib/record_store/record/txt.rb +1 -1
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +22 -16
- data/lib/record_store/zone/config.rb +1 -1
- data/lib/record_store/zone/yaml_definitions.rb +3 -2
- data/record_store.gemspec +4 -3
- data/template/bin/record-store +1 -1
- metadata +19 -2
@@ -3,7 +3,7 @@ require 'google/cloud/dns'
|
|
3
3
|
module RecordStore
|
4
4
|
class Provider::GoogleCloudDNS < Provider
|
5
5
|
class << self
|
6
|
-
def apply_changeset(changeset,
|
6
|
+
def apply_changeset(changeset, _stdout = nil)
|
7
7
|
zone = session.zone(convert_to_name(changeset.zone))
|
8
8
|
|
9
9
|
deletions = convert_records_to_gcloud_record_sets(zone, changeset.current_records)
|
@@ -45,10 +45,10 @@ module RecordStore
|
|
45
45
|
|
46
46
|
def session
|
47
47
|
@dns ||= begin
|
48
|
-
Google::Cloud::Dns.new(
|
48
|
+
Google::Cloud::Dns.new(
|
49
49
|
project_id: secrets.fetch('project_id'),
|
50
50
|
credentials: Google::Cloud::Dns::Credentials.new(secrets),
|
51
|
-
|
51
|
+
)
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
@@ -72,9 +72,8 @@ module RecordStore
|
|
72
72
|
[record.type, record.fqdn]
|
73
73
|
end
|
74
74
|
|
75
|
-
|
76
75
|
record_sets.map do |(rr_type, rr_fqdn), records_for_set|
|
77
|
-
zone.record(rr_fqdn, rr_type, records_for_set[0].ttl, records_for_set.map(&:rdata_txt))
|
76
|
+
zone.record(rr_fqdn, rr_type, records_for_set[0].ttl, Record.long_quote(records_for_set.map(&:rdata_txt)))
|
78
77
|
end
|
79
78
|
end
|
80
79
|
|
@@ -109,7 +108,7 @@ module RecordStore
|
|
109
108
|
when 'NS'
|
110
109
|
record_params.merge!(nsdname: record.data[0])
|
111
110
|
when 'SPF', 'TXT'
|
112
|
-
txtdata = Record.
|
111
|
+
txtdata = Record.unlong_quote(record.data[0]).gsub(';', '\;')
|
113
112
|
record_params.merge!(txtdata: txtdata)
|
114
113
|
when 'SRV'
|
115
114
|
priority, weight, port, target = record.data[0].split(' ')
|
@@ -12,7 +12,7 @@ module RecordStore
|
|
12
12
|
# Downloads all the records from the provider.
|
13
13
|
#
|
14
14
|
# Returns: an array of `Record` for each record in the provider's zone
|
15
|
-
def retrieve_current_records(zone:, stdout: $stdout)
|
15
|
+
def retrieve_current_records(zone:, stdout: $stdout) # rubocop:disable Lint/UnusedMethodArgument
|
16
16
|
full_api_records = records_for_zone(zone).map do |short_record|
|
17
17
|
client.record(
|
18
18
|
zone: zone,
|
@@ -22,7 +22,7 @@ module RecordStore
|
|
22
22
|
)
|
23
23
|
end
|
24
24
|
|
25
|
-
full_api_records.map { |r| build_from_api(r
|
25
|
+
full_api_records.map { |r| build_from_api(r) }.flatten.compact
|
26
26
|
end
|
27
27
|
|
28
28
|
# Returns an array of the zones managed by provider as strings
|
@@ -129,7 +129,11 @@ module RecordStore
|
|
129
129
|
answer["answer"] = build_api_answer_from_record(record)
|
130
130
|
end
|
131
131
|
|
132
|
-
|
132
|
+
unless updated
|
133
|
+
error = +'while trying to update a record, could not find answer with fqdn: '
|
134
|
+
error << "#{record.fqdn}, type; #{record.type}, id: #{id}"
|
135
|
+
raise Error, error
|
136
|
+
end
|
133
137
|
|
134
138
|
client.modify_record(
|
135
139
|
zone: zone,
|
@@ -139,7 +143,7 @@ module RecordStore
|
|
139
143
|
)
|
140
144
|
end
|
141
145
|
|
142
|
-
def build_from_api(api_record
|
146
|
+
def build_from_api(api_record)
|
143
147
|
fqdn = Record.ensure_ends_with_dot(api_record["domain"])
|
144
148
|
|
145
149
|
record_type = api_record["type"]
|
@@ -150,7 +154,7 @@ module RecordStore
|
|
150
154
|
record = {
|
151
155
|
ttl: api_record["ttl"],
|
152
156
|
fqdn: fqdn.downcase,
|
153
|
-
record_id: api_answer["id"]
|
157
|
+
record_id: api_answer["id"],
|
154
158
|
}
|
155
159
|
|
156
160
|
case record_type
|
@@ -179,7 +183,7 @@ module RecordStore
|
|
179
183
|
when 'NS'
|
180
184
|
record.merge!(nsdname: answer.first)
|
181
185
|
when 'SPF', 'TXT'
|
182
|
-
record.merge!(txtdata: Record.unescape(answer.first).gsub(';', '\;'))
|
186
|
+
record.merge!(txtdata: Record.unlong_quote(Record.unescape(answer.first).gsub(';', '\;')))
|
183
187
|
when 'SRV'
|
184
188
|
priority, weight, port, host = answer
|
185
189
|
|
@@ -197,8 +201,8 @@ module RecordStore
|
|
197
201
|
def build_api_answer_from_record(record)
|
198
202
|
if record.is_a?(Record::MX)
|
199
203
|
[record.preference, record.exchange]
|
200
|
-
elsif record.is_a?(Record::TXT)
|
201
|
-
[record.txtdata]
|
204
|
+
elsif record.is_a?(Record::TXT) || record.is_a?(Record::SPF)
|
205
|
+
[Record.long_quote(record.txtdata)]
|
202
206
|
elsif record.is_a?(Record::CAA)
|
203
207
|
[record.flags, record.tag, record.value]
|
204
208
|
elsif record.is_a?(Record::SRV)
|
@@ -209,7 +213,7 @@ module RecordStore
|
|
209
213
|
end
|
210
214
|
|
211
215
|
def symbolize_keys(hash)
|
212
|
-
hash.map{ |key, value| [key.to_sym, value] }.to_h
|
216
|
+
hash.map { |key, value| [key.to_sym, value] }.to_h
|
213
217
|
end
|
214
218
|
|
215
219
|
def secrets
|
@@ -26,22 +26,21 @@ module RecordStore
|
|
26
26
|
|
27
27
|
def create_record(zone:, fqdn:, type:, params:)
|
28
28
|
result = super(zone, fqdn, type, params)
|
29
|
-
raise(Error, result.to_s) if result.is_a?
|
29
|
+
raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
|
30
30
|
nil
|
31
31
|
end
|
32
32
|
|
33
33
|
def modify_record(zone:, fqdn:, type:, params:)
|
34
34
|
result = super(zone, fqdn, type, params)
|
35
|
-
raise(Error, result.to_s) if result.is_a?
|
35
|
+
raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
|
36
36
|
nil
|
37
37
|
end
|
38
38
|
|
39
39
|
def delete_record(zone:, fqdn:, type:)
|
40
40
|
result = super(zone, fqdn, type)
|
41
|
-
raise(Error, result.to_s) if result.is_a?
|
41
|
+
raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
|
42
42
|
nil
|
43
43
|
end
|
44
44
|
end
|
45
45
|
end
|
46
|
-
|
47
46
|
end
|
data/lib/record_store/record.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module RecordStore
|
2
2
|
class Record
|
3
3
|
FQDN_REGEX = /\A(\*\.)?([a-z0-9_]+(-[a-z0-9]+)*\._?)+[a-z]{2,}\.\Z/i
|
4
|
-
CNAME_REGEX =
|
4
|
+
CNAME_REGEX = /\A(\*\.)?([a-z0-9_]+((-|--)?[a-z0-9]+)*\._?)+[a-z]{2,}\.\Z/i
|
5
5
|
|
6
6
|
include ActiveModel::Validations
|
7
7
|
|
@@ -17,7 +17,21 @@ module RecordStore
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def quote(value)
|
20
|
-
|
20
|
+
result = escape(value)
|
21
|
+
%("#{result}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def long_quote(value)
|
25
|
+
result = value
|
26
|
+
if needs_long_quotes?(value)
|
27
|
+
result = unquote(value).scan(/.{1,255}/).join('" "')
|
28
|
+
result = %("#{result}")
|
29
|
+
end
|
30
|
+
result
|
31
|
+
end
|
32
|
+
|
33
|
+
def unlong_quote(value)
|
34
|
+
value.length > 255 ? value.scan(/.{1,258}/).map { |x| x.sub(/^\"/, "").sub(/\" ?$/, "") }.join : unquote(value)
|
21
35
|
end
|
22
36
|
|
23
37
|
def unescape(value)
|
@@ -27,6 +41,14 @@ module RecordStore
|
|
27
41
|
def unquote(value)
|
28
42
|
unescape(value.sub(/\A"(.*)"\z/, '\1'))
|
29
43
|
end
|
44
|
+
|
45
|
+
def ensure_ends_with_dot(fqdn)
|
46
|
+
fqdn.end_with?(".") ? fqdn : "#{fqdn}."
|
47
|
+
end
|
48
|
+
|
49
|
+
def needs_long_quotes?(value)
|
50
|
+
value.length > 255 && value !~ /^((\\)?"((\\"|[^"])){1,255}(\\)?"\s*)+$/
|
51
|
+
end
|
30
52
|
end
|
31
53
|
|
32
54
|
def initialize(record)
|
@@ -40,7 +62,7 @@ module RecordStore
|
|
40
62
|
Record.const_get(record_type).new(yaml_definition)
|
41
63
|
end
|
42
64
|
|
43
|
-
def log!(logger=STDOUT)
|
65
|
+
def log!(logger = STDOUT)
|
44
66
|
logger.puts to_s
|
45
67
|
end
|
46
68
|
|
@@ -48,7 +70,7 @@ module RecordStore
|
|
48
70
|
{
|
49
71
|
type: type,
|
50
72
|
fqdn: fqdn,
|
51
|
-
ttl: ttl
|
73
|
+
ttl: ttl,
|
52
74
|
}.merge(rdata)
|
53
75
|
end
|
54
76
|
|
@@ -57,7 +79,7 @@ module RecordStore
|
|
57
79
|
end
|
58
80
|
|
59
81
|
def ==(other)
|
60
|
-
other.class == self.class && other.to_hash ==
|
82
|
+
other.class == self.class && other.to_hash == to_hash
|
61
83
|
end
|
62
84
|
|
63
85
|
alias_method :eql?, :==
|
@@ -93,9 +115,5 @@ module RecordStore
|
|
93
115
|
errors.add(:fqdn, "A label should be at most 63 characters")
|
94
116
|
end
|
95
117
|
end
|
96
|
-
|
97
|
-
def self.ensure_ends_with_dot(fqdn)
|
98
|
-
fqdn.end_with?(".") ? fqdn : "#{fqdn}."
|
99
|
-
end
|
100
118
|
end
|
101
119
|
end
|
@@ -21,12 +21,10 @@ module RecordStore
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def valid_address?
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
errors.add(:address, 'is invalid')
|
29
|
-
end
|
24
|
+
ip = IPAddr.new(address)
|
25
|
+
errors.add(:address, 'is not an IPv4 address') unless ip.ipv4?
|
26
|
+
rescue IPAddr::InvalidAddressError
|
27
|
+
errors.add(:address, 'is invalid')
|
30
28
|
end
|
31
29
|
end
|
32
30
|
end
|
@@ -21,12 +21,10 @@ module RecordStore
|
|
21
21
|
private
|
22
22
|
|
23
23
|
def valid_address?
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
errors.add(:address, 'is invalid')
|
29
|
-
end
|
24
|
+
ip = IPAddr.new(address)
|
25
|
+
errors.add(:address, 'is not an IPv6 address') unless ip.ipv6?
|
26
|
+
rescue IPAddr::InvalidAddressError
|
27
|
+
errors.add(:address, 'is invalid')
|
30
28
|
end
|
31
29
|
end
|
32
30
|
end
|
@@ -2,7 +2,11 @@ module RecordStore
|
|
2
2
|
class Record::ALIAS < Record
|
3
3
|
attr_accessor :alias
|
4
4
|
|
5
|
-
validates :alias, presence: true, format:
|
5
|
+
validates :alias, presence: true, format:
|
6
|
+
{
|
7
|
+
with: Record::CNAME_REGEX,
|
8
|
+
message: 'is not a fully qualified domain name',
|
9
|
+
}
|
6
10
|
validate :validate_circular_reference
|
7
11
|
|
8
12
|
def initialize(record)
|
@@ -5,10 +5,14 @@ module RecordStore
|
|
5
5
|
LABEL_REGEX = '[a-z0-9](?:-*[a-z0-9])*'
|
6
6
|
DOMAIN_REGEX = /\A#{LABEL_REGEX}(?:\.#{LABEL_REGEX})\z/i
|
7
7
|
|
8
|
-
validates :flags,
|
8
|
+
validates :flags, presence: true, numericality:
|
9
|
+
{
|
10
|
+
only_integer: true, greater_than_or_equal_to: 0,
|
11
|
+
less_than_or_equal_to: 255
|
12
|
+
}
|
9
13
|
validates :tag, inclusion: { in: %w(issue issuewild iodef) }, presence: true
|
10
14
|
validate :validate_uri_value, if: :iodef?
|
11
|
-
validates :value, format: { with: DOMAIN_REGEX, message: 'is not a fully qualified domain name'}, unless: :iodef?
|
15
|
+
validates :value, format: { with: DOMAIN_REGEX, message: 'is not a fully qualified domain name' }, unless: :iodef?
|
12
16
|
|
13
17
|
def initialize(record)
|
14
18
|
super
|
@@ -37,7 +41,7 @@ module RecordStore
|
|
37
41
|
|
38
42
|
def validate_uri_value
|
39
43
|
uri = URI(value)
|
40
|
-
return if uri.
|
44
|
+
return if uri.is_a?(URI::MailTo) || uri.is_a?(URI::HTTP)
|
41
45
|
errors.add(:value, "URL scheme should be mailto, http, or https")
|
42
46
|
rescue URI::Error
|
43
47
|
errors.add(:value, "Value should be a valid URI")
|
@@ -2,7 +2,11 @@ module RecordStore
|
|
2
2
|
class Record::CNAME < Record
|
3
3
|
attr_accessor :cname
|
4
4
|
|
5
|
-
validates :cname, presence: true, format:
|
5
|
+
validates :cname, presence: true, format:
|
6
|
+
{
|
7
|
+
with: Record::CNAME_REGEX,
|
8
|
+
message: 'is not a fully qualified domain name',
|
9
|
+
}
|
6
10
|
validate :validate_circular_reference
|
7
11
|
|
8
12
|
def initialize(record)
|
@@ -2,8 +2,16 @@ module RecordStore
|
|
2
2
|
class Record::MX < Record
|
3
3
|
attr_accessor :exchange, :preference
|
4
4
|
|
5
|
-
validates :preference,
|
6
|
-
|
5
|
+
validates :preference, presence: true, numericality:
|
6
|
+
{
|
7
|
+
only_integer: true,
|
8
|
+
greater_than_or_equal_to: 0,
|
9
|
+
}
|
10
|
+
validates :exchange, presence: true, format:
|
11
|
+
{
|
12
|
+
with: Record::FQDN_REGEX,
|
13
|
+
message: 'is not a fully qualified domain name',
|
14
|
+
}
|
7
15
|
|
8
16
|
def initialize(record)
|
9
17
|
super
|
@@ -14,7 +22,7 @@ module RecordStore
|
|
14
22
|
def rdata
|
15
23
|
{
|
16
24
|
preference: preference,
|
17
|
-
exchange: exchange
|
25
|
+
exchange: exchange,
|
18
26
|
}
|
19
27
|
end
|
20
28
|
|
@@ -2,7 +2,11 @@ module RecordStore
|
|
2
2
|
class Record::NS < Record
|
3
3
|
attr_accessor :nsdname
|
4
4
|
|
5
|
-
validates :nsdname, presence: true, format:
|
5
|
+
validates :nsdname, presence: true, format:
|
6
|
+
{
|
7
|
+
with: Record::FQDN_REGEX,
|
8
|
+
message: 'is not a fully qualified domain name',
|
9
|
+
}
|
6
10
|
|
7
11
|
def initialize(record)
|
8
12
|
super
|
@@ -2,10 +2,26 @@ module RecordStore
|
|
2
2
|
class Record::SRV < Record
|
3
3
|
attr_accessor :priority, :port, :weight, :target
|
4
4
|
|
5
|
-
validates :priority, presence: true, numericality:
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
validates :priority, presence: true, numericality:
|
6
|
+
{
|
7
|
+
only_integer: true,
|
8
|
+
greater_than_or_equal_to: 0,
|
9
|
+
}
|
10
|
+
validates :port, presence: true, numericality:
|
11
|
+
{
|
12
|
+
only_integer: true,
|
13
|
+
greater_than_or_equal_to: 0,
|
14
|
+
}
|
15
|
+
validates :weight, presence: true, numericality:
|
16
|
+
{
|
17
|
+
only_integer: true,
|
18
|
+
greater_than_or_equal_to: 0,
|
19
|
+
}
|
20
|
+
validates :target, presence: true, format:
|
21
|
+
{
|
22
|
+
with: Record::CNAME_REGEX,
|
23
|
+
message: 'is not a fully qualified domain name',
|
24
|
+
}
|
9
25
|
|
10
26
|
def initialize(record)
|
11
27
|
super
|
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -6,7 +6,11 @@ module RecordStore
|
|
6
6
|
attr_accessor :name
|
7
7
|
attr_reader :config
|
8
8
|
|
9
|
-
validates :name, presence: true, format:
|
9
|
+
validates :name, presence: true, format:
|
10
|
+
{
|
11
|
+
with: Record::FQDN_REGEX,
|
12
|
+
message: 'is not a fully qualified domain name',
|
13
|
+
}
|
10
14
|
validate :validate_records
|
11
15
|
validate :validate_config
|
12
16
|
validate :validate_all_records_are_unique
|
@@ -18,14 +22,14 @@ module RecordStore
|
|
18
22
|
|
19
23
|
class << self
|
20
24
|
def download(name, provider_name, **write_options)
|
21
|
-
zone = Zone.new(name: name, config: {providers: [provider_name]})
|
25
|
+
zone = Zone.new(name: name, config: { providers: [provider_name] })
|
22
26
|
raise ArgumentError, zone.errors.full_messages.join("\n") unless zone.valid?
|
23
27
|
|
24
28
|
zone.records = zone.providers.first.retrieve_current_records(zone: name)
|
25
29
|
|
26
30
|
zone.config = Zone::Config.new(
|
27
31
|
providers: [provider_name],
|
28
|
-
ignore_patterns: [{type: "NS", fqdn: "#{name}."}],
|
32
|
+
ignore_patterns: [{ type: "NS", fqdn: "#{name}." }],
|
29
33
|
supports_alias: (zone.records.map(&:type).include?('ALIAS') || nil)
|
30
34
|
)
|
31
35
|
|
@@ -41,13 +45,15 @@ module RecordStore
|
|
41
45
|
end
|
42
46
|
|
43
47
|
MAX_PARALLEL_THREADS = 10
|
44
|
-
def modified(verbose: false)
|
45
|
-
modified_zones
|
48
|
+
def modified(verbose: false) # rubocop:disable Lint/UnusedMethodArgument
|
49
|
+
modified_zones = []
|
50
|
+
mutex = Mutex.new
|
51
|
+
zones = all
|
46
52
|
|
47
53
|
(1..MAX_PARALLEL_THREADS).map do
|
48
54
|
Thread.new do
|
49
55
|
current_zone = nil
|
50
|
-
while
|
56
|
+
while zones.any?
|
51
57
|
mutex.synchronize { current_zone = zones.shift }
|
52
58
|
mutex.synchronize { modified_zones << current_zone } unless current_zone.unchanged?
|
53
59
|
end
|
@@ -147,15 +153,15 @@ module RecordStore
|
|
147
153
|
cname_records = records.select { |record| record.is_a?(Record::CNAME) }
|
148
154
|
cname_records.each do |cname_record|
|
149
155
|
records.each do |record|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
156
|
+
next unless record.fqdn == cname_record.fqdn && record != cname_record
|
157
|
+
case record.type
|
158
|
+
when 'SIG', 'NXT', 'KEY'
|
159
|
+
# this is fine
|
160
|
+
when 'CNAME'
|
161
|
+
errors.add(:records, "Multiple CNAME records are defined for #{record.fqdn}: #{record}")
|
162
|
+
else
|
163
|
+
cname_error = "A CNAME record is defined for #{cname_record.fqdn}, so this record is not allowed: #{record}"
|
164
|
+
errors.add(:records, cname_error)
|
159
165
|
end
|
160
166
|
end
|
161
167
|
end
|
@@ -175,7 +181,7 @@ module RecordStore
|
|
175
181
|
|
176
182
|
providers.each do |provider|
|
177
183
|
(record_types - provider.record_types).each do |record_type|
|
178
|
-
errors.add(:records, "#{record_type} is a not a supported record type in #{provider
|
184
|
+
errors.add(:records, "#{record_type} is a not a supported record type in #{provider}")
|
179
185
|
end
|
180
186
|
end
|
181
187
|
end
|