record_store 6.4.1 → 6.5.3

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: 2537c52a9350b969e64d8fab36f1281813e3d1db528accd4ee0700cdc6217b80
4
- data.tar.gz: 8561a6ea4212f180a47240d5f1234f43e1d1896e620f2ea0acb427e06b2306fc
3
+ metadata.gz: 9af43560e4a84302824fb5b1abef5476bc0995e752d3960de068b45949b1d154
4
+ data.tar.gz: 4bc999da542a5484d7cf51a7bb652593c323fecac6d79d8e73ccbb62bd638ea0
5
5
  SHA512:
6
- metadata.gz: 81048f68e0c0710fa7d61bcc25f97583e0b73dd7a0debea7281a8e22403a46f8fb245607d1f9a74ee8a6412915184d4c1faf7a5178fd3a666b64217201715115
7
- data.tar.gz: 88ed41158b07af0b2efed051d2794e54efacc90e36457b51aacb9e53e14307c15fcde259f5beda9b4df5056d7fd517fa52852e5b828d264d90433ccf62464977
6
+ metadata.gz: d72d0a1bffea56a12070eab25df96cd120f00107779063ae651a141310b369764eeaa5fa8993d77c67f81e2f4e98e16641e6d8dacde4f35fa09be4a6fe08236e
7
+ data.tar.gz: e84f52373f10caa1fa464845a13a33383c7461392f69c88c17c032f9d91e8b96ffcd87fc9f1fc798478b05b2de2660629e2d6d4b89e232b405d80009676eac5b
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ env:
6
+ SRB_SKIP_GEM_RBIS: true
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ ruby: [ 2.7.1 ]
15
+ name: Test Ruby ${{ matrix.ruby }}
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Set up Ruby
19
+ uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+ bundler-cache: true
23
+ - name: rubocop
24
+ run: bin/rubocop --version && bin/rubocop
25
+ - name: setup
26
+ run: bin/setup
27
+ - name: test
28
+ run: bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 6.5.3
4
+ - Adds check for detecting shadowed records, used when a record being added to a zone in record-store will have no effect because it is shadowed by another record.
5
+
6
+ ## 6.5.2
7
+ - Ensure filters for implicit_records, `except_record` and `conflict_with`, are truly optional [BUGFIX]
8
+
9
+ ## 6.5.1
10
+
11
+ Add support for a new parameter in the implicit records templates:
12
+
13
+ - `except_record`: the template will *NOT* generate the `injected_records` for records matching `except_record`, even if they matched `each_record`.
14
+
15
+ Also added support for regular expressions in the matching, using the already available `!ruby/regexp` text in a YAML value. The object loaded from the YAML
16
+ will be of `Regexp` type, and will thus be used to _match_ the value, instead of being identical only. This is supported in both `except_record` and
17
+ `each_record` fields.
18
+
19
+ ## 6.5.0
20
+
21
+ ...
22
+
3
23
  ## 6.4.0
4
24
 
5
25
  Add support for injecting implicit records into a zone based on a pre-configured template. Brief overview of template keys:
@@ -7,7 +7,7 @@ module RecordStore
7
7
 
8
8
  def initialize(*args)
9
9
  super
10
- RecordStore.config_path = options.fetch('config', "#{Dir.pwd}/config.yml")
10
+ RecordStore.config_path = options.fetch('config', "#{Dir.pwd}/template/config.yml")
11
11
  end
12
12
 
13
13
  def self.exit_on_failure?
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '6.4.1'.freeze
2
+ VERSION = '6.5.3'.freeze
3
3
  end
@@ -26,9 +26,14 @@ module RecordStore
26
26
  def from_file(filename:)
27
27
  filepath = template_filepath_for(filename: filename)
28
28
  template_file = File.read(filepath)
29
- filters_for_records_to_template = YAML.load(template_file).deep_symbolize_keys[:each_record]
30
29
 
31
- new(template: ERB.new(template_file), filters_for_records_to_template: filters_for_records_to_template)
30
+ template_file_yaml = YAML.load(template_file).deep_symbolize_keys
31
+ filters_for_records_to_template = template_file_yaml[:each_record]
32
+ filters_for_records_to_exclude = template_file_yaml[:except_record] || []
33
+
34
+ new(template: ERB.new(template_file),
35
+ filters_for_records_to_template: filters_for_records_to_template,
36
+ filters_for_records_to_exclude: filters_for_records_to_exclude)
32
37
  end
33
38
 
34
39
  private
@@ -38,9 +43,10 @@ module RecordStore
38
43
  end
39
44
  end
40
45
 
41
- def initialize(template:, filters_for_records_to_template:)
46
+ def initialize(template:, filters_for_records_to_template:, filters_for_records_to_exclude:)
42
47
  @template = template
43
48
  @filters_for_records_to_template = filters_for_records_to_template
49
+ @filters_for_records_to_exclude = filters_for_records_to_exclude
44
50
  end
45
51
 
46
52
  def generate_records_to_inject(current_records:)
@@ -61,23 +67,29 @@ module RecordStore
61
67
 
62
68
  private
63
69
 
64
- attr_reader :template, :filters_for_records_to_template
70
+ attr_reader :template, :filters_for_records_to_template, :filters_for_records_to_exclude
65
71
 
66
72
  def should_inject?(template_records:, current_records:)
73
+ conflict_with = template_records[:conflict_with] || []
67
74
  current_records.none? do |record|
68
- template_records[:conflict_with].any? do |filter|
75
+ conflict_with.any? do |filter|
69
76
  record_match?(record: record, filter: filter)
70
77
  end
71
78
  end
72
79
  end
73
80
 
74
81
  def should_template?(record:)
75
- filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) }
82
+ filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) } && \
83
+ filters_for_records_to_exclude.none? { |filter| record_match?(record: record, filter: filter) }
76
84
  end
77
85
 
78
86
  def record_match?(record:, filter:)
79
87
  filter.all? do |key, value|
80
- record.public_send(key) == value
88
+ if value.is_a?(Regexp)
89
+ value.match(record.public_send(key))
90
+ else
91
+ record.public_send(key) == value
92
+ end
81
93
  end
82
94
  end
83
95
 
@@ -66,7 +66,10 @@ module RecordStore
66
66
  Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
67
67
  definition['records'] += load_yml_record_definitions(name, record_file)
68
68
  end
69
- Zone.new(name: name, records: definition['records'], config: definition['config'])
69
+
70
+ asts = { filename => Psych.parse_file(filename) }
71
+
72
+ Zone.new(name: name, records: definition['records'], config: definition['config'], abstract_syntax_trees: asts)
70
73
  end
71
74
 
72
75
  def load_yml_record_definitions(name, record_file)
@@ -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)
@@ -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
- mutex.synchronize { modified_zones << current_zone } unless current_zone.unchanged?
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,10 +77,11 @@ 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
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)
@@ -270,6 +278,26 @@ module RecordStore
270
278
  end
271
279
  end
272
280
 
281
+ def validate_zone_record_not_shadowed
282
+ nameserver_fqdns = records
283
+ .select { |record| record.is_a?(Record::NS) && name != record.fqdn }
284
+ .map { |record| record.fqdn.delete_suffix(".") }
285
+ .uniq
286
+
287
+ nameserver_fqdns.each do |ns_record|
288
+ selected_records = records.reject do |record|
289
+ record.is_a?(Record::NS) && \
290
+ record.fqdn.delete_suffix(".") == ns_record
291
+ end
292
+ selected_records.each do |record|
293
+ normalized_record = record.fqdn.delete_suffix(".")
294
+ next unless normalized_record.end_with?(".#{ns_record}") || normalized_record == ns_record
295
+ errors.add(:records, "Record #{record.fqdn} #{record.type} in Zone #{name} " \
296
+ "is shadowed by #{ns_record} and will be ignored")
297
+ end
298
+ end
299
+ end
300
+
273
301
  def validate_no_empty_non_terminal
274
302
  return unless config.empty_non_terminal_over_wildcard?
275
303
 
@@ -307,5 +335,34 @@ module RecordStore
307
335
 
308
336
  errors.add(:records, "ALIAS record should be defined on the root of the zone: #{alias_record}")
309
337
  end
338
+
339
+ def validate_no_duplicate_keys
340
+ @abstract_syntax_trees.each do |filename, ast|
341
+ validate_no_duplicate_keys_in_node(filename, ast)
342
+ end
343
+ end
344
+
345
+ def validate_no_duplicate_keys_in_node(filename, node)
346
+ if node.mapping?
347
+ keys = node
348
+ .children
349
+ .each_slice(2)
350
+ .map(&:first)
351
+ .map(&:value)
352
+ .sort
353
+ dup_keys = keys
354
+ .find_all { |k| keys.count(k) > 1 }
355
+ .uniq
356
+ unless dup_keys.empty?
357
+ location = "#{File.basename(filename)}:#{node.start_line}"
358
+ description = "multiple definitions for keys #{dup_keys}"
359
+ errors.add(:records, "#{location}: #{description}")
360
+ end
361
+ end
362
+
363
+ node.children&.each do |child|
364
+ validate_no_duplicate_keys_in_node(filename, child)
365
+ end
366
+ end
310
367
  end
311
368
  end
data/lib/record_store.rb CHANGED
@@ -44,6 +44,7 @@ module RecordStore
44
44
  class << self
45
45
  attr_writer :secrets_path
46
46
  attr_writer :zones_path
47
+ attr_writer :implicit_records_templates_path
47
48
 
48
49
  def secrets_path
49
50
  @secrets_path ||= File.expand_path(config.fetch('secrets_path'), File.dirname(config_path))
@@ -2,6 +2,8 @@ each_record:
2
2
  - type: A
3
3
  fqdn: abc.123.com.
4
4
  - type: TXT
5
+ except_record:
6
+ - fqdn: !ruby/regexp /^_/
5
7
  conflict_with:
6
8
  - type: TXT
7
9
  fqdn: <%= record.fqdn %>.more.domain.com.
@@ -5,7 +5,7 @@ dynect.example.com:
5
5
  ignore_patterns:
6
6
  - type: NS
7
7
  fqdn: dynect.example.com.
8
- implicit_record_templates:
8
+ implicit_records_templates:
9
9
  - implicit_example.yml.erb
10
10
  records:
11
11
  - type: A
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: 6.4.1
4
+ version: 6.5.3
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-02 00:00:00.000000000 Z
12
+ date: 2021-09-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -335,6 +335,7 @@ executables:
335
335
  extensions: []
336
336
  extra_rdoc_files: []
337
337
  files:
338
+ - ".github/workflows/ci.yml"
338
339
  - ".gitignore"
339
340
  - ".rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml"
340
341
  - ".rubocop.yml"
@@ -416,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
416
417
  - !ruby/object:Gem::Version
417
418
  version: '0'
418
419
  requirements: []
419
- rubygems_version: 3.0.3
420
+ rubygems_version: 3.2.20
420
421
  signing_key:
421
422
  specification_version: 4
422
423
  summary: Manage DNS using git