* `Processors.process_zone_config` method added to allow processors that work with the zone config data. Configured with `manager.zone-processors: []`, default is ['dynamic-zone-config'] * Converted dynamic zone config to be a processors ^, if zone-processors are explicitely configured and dynamic zone config is desired `dyanmic-zone-config` must be included in the list as the desired position * Loads of additional testin/vetting for the above functionalityprocess-zone-config
| @ -0,0 +1,45 @@ | |||
| # | |||
| # | |||
| # | |||
| from logging import getLogger | |||
| from .base import BaseProcessor, ProcessorException | |||
| class DynamicZoneConfigProcessor(BaseProcessor): | |||
| log = getLogger('DynamicZoneConfigProcessor') | |||
| def process_zone_config(self, zones, get_sources): | |||
| for name, config in list(zones.items()): | |||
| if not name.startswith('*'): | |||
| continue | |||
| # we've found a dynamic config element | |||
| # find its sources | |||
| found_sources = get_sources(name, config) | |||
| self.log.info( | |||
| 'sync: dynamic zone=%s, sources=%s', name, found_sources | |||
| ) | |||
| for source in found_sources: | |||
| if not hasattr(source, 'list_zones'): | |||
| raise ProcessorException( | |||
| f'dynamic zone={name} includes a source, {source.id}, that does not support `list_zones`' | |||
| ) | |||
| for zone_name in source.list_zones(): | |||
| if zone_name in zones: | |||
| self.log.info( | |||
| 'sync: zone=%s already in config, ignoring', | |||
| zone_name, | |||
| ) | |||
| continue | |||
| self.log.info( | |||
| 'sync: adding dynamic zone=%s', zone_name | |||
| ) | |||
| zones[zone_name] = config | |||
| # remove the dynamic config element so we don't try and populate it | |||
| del zones[name] | |||
| return zones | |||
| @ -0,0 +1,31 @@ | |||
| manager: | |||
| zone-processors: | |||
| - dynamic-zone-config | |||
| - this-does-not-exist | |||
| providers: | |||
| config: | |||
| # This helps us get coverage when printing out provider versions | |||
| class: helpers.TestYamlProvider | |||
| directory: tests/config | |||
| strict_supports: False | |||
| dump: | |||
| class: octodns.provider.yaml.YamlProvider | |||
| directory: env/YAML_TMP_DIR | |||
| supports_root_ns: False | |||
| strict_supports: False | |||
| processors: | |||
| # manually define the dynamic zone config, just to do it | |||
| dynamic-zone-config: | |||
| class: octodns.processor.zone.DynamicZoneConfigProcessor | |||
| zones: | |||
| unit.tests.: | |||
| processors: | |||
| - noop | |||
| sources: | |||
| - config | |||
| targets: | |||
| - dump | |||
| @ -0,0 +1,43 @@ | |||
| # | |||
| # | |||
| # | |||
| from unittest import TestCase | |||
| from octodns.processor.base import BaseProcessor | |||
| class BaseProcessorTest(TestCase): | |||
| proc = BaseProcessor('test') | |||
| def test_process_zone_config(self): | |||
| def get_sources(name, config): | |||
| return [] | |||
| zones = {} | |||
| got = self.proc.process_zone_config(zones, get_sources) | |||
| self.assertIs(zones, got) | |||
| def test_process_source_zone(self): | |||
| desired = 42 | |||
| got = self.proc.process_source_zone(desired, []) | |||
| self.assertIs(desired, got) | |||
| def test_process_target_zone(self): | |||
| existing = 43 | |||
| got = self.proc.process_target_zone(existing, None) | |||
| self.assertIs(existing, got) | |||
| def test_process_source_and_target_zones(self): | |||
| desired = 42 | |||
| existing = 43 | |||
| got_desired, got_existing = self.proc.process_source_and_target_zones( | |||
| desired, existing, None | |||
| ) | |||
| self.assertIs(desired, got_desired) | |||
| self.assertIs(existing, got_existing) | |||
| def test_process_plan(self): | |||
| plan = 42 | |||
| got = self.proc.process_plan(plan, [], None) | |||
| self.assertIs(plan, got) | |||
| @ -0,0 +1,107 @@ | |||
| # | |||
| # | |||
| # | |||
| from unittest import TestCase | |||
| from octodns.processor.base import ProcessorException | |||
| from octodns.processor.zone import DynamicZoneConfigProcessor | |||
| class _GetSourcesMock: | |||
| def __init__(self, sources=[]): | |||
| self.data = {} | |||
| self.sources = sources | |||
| self.called = 0 | |||
| def get_sources(self, name, config): | |||
| self.data[name] = config | |||
| self.called += 1 | |||
| return self.sources | |||
| class _ListZonesMock: | |||
| id = '_ListZonesMock' | |||
| def __init__(self, zones): | |||
| self.zones = zones | |||
| self.called = 0 | |||
| def list_zones(self): | |||
| self.called += 1 | |||
| return self.zones | |||
| class _NoListZonesMock: | |||
| id = '_NoListZonesMock' | |||
| class BaseProcessorTest(TestCase): | |||
| proc = DynamicZoneConfigProcessor('test') | |||
| def test_process_zone_config_empty(self): | |||
| mock = _GetSourcesMock() | |||
| zones = {} | |||
| got = self.proc.process_zone_config(zones, mock.get_sources) | |||
| self.assertFalse(mock.called) | |||
| self.assertIs(zones, got) | |||
| def test_process_zone_config_static(self): | |||
| mock = _GetSourcesMock() | |||
| zones = {'unit.tests.': {'key': 'value'}} | |||
| got = self.proc.process_zone_config(zones, mock.get_sources) | |||
| self.assertFalse(mock.called) | |||
| self.assertIs(zones, got) | |||
| def test_process_zone_config_dynamic(self): | |||
| lz_mock = _ListZonesMock( | |||
| [ | |||
| 'dynamic1.unit.tests.', | |||
| 'dynamic2.unit.tests.', | |||
| 'existing.unit.tests.', | |||
| ] | |||
| ) | |||
| gs_mock = _GetSourcesMock([lz_mock]) | |||
| zones = { | |||
| '*': {'type': 'dynamic'}, | |||
| 'unit.tests.': {'type': 'static'}, | |||
| 'existing.unit.tests.': {'type': 'exsiting'}, | |||
| } | |||
| got = self.proc.process_zone_config(zones, gs_mock.get_sources) | |||
| self.assertEqual(1, gs_mock.called) | |||
| self.assertEqual({'*': {'type': 'dynamic'}}, gs_mock.data) | |||
| self.assertEqual(1, lz_mock.called) | |||
| self.assertIs(zones, got) | |||
| self.assertEqual({'type': 'dynamic'}, got['dynamic1.unit.tests.']) | |||
| self.assertEqual({'type': 'dynamic'}, got['dynamic2.unit.tests.']) | |||
| def test_process_zone_config_dynamic_prefix(self): | |||
| lz_mock = _ListZonesMock(['dyn-pre.unit.tests.']) | |||
| gs_mock = _GetSourcesMock([lz_mock]) | |||
| zones = {'*.foo': {'type': 'dynamic-too'}} | |||
| got = self.proc.process_zone_config(zones, gs_mock.get_sources) | |||
| self.assertEqual(1, gs_mock.called) | |||
| self.assertEqual({'*.foo': {'type': 'dynamic-too'}}, gs_mock.data) | |||
| self.assertEqual(1, lz_mock.called) | |||
| self.assertIs(zones, got) | |||
| from pprint import pprint | |||
| pprint(got) | |||
| self.assertEqual({'type': 'dynamic-too'}, got['dyn-pre.unit.tests.']) | |||
| def test_process_zone_config_no_list_zones(self): | |||
| gs_mock = _GetSourcesMock([_NoListZonesMock()]) | |||
| zones = {'*': {'type': 'dynamic'}} | |||
| with self.assertRaises(ProcessorException) as ctx: | |||
| self.proc.process_zone_config(zones, gs_mock.get_sources) | |||
| self.assertEqual( | |||
| 'dynamic zone=* includes a source, _NoListZonesMock, that does not support `list_zones`', | |||
| str(ctx.exception), | |||
| ) | |||