From eec7cadb8664a446b741f94590fcfbd20ba36cfc Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 6 Oct 2023 15:10:47 -0700 Subject: [PATCH 1/3] refactor filter based processors to pull out shared logic --- octodns/processor/filter.py | 117 ++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 60 deletions(-) diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index f661dbf..078fcf5 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -7,7 +7,41 @@ from re import compile as re_compile from .base import BaseProcessor -class TypeAllowlistFilter(BaseProcessor): +class AllowsMixin: + def matches(self, zone, record): + pass + + def doesnt_match(self, zone, record): + zone.remove_record(record) + + +class RejectsMixin: + def matches(self, zone, record): + zone.remove_record(record) + + def doesnt_match(self, zone, record): + pass + + +class _TypeBaseFilter(BaseProcessor): + def __init__(self, name, _list): + super().__init__(name) + self._list = set(_list) + + def _process(self, zone, *args, **kwargs): + for record in zone.records: + if record._type in self._list: + self.matches(zone, record) + else: + self.doesnt_match(zone, record) + + return zone + + process_source_zone = _process + process_target_zone = _process + + +class TypeAllowlistFilter(_TypeBaseFilter, AllowsMixin): '''Only manage records of the specified type(s). Example usage: @@ -30,21 +64,10 @@ class TypeAllowlistFilter(BaseProcessor): ''' def __init__(self, name, allowlist): - super().__init__(name) - self.allowlist = set(allowlist) - - def _process(self, zone, *args, **kwargs): - for record in zone.records: - if record._type not in self.allowlist: - zone.remove_record(record) - - return zone - - process_source_zone = _process - process_target_zone = _process + super().__init__(name, allowlist) -class TypeRejectlistFilter(BaseProcessor): +class TypeRejectlistFilter(_TypeBaseFilter, RejectsMixin): '''Ignore records of the specified type(s). Example usage: @@ -66,18 +89,7 @@ class TypeRejectlistFilter(BaseProcessor): ''' def __init__(self, name, rejectlist): - super().__init__(name) - self.rejectlist = set(rejectlist) - - def _process(self, zone, *args, **kwargs): - for record in zone.records: - if record._type in self.rejectlist: - zone.remove_record(record) - - return zone - - process_source_zone = _process - process_target_zone = _process + super().__init__(name, rejectlist) class _NameBaseFilter(BaseProcessor): @@ -93,8 +105,25 @@ class _NameBaseFilter(BaseProcessor): self.exact = exact self.regex = regex + def _process(self, zone, *args, **kwargs): + for record in zone.records: + name = record.name + if name in self.exact: + self.matches(zone, record) + continue + elif any(r.search(name) for r in self.regex): + self.matches(zone, record) + continue + + self.doesnt_match(zone, record) -class NameAllowlistFilter(_NameBaseFilter): + return zone + + process_source_zone = _process + process_target_zone = _process + + +class NameAllowlistFilter(_NameBaseFilter, AllowsMixin): '''Only manage records with names that match the provider patterns Example usage: @@ -125,23 +154,8 @@ class NameAllowlistFilter(_NameBaseFilter): def __init__(self, name, allowlist): super().__init__(name, allowlist) - def _process(self, zone, *args, **kwargs): - for record in zone.records: - name = record.name - if name in self.exact: - continue - elif any(r.search(name) for r in self.regex): - continue - zone.remove_record(record) - - return zone - - process_source_zone = _process - process_target_zone = _process - - -class NameRejectlistFilter(_NameBaseFilter): +class NameRejectlistFilter(_NameBaseFilter, RejectsMixin): '''Reject managing records with names that match the provider patterns Example usage: @@ -172,23 +186,6 @@ class NameRejectlistFilter(_NameBaseFilter): def __init__(self, name, rejectlist): super().__init__(name, rejectlist) - def _process(self, zone, *args, **kwargs): - for record in zone.records: - name = record.name - if name in self.exact: - zone.remove_record(record) - continue - - for regex in self.regex: - if regex.search(name): - zone.remove_record(record) - break - - return zone - - process_source_zone = _process - process_target_zone = _process - class IgnoreRootNsFilter(BaseProcessor): '''Do not manage Root NS Records. From da818d12e43c7f35d9183e7d4d93a1d1a0d7ae59 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 6 Oct 2023 16:14:38 -0700 Subject: [PATCH 2/3] ZoneNameFilter to error/ignore when record names end with the zone name --- CHANGELOG.md | 5 ++ octodns/processor/filter.py | 51 +++++++++++++++++++ tests/test_octodns_processor_filter.py | 69 ++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4dff5..d0ab5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v1.3.0 - 2023-??-?? - ??? + +* Added ZoneNameFilter processor to enable ignoring/alerting on type-os like + octodns.com.octodns.com + ## v1.2.1 - 2023-09-29 - Now with fewer stale files * Update script/release to do clean room dist builds diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index 078fcf5..e02bc84 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -4,6 +4,7 @@ from re import compile as re_compile +from ..record.exception import ValidationError from .base import BaseProcessor @@ -215,3 +216,53 @@ class IgnoreRootNsFilter(BaseProcessor): process_source_zone = _process process_target_zone = _process + + +class ZoneNameFilter(BaseProcessor): + '''Filter or error on record names that contain the zone name + + Example usage: + + processors: + zone-name: + class: octodns.processor.filter.ZoneNameFilter + # If true a ValidationError will be throw when such records are + # encouterd, if false the records will just be ignored/omitted. + # (default: false) + + zones: + exxampled.com.: + sources: + - config + processors: + - zone-name + targets: + - azure + ''' + + def __init__(self, name, error=False): + super().__init__(name) + self.error = error + + def _process(self, zone, *args, **kwargs): + zone_name_with_dot = zone.name + zone_name_without_dot = zone_name_with_dot[:-1] + for record in zone.records: + name = record.name + if name.endswith(zone_name_with_dot) or name.endswith( + zone_name_without_dot + ): + if self.error: + raise ValidationError( + record.fqdn, + ['record name ends with zone name'], + record.context, + ) + else: + # just remove it + zone.remove_record(record) + + return zone + + process_source_zone = _process + process_target_zone = _process diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index a18eb51..0a1b6dd 100644 --- a/tests/test_octodns_processor_filter.py +++ b/tests/test_octodns_processor_filter.py @@ -10,8 +10,10 @@ from octodns.processor.filter import ( NameRejectlistFilter, TypeAllowlistFilter, TypeRejectlistFilter, + ZoneNameFilter, ) from octodns.record import Record +from octodns.record.exception import ValidationError from octodns.zone import Zone zone = Zone('unit.tests.', []) @@ -180,3 +182,70 @@ class TestIgnoreRootNsFilter(TestCase): [('A', ''), ('NS', 'sub')], sorted([(r._type, r.name) for r in filtered.records]), ) + + +class TestZoneNameFilter(TestCase): + def test_ends_with_zone(self): + zone_name_filter = ZoneNameFilter('zone-name') + + zone = Zone('unit.tests.', []) + + # something that doesn't come into play + zone.add_record( + Record.new( + zone, 'www', {'type': 'A', 'ttl': 43, 'value': '1.2.3.4'} + ) + ) + + # something that has the zone name, but doesn't end with it + zone.add_record( + Record.new( + zone, + f'{zone.name}more', + {'type': 'A', 'ttl': 43, 'value': '1.2.3.4'}, + ) + ) + + self.assertEqual(2, len(zone.records)) + filtered = zone_name_filter.process_source_zone(zone.copy()) + # get everything back + self.assertEqual(2, len(filtered.records)) + + with_dot = zone.copy() + with_dot.add_record( + Record.new( + zone, zone.name, {'type': 'A', 'ttl': 43, 'value': '1.2.3.4'} + ) + ) + self.assertEqual(3, len(with_dot.records)) + filtered = zone_name_filter.process_source_zone(with_dot.copy()) + # don't get the one that ends with the zone name + self.assertEqual(2, len(filtered.records)) + + without_dot = zone.copy() + without_dot.add_record( + Record.new( + zone, + zone.name[:-1], + {'type': 'A', 'ttl': 43, 'value': '1.2.3.4'}, + ) + ) + self.assertEqual(3, len(without_dot.records)) + filtered = zone_name_filter.process_source_zone(without_dot.copy()) + # don't get the one that ends with the zone name + self.assertEqual(2, len(filtered.records)) + + def test_error(self): + errors = ZoneNameFilter('zone-name', error=True) + + zone = Zone('unit.tests.', []) + zone.add_record( + Record.new( + zone, zone.name, {'type': 'A', 'ttl': 43, 'value': '1.2.3.4'} + ) + ) + with self.assertRaises(ValidationError) as ctx: + errors.process_source_zone(zone) + self.assertEqual( + ['record name ends with zone name'], ctx.exception.reasons + ) From e6ad64f25f59e32f189a0eae6de8c96f4822cb80 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 6 Oct 2023 16:16:48 -0700 Subject: [PATCH 3/3] ZoneNameFilter error defaults to True --- octodns/processor/filter.py | 4 ++-- tests/test_octodns_processor_filter.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/octodns/processor/filter.py b/octodns/processor/filter.py index e02bc84..4723073 100644 --- a/octodns/processor/filter.py +++ b/octodns/processor/filter.py @@ -228,7 +228,7 @@ class ZoneNameFilter(BaseProcessor): class: octodns.processor.filter.ZoneNameFilter # If true a ValidationError will be throw when such records are # encouterd, if false the records will just be ignored/omitted. - # (default: false) + # (default: true) zones: exxampled.com.: @@ -240,7 +240,7 @@ class ZoneNameFilter(BaseProcessor): - azure ''' - def __init__(self, name, error=False): + def __init__(self, name, error=True): super().__init__(name) self.error = error diff --git a/tests/test_octodns_processor_filter.py b/tests/test_octodns_processor_filter.py index 0a1b6dd..9857926 100644 --- a/tests/test_octodns_processor_filter.py +++ b/tests/test_octodns_processor_filter.py @@ -186,7 +186,7 @@ class TestIgnoreRootNsFilter(TestCase): class TestZoneNameFilter(TestCase): def test_ends_with_zone(self): - zone_name_filter = ZoneNameFilter('zone-name') + zone_name_filter = ZoneNameFilter('zone-name', error=False) zone = Zone('unit.tests.', [])