Browse Source

Merge remote-tracking branch 'origin/main' into fix-alias-templating

pull/1279/head
Ross McFarland 5 months ago
parent
commit
e7016ae03d
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
4 changed files with 138 additions and 11 deletions
  1. +4
    -0
      .changelog/3e57e696039c4f37a3062043be81199c.md
  2. +4
    -0
      .changelog/af8522cac7e54d22a615eab351d445b3.md
  3. +49
    -10
      octodns/processor/templating.py
  4. +81
    -1
      tests/test_octodns_processor_templating.py

+ 4
- 0
.changelog/3e57e696039c4f37a3062043be81199c.md View File

@ -0,0 +1,4 @@
---
type: minor
---
Add trailing_dots parameter to templating processor

+ 4
- 0
.changelog/af8522cac7e54d22a615eab351d445b3.md View File

@ -0,0 +1,4 @@
---
type: patch
---
Improve error messaging for unknown templating parameters

+ 49
- 10
octodns/processor/templating.py View File

@ -5,6 +5,14 @@
from octodns.processor.base import BaseProcessor from octodns.processor.base import BaseProcessor
class TemplatingError(Exception):
def __init__(self, record, msg):
self.record = record
msg = f'Invalid record "{record.fqdn}", {msg}'
super().__init__(msg)
class Templating(BaseProcessor): class Templating(BaseProcessor):
''' '''
Record templating using python format. For simple records like TXT and CAA Record templating using python format. For simple records like TXT and CAA
@ -15,6 +23,10 @@ class Templating(BaseProcessor):
templating: templating:
class: octodns.processor.templating.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 # 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 # thus be available as additional variables in the template. This is all
# optional. # optional.
@ -55,15 +67,23 @@ 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) super().__init__(id, *args, **kwargs)
self.trailing_dots = trailing_dots
self.context = context self.context = context
def process_source_and_target_zones(self, desired, existing, provider): def process_source_and_target_zones(self, desired, existing, provider):
zone_name = desired.decoded_name
zone_decoded_name = desired.decoded_name
zone_encoded_name = desired.name
if not self.trailing_dots:
zone_name = zone_name[:-1]
zone_decoded_name = zone_decoded_name[:-1]
zone_encoded_name = zone_encoded_name[:-1]
zone_params = { zone_params = {
'zone_name': desired.decoded_name,
'zone_decoded_name': desired.decoded_name,
'zone_encoded_name': desired.name,
'zone_name': zone_name,
'zone_decoded_name': zone_decoded_name,
'zone_encoded_name': zone_encoded_name,
'zone_num_records': len(desired.records), 'zone_num_records': len(desired.records),
# add any extra context provided to us, if the value is a callable # add any extra context provided to us, if the value is a callable
# object call it passing our params so that arbitrary dynamic # object call it passing our params so that arbitrary dynamic
@ -74,26 +94,45 @@ class Templating(BaseProcessor):
}, },
} }
def params(record):
def build_params(record):
record_fqdn = record.decoded_fqdn
record_decoded_fqdn = record.decoded_fqdn
record_encoded_fqdn = record.fqdn
if not self.trailing_dots:
record_fqdn = record_fqdn[:-1]
record_decoded_fqdn = record_decoded_fqdn[:-1]
record_encoded_fqdn = record_encoded_fqdn[:-1]
return { return {
'record_name': record.decoded_name, 'record_name': record.decoded_name,
'record_decoded_name': record.decoded_name, 'record_decoded_name': record.decoded_name,
'record_encoded_name': record.name, 'record_encoded_name': record.name,
'record_fqdn': record.decoded_fqdn,
'record_decoded_fqdn': record.decoded_fqdn,
'record_encoded_fqdn': record.fqdn,
'record_fqdn': record_fqdn,
'record_decoded_fqdn': record_decoded_fqdn,
'record_encoded_fqdn': record_encoded_fqdn,
'record_type': record._type, 'record_type': record._type,
'record_ttl': record.ttl, 'record_ttl': record.ttl,
'record_source_id': record.source.id if record.source else None, 'record_source_id': record.source.id if record.source else None,
**zone_params, **zone_params,
} }
def template(value, params, record):
try:
return value.template(params)
except KeyError as e:
raise TemplatingError(
record,
f'undefined template parameter "{e.args[0]}" in value',
) from e
for record in desired.records: for record in desired.records:
params = build_params(record)
if hasattr(record, 'values'): if hasattr(record, 'values'):
if record.values and not hasattr(record.values[0], 'template'): if record.values and not hasattr(record.values[0], 'template'):
# the (custom) value type does not support templating # the (custom) value type does not support templating
continue continue
new_values = [v.template(params(record)) for v in record.values]
new_values = [
template(v, params, record) for v in record.values
]
if record.values != new_values: if record.values != new_values:
new = record.copy() new = record.copy()
new.values = new_values new.values = new_values
@ -102,7 +141,7 @@ class Templating(BaseProcessor):
if not hasattr(record.value, 'template'): if not hasattr(record.value, 'template'):
# the (custom) value type does not support templating # the (custom) value type does not support templating
continue continue
new_value = record.value.template(params(record))
new_value = template(record.value, params, record)
if record.value != new_value: if record.value != new_value:
new = record.copy() new = record.copy()
new.value = new_value new.value = new_value


+ 81
- 1
tests/test_octodns_processor_templating.py View File

@ -3,8 +3,9 @@
# #
from unittest import TestCase from unittest import TestCase
from unittest.mock import call, patch
from octodns.processor.templating import Templating
from octodns.processor.templating import Templating, TemplatingError
from octodns.record import Record, ValueMixin, ValuesMixin from octodns.record import Record, ValueMixin, ValuesMixin
from octodns.zone import Zone from octodns.zone import Zone
@ -143,6 +144,47 @@ class TemplatingTest(TestCase):
self.assertEqual({'template'}, s.value._asked_for) self.assertEqual({'template'}, s.value._asked_for)
self.assertEqual({'template'}, m.values[0]._asked_for) self.assertEqual({'template'}, m.values[0]._asked_for)
@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_and_target_zones(zone, None, None)
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_source_id': 'record',
'record_type': 'TXT',
'record_ttl': 42,
'zone_name': 'unit.tests',
'zone_decoded_name': 'unit.tests',
'zone_encoded_name': 'unit.tests',
'zone_num_records': 1,
}
),
mock_template.call_args,
)
def test_context(self): def test_context(self):
templ = Templating( templ = Templating(
'test', 'test',
@ -178,3 +220,41 @@ class TemplatingTest(TestCase):
self.assertEqual('provider: da-pro', txt.values[0]) self.assertEqual('provider: da-pro', txt.values[0])
self.assertEqual('the_answer: 42', txt.values[1]) self.assertEqual('the_answer: 42', txt.values[1])
self.assertEqual('the_date: today', txt.values[2]) self.assertEqual('the_date: today', txt.values[2])
def test_bad_key(self):
templ = Templating('test')
zone = Zone('unit.tests.', [])
txt = Record.new(
zone,
'txt',
{'type': 'TXT', 'ttl': 42, 'value': 'this {bad} does not exist'},
)
zone.add_record(txt)
with self.assertRaises(TemplatingError) as ctx:
templ.process_source_and_target_zones(zone, None, None)
self.assertEqual(
'Invalid record "txt.unit.tests.", undefined template parameter "bad" in value',
str(ctx.exception),
)
zone = Zone('unit.tests.', [])
cname = Record.new(
zone,
'cname',
{
'type': 'CNAME',
'ttl': 42,
'value': '_cname.{bad}something.else.',
},
lenient=True,
)
zone.add_record(cname)
with self.assertRaises(TemplatingError) as ctx:
templ.process_source_and_target_zones(zone, None, None)
self.assertEqual(
'Invalid record "cname.unit.tests.", undefined template parameter "bad" in value',
str(ctx.exception),
)

Loading…
Cancel
Save