From 93a00f12a9e0c34eeb6f6d5e8e032f11fa6d1d48 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 28 Nov 2023 15:42:53 -0800 Subject: [PATCH] Add Processor.process_source_and_target_zones --- CHANGELOG.md | 2 ++ octodns/processor/base.py | 29 +++++++++++++++++++++- octodns/provider/base.py | 5 ++++ tests/test_octodns_manager.py | 46 ++++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3fef1..1d32bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## v1.?.0 - 2023-??-?? - * Record.lenient property added similar to other common/standard _octodns data +* Processor.process_source_and_target_zones added to support modifying both the + desired and/or existing zones just prior to computing changes. ## v1.3.0 - 2023-11-14 - New and improved processors diff --git a/octodns/processor/base.py b/octodns/processor/base.py index f0890e0..eb584d5 100644 --- a/octodns/processor/base.py +++ b/octodns/processor/base.py @@ -37,7 +37,7 @@ class BaseProcessor(object): computed between `existing` and `desired`. This provides an opportunity to modify the `existing` `Zone`. - - Will see `existing` after any modifrications done by processors + - Will see `existing` after any modifications done by processors configured to run before this one. - May modify `existing` directly. - Must return `existing` which will normally be the `existing` param. @@ -48,6 +48,33 @@ class BaseProcessor(object): ''' return existing + def process_source_and_target_zones(self, desired, existing, target): + ''' + Called just prior to computing changes for `target` between `desired` + and `existing`. Provides an opportunity for the processor to modify + either the desired or existing `Zone`s that will be used to compute the + changes and create the initial plan. + + - Will see `desired` after any modifications done by + `Provider._process_desired_zone` and all processors via + `Processor.process_source_zone` + - Will see `existing` after any modifications done by all processors + via `Processor.process_target_zone` + - Will see both `desired` and `existing` after any modifications done by + any processors configured to run before this one via + `Processor.process_source_and_target_zones`. + - May modify `desired` directly. + - Must return `desired` which will normally be the `desired` param. + - May modify `existing` directly. + - Must return `existing` which will normally be the `existing` param. + - Must not modify records directly, `record.copy` should be called, + the results of which can be modified, and then `Zone.add_record` may + be used with `replace=True`. + - May call `Zone.remove_record` to remove records from `desired`. + - May call `Zone.remove_record` to remove records from `existing`. + ''' + return desired, existing + def process_plan(self, plan, sources, target): ''' Called after the planning phase has completed. Provides an opportunity diff --git a/octodns/provider/base.py b/octodns/provider/base.py index e1e2234..9696c57 100644 --- a/octodns/provider/base.py +++ b/octodns/provider/base.py @@ -243,6 +243,11 @@ class BaseProvider(BaseSource): for processor in processors: existing = processor.process_target_zone(existing, target=self) + for processor in processors: + desired, existing = processor.process_source_and_target_zones( + desired, existing, self + ) + # compute the changes at the zone/record level changes = existing.changes(desired, self) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py index 2bec87e..72f0ec5 100644 --- a/tests/test_octodns_manager.py +++ b/tests/test_octodns_manager.py @@ -25,7 +25,7 @@ from octodns.manager import ( _AggregateTarget, ) from octodns.processor.base import BaseProcessor -from octodns.record import Create, Delete, Record +from octodns.record import Create, Delete, Record, Update from octodns.yaml import safe_load from octodns.zone import Zone @@ -723,6 +723,50 @@ class TestManager(TestCase): # We got a delete for the thing added to the existing state (target) self.assertIsInstance(plans[0][1].changes[0], Delete) + # source & target + record2 = Record.new( + zone, 'a2', {'ttl': 31, 'type': 'A', 'value': '1.2.3.4'} + ) + record3 = Record.new( + zone, 'a3', {'ttl': 32, 'type': 'A', 'value': '1.2.3.4'} + ) + + class MockProcessor(BaseProcessor): + def process_source_and_target_zones( + self, desired, existing, target + ): + # add something to desired + desired.add_record(record2) + # add something to existing + existing.add_record(record3) + # add something to both, but with a modification + desired.add_record(record) + mod = record.copy() + mod.ttl += 1 + existing.add_record(mod) + return desired, existing + + mock = MockProcessor('mock') + plans, zone = manager._populate_and_plan( + 'unit.tests.', [mock], [], targets + ) + # we should see a plan + self.assertTrue(plans) + plan = plans[0][1] + # it shoudl have a create, an update, and a delete + self.assertEqual( + 'a', + next(c.record.name for c in plan.changes if isinstance(c, Update)), + ) + self.assertEqual( + 'a2', + next(c.record.name for c in plan.changes if isinstance(c, Create)), + ) + self.assertEqual( + 'a3', + next(c.record.name for c in plan.changes if isinstance(c, Delete)), + ) + # muck with plans class MockProcessor(BaseProcessor): def process_target_zone(self, zone, target):