Browse Source

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

pull/1279/head
Ross McFarland 4 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
class TemplatingError(Exception):
def __init__(self, record, msg):
self.record = record
msg = f'Invalid record "{record.fqdn}", {msg}'
super().__init__(msg)
class Templating(BaseProcessor):
'''
Record templating using python format. For simple records like TXT and CAA
@ -15,6 +23,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,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)
self.trailing_dots = trailing_dots
self.context = context
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_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),
# add any extra context provided to us, if the value is a callable
# 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 {
'record_name': record.decoded_name,
'record_decoded_name': record.decoded_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_ttl': record.ttl,
'record_source_id': record.source.id if record.source else None,
**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:
params = build_params(record)
if hasattr(record, 'values'):
if record.values and not hasattr(record.values[0], 'template'):
# the (custom) value type does not support templating
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:
new = record.copy()
new.values = new_values
@ -102,7 +141,7 @@ class Templating(BaseProcessor):
if not hasattr(record.value, 'template'):
# the (custom) value type does not support templating
continue
new_value = record.value.template(params(record))
new_value = template(record.value, params, record)
if record.value != new_value:
new = record.copy()
new.value = new_value


+ 81
- 1
tests/test_octodns_processor_templating.py View File

@ -3,8 +3,9 @@
#
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.zone import Zone
@ -143,6 +144,47 @@ class TemplatingTest(TestCase):
self.assertEqual({'template'}, s.value._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):
templ = Templating(
'test',
@ -178,3 +220,41 @@ class TemplatingTest(TestCase):
self.assertEqual('provider: da-pro', txt.values[0])
self.assertEqual('the_answer: 42', txt.values[1])
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