From 4c48609848d585275dd863056f549b1d16c797c6 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 2 Feb 2025 13:55:38 -0800 Subject: [PATCH] Add Plan.meta support for making non-record changes to zones --- CHANGELOG.md | 2 ++ octodns/provider/base.py | 19 ++++++++++++--- octodns/provider/plan.py | 49 +++++++++++++++++++++----------------- tests/test_octodns_plan.py | 11 +++++---- 4 files changed, 51 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a463ce..4b66533 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ the default when enforce_order=True and simple `sort`. * Fix type-o in _build_kwargs handler notification * Add support for configuring OwnershipProcessor TXT record's TTL +* Add Plan.meta to allow providers to indicate they need to make changes to the + zone that are not record specific ## v1.10.0 - 2024-10-06 - Lots of little stuff diff --git a/octodns/provider/base.py b/octodns/provider/base.py index 9696c57..addb07b 100644 --- a/octodns/provider/base.py +++ b/octodns/provider/base.py @@ -214,6 +214,14 @@ class BaseProvider(BaseSource): ''' return [] + def _plan_meta(self, existing, desired, changes): + ''' + An opportunity for providers to indicate they have "meta" changes + to the zone which are unrelated to records. Examples may include servive + plan changes, replication settings, and notes. + ''' + return None + def supports_warn_or_except(self, msg, fallback): if self.strict_supports: raise SupportsException(f'{self.id}: {msg}') @@ -269,14 +277,19 @@ class BaseProvider(BaseSource): ) changes += extra - if changes: + meta = self._plan_meta( + existing=existing, desired=desired, changes=changes + ) + + if changes or meta: plan = Plan( existing, desired, changes, exists, - self.update_pcent_threshold, - self.delete_pcent_threshold, + update_pcent_threshold=self.update_pcent_threshold, + delete_pcent_threshold=self.delete_pcent_threshold, + meta=meta, ) self.log.info('plan: %s', plan) return plan diff --git a/octodns/provider/plan.py b/octodns/provider/plan.py index 18939c8..296afed 100644 --- a/octodns/provider/plan.py +++ b/octodns/provider/plan.py @@ -6,6 +6,7 @@ from collections import defaultdict from io import StringIO from json import dumps from logging import DEBUG, ERROR, INFO, WARN, getLogger +from pprint import pformat from sys import stdout @@ -50,6 +51,7 @@ class Plan(object): exists, update_pcent_threshold=MAX_SAFE_UPDATE_PCENT, delete_pcent_threshold=MAX_SAFE_DELETE_PCENT, + meta=None, ): self.existing = existing self.desired = desired @@ -59,6 +61,7 @@ class Plan(object): # them and/or is as safe as possible. self.changes = sorted(changes) self.exists = exists + self.meta = meta # Zone thresholds take precedence over provider if existing and existing.update_pcent_threshold is not None: @@ -74,22 +77,11 @@ class Plan(object): change_counts[change.__class__.__name__] += 1 self.change_counts = change_counts - try: - existing_n = len(self.existing.records) - except AttributeError: - existing_n = 0 - - self.log.debug( - '__init__: Creates=%d, Updates=%d, Deletes=%d Existing=%d', - self.change_counts['Create'], - self.change_counts['Update'], - self.change_counts['Delete'], - existing_n, - ) + self.log.debug('__init__: %s', self.__repr__()) @property def data(self): - return {'changes': [c.data for c in self.changes]} + return {'changes': [c.data for c in self.changes], 'meta': self.meta} def raise_if_unsafe(self): if ( @@ -140,11 +132,12 @@ class Plan(object): creates = self.change_counts['Create'] updates = self.change_counts['Update'] deletes = self.change_counts['Delete'] - existing = len(self.existing.records) - return ( - f'Creates={creates}, Updates={updates}, Deletes={deletes}, ' - f'Existing Records={existing}' - ) + try: + existing = len(self.existing.records) + except AttributeError: + existing = 0 + meta = self.meta is not None + return f'Creates={creates}, Updates={updates}, Deletes={deletes}, Existing={existing}, Meta={meta}' class _PlanOutput(object): @@ -167,10 +160,7 @@ class PlanLogger(_PlanOutput): raise Exception(f'Unsupported level: {level}') def run(self, log, plans, *args, **kwargs): - hr = ( - '*************************************************************' - '*******************\n' - ) + hr = '********************************************************************************\n' buf = StringIO() buf.write('\n') if plans: @@ -199,6 +189,11 @@ class PlanLogger(_PlanOutput): buf.write(change.__repr__(leader='* ')) buf.write('\n* ') + if plan.meta: + buf.write('Meta: \n') + buf.write(pformat(plan.meta, indent=2, sort_dicts=True)) + buf.write('\n') + buf.write('Summary: ') buf.write(str(plan)) buf.write('\n') @@ -291,6 +286,11 @@ class PlanMarkdown(_PlanOutput): fh.write(new.source.id) fh.write(' |\n') + if plan.meta: + fh.write('\nMeta: ') + fh.write(pformat(plan.meta, indent=2, sort_dicts=True)) + fh.write('\n') + fh.write('\nSummary: ') fh.write(str(plan)) fh.write('\n\n') @@ -361,6 +361,11 @@ class PlanHtml(_PlanOutput): fh.write(new.source.id) fh.write('\n \n') + if plan.meta: + fh.write(' \n Meta: ') + fh.write(pformat(plan.meta, indent=2, sort_dicts=True)) + fh.write('\n \n\n') + fh.write(' \n Summary: ') fh.write(str(plan)) fh.write('\n \n\n') diff --git a/tests/test_octodns_plan.py b/tests/test_octodns_plan.py index bb2ff0b..b261af9 100644 --- a/tests/test_octodns_plan.py +++ b/tests/test_octodns_plan.py @@ -64,6 +64,7 @@ changes = [create, create2, delete, update] plans = [ (simple, Plan(zone, zone, changes, True)), (simple, Plan(zone, zone, changes, False)), + (simple, Plan(zone, zone, changes, False, meta={'key': 'val'})), ] @@ -105,8 +106,8 @@ class TestPlanLogger(TestCase): PlanLogger('logger').run(log, plans) out = log.out.getvalue() self.assertTrue( - 'Summary: Creates=2, Updates=1, ' - 'Deletes=1, Existing Records=0' in out + 'Summary: Creates=2, Updates=1, Deletes=1, Existing=0, Meta=False' + in out ) @@ -123,8 +124,8 @@ class TestPlanHtml(TestCase): PlanHtml('html').run(plans, fh=out) out = out.getvalue() self.assertTrue( - ' Summary: Creates=2, Updates=1, ' - 'Deletes=1, Existing Records=0' in out + ' Summary: Creates=2, Updates=1, Deletes=1, Existing=0, Meta=False' + in out ) @@ -398,7 +399,7 @@ class TestPlanSafety(TestCase): def test_data(self): data = plans[0][1].data # plans should have a single key, changes - self.assertEqual(('changes',), tuple(data.keys())) + self.assertEqual(('changes', 'meta'), tuple(data.keys())) # it should be a list self.assertIsInstance(data['changes'], list) # w/4 elements