record_store 6.3.1 → 6.7.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/.github/dependabot.yml +17 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/cla.yml +23 -0
- data/.github/workflows/stale-action-handling.yml +31 -0
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +10 -5
- data/.rubocop.yml +26 -5
- data/.travis.yml +1 -1
- data/CHANGELOG.md +52 -0
- data/README.md +13 -0
- data/bin/console +1 -0
- data/bin/rubocop +4 -2
- data/dev.yml +1 -1
- data/lib/record_store/changeset.rb +22 -18
- data/lib/record_store/cli.rb +13 -14
- data/lib/record_store/provider/dnsimple/patch_request_error_to_include_errors.rb +20 -0
- data/lib/record_store/provider/dnsimple.rb +10 -11
- data/lib/record_store/provider/dynect.rb +5 -7
- data/lib/record_store/provider/google_cloud_dns.rb +9 -13
- data/lib/record_store/provider/ns1/client.rb +3 -1
- data/lib/record_store/provider/ns1/patch_api_header.rb +1 -1
- data/lib/record_store/provider/ns1.rb +11 -8
- data/lib/record_store/provider/oracle_cloud_dns.rb +4 -3
- data/lib/record_store/provider.rb +23 -19
- data/lib/record_store/record/alias.rb +1 -1
- data/lib/record_store/record/caa.rb +3 -1
- data/lib/record_store/record/cname.rb +1 -1
- data/lib/record_store/record.rb +6 -4
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone/config/implicit_record_template.rb +112 -0
- data/lib/record_store/zone/config.rb +5 -3
- data/lib/record_store/zone/yaml_definitions.rb +7 -2
- data/lib/record_store/zone.rb +80 -18
- data/lib/record_store.rb +15 -2
- data/record_store.gemspec +14 -14
- data/template/config.yml +1 -0
- data/template/templates/implicit_records/implicit_example.yml.erb +15 -0
- data/template/zones/dynect.example.com.yml +2 -0
- metadata +95 -70
|
@@ -13,6 +13,9 @@ module RecordStore
|
|
|
13
13
|
rrdata_fields = case type
|
|
14
14
|
when 'SPF', 'TXT'
|
|
15
15
|
[answer]
|
|
16
|
+
when 'SRV'
|
|
17
|
+
priority, weight, port, host = answer.split
|
|
18
|
+
[priority.to_i, weight.to_i, port.to_i, Record.ensure_ends_with_dot(host)]
|
|
16
19
|
else
|
|
17
20
|
answer.split
|
|
18
21
|
end
|
|
@@ -50,7 +53,7 @@ module RecordStore
|
|
|
50
53
|
# Downloads all the records from the provider.
|
|
51
54
|
#
|
|
52
55
|
# Returns: an array of `Record` for each record in the provider's zone
|
|
53
|
-
def retrieve_current_records(zone:, stdout: $stdout)
|
|
56
|
+
def retrieve_current_records(zone:, stdout: $stdout)
|
|
54
57
|
records_for_zone(zone)
|
|
55
58
|
.flat_map { |short_record| build_from_api(short_record) }
|
|
56
59
|
.compact
|
|
@@ -84,7 +87,7 @@ module RecordStore
|
|
|
84
87
|
existing_record = client.record(
|
|
85
88
|
zone: zone,
|
|
86
89
|
fqdn: record_fqdn,
|
|
87
|
-
type: record.type
|
|
90
|
+
type: record.type,
|
|
88
91
|
)
|
|
89
92
|
|
|
90
93
|
if existing_record.nil?
|
|
@@ -96,7 +99,7 @@ module RecordStore
|
|
|
96
99
|
answers: new_answers,
|
|
97
100
|
ttl: record.ttl,
|
|
98
101
|
use_client_subnet: false, # only required for filter chains that are not supported by record_store
|
|
99
|
-
}
|
|
102
|
+
},
|
|
100
103
|
)
|
|
101
104
|
return
|
|
102
105
|
end
|
|
@@ -106,7 +109,7 @@ module RecordStore
|
|
|
106
109
|
zone: zone,
|
|
107
110
|
fqdn: record_fqdn,
|
|
108
111
|
type: record.type,
|
|
109
|
-
params: { answers: existing_answers + new_answers, ttl: record.ttl }
|
|
112
|
+
params: { answers: existing_answers + new_answers, ttl: record.ttl },
|
|
110
113
|
)
|
|
111
114
|
end
|
|
112
115
|
|
|
@@ -120,7 +123,7 @@ module RecordStore
|
|
|
120
123
|
existing_record = client.record(
|
|
121
124
|
zone: zone,
|
|
122
125
|
fqdn: record_fqdn,
|
|
123
|
-
type: record.type
|
|
126
|
+
type: record.type,
|
|
124
127
|
)
|
|
125
128
|
return if existing_record.nil?
|
|
126
129
|
|
|
@@ -132,7 +135,7 @@ module RecordStore
|
|
|
132
135
|
client.delete_record(
|
|
133
136
|
zone: zone,
|
|
134
137
|
fqdn: record_fqdn,
|
|
135
|
-
type: record.type
|
|
138
|
+
type: record.type,
|
|
136
139
|
)
|
|
137
140
|
return
|
|
138
141
|
end
|
|
@@ -141,7 +144,7 @@ module RecordStore
|
|
|
141
144
|
zone: zone,
|
|
142
145
|
fqdn: record_fqdn,
|
|
143
146
|
type: record.type,
|
|
144
|
-
params: { answers: pruned_answers }
|
|
147
|
+
params: { answers: pruned_answers },
|
|
145
148
|
)
|
|
146
149
|
end
|
|
147
150
|
|
|
@@ -185,7 +188,7 @@ module RecordStore
|
|
|
185
188
|
zone: zone,
|
|
186
189
|
fqdn: record_fqdn,
|
|
187
190
|
type: record.type,
|
|
188
|
-
params: { answers: existing_record['answers'], ttl: record.ttl }
|
|
191
|
+
params: { answers: existing_record['answers'], ttl: record.ttl },
|
|
189
192
|
)
|
|
190
193
|
end
|
|
191
194
|
|
|
@@ -55,7 +55,7 @@ module RecordStore
|
|
|
55
55
|
|
|
56
56
|
client.patch_zone_records(
|
|
57
57
|
zone,
|
|
58
|
-
OCI::Dns::Models::PatchZoneRecordsDetails.new(items: patch_add_record)
|
|
58
|
+
OCI::Dns::Models::PatchZoneRecordsDetails.new(items: patch_add_record),
|
|
59
59
|
)
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -72,6 +72,7 @@ module RecordStore
|
|
|
72
72
|
).data.items.select { |r| r.rdata == record.rdata_txt }
|
|
73
73
|
|
|
74
74
|
return unless found_record
|
|
75
|
+
|
|
75
76
|
begin
|
|
76
77
|
record_hash = found_record.first.record_hash if found_record.length == 1
|
|
77
78
|
rescue NoMethodError
|
|
@@ -90,7 +91,7 @@ module RecordStore
|
|
|
90
91
|
|
|
91
92
|
client.patch_zone_records(
|
|
92
93
|
zone,
|
|
93
|
-
OCI::Dns::Models::PatchZoneRecordsDetails.new(items: patch_remove_record)
|
|
94
|
+
OCI::Dns::Models::PatchZoneRecordsDetails.new(items: patch_remove_record),
|
|
94
95
|
)
|
|
95
96
|
end
|
|
96
97
|
end
|
|
@@ -122,7 +123,7 @@ module RecordStore
|
|
|
122
123
|
domain: record_fqdn,
|
|
123
124
|
ttl: record.ttl,
|
|
124
125
|
rtype: record.type,
|
|
125
|
-
rdata: record.rdata_txt
|
|
126
|
+
rdata: record.rdata_txt,
|
|
126
127
|
)
|
|
127
128
|
update_zone_record_items.delete_if { |r| id == r.record_hash }
|
|
128
129
|
|
|
@@ -3,10 +3,12 @@ require 'resolv'
|
|
|
3
3
|
module RecordStore
|
|
4
4
|
class Provider
|
|
5
5
|
class Error < StandardError; end
|
|
6
|
+
|
|
6
7
|
class UnparseableBodyError < Error; end
|
|
7
8
|
|
|
8
9
|
class << self
|
|
9
10
|
def provider_for(object)
|
|
11
|
+
lookup_error = false
|
|
10
12
|
ns_server =
|
|
11
13
|
case object
|
|
12
14
|
when Record::NS
|
|
@@ -16,9 +18,10 @@ module RecordStore
|
|
|
16
18
|
master_nameserver_for(object)
|
|
17
19
|
rescue Resolv::ResolvError
|
|
18
20
|
$stderr.puts "Domain doesn't exist (#{object})"
|
|
19
|
-
|
|
21
|
+
lookup_error = true
|
|
20
22
|
end
|
|
21
23
|
end
|
|
24
|
+
return if lookup_error
|
|
22
25
|
|
|
23
26
|
case ns_server
|
|
24
27
|
when /\.dnsimple\.com\z/
|
|
@@ -152,24 +155,25 @@ module RecordStore
|
|
|
152
155
|
)
|
|
153
156
|
|
|
154
157
|
loop do
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
158
|
+
return yield
|
|
159
|
+
rescue UnparseableBodyError
|
|
160
|
+
raise if max_retries <= 0
|
|
161
|
+
|
|
162
|
+
max_retries -= 1
|
|
163
|
+
|
|
164
|
+
waiter.wait(message: 'Waiting to retry after receiving an unparseable response')
|
|
165
|
+
rescue Net::OpenTimeout, Errno::ETIMEDOUT
|
|
166
|
+
raise if max_timeouts <= 0
|
|
167
|
+
|
|
168
|
+
max_timeouts -= 1
|
|
169
|
+
|
|
170
|
+
$stderr.puts('Retrying after a connection timeout')
|
|
171
|
+
rescue Errno::ECONNRESET
|
|
172
|
+
raise if max_conn_resets <= 0
|
|
173
|
+
|
|
174
|
+
max_conn_resets -= 1
|
|
175
|
+
|
|
176
|
+
waiter.wait
|
|
173
177
|
end
|
|
174
178
|
end
|
|
175
179
|
end
|
|
@@ -7,7 +7,8 @@ module RecordStore
|
|
|
7
7
|
|
|
8
8
|
validates :flags, presence: true, numericality:
|
|
9
9
|
{
|
|
10
|
-
only_integer: true,
|
|
10
|
+
only_integer: true,
|
|
11
|
+
greater_than_or_equal_to: 0,
|
|
11
12
|
less_than_or_equal_to: 255
|
|
12
13
|
}
|
|
13
14
|
validates :tag, inclusion: { in: %w(issue issuewild iodef) }, presence: true
|
|
@@ -42,6 +43,7 @@ module RecordStore
|
|
|
42
43
|
def validate_uri_value
|
|
43
44
|
uri = URI(value)
|
|
44
45
|
return if uri.is_a?(URI::MailTo) || uri.is_a?(URI::HTTP)
|
|
46
|
+
|
|
45
47
|
errors.add(:value, "URL scheme should be mailto, http, or https")
|
|
46
48
|
rescue URI::Error
|
|
47
49
|
errors.add(:value, "Value should be a valid URI")
|
data/lib/record_store/record.rb
CHANGED
|
@@ -61,12 +61,14 @@ module RecordStore
|
|
|
61
61
|
@id = record.fetch(:record_id, nil)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
class << self
|
|
65
|
+
def build_from_yaml_definition(yaml_definition)
|
|
66
|
+
record_type = yaml_definition.fetch(:type)
|
|
67
|
+
Record.const_get(record_type).new(yaml_definition)
|
|
68
|
+
end
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
def log!(logger =
|
|
71
|
+
def log!(logger = $stdout)
|
|
70
72
|
logger.puts to_s
|
|
71
73
|
end
|
|
72
74
|
|
data/lib/record_store/version.rb
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RecordStore
|
|
4
|
+
class Zone
|
|
5
|
+
class Config
|
|
6
|
+
class TemplateContext
|
|
7
|
+
class << self
|
|
8
|
+
def build(record:, current_records:)
|
|
9
|
+
new(record: record, current_records: current_records)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(record:, current_records:)
|
|
14
|
+
@record = record
|
|
15
|
+
@current_records = current_records
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch_binding
|
|
19
|
+
binding
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :record, :current_records
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ImplicitRecordTemplate
|
|
28
|
+
class << self
|
|
29
|
+
def from_file(filename:)
|
|
30
|
+
filepath = template_filepath_for(filename: filename)
|
|
31
|
+
template_file = File.read(filepath)
|
|
32
|
+
|
|
33
|
+
template_file_yaml = YAML.safe_load(template_file, permitted_classes: [Regexp]).deep_symbolize_keys
|
|
34
|
+
filters_for_records_to_template = template_file_yaml[:each_record]
|
|
35
|
+
filters_for_records_to_exclude = template_file_yaml[:except_record] || []
|
|
36
|
+
|
|
37
|
+
new(
|
|
38
|
+
template: ERB.new(template_file),
|
|
39
|
+
filters_for_records_to_template: filters_for_records_to_template,
|
|
40
|
+
filters_for_records_to_exclude: filters_for_records_to_exclude,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def template_filepath_for(filename:)
|
|
47
|
+
"#{RecordStore.implicit_records_templates_path}/#{filename}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(template:, filters_for_records_to_template:, filters_for_records_to_exclude:)
|
|
52
|
+
@template = template
|
|
53
|
+
@filters_for_records_to_template = filters_for_records_to_template
|
|
54
|
+
@filters_for_records_to_exclude = filters_for_records_to_exclude
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generate_records_to_inject(current_records:)
|
|
58
|
+
current_records
|
|
59
|
+
.select { |record| should_template?(record: record) }
|
|
60
|
+
.map { |record| template_record_for(record: record, current_records: current_records) }
|
|
61
|
+
.each_with_object([]) do |template_records, records_to_inject|
|
|
62
|
+
next unless should_inject?(
|
|
63
|
+
template_records: template_records,
|
|
64
|
+
current_records: current_records + records_to_inject,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
records_to_inject.push(
|
|
68
|
+
*template_records.fetch(:injected_records, []).map { |r_yml| Record.build_from_yaml_definition(r_yml) },
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :template, :filters_for_records_to_template, :filters_for_records_to_exclude
|
|
76
|
+
|
|
77
|
+
def should_inject?(template_records:, current_records:)
|
|
78
|
+
conflict_with = template_records[:conflict_with] || []
|
|
79
|
+
current_records.none? do |record|
|
|
80
|
+
conflict_with.any? do |filter|
|
|
81
|
+
record_match?(record: record, filter: filter)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def should_template?(record:)
|
|
87
|
+
filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) } &&
|
|
88
|
+
filters_for_records_to_exclude.none? { |filter| record_match?(record: record, filter: filter) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def record_match?(record:, filter:)
|
|
92
|
+
filter.all? do |key, value|
|
|
93
|
+
if value.is_a?(Regexp)
|
|
94
|
+
value.match(record.public_send(key))
|
|
95
|
+
else
|
|
96
|
+
record.public_send(key) == value
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def template_record_for(record:, current_records:)
|
|
102
|
+
context = TemplateContext.build(record: record, current_records: current_records)
|
|
103
|
+
|
|
104
|
+
YAML.safe_load(
|
|
105
|
+
template.result(context.fetch_binding),
|
|
106
|
+
permitted_classes: [Regexp],
|
|
107
|
+
).deep_symbolize_keys
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -3,15 +3,17 @@ module RecordStore
|
|
|
3
3
|
class Config
|
|
4
4
|
include ActiveModel::Validations
|
|
5
5
|
|
|
6
|
-
attr_reader :ignore_patterns, :providers, :supports_alias
|
|
6
|
+
attr_reader :ignore_patterns, :providers, :supports_alias, :implicit_records_templates
|
|
7
7
|
|
|
8
8
|
validate :validate_zone_config
|
|
9
9
|
|
|
10
|
-
def initialize(ignore_patterns: [], providers: nil, supports_alias: nil)
|
|
10
|
+
def initialize(ignore_patterns: [], providers: nil, supports_alias: nil, implicit_records_templates: [])
|
|
11
11
|
@ignore_patterns = ignore_patterns.map do |ignore_pattern|
|
|
12
12
|
Zone::Config::IgnorePattern.new(ignore_pattern)
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
@implicit_records_templates = implicit_records_templates.map do |filename|
|
|
15
|
+
Zone::Config::ImplicitRecordTemplate.from_file(filename: filename)
|
|
16
|
+
end
|
|
15
17
|
@providers = providers
|
|
16
18
|
@supports_alias = supports_alias
|
|
17
19
|
end
|
|
@@ -31,7 +31,8 @@ module RecordStore
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def write(name, config:, records:, format: :file)
|
|
34
|
-
raise ArgumentError, "format must be :directory or :file" unless
|
|
34
|
+
raise ArgumentError, "format must be :directory or :file" unless [:file, :directory].include?(format)
|
|
35
|
+
|
|
35
36
|
name = name.chomp('.')
|
|
36
37
|
zone_file = "#{RecordStore.zones_path}/#{name}.yml"
|
|
37
38
|
zone = { name => { config: config.to_hash } }
|
|
@@ -60,13 +61,17 @@ module RecordStore
|
|
|
60
61
|
dir = File.dirname(filename)
|
|
61
62
|
data = YAML.load_file(filename)
|
|
62
63
|
raise 'more than one zone in file' if data.size > 1
|
|
64
|
+
|
|
63
65
|
name, definition = data.first
|
|
64
66
|
definition['records'] ||= []
|
|
65
67
|
definition['records'] = definition['records'].map(&:deep_symbolize_keys)
|
|
66
68
|
Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
|
|
67
69
|
definition['records'] += load_yml_record_definitions(name, record_file)
|
|
68
70
|
end
|
|
69
|
-
|
|
71
|
+
|
|
72
|
+
asts = { filename => Psych.parse_file(filename) }
|
|
73
|
+
|
|
74
|
+
Zone.new(name: name, records: definition['records'], config: definition['config'], abstract_syntax_trees: asts)
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
def load_yml_record_definitions(name, record_file)
|
data/lib/record_store/zone.rb
CHANGED
|
@@ -20,6 +20,8 @@ module RecordStore
|
|
|
20
20
|
validate :validate_provider_can_handle_zone_records
|
|
21
21
|
validate :validate_no_empty_non_terminal
|
|
22
22
|
validate :validate_can_handle_alias_records
|
|
23
|
+
validate :validate_no_duplicate_keys
|
|
24
|
+
validate :validate_zone_record_not_shadowed
|
|
23
25
|
|
|
24
26
|
class << self
|
|
25
27
|
def download(name, provider_name, **write_options)
|
|
@@ -31,7 +33,7 @@ module RecordStore
|
|
|
31
33
|
zone.config = Zone::Config.new(
|
|
32
34
|
providers: [provider_name],
|
|
33
35
|
ignore_patterns: [{ type: "NS", fqdn: "#{name}." }],
|
|
34
|
-
supports_alias:
|
|
36
|
+
supports_alias: zone.records.map(&:type).include?('ALIAS') || nil,
|
|
35
37
|
)
|
|
36
38
|
|
|
37
39
|
zone.write(**write_options)
|
|
@@ -51,7 +53,7 @@ module RecordStore
|
|
|
51
53
|
(ENV['RECORD_STORE_MAX_THREADS'] || DEFAULT_MAX_PARALLEL_THREADS).to_i
|
|
52
54
|
end
|
|
53
55
|
|
|
54
|
-
def modified(verbose: false)
|
|
56
|
+
def modified(verbose: false)
|
|
55
57
|
modified_zones = []
|
|
56
58
|
mutex = Mutex.new
|
|
57
59
|
zones = all
|
|
@@ -61,7 +63,12 @@ module RecordStore
|
|
|
61
63
|
current_zone = nil
|
|
62
64
|
while zones.any?
|
|
63
65
|
mutex.synchronize { current_zone = zones.shift }
|
|
64
|
-
|
|
66
|
+
break if current_zone.nil? # account for the race between `zones.any?` and `zones.shift`
|
|
67
|
+
|
|
68
|
+
# `unchanged?` is deliberately outside locked context since it's a bit CPU/time heavy
|
|
69
|
+
unless current_zone.unchanged?
|
|
70
|
+
mutex.synchronize { modified_zones << current_zone }
|
|
71
|
+
end
|
|
65
72
|
end
|
|
66
73
|
end
|
|
67
74
|
end.each(&:join)
|
|
@@ -70,17 +77,16 @@ module RecordStore
|
|
|
70
77
|
end
|
|
71
78
|
end
|
|
72
79
|
|
|
73
|
-
def initialize(name:, records: [], config: {})
|
|
80
|
+
def initialize(name:, records: [], config: {}, abstract_syntax_trees: {})
|
|
74
81
|
@name = Record.ensure_ends_with_dot(name)
|
|
75
|
-
@config = RecordStore::Zone::Config.new(config.deep_symbolize_keys)
|
|
82
|
+
@config = RecordStore::Zone::Config.new(**config.deep_symbolize_keys)
|
|
76
83
|
@records = build_records(records)
|
|
84
|
+
@abstract_syntax_trees = abstract_syntax_trees
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def build_changesets(all: false)
|
|
80
|
-
@changesets ||=
|
|
81
|
-
|
|
82
|
-
Changeset.build_from(provider: provider, zone: self, all: all)
|
|
83
|
-
end
|
|
88
|
+
@changesets ||= providers.map do |provider|
|
|
89
|
+
Changeset.build_from(provider: provider, zone: self, all: all)
|
|
84
90
|
end
|
|
85
91
|
end
|
|
86
92
|
|
|
@@ -138,24 +144,22 @@ module RecordStore
|
|
|
138
144
|
authority = fetch_soa(nameserver) do |reply, _name|
|
|
139
145
|
break if reply.answer.any?
|
|
140
146
|
|
|
141
|
-
raise "No authority found (#{name})"
|
|
147
|
+
raise "No authority found (#{name})" if reply.authority.none?
|
|
142
148
|
|
|
143
149
|
break extract_authority(reply.authority)
|
|
144
150
|
end
|
|
145
151
|
|
|
146
152
|
# candidate DNS name is returned instead when NXDomain or other error
|
|
147
|
-
return
|
|
153
|
+
return if unrooted_name.casecmp?(Array(authority).first.to_s)
|
|
148
154
|
|
|
149
155
|
authority
|
|
150
156
|
end
|
|
151
157
|
|
|
152
158
|
private
|
|
153
159
|
|
|
154
|
-
def fetch_soa(nameserver)
|
|
160
|
+
def fetch_soa(nameserver, &block)
|
|
155
161
|
Resolv::DNS.open(nameserver: nameserver) do |resolv|
|
|
156
|
-
resolv.fetch_resource(name, Resolv::DNS::Resource::IN::SOA
|
|
157
|
-
yield reply, name
|
|
158
|
-
end
|
|
162
|
+
resolv.fetch_resource(name, Resolv::DNS::Resource::IN::SOA, &block)
|
|
159
163
|
end
|
|
160
164
|
end
|
|
161
165
|
|
|
@@ -168,6 +172,7 @@ module RecordStore
|
|
|
168
172
|
rescue Errno::EHOSTUNREACH => e
|
|
169
173
|
$stderr.puts "Warning: #{e} [host=#{nameserver}]"
|
|
170
174
|
raise if nameservers.empty?
|
|
175
|
+
|
|
171
176
|
retry
|
|
172
177
|
end
|
|
173
178
|
end
|
|
@@ -189,7 +194,13 @@ module RecordStore
|
|
|
189
194
|
end
|
|
190
195
|
|
|
191
196
|
def build_records(records)
|
|
192
|
-
records.map { |record| Record.build_from_yaml_definition(record) }
|
|
197
|
+
all_records = records.map { |record| Record.build_from_yaml_definition(record) }
|
|
198
|
+
|
|
199
|
+
config.implicit_records_templates.each do |template|
|
|
200
|
+
all_records.push(*template.generate_records_to_inject(current_records: all_records))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
all_records
|
|
193
204
|
end
|
|
194
205
|
|
|
195
206
|
def validate_records
|
|
@@ -232,6 +243,7 @@ module RecordStore
|
|
|
232
243
|
cname_records.each do |cname_record|
|
|
233
244
|
records.each do |record|
|
|
234
245
|
next unless record.fqdn == cname_record.fqdn && record != cname_record
|
|
246
|
+
|
|
235
247
|
case record.type
|
|
236
248
|
when 'SIG', 'NXT', 'KEY'
|
|
237
249
|
# this is fine
|
|
@@ -264,6 +276,27 @@ module RecordStore
|
|
|
264
276
|
end
|
|
265
277
|
end
|
|
266
278
|
|
|
279
|
+
def validate_zone_record_not_shadowed
|
|
280
|
+
nameserver_fqdns = records
|
|
281
|
+
.select { |record| record.is_a?(Record::NS) && name != record.fqdn }
|
|
282
|
+
.map { |record| record.fqdn.delete_suffix(".") }
|
|
283
|
+
.uniq
|
|
284
|
+
|
|
285
|
+
nameserver_fqdns.each do |ns_record|
|
|
286
|
+
selected_records = records.reject do |record|
|
|
287
|
+
record.is_a?(Record::NS) &&
|
|
288
|
+
record.fqdn.delete_suffix(".") == ns_record
|
|
289
|
+
end
|
|
290
|
+
selected_records.each do |record|
|
|
291
|
+
normalized_record = record.fqdn.delete_suffix(".")
|
|
292
|
+
next unless normalized_record.end_with?(".#{ns_record}") || normalized_record == ns_record
|
|
293
|
+
|
|
294
|
+
errors.add(:records, "Record #{record.fqdn} #{record.type} in Zone #{name} " \
|
|
295
|
+
"is shadowed by #{ns_record} and will be ignored")
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
267
300
|
def validate_no_empty_non_terminal
|
|
268
301
|
return unless config.empty_non_terminal_over_wildcard?
|
|
269
302
|
|
|
@@ -273,14 +306,14 @@ module RecordStore
|
|
|
273
306
|
|
|
274
307
|
terminal_records = records.map(&:fqdn)
|
|
275
308
|
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_])#{Regexp.escape(suffix)}$/) }
|
|
276
|
-
next
|
|
309
|
+
next if terminal_records.none?
|
|
277
310
|
|
|
278
311
|
intermediate_records = records.map(&:fqdn)
|
|
279
312
|
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+)#{Regexp.escape(suffix)}$/) }
|
|
280
313
|
terminal_records.each do |terminal_record|
|
|
281
314
|
non_terminal = terminal_record.partition('.').last
|
|
282
315
|
errors.add(:records, "found empty non-terminal #{non_terminal} "\
|
|
283
|
-
|
|
316
|
+
"(caused by existing records #{wildcard} and #{terminal_record})")\
|
|
284
317
|
unless intermediate_records.include?(non_terminal)
|
|
285
318
|
end
|
|
286
319
|
end
|
|
@@ -301,5 +334,34 @@ module RecordStore
|
|
|
301
334
|
|
|
302
335
|
errors.add(:records, "ALIAS record should be defined on the root of the zone: #{alias_record}")
|
|
303
336
|
end
|
|
337
|
+
|
|
338
|
+
def validate_no_duplicate_keys
|
|
339
|
+
@abstract_syntax_trees.each do |filename, ast|
|
|
340
|
+
validate_no_duplicate_keys_in_node(filename, ast)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def validate_no_duplicate_keys_in_node(filename, node)
|
|
345
|
+
if node.mapping?
|
|
346
|
+
keys = node
|
|
347
|
+
.children
|
|
348
|
+
.each_slice(2)
|
|
349
|
+
.map(&:first)
|
|
350
|
+
.map(&:value)
|
|
351
|
+
.sort
|
|
352
|
+
dup_keys = keys
|
|
353
|
+
.find_all { |k| keys.count(k) > 1 }
|
|
354
|
+
.uniq
|
|
355
|
+
unless dup_keys.empty?
|
|
356
|
+
location = "#{File.basename(filename)}:#{node.start_line}"
|
|
357
|
+
description = "multiple definitions for keys #{dup_keys}"
|
|
358
|
+
errors.add(:records, "#{location}: #{description}")
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
node.children&.each do |child|
|
|
363
|
+
validate_no_duplicate_keys_in_node(filename, child)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
304
366
|
end
|
|
305
367
|
end
|
data/lib/record_store.rb
CHANGED
|
@@ -28,6 +28,7 @@ require 'record_store/zone/yaml_definitions'
|
|
|
28
28
|
require 'record_store/zone'
|
|
29
29
|
require 'record_store/zone/config'
|
|
30
30
|
require 'record_store/zone/config/ignore_pattern'
|
|
31
|
+
require 'record_store/zone/config/implicit_record_template'
|
|
31
32
|
require 'record_store/changeset'
|
|
32
33
|
require 'record_store/provider'
|
|
33
34
|
require 'record_store/provider/dynect'
|
|
@@ -43,6 +44,7 @@ module RecordStore
|
|
|
43
44
|
class << self
|
|
44
45
|
attr_writer :secrets_path
|
|
45
46
|
attr_writer :zones_path
|
|
47
|
+
attr_writer :implicit_records_templates_path
|
|
46
48
|
|
|
47
49
|
def secrets_path
|
|
48
50
|
@secrets_path ||= File.expand_path(config.fetch('secrets_path'), File.dirname(config_path))
|
|
@@ -50,8 +52,10 @@ module RecordStore
|
|
|
50
52
|
|
|
51
53
|
def zones_path
|
|
52
54
|
@zones_path ||= Pathname.new(
|
|
53
|
-
File.expand_path(
|
|
54
|
-
|
|
55
|
+
File.expand_path(
|
|
56
|
+
config.fetch('zones_path'),
|
|
57
|
+
File.dirname(config_path),
|
|
58
|
+
),
|
|
55
59
|
).realpath.to_s
|
|
56
60
|
end
|
|
57
61
|
|
|
@@ -59,6 +63,15 @@ module RecordStore
|
|
|
59
63
|
@config_path ||= File.expand_path('config.yml', Dir.pwd)
|
|
60
64
|
end
|
|
61
65
|
|
|
66
|
+
def implicit_records_templates_path
|
|
67
|
+
@implicit_records_templates_path ||= Pathname.new(
|
|
68
|
+
File.expand_path(
|
|
69
|
+
config.fetch('implicit_records_templates_path'),
|
|
70
|
+
File.dirname(config_path),
|
|
71
|
+
),
|
|
72
|
+
).realpath.to_s
|
|
73
|
+
end
|
|
74
|
+
|
|
62
75
|
def config_path=(config_path)
|
|
63
76
|
@config = @zones_path = @secrets_path = nil
|
|
64
77
|
@config_path = config_path
|