Repository: octodns/octodns-cloudflare (language: python)

Issue:
Proxy based on `0` TTL
I have a bit of a funky use case - I'm using https://github.com/Janik-Haag/NixOS-DNS to generate my octodns configuration, and it works by generating a ZoneFile which octodns consumes (https://github.com/Janik-Haag/NixOS-DNS/blob/main/utils/zonefiles.nix). Unfortunately because of this we can't set the proxied status of a domain.

My pitch is the ability to set the TTL to 0 and this provider sees that as a request to proxy the domain, since the TTL of 0 doesn't make sense in other use cases.

Alternatively, I might look at the `ZoneFileSource` and retain the `octodns.*` configuration options as comments, which might be better long term?


Discussion / hints:
There's already a special ttl value, defined by Cloudflare not octodns-cloudflare, that enables proxied. 

https://github.com/octodns/octodns-cloudflare/blob/59de7e37905605f8126bd7d1b391e55000474c89/octodns_cloudflare/__init__.py#L441

So if it sees a ttl of `1` it'll enable proxied.

If the source can't use make them 1 the other option would be to use a custom processor, defining a process_source_zone that looks for the 0, or whatever you want to drive it off of, and sets `record._octodns['cloudflare']['proxied'] = true` or whatever you'd like to do.
Oh, neat! Was digging through the code and must have missed this - thanks :) 
Oh, right, I remember why this didn't occur to me

https://github.com/octodns/octodns-cloudflare/blob/v0.0.4/octodns_cloudflare/__init__.py#L188-L189

From what I'm reading, this effectively overrides the ttl of `1` every time? From my testing the TTL of `1` doesn't get used and proxying never gets added. I am going to look at the processor route but curious if this is intentional behaviour.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08adec8..ba23a86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## v0.0.4 - 2024-??-?? - ???
+
+* TtlToProxy processor added to enable the proxied flag based on a sentinel
+  ttl value. Useful when the source is not YamlProvider
+
 ## v0.0.4 - 2024-02-08 - Know your zones
 
 * Support for Provider.list_zones to enable dynamic zone config when operating
diff --git a/octodns_cloudflare/processor/__init__.py b/octodns_cloudflare/processor/__init__.py
new file mode 100644
index 0000000..407eb4e
--- /dev/null
+++ b/octodns_cloudflare/processor/__init__.py
@@ -0,0 +1,3 @@
+#
+#
+#
diff --git a/octodns_cloudflare/processor/ttl.py b/octodns_cloudflare/processor/ttl.py
new file mode 100644
index 0000000..301ad14
--- /dev/null
+++ b/octodns_cloudflare/processor/ttl.py
@@ -0,0 +1,46 @@
+#
+#
+#
+
+from octodns.processor.base import BaseProcessor, ProcessorException
+
+
+class TtlToProxy(BaseProcessor):
+    '''
+    Ensure Cloudflare's proxy status is setup depending on the TTL set for the record. This
+    can be helpful for `octodns_bind.ZoneFileSource` or the like.
+
+    Example usage:
+
+    processors:
+      ttl-to-proxy:
+        class: octodns_cloudflare.processor.ttl.TtlToProxy
+        ttl: 0
+
+    zones:
+      exxampled.com.:
+        sources:
+          - config
+        processors:
+          - ttl-to-proxy
+        targets:
+          - cloudflare
+    '''
+
+    def __init__(self, name, ttl=0):
+        super().__init__(name)
+        self.ttl = ttl
+
+    def process_source_zone(self, zone, *args, **kwargs):
+        for record in zone.records:
+            if record.ttl == self.ttl:
+                record = record.copy()
+                record._octodns['cloudflare'] = {
+                    'proxied': True,
+                    'auto-ttl': True,
+                }
+                record.ttl = 1
+                # Ensure we set to valid TTL.
+                zone.add_record(record, replace=True, lenient=True)
+
+        return zone


Test patch:
diff --git a/tests/test_octodns_provider_cloudflare_processor_ttl.py b/tests/test_octodns_provider_cloudflare_processor_ttl.py
new file mode 100644
index 0000000..47f442b
--- /dev/null
+++ b/tests/test_octodns_provider_cloudflare_processor_ttl.py
@@ -0,0 +1,50 @@
+from unittest import TestCase
+
+from octodns.record import Record
+from octodns.zone import Zone
+
+from octodns_cloudflare.processor.ttl import TtlToProxy
+
+
+class TestTtlToProxy(TestCase):
+    def test_ttl_to_proxy(self):
+        processor = TtlToProxy('test', ttl=0)
+
+        zone = Zone('unit.tests.', [])
+        zone_expected = Zone('unit.tests.', [])
+
+        with_ttl = Record.new(
+            zone, 'good', {'type': 'A', 'ttl': 0, 'value': '1.2.3.4'}
+        )
+        without_ttl = Record.new(
+            zone, 'bad', {'type': 'A', 'ttl': 10, 'value': '1.2.3.4'}
+        )
+        zone.add_record(with_ttl)
+        zone.add_record(without_ttl)
+
+        expected_with = Record.new(
+            zone,
+            'good',
+            {
+                'type': 'A',
+                'ttl': 0,
+                'value': '1.2.3.4',
+                '_octodns': {'cloudflare': {'proxied': True, 'auto-ttl': True}},
+            },
+        )
+        expected_without = Record.new(
+            zone, 'bad', {'type': 'A', 'ttl': 10, 'value': '1.2.3.4'}
+        )
+        zone_expected.add_record(expected_with)
+        zone_expected.add_record(expected_without)
+
+        added_proxy = processor.process_source_zone(zone)
+        self.assertEqual(zone_expected.records, added_proxy.records)
+        good = next(r for r in added_proxy.records if r.name == 'good')
+        self.assertEqual(1, good.ttl)
+        self.assertEqual(
+            {'cloudflare': {'proxied': True, 'auto-ttl': True}}, good._octodns
+        )
+        bad = next(r for r in added_proxy.records if r.name == 'bad')
+        self.assertEqual(10, bad.ttl)
+        self.assertFalse('cloudflare' in bad._octodns)