diff --git a/.changelog/a036eb2d018549f0952f01e0790e8f32.md b/.changelog/a036eb2d018549f0952f01e0790e8f32.md new file mode 100644 index 0000000..b50fa9d --- /dev/null +++ b/.changelog/a036eb2d018549f0952f01e0790e8f32.md @@ -0,0 +1,4 @@ +--- +type: minor +--- +Add ignore_missing_zones parameter to YamlProvider \ No newline at end of file diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py index e56f09d..24ed75e 100644 --- a/octodns/provider/yaml.py +++ b/octodns/provider/yaml.py @@ -64,6 +64,10 @@ Core provider for records configured in yaml files on disk:: # (optional, default True) escaped_semicolons: True + # Whether to ignore missing zone files when used as a source + # (optional, default False) + ignore_missing_zones: False + .. Note:: When using this provider as a target any existing comments or formatting in @@ -203,13 +207,14 @@ class YamlProvider(BaseProvider): shared_filename=False, disable_zonefile=False, escaped_semicolons=True, + ignore_missing_zones=False, *args, **kwargs, ): klass = self.__class__.__name__ self.log = logging.getLogger(f'{klass}[{id}]') self.log.debug( - '__init__: id=%s, directory=%s, default_ttl=%d, enforce_order=%d, order_mode=%s, populate_should_replace=%s, supports_root_ns=%s, split_extension=%s, split_catchall=%s, shared_filename=%s, disable_zonefile=%s, escaped_semicolons=%s', + '__init__: id=%s, directory=%s, default_ttl=%d, enforce_order=%d, order_mode=%s, populate_should_replace=%s, supports_root_ns=%s, split_extension=%s, split_catchall=%s, shared_filename=%s, disable_zonefile=%s, escaped_semicolons=%s, ignore_missing_zones=%s', id, directory, default_ttl, @@ -222,6 +227,7 @@ class YamlProvider(BaseProvider): shared_filename, disable_zonefile, escaped_semicolons, + ignore_missing_zones, ) super().__init__(id, *args, **kwargs) self.directory = directory @@ -235,6 +241,7 @@ class YamlProvider(BaseProvider): self.shared_filename = shared_filename self.disable_zonefile = disable_zonefile self.escaped_semicolons = escaped_semicolons + self.ignore_missing_zones = ignore_missing_zones def copy(self): kwargs = dict(self.__dict__) @@ -388,7 +395,7 @@ class YamlProvider(BaseProvider): if self.shared_filename: sources.append(join(self.directory, self.shared_filename)) - if not sources and not target: + if not sources and not target and not self.ignore_missing_zones: raise ProviderException(f'no YAMLs found for {zone.decoded_name}') # deterministically order our sources diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py index 61c1e14..f2791a7 100644 --- a/tests/test_octodns_provider_yaml.py +++ b/tests/test_octodns_provider_yaml.py @@ -242,6 +242,67 @@ xn--dj-kia8a: source.populate(zone) self.assertEqual(0, len(zone.records)) + def test_ignore_missing_zones(self): + # Test that ignore_missing_zones prevents errors when zone files are missing + with TemporaryDirectory() as td: + directory = td.dirname + + # Create a provider with ignore_missing_zones disabled (default) + provider = YamlProvider( + 'test', directory, ignore_missing_zones=False + ) + zone = Zone('missing.zone.', []) + + # Should raise an exception when zone file is missing + with self.assertRaises(ProviderException) as ctx: + provider.populate(zone) + self.assertEqual( + 'no YAMLs found for missing.zone.', str(ctx.exception) + ) + + # Create a provider with ignore_missing_zones enabled + provider = YamlProvider( + 'test', directory, ignore_missing_zones=True + ) + zone = Zone('missing.zone.', []) + + # Should not raise an exception and should return False (zone doesn't exist) + exists = provider.populate(zone) + self.assertFalse(exists) + self.assertEqual(0, len(zone.records)) + + # Test with multiple sources where some have the zone and some don't + # Create one zone file + zone_file = join(directory, 'exists.zone.yaml') + with open(zone_file, 'w') as fh: + fh.write( + '''--- +www: + type: A + value: 1.2.3.4 +''' + ) + + # Create a second directory without the zone file + directory2 = join(td.dirname, 'other') + makedirs(directory2) + + # Provider 1 has the zone + provider1 = YamlProvider('test1', directory) + # Provider 2 doesn't have the zone but ignores missing + provider2 = YamlProvider( + 'test2', directory2, ignore_missing_zones=True + ) + + zone = Zone('exists.zone.', []) + provider1.populate(zone) + self.assertEqual(1, len(zone.records)) + + # Provider 2 should not error when the zone is missing + provider2.populate(zone) + # Still just 1 record from provider1 + self.assertEqual(1, len(zone.records)) + def test_unsorted(self): source = YamlProvider( 'test', join(dirname(__file__), 'config'), supports_root_ns=False