From 44b7c6880dcf4e793fc18d05cf0fb2afe163dc29 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 5 Aug 2021 08:57:29 -0700 Subject: [PATCH 1/5] Implement AcmeIgnoringProcessor --- octodns/processor/acme.py | 33 ++++++++++++++++++ tests/test_octodns_processor_acme.py | 51 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 octodns/processor/acme.py create mode 100644 tests/test_octodns_processor_acme.py diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py new file mode 100644 index 0000000..e786859 --- /dev/null +++ b/octodns/processor/acme.py @@ -0,0 +1,33 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from logging import getLogger + +from .base import BaseProcessor + + +class AcmeIgnoringProcessor(BaseProcessor): + log = getLogger('AcmeIgnoringProcessor') + + def __init__(self, name): + super(AcmeIgnoringProcessor, self).__init__(name) + + def _process(self, zone, *args, **kwargs): + ret = self._clone_zone(zone) + for record in zone.records: + # Uses a startswith rather than == to ignore subdomain challenges, + # e.g. _acme-challenge.foo.domain.com when managing domain.com + if record._type == 'TXT' and \ + record.name.startswith('_acme-challenge'): + self.log.info('_process: ignoring %s', record.fqdn) + continue + ret.add_record(record) + + return ret + + process_source_zone = _process + process_target_zone = _process diff --git a/tests/test_octodns_processor_acme.py b/tests/test_octodns_processor_acme.py new file mode 100644 index 0000000..aae55ba --- /dev/null +++ b/tests/test_octodns_processor_acme.py @@ -0,0 +1,51 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.processor.acme import AcmeIgnoringProcessor +from octodns.record import Record +from octodns.zone import Zone + +zone = Zone('unit.tests.', []) +for record in [ + # Will be ignored + Record.new(zone, '_acme-challenge', { + 'ttl': 30, + 'type': 'TXT', + 'value': 'magic bit', + }), + # Not TXT so will live + Record.new(zone, '_acme-challenge.aaaa', { + 'ttl': 30, + 'type': 'AAAA', + 'value': '::1', + }), + # Will be ignored + Record.new(zone, '_acme-challenge.foo', { + 'ttl': 30, + 'type': 'TXT', + 'value': 'magic bit', + }), + # Not acme-challenge so will live + Record.new(zone, 'txt', { + 'ttl': 30, + 'type': 'TXT', + 'value': 'Hello World!', + }), +]: + zone.add_record(record) + + +class TestAcmeIgnoringProcessor(TestCase): + + def test_basics(self): + acme = AcmeIgnoringProcessor('acme') + + got = acme.process_source_zone(zone) + self.assertEquals(['_acme-challenge.aaaa', 'txt'], + sorted([r.name for r in got.records])) From 181dcec71bc079bf905ba00915836f700424b222 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 11 Aug 2021 07:21:37 -0700 Subject: [PATCH 2/5] Allow octoDNS managed _acme-challenge records to exist by marking them --- octodns/processor/acme.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py index e786859..f50d924 100644 --- a/octodns/processor/acme.py +++ b/octodns/processor/acme.py @@ -16,18 +16,29 @@ class AcmeIgnoringProcessor(BaseProcessor): def __init__(self, name): super(AcmeIgnoringProcessor, self).__init__(name) - def _process(self, zone, *args, **kwargs): + def process_source_zone(self, zone, *args, **kwargs): + ret = self._clone_zone(zone) + for record in zone.records: + if record._type == 'TXT' and \ + record.name.startswith('_acme-challenge'): + # We have a managed acme challenge record (owned by octoDNS) so + # we should mark it as such + record = record.copy() + record.values.append('*octoDNS*') + record.values.sort() + ret.add_record(record) + return ret + + def process_target_zone(self, zone, *args, **kwargs): ret = self._clone_zone(zone) for record in zone.records: # Uses a startswith rather than == to ignore subdomain challenges, # e.g. _acme-challenge.foo.domain.com when managing domain.com if record._type == 'TXT' and \ - record.name.startswith('_acme-challenge'): + record.name.startswith('_acme-challenge') and \ + '*octoDNS*' not in record.values: self.log.info('_process: ignoring %s', record.fqdn) continue ret.add_record(record) return ret - - process_source_zone = _process - process_target_zone = _process From ea5093935b9988c3edb6abb6cf9f1d34cafb985f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 11 Aug 2021 09:31:54 -0700 Subject: [PATCH 3/5] Support taking ownership of acme records --- octodns/processor/acme.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py index f50d924..8516ce2 100644 --- a/octodns/processor/acme.py +++ b/octodns/processor/acme.py @@ -16,6 +16,8 @@ class AcmeIgnoringProcessor(BaseProcessor): def __init__(self, name): super(AcmeIgnoringProcessor, self).__init__(name) + self._owned = set() + def process_source_zone(self, zone, *args, **kwargs): ret = self._clone_zone(zone) for record in zone.records: @@ -26,6 +28,7 @@ class AcmeIgnoringProcessor(BaseProcessor): record = record.copy() record.values.append('*octoDNS*') record.values.sort() + self._owned.add(record) ret.add_record(record) return ret @@ -36,7 +39,8 @@ class AcmeIgnoringProcessor(BaseProcessor): # e.g. _acme-challenge.foo.domain.com when managing domain.com if record._type == 'TXT' and \ record.name.startswith('_acme-challenge') and \ - '*octoDNS*' not in record.values: + '*octoDNS*' not in record.values and \ + record not in self._owned: self.log.info('_process: ignoring %s', record.fqdn) continue ret.add_record(record) From 5c1ea34bc6ed1a58cc4f97e63bd97dbc81317177 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 11 Aug 2021 10:41:52 -0700 Subject: [PATCH 4/5] Tests for acme processor with ownership --- tests/test_octodns_processor_acme.py | 90 ++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/tests/test_octodns_processor_acme.py b/tests/test_octodns_processor_acme.py index aae55ba..8d30368 100644 --- a/tests/test_octodns_processor_acme.py +++ b/tests/test_octodns_processor_acme.py @@ -12,40 +12,92 @@ from octodns.record import Record from octodns.zone import Zone zone = Zone('unit.tests.', []) -for record in [ - # Will be ignored - Record.new(zone, '_acme-challenge', { +records = { + 'root-unowned': Record.new(zone, '_acme-challenge', { 'ttl': 30, 'type': 'TXT', 'value': 'magic bit', }), - # Not TXT so will live - Record.new(zone, '_acme-challenge.aaaa', { + 'sub-unowned': Record.new(zone, '_acme-challenge.sub-unowned', { + 'ttl': 30, + 'type': 'TXT', + 'value': 'magic bit', + }), + 'not-txt': Record.new(zone, '_acme-challenge.not-txt', { 'ttl': 30, 'type': 'AAAA', 'value': '::1', }), - # Will be ignored - Record.new(zone, '_acme-challenge.foo', { - 'ttl': 30, - 'type': 'TXT', - 'value': 'magic bit', - }), - # Not acme-challenge so will live - Record.new(zone, 'txt', { + 'not-acme': Record.new(zone, 'not-acme', { 'ttl': 30, 'type': 'TXT', 'value': 'Hello World!', }), -]: - zone.add_record(record) + 'managed': Record.new(zone, '_acme-challenge.managed', { + 'ttl': 30, + 'type': 'TXT', + 'value': 'magic bit', + }), + 'owned': Record.new(zone, '_acme-challenge.owned', { + 'ttl': 30, + 'type': 'TXT', + 'values': ['*octoDNS*', 'magic bit'], + }), + 'going-away': Record.new(zone, '_acme-challenge.going-away', { + 'ttl': 30, + 'type': 'TXT', + 'values': ['*octoDNS*', 'magic bit'], + }), +} class TestAcmeIgnoringProcessor(TestCase): - def test_basics(self): + def test_process_zones(self): acme = AcmeIgnoringProcessor('acme') - got = acme.process_source_zone(zone) - self.assertEquals(['_acme-challenge.aaaa', 'txt'], - sorted([r.name for r in got.records])) + source = Zone(zone.name, []) + # Unrelated stuff that should be untouched + source.add_record(records['not-txt']) + source.add_record(records['not-acme']) + # A managed acme that will have ownership value added + source.add_record(records['managed']) + + got = acme.process_source_zone(source) + self.assertEquals([ + '_acme-challenge.managed', + '_acme-challenge.not-txt', + 'not-acme', + ], sorted([r.name for r in got.records])) + managed = None + for record in got.records: + print(record.name) + if record.name.endswith('managed'): + managed = record + break + self.assertTrue(managed) + # Ownership was marked with an extra value + self.assertEquals(['*octoDNS*', 'magic bit'], record.values) + + existing = Zone(zone.name, []) + # Unrelated stuff that should be untouched + existing.add_record(records['not-txt']) + existing.add_record(records['not-acme']) + # Stuff that will be ignored + existing.add_record(records['root-unowned']) + existing.add_record(records['sub-unowned']) + # A managed acme that needs ownership value added + existing.add_record(records['managed']) + # A managed acme that has ownershp managed + existing.add_record(records['owned']) + # A managed acme that needs to go away + existing.add_record(records['going-away']) + + got = acme.process_target_zone(existing) + self.assertEquals([ + '_acme-challenge.going-away', + '_acme-challenge.managed', + '_acme-challenge.not-txt', + '_acme-challenge.owned', + 'not-acme' + ], sorted([r.name for r in got.records])) From c0c3a93d4aa6d9670a8193f4216306e74e038a2f Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 11 Aug 2021 20:52:23 -0700 Subject: [PATCH 5/5] Rename to AcmeManagingProcess, doc usage --- octodns/processor/acme.py | 22 +++++++++++++++++++--- tests/test_octodns_processor_acme.py | 6 +++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/octodns/processor/acme.py b/octodns/processor/acme.py index 8516ce2..685130d 100644 --- a/octodns/processor/acme.py +++ b/octodns/processor/acme.py @@ -10,11 +10,25 @@ from logging import getLogger from .base import BaseProcessor -class AcmeIgnoringProcessor(BaseProcessor): - log = getLogger('AcmeIgnoringProcessor') +class AcmeMangingProcessor(BaseProcessor): + log = getLogger('AcmeMangingProcessor') def __init__(self, name): - super(AcmeIgnoringProcessor, self).__init__(name) + ''' + processors: + acme: + class: octodns.processor.acme.AcmeMangingProcessor + + ... + + zones: + something.com.: + ... + processors: + - acme + ... + ''' + super(AcmeMangingProcessor, self).__init__(name) self._owned = set() @@ -28,6 +42,8 @@ class AcmeIgnoringProcessor(BaseProcessor): record = record.copy() record.values.append('*octoDNS*') record.values.sort() + # This assumes we'll see things as sources before targets, + # which is the case... self._owned.add(record) ret.add_record(record) return ret diff --git a/tests/test_octodns_processor_acme.py b/tests/test_octodns_processor_acme.py index 8d30368..c927608 100644 --- a/tests/test_octodns_processor_acme.py +++ b/tests/test_octodns_processor_acme.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function, \ from unittest import TestCase -from octodns.processor.acme import AcmeIgnoringProcessor +from octodns.processor.acme import AcmeMangingProcessor from octodns.record import Record from octodns.zone import Zone @@ -51,10 +51,10 @@ records = { } -class TestAcmeIgnoringProcessor(TestCase): +class TestAcmeMangingProcessor(TestCase): def test_process_zones(self): - acme = AcmeIgnoringProcessor('acme') + acme = AcmeMangingProcessor('acme') source = Zone(zone.name, []) # Unrelated stuff that should be untouched