1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Merge pull request #917 from octodns/subzones

Fix issues with sub-zone handling
This commit is contained in:
Ross McFarland
2022-08-14 11:42:14 -07:00
committed by GitHub
4 changed files with 182 additions and 49 deletions

View File

@@ -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
@@ -102,6 +103,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(
@@ -185,25 +188,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 +228,8 @@ class Manager(object):
'Incorrect plan_output config for ' + plan_output_name
)
self._configured_sub_zones = None
def _try_version(self, module_name, module=None, version=None):
try:
# Always try and use the official lookup first
@@ -314,23 +300,36 @@ 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)
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()
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 dotted name, it's a sub
if candidate.endswith(dotted):
# 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,
@@ -708,6 +707,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)

View File

@@ -73,19 +73,23 @@ 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} 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
# 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

View File

@@ -712,6 +712,125 @@ 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.': {},
'another.sub.unit.tests.': {},
'skipped.alevel.unit.tests.': {},
}
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.')
)
# 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.': {},
'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._configured_sub_zones = None
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.')
)
# 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.'))
# 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):

View File

@@ -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.', [])