From c64e279dd2c5a27ab87cdc8ceb6f9261def6c5f6 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Aug 2023 13:45:01 -0700 Subject: [PATCH 1/3] Add support for !include YAML directive --- octodns/yaml.py | 12 ++++++++++ tests/config/include/array.yaml | 5 ++++ tests/config/include/dict.yaml | 3 +++ tests/config/include/empty.yaml | 1 + .../config/include/include-doesnt-exist.yaml | 2 ++ tests/config/include/main.yaml | 8 +++++++ tests/config/include/nested.yaml | 2 ++ tests/config/include/subdir/value.yaml | 2 ++ tests/test_octodns_yaml.py | 24 +++++++++++++++++++ 9 files changed, 59 insertions(+) create mode 100644 tests/config/include/array.yaml create mode 100644 tests/config/include/dict.yaml create mode 100644 tests/config/include/empty.yaml create mode 100644 tests/config/include/include-doesnt-exist.yaml create mode 100644 tests/config/include/main.yaml create mode 100644 tests/config/include/nested.yaml create mode 100644 tests/config/include/subdir/value.yaml diff --git a/octodns/yaml.py b/octodns/yaml.py index 09433d9..433486a 100644 --- a/octodns/yaml.py +++ b/octodns/yaml.py @@ -2,6 +2,8 @@ # # +from os.path import dirname, join + from natsort import natsort_keygen from yaml import SafeDumper, SafeLoader, dump, load from yaml.constructor import ConstructorError @@ -23,7 +25,17 @@ class ContextLoader(SafeLoader): def _construct(self, node): return self._pairs(node)[0] + def include(self, node): + mark = self.get_mark() + directory = dirname(mark.name) + + filename = join(directory, self.construct_scalar(node)) + + with open(filename, 'r') as fh: + return safe_load(fh, self.__class__) + +ContextLoader.add_constructor('!include', ContextLoader.include) ContextLoader.add_constructor( ContextLoader.DEFAULT_MAPPING_TAG, ContextLoader._construct ) diff --git a/tests/config/include/array.yaml b/tests/config/include/array.yaml new file mode 100644 index 0000000..a97221c --- /dev/null +++ b/tests/config/include/array.yaml @@ -0,0 +1,5 @@ +--- +- 14 +- 15 +- 16 +- 72 diff --git a/tests/config/include/dict.yaml b/tests/config/include/dict.yaml new file mode 100644 index 0000000..da2e22f --- /dev/null +++ b/tests/config/include/dict.yaml @@ -0,0 +1,3 @@ +--- +k: v +z: 42 diff --git a/tests/config/include/empty.yaml b/tests/config/include/empty.yaml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/tests/config/include/empty.yaml @@ -0,0 +1 @@ +--- diff --git a/tests/config/include/include-doesnt-exist.yaml b/tests/config/include/include-doesnt-exist.yaml new file mode 100644 index 0000000..7f58025 --- /dev/null +++ b/tests/config/include/include-doesnt-exist.yaml @@ -0,0 +1,2 @@ +--- +key: !include does-not-exist.yaml diff --git a/tests/config/include/main.yaml b/tests/config/include/main.yaml new file mode 100644 index 0000000..11f6c6c --- /dev/null +++ b/tests/config/include/main.yaml @@ -0,0 +1,8 @@ +--- +included-array: !include array.yaml +included-dict: !include dict.yaml +included-empty: !include empty.yaml +included-nested: !include nested.yaml +included-subdir: !include subdir/value.yaml +key: value +name: main diff --git a/tests/config/include/nested.yaml b/tests/config/include/nested.yaml new file mode 100644 index 0000000..02da7f4 --- /dev/null +++ b/tests/config/include/nested.yaml @@ -0,0 +1,2 @@ +--- +!include subdir/value.yaml diff --git a/tests/config/include/subdir/value.yaml b/tests/config/include/subdir/value.yaml new file mode 100644 index 0000000..6cdc809 --- /dev/null +++ b/tests/config/include/subdir/value.yaml @@ -0,0 +1,2 @@ +--- +Hello World! diff --git a/tests/test_octodns_yaml.py b/tests/test_octodns_yaml.py index 3a5990f..396d59c 100644 --- a/tests/test_octodns_yaml.py +++ b/tests/test_octodns_yaml.py @@ -62,3 +62,27 @@ class TestYaml(TestCase): buf = StringIO() safe_dump({'45a03129': 42, '45a0392a': 43}, buf) self.assertEqual("---\n45a0392a: 43\n45a03129: 42\n", buf.getvalue()) + + def test_include(self): + with open('tests/config/include/main.yaml') as fh: + data = safe_load(fh) + self.assertEqual( + { + 'included-array': [14, 15, 16, 72], + 'included-dict': {'k': 'v', 'z': 42}, + 'included-empty': None, + 'included-nested': 'Hello World!', + 'included-subdir': 'Hello World!', + 'key': 'value', + 'name': 'main', + }, + data, + ) + + with open('tests/config/include/include-doesnt-exist.yaml') as fh: + with self.assertRaises(FileNotFoundError) as ctx: + data = safe_load(fh) + self.assertEqual( + "[Errno 2] No such file or directory: 'tests/config/include/does-not-exist.yaml'", + str(ctx.exception), + ) From b5853ddc919fe098bedb427e3a8740b823db851a Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Aug 2023 14:07:52 -0700 Subject: [PATCH 2/3] CHANGELOG entry for shared_filename --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdef09..ce2ebed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ possible. This includes the ability to split some zones and not others and even to have partially split zones with some records in the primary zone YAML and others in a split directory. See YamlProvider documentation for more info. +* YamlProvider now supports a `shared_filename` that can be used to add a set of + common records across all zones using the provider. It can be used stand-alone + or in combination with zone files and/or split configs to aid in DRYing up DNS + configs. #### Stuff From 645b088a386112cda9177be53d0ba9d0eeedfdef Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Aug 2023 14:11:44 -0700 Subject: [PATCH 3/3] CHANGELOG entry for YamlProvider !include support --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2ebed..71dcd47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ common records across all zones using the provider. It can be used stand-alone or in combination with zone files and/or split configs to aid in DRYing up DNS configs. +* YamlProvider now supports an `!include` directive which enables shared + snippets of config to be reused across many records, e.g. common dynamic rules + across a set of services with service-specific pool values or a unified SFP + value included in TXT records at the root of all zones. #### Stuff