From 35a6c85bbb5cfb304112dee83468d0752847610d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 06:07:03 -0700 Subject: [PATCH 01/10] Rework Manager.zone_tree into a property --- octodns/manager.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 0faa0bf..f9542aa 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -185,25 +185,6 @@ class Manager(object): 'Incorrect processor config for ' + processor_name ) - zone_tree = {} - # Sort so we iterate on the deepest nodes first, ensuring if a parent - # zone exists it will be seen after the subzone, thus we can easily - # reparent children to their parent zone from the tree root. - for name in sorted( - self.config['zones'].keys(), key=lambda s: 0 - s.count('.') - ): - # Trim the trailing dot from FQDN - name = name[:-1] - this = {} - for sz in [k for k in zone_tree.keys() if k.endswith(name)]: - # Found a zone in tree root that is our child, slice the - # name and move its tree under ours. - this[sz[: -(len(name) + 1)]] = zone_tree.pop(sz) - # Add to tree root where it will be reparented as we iterate up - # the tree. - zone_tree[name] = this - self.zone_tree = zone_tree - self.plan_outputs = {} plan_outputs = manager_config.get( 'plan_outputs', @@ -244,6 +225,32 @@ class Manager(object): 'Incorrect plan_output config for ' + plan_output_name ) + self._zone_tree = None + + @property + def zone_tree(self): + if self._zone_tree is None: + zone_tree = {} + # Sort so we iterate on the deepest nodes first, ensuring if a parent + # zone exists it will be seen after the subzone, thus we can easily + # reparent children to their parent zone from the tree root. + for name in sorted( + self.config['zones'].keys(), key=lambda s: 0 - s.count('.') + ): + # Trim the trailing dot from FQDN + name = name[:-1] + this = {} + for sz in [k for k in zone_tree.keys() if k.endswith(name)]: + # Found a zone in tree root that is our child, slice the + # name and move its tree under ours. + this[sz[: -(len(name) + 1)]] = zone_tree.pop(sz) + # Add to tree root where it will be reparented as we iterate up + # the tree. + zone_tree[name] = this + self._zone_tree = zone_tree + + return self._zone_tree + def _try_version(self, module_name, module=None, version=None): try: # Always try and use the official lookup first From 6e5c7a8b703b27b321cc390f657294698900316b Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 06:18:05 -0700 Subject: [PATCH 02/10] Add failing tests of subzone handling --- octodns/manager.py | 2 ++ tests/test_octodns_manager.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/octodns/manager.py b/octodns/manager.py index f9542aa..e84b6f6 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -102,6 +102,8 @@ class Manager(object): plan = p[1] return len(plan.changes[0].record.zone.name) if plan.changes else 0 + # TODO: all of this should get broken up, mainly so that it's not so huge + # and each bit can be cleanly tested independently def __init__(self, config_file, max_workers=None, include_meta=False): version = self._try_version('octodns', version=__VERSION__) self.log.info( diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index ab987d2..e22931a 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -712,6 +712,34 @@ class TestManager(TestCase): ), ) + def test_subzone_handling(self): + manager = Manager(get_config_filename('simple.yaml')) + + manager.config['zones'] = { + 'unit.tests.': {}, + 'sub.unit.tests.': {}, + 'another.sub.unit.tests.': {}, + 'skipped.alevel.unit.tests.': {}, + } + + self.assertEqual( + {'unit.tests': {'skipped.alevel': {}, 'sub': {'another': {}}}}, + manager.zone_tree, + ) + self.assertEqual( + {'sub', 'skipped.alevel'}, + manager.configured_sub_zones('unit.tests.'), + ) + self.assertEqual( + {'another'}, manager.configured_sub_zones('sub.unit.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('another.unit.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('skipped.alevel.unit.tests.') + ) + class TestMainThreadExecutor(TestCase): def test_success(self): From 6a1b86af6f0b22257c4c862c1f715ae0237245cb Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 07:50:32 -0700 Subject: [PATCH 03/10] Rework configured_sub_zones and add tests specifically for it --- octodns/manager.py | 56 +++++++++++++----------------- tests/test_octodns_manager.py | 64 +++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index e84b6f6..b356524 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -233,22 +233,27 @@ class Manager(object): def zone_tree(self): if self._zone_tree is None: zone_tree = {} - # Sort so we iterate on the deepest nodes first, ensuring if a parent - # zone exists it will be seen after the subzone, thus we can easily - # reparent children to their parent zone from the tree root. - for name in sorted( - self.config['zones'].keys(), key=lambda s: 0 - s.count('.') - ): - # Trim the trailing dot from FQDN - name = name[:-1] - this = {} - for sz in [k for k in zone_tree.keys() if k.endswith(name)]: - # Found a zone in tree root that is our child, slice the - # name and move its tree under ours. - this[sz[: -(len(name) + 1)]] = zone_tree.pop(sz) - # Add to tree root where it will be reparented as we iterate up - # the tree. - zone_tree[name] = this + + # Get a list of all of our zone names + zones = list(self.config['zones'].keys()) + # Sort them from shortest to longest so that parents will always + # come before their subzones + zones.sort(key=lambda z: len(z)) + # Until we're done processing zones + while zones: + # Grab the one we'lre going to work on now + zone = zones.pop(0) + trimmer = len(zone) + 1 + subs = set() + # look at all the zone names that come after it + for candidate in zones: + # If they end with this zone's name them they're a sub + if candidate.endswith(zone): + # We want subs to exclude the zone portion + subs.add(candidate[:-trimmer]) + + zone_tree[zone] = subs + self._zone_tree = zone_tree return self._zone_tree @@ -323,23 +328,7 @@ class Manager(object): return kwargs def configured_sub_zones(self, zone_name): - name = zone_name[:-1] - where = self.zone_tree - while True: - # Find parent if it exists - parent = next((k for k in where if name.endswith(k)), None) - if not parent: - # The zone_name in the tree has been reached, stop searching. - break - # Move down the tree and slice name to get the remainder for the - # next round of the search. - where = where[parent] - name = name[: -(len(parent) + 1)] - # `where` is now pointing at the dictionary of children for zone_name - # in the tree - sub_zone_names = where.keys() - self.log.debug('configured_sub_zones: subs=%s', sub_zone_names) - return set(sub_zone_names) + return self.zone_tree.get(zone_name, set()) def _populate_and_plan( self, @@ -717,6 +706,7 @@ class Manager(object): clz = SplitYamlProvider target = clz('dump', output_dir) + # TODO: use get_zone??? zone = Zone(zone, self.configured_sub_zones(zone)) for source in sources: source.populate(zone, lenient=lenient) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index e22931a..7a1b58d 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -715,6 +715,7 @@ class TestManager(TestCase): def test_subzone_handling(self): manager = Manager(get_config_filename('simple.yaml')) + # tree with multiple branches, one that skips manager.config['zones'] = { 'unit.tests.': {}, 'sub.unit.tests.': {}, @@ -723,23 +724,80 @@ class TestManager(TestCase): } self.assertEqual( - {'unit.tests': {'skipped.alevel': {}, 'sub': {'another': {}}}}, + { + 'unit.tests.': {'sub', 'another.sub', 'skipped.alevel'}, + 'sub.unit.tests.': {'another'}, + 'another.sub.unit.tests.': set(), + 'skipped.alevel.unit.tests.': set(), + }, manager.zone_tree, ) self.assertEqual( - {'sub', 'skipped.alevel'}, + {'another.sub', 'sub', 'skipped.alevel'}, manager.configured_sub_zones('unit.tests.'), ) self.assertEqual( {'another'}, manager.configured_sub_zones('sub.unit.tests.') ) self.assertEqual( - set(), manager.configured_sub_zones('another.unit.tests.') + set(), manager.configured_sub_zones('another.sub.unit.tests.') ) self.assertEqual( set(), manager.configured_sub_zones('skipped.alevel.unit.tests.') ) + # two parallel trees, make sure they don't interfere + manager.config['zones'] = { + 'unit.tests.': {}, + 'unit2.tests.': {}, + 'sub.unit.tests.': {}, + 'sub.unit2.tests.': {}, + 'another.sub.unit.tests.': {}, + 'another.sub.unit2.tests.': {}, + 'skipped.alevel.unit.tests.': {}, + 'skipped.alevel.unit2.tests.': {}, + } + manager._zone_tree = None + self.assertEqual( + { + 'unit.tests.': {'sub', 'another.sub', 'skipped.alevel'}, + 'sub.unit.tests.': {'another'}, + 'another.sub.unit.tests.': set(), + 'skipped.alevel.unit.tests.': set(), + 'unit2.tests.': {'sub', 'another.sub', 'skipped.alevel'}, + 'sub.unit2.tests.': {'another'}, + 'another.sub.unit2.tests.': set(), + 'skipped.alevel.unit2.tests.': set(), + }, + manager.zone_tree, + ) + self.assertEqual( + {'another.sub', 'sub', 'skipped.alevel'}, + manager.configured_sub_zones('unit.tests.'), + ) + self.assertEqual( + {'another'}, manager.configured_sub_zones('sub.unit.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('another.sub.unit.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('skipped.alevel.unit.tests.') + ) + self.assertEqual( + {'another.sub', 'sub', 'skipped.alevel'}, + manager.configured_sub_zones('unit2.tests.'), + ) + self.assertEqual( + {'another'}, manager.configured_sub_zones('sub.unit2.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('another.sub.unit2.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('skipped.alevel.unit2.tests.') + ) + class TestMainThreadExecutor(TestCase): def test_success(self): From 33794f3796c72299495e6450d830aaf7fd307bed Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 08:40:32 -0700 Subject: [PATCH 04/10] Fix a bug in Zone.add_record subzone handling when name ends with non-dotted subzone name --- octodns/zone.py | 33 ++++++++++++++++++++------------- tests/test_octodns_zone.py | 10 ++++++++++ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/octodns/zone.py b/octodns/zone.py index f93b373..ea60f72 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -73,19 +73,26 @@ class Zone(object): name = record.name - if not lenient and any((name.endswith(sz) for sz in self.sub_zones)): - if name not in self.sub_zones: - # it's a record for something under a sub-zone - raise SubzoneRecordException( - f'Record {record.fqdn} is under ' 'a managed subzone' - ) - elif record._type != 'NS': - # It's a non NS record for exactly a sub-zone - raise SubzoneRecordException( - f'Record {record.fqdn} a ' - 'managed sub-zone and not of ' - 'type NS' - ) + if not lenient: + if name in self.sub_zones: + # It's an exact match for a sub-zone + if not record._type == 'NS': + # and not a NS record, this should be in the sub + raise SubzoneRecordException( + f'Record {record.fqdn} a ' + 'managed sub-zone and not of ' + 'type NS' + ) + else: + # It's not an exact match so there has to be a `.` before the + # sub-zone for it to belong in there + for sub_zone in self.sub_zones: + if name.endswith(f'.{sub_zone}'): + # this should be in a sub + raise SubzoneRecordException( + f'Record {record.fqdn} is under ' + 'a managed subzone' + ) if replace: # will remove it if it exists diff --git a/tests/test_octodns_zone.py b/tests/test_octodns_zone.py index b884397..37e893f 100644 --- a/tests/test_octodns_zone.py +++ b/tests/test_octodns_zone.py @@ -208,6 +208,16 @@ class TestZone(TestCase): zone.add_record(record, lenient=True) self.assertEqual(set([record]), zone.records) + # A that happens to end with a string that matches a sub (no .) is OK + zone = Zone('unit.tests.', set(['sub', 'barred'])) + record = Record.new( + zone, + 'foo.bar_sub', + {'ttl': 3600, 'type': 'A', 'values': ['1.2.3.4', '2.3.4.5']}, + ) + zone.add_record(record) + self.assertEqual(1, len(zone.records)) + def test_ignored_records(self): zone_normal = Zone('unit.tests.', []) zone_ignored = Zone('unit.tests.', []) From 611431b042984206791d4263c134022e0cc8e758 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 08:42:16 -0700 Subject: [PATCH 05/10] Broken up string formatting cleanup --- octodns/zone.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/octodns/zone.py b/octodns/zone.py index ea60f72..7ffbb90 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -79,9 +79,7 @@ class Zone(object): if not record._type == 'NS': # and not a NS record, this should be in the sub raise SubzoneRecordException( - f'Record {record.fqdn} a ' - 'managed sub-zone and not of ' - 'type NS' + f'Record {record.fqdn} a managed sub-zone and not of type NS' ) else: # It's not an exact match so there has to be a `.` before the @@ -90,8 +88,7 @@ class Zone(object): if name.endswith(f'.{sub_zone}'): # this should be in a sub raise SubzoneRecordException( - f'Record {record.fqdn} is under ' - 'a managed subzone' + f'Record {record.fqdn} is under a managed subzone' ) if replace: From 810cc7faff87190e941fd5e7a8942fba27e58984 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 08:53:19 -0700 Subject: [PATCH 06/10] Use collections.deque for pop'ing --- octodns/manager.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index b356524..2977aaf 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -9,6 +9,7 @@ from __future__ import ( unicode_literals, ) +from collections import deque from concurrent.futures import ThreadPoolExecutor from importlib import import_module from os import environ @@ -234,15 +235,16 @@ class Manager(object): if self._zone_tree is None: zone_tree = {} - # Get a list of all of our zone names - zones = list(self.config['zones'].keys()) - # Sort them from shortest to longest so that parents will always - # come before their subzones - zones.sort(key=lambda z: len(z)) + # Get a list of all of our zone names. Sort them from shortest to + # longest so that parents will always come before their subzones + zones = sorted( + self.config['zones'].keys(), key=lambda z: len(z), reverse=True + ) + zones = deque(zones) # Until we're done processing zones while zones: # Grab the one we'lre going to work on now - zone = zones.pop(0) + zone = zones.pop() trimmer = len(zone) + 1 subs = set() # look at all the zone names that come after it From af010121eab789e973d2b955b6f6933cb3599772 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 08:58:40 -0700 Subject: [PATCH 07/10] Do away with zone_tree, not a tree and unnecessary now --- octodns/manager.py | 62 +++++++++++++++++------------------ tests/test_octodns_manager.py | 24 +------------- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 2977aaf..566b979 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -228,37 +228,7 @@ class Manager(object): 'Incorrect plan_output config for ' + plan_output_name ) - self._zone_tree = None - - @property - def zone_tree(self): - if self._zone_tree is None: - zone_tree = {} - - # Get a list of all of our zone names. Sort them from shortest to - # longest so that parents will always come before their subzones - zones = sorted( - self.config['zones'].keys(), key=lambda z: len(z), reverse=True - ) - zones = deque(zones) - # Until we're done processing zones - while zones: - # Grab the one we'lre going to work on now - zone = zones.pop() - trimmer = len(zone) + 1 - subs = set() - # look at all the zone names that come after it - for candidate in zones: - # If they end with this zone's name them they're a sub - if candidate.endswith(zone): - # We want subs to exclude the zone portion - subs.add(candidate[:-trimmer]) - - zone_tree[zone] = subs - - self._zone_tree = zone_tree - - return self._zone_tree + self._configured_sub_zones = None def _try_version(self, module_name, module=None, version=None): try: @@ -330,7 +300,35 @@ class Manager(object): return kwargs def configured_sub_zones(self, zone_name): - return self.zone_tree.get(zone_name, set()) + if self._configured_sub_zones is None: + # First time through we compute all the sub-zones + + configured_sub_zones = {} + + # Get a list of all of our zone names. Sort them from shortest to + # longest so that parents will always come before their subzones + zones = sorted( + self.config['zones'].keys(), key=lambda z: len(z), reverse=True + ) + zones = deque(zones) + # Until we're done processing zones + while zones: + # Grab the one we'lre going to work on now + zone = zones.pop() + trimmer = len(zone) + 1 + subs = set() + # look at all the zone names that come after it + for candidate in zones: + # If they end with this zone's name them they're a sub + if candidate.endswith(zone): + # We want subs to exclude the zone portion + subs.add(candidate[:-trimmer]) + + configured_sub_zones[zone] = subs + + self._configured_sub_zones = configured_sub_zones + + return self._configured_sub_zones.get(zone_name, set()) def _populate_and_plan( self, diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 7a1b58d..e3a5a50 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -723,15 +723,6 @@ class TestManager(TestCase): 'skipped.alevel.unit.tests.': {}, } - self.assertEqual( - { - 'unit.tests.': {'sub', 'another.sub', 'skipped.alevel'}, - 'sub.unit.tests.': {'another'}, - 'another.sub.unit.tests.': set(), - 'skipped.alevel.unit.tests.': set(), - }, - manager.zone_tree, - ) self.assertEqual( {'another.sub', 'sub', 'skipped.alevel'}, manager.configured_sub_zones('unit.tests.'), @@ -757,20 +748,7 @@ class TestManager(TestCase): 'skipped.alevel.unit.tests.': {}, 'skipped.alevel.unit2.tests.': {}, } - manager._zone_tree = None - self.assertEqual( - { - 'unit.tests.': {'sub', 'another.sub', 'skipped.alevel'}, - 'sub.unit.tests.': {'another'}, - 'another.sub.unit.tests.': set(), - 'skipped.alevel.unit.tests.': set(), - 'unit2.tests.': {'sub', 'another.sub', 'skipped.alevel'}, - 'sub.unit2.tests.': {'another'}, - 'another.sub.unit2.tests.': set(), - 'skipped.alevel.unit2.tests.': set(), - }, - manager.zone_tree, - ) + manager._configured_sub_zones = None self.assertEqual( {'another.sub', 'sub', 'skipped.alevel'}, manager.configured_sub_zones('unit.tests.'), From 18ee70ddb5928d2df0f551b8927a9ca5215fc3b7 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 09:10:56 -0700 Subject: [PATCH 08/10] Fix issue when subzone is a non-dotted endswith for zone --- octodns/manager.py | 7 ++++--- tests/test_octodns_manager.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/octodns/manager.py b/octodns/manager.py index 566b979..40a9c4f 100644 --- a/octodns/manager.py +++ b/octodns/manager.py @@ -315,12 +315,13 @@ class Manager(object): while zones: # Grab the one we'lre going to work on now zone = zones.pop() - trimmer = len(zone) + 1 + dotted = f'.{zone}' + trimmer = len(dotted) subs = set() # look at all the zone names that come after it for candidate in zones: - # If they end with this zone's name them they're a sub - if candidate.endswith(zone): + # If they end with this zone's dotted name, it's a sub + if candidate.endswith(dotted): # We want subs to exclude the zone portion subs.add(candidate[:-trimmer]) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index e3a5a50..420dfe3 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -776,6 +776,17 @@ class TestManager(TestCase): set(), manager.configured_sub_zones('skipped.alevel.unit2.tests.') ) + # zones that end with names of others + manager.config['zones'] = { + 'unit.tests.': {}, + 'uunit.tests.': {}, + 'uuunit.tests.': {}, + } + manager._configured_sub_zones = None + self.assertEqual(set(), manager.configured_sub_zones('unit.tests.')) + self.assertEqual(set(), manager.configured_sub_zones('uunit.tests.')) + self.assertEqual(set(), manager.configured_sub_zones('uuunit.tests.')) + class TestMainThreadExecutor(TestCase): def test_success(self): From 4a847cb38c85bb6a24287a5b5dbfbbb7925a8cb5 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 11 Aug 2022 12:25:37 -0700 Subject: [PATCH 09/10] More test cases for configured_sub_zones --- tests/test_octodns_manager.py | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 420dfe3..bdacb08 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -737,6 +737,9 @@ class TestManager(TestCase): set(), manager.configured_sub_zones('skipped.alevel.unit.tests.') ) + # unknown zone names return empty set + self.assertEqual(set(), manager.configured_sub_zones('unknown.tests.')) + # two parallel trees, make sure they don't interfere manager.config['zones'] = { 'unit.tests.': {}, @@ -787,6 +790,47 @@ class TestManager(TestCase): self.assertEqual(set(), manager.configured_sub_zones('uunit.tests.')) self.assertEqual(set(), manager.configured_sub_zones('uuunit.tests.')) + # skipping multiple levels + manager.config['zones'] = { + 'unit.tests.': {}, + 'foo.bar.baz.unit.tests.': {}, + } + manager._configured_sub_zones = None + self.assertEqual( + {'foo.bar.baz'}, manager.configured_sub_zones('unit.tests.') + ) + self.assertEqual( + set(), manager.configured_sub_zones('foo.bar.baz.unit.tests.') + ) + + # different TLDs + manager.config['zones'] = { + 'unit.tests.': {}, + 'foo.unit.tests.': {}, + 'unit.org.': {}, + 'bar.unit.org.': {}, + } + manager._configured_sub_zones = None + self.assertEqual({'foo'}, manager.configured_sub_zones('unit.tests.')) + self.assertEqual(set(), manager.configured_sub_zones('foo.unit.tests.')) + self.assertEqual({'bar'}, manager.configured_sub_zones('unit.org.')) + self.assertEqual(set(), manager.configured_sub_zones('bar.unit.org.')) + + # starting a beyond 2 levels + manager.config['zones'] = { + 'foo.unit.tests.': {}, + 'bar.foo.unit.tests.': {}, + 'bleep.bloop.foo.unit.tests.': {}, + } + manager._configured_sub_zones = None + self.assertEqual( + {'bar', 'bleep.bloop'}, + manager.configured_sub_zones('foo.unit.tests.'), + ) + self.assertEqual( + set(), manager.configured_sub_zones('bar.foo.unit.tests.') + ) + class TestMainThreadExecutor(TestCase): def test_success(self): From ffe456714cab311168cf9a42167e527146143a45 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 12 Aug 2022 13:28:06 -0700 Subject: [PATCH 10/10] Correct error message grammar Co-authored-by: Sachi King --- octodns/zone.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octodns/zone.py b/octodns/zone.py index 7ffbb90..1bc3724 100644 --- a/octodns/zone.py +++ b/octodns/zone.py @@ -79,7 +79,7 @@ class Zone(object): if not record._type == 'NS': # and not a NS record, this should be in the sub raise SubzoneRecordException( - f'Record {record.fqdn} a managed sub-zone and not of type NS' + f'Record {record.fqdn} is a managed sub-zone and not of type NS' ) else: # It's not an exact match so there has to be a `.` before the