From a5a1d504535985532e213751c73113efd21bd401 Mon Sep 17 00:00:00 2001 From: Jonathan Leroy Date: Tue, 29 Jul 2025 19:13:57 +0200 Subject: [PATCH] Add trailing_dots parameter to templating processor --- .../3e57e696039c4f37a3062043be81199c.md | 4 ++ octodns/processor/templating.py | 34 +++++++++---- tests/test_octodns_processor_templating.py | 48 ++++++++++++++++++- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 .changelog/3e57e696039c4f37a3062043be81199c.md diff --git a/.changelog/3e57e696039c4f37a3062043be81199c.md b/.changelog/3e57e696039c4f37a3062043be81199c.md new file mode 100644 index 0000000..8b9ac97 --- /dev/null +++ b/.changelog/3e57e696039c4f37a3062043be81199c.md @@ -0,0 +1,4 @@ +--- +type: patch +--- +Add trailing_dots parameter to templating processor \ No newline at end of file diff --git a/octodns/processor/templating.py b/octodns/processor/templating.py index 6477872..0f34454 100644 --- a/octodns/processor/templating.py +++ b/octodns/processor/templating.py @@ -15,6 +15,10 @@ class Templating(BaseProcessor): templating: class: octodns.processor.templating.Templating + # When `trailing_dots` is disabled, trailing dots are removed from all + # built-in variables values who represent a FQDN, like `{zone_name}` + # or `{record_fqdn}`. Optional. Default to `True`. + trailing_dots: False # Any k/v present in context will be passed into the .format method and # thus be available as additional variables in the template. This is all # optional. @@ -55,16 +59,17 @@ class Templating(BaseProcessor): ''' - def __init__(self, id, *args, context={}, **kwargs): + def __init__(self, id, *args, trailing_dots=True, context={}, **kwargs): super().__init__(id, *args, **kwargs) + self.trailing_dots = trailing_dots self.context = context def process_source_zone(self, desired, sources): sources = sources or [] zone_params = { - 'zone_name': desired.decoded_name.rstrip('.'), - 'zone_decoded_name': desired.decoded_name.rstrip('.'), - 'zone_encoded_name': desired.name.rstrip('.'), + 'zone_name': desired.decoded_name, + 'zone_decoded_name': desired.decoded_name, + 'zone_encoded_name': desired.name, 'zone_num_records': len(desired.records), 'zone_source_ids': ', '.join(s.id for s in sources), # add any extra context provided to us, if the value is a callable @@ -75,20 +80,33 @@ class Templating(BaseProcessor): for k, v in self.context.items() }, } + if not self.trailing_dots: + zone_params = zone_params | { + 'zone_name': desired.decoded_name[:-1], + 'zone_decoded_name': desired.decoded_name[:-1], + 'zone_encoded_name': desired.name[:-1], + } def params(record): - return { + record_params = { 'record_name': record.decoded_name, 'record_decoded_name': record.decoded_name, 'record_encoded_name': record.name, - 'record_fqdn': record.decoded_fqdn.rstrip('.'), - 'record_decoded_fqdn': record.decoded_fqdn.rstrip('.'), - 'record_encoded_fqdn': record.fqdn.rstrip('.'), + 'record_fqdn': record.decoded_fqdn, + 'record_decoded_fqdn': record.decoded_fqdn, + 'record_encoded_fqdn': record.fqdn, 'record_type': record._type, 'record_ttl': record.ttl, 'record_source_id': record.source.id if record.source else None, **zone_params, } + if not self.trailing_dots: + record_params = record_params | { + 'record_fqdn': record.decoded_fqdn[:-1], + 'record_decoded_fqdn': record.decoded_fqdn[:-1], + 'record_encoded_fqdn': record.fqdn[:-1], + } + return record_params for record in desired.records: if hasattr(record, 'values'): diff --git a/tests/test_octodns_processor_templating.py b/tests/test_octodns_processor_templating.py index 7c90706..a313286 100644 --- a/tests/test_octodns_processor_templating.py +++ b/tests/test_octodns_processor_templating.py @@ -74,7 +74,7 @@ class TemplatingTest(TestCase): { 'type': 'CNAME', 'ttl': 42, - 'value': '_cname.{zone_name}.something.else.', + 'value': '_cname.{zone_name}something.else.', }, lenient=True, ) @@ -107,7 +107,7 @@ class TemplatingTest(TestCase): { 'type': 'TXT', 'ttl': 42, - 'value': 'There are {zone_num_records} record(s) in {zone_name}.', + 'value': 'There are {zone_num_records} record(s) in {zone_name}', }, ) zone.add_record(txt) @@ -162,6 +162,50 @@ class TemplatingTest(TestCase): ) zone.add_record(txt) + templ.process_source_zone( + zone, sources=[record_source, DummySource('other')] + ) + mock_template.assert_called_once() + self.assertEqual( + call( + { + 'record_name': 'txt', + 'record_decoded_name': 'txt', + 'record_encoded_name': 'txt', + 'record_fqdn': 'txt.unit.tests.', + 'record_decoded_fqdn': 'txt.unit.tests.', + 'record_encoded_fqdn': 'txt.unit.tests.', + 'record_type': 'TXT', + 'record_ttl': 42, + 'record_source_id': 'record', + 'zone_name': 'unit.tests.', + 'zone_decoded_name': 'unit.tests.', + 'zone_encoded_name': 'unit.tests.', + 'zone_num_records': 1, + 'zone_source_ids': 'record, other', + } + ), + mock_template.call_args, + ) + + @patch('octodns.record.TxtValue.template') + def test_trailing_dots(self, mock_template): + templ = Templating('test', trailing_dots=False) + + zone = Zone('unit.tests.', []) + record_source = DummySource('record') + txt = Record.new( + zone, + 'txt', + { + 'type': 'TXT', + 'ttl': 42, + 'value': 'There are {zone_num_records} record(s) in {zone_name}.', + }, + source=record_source, + ) + zone.add_record(txt) + templ.process_source_zone( zone, sources=[record_source, DummySource('other')] )