From 7f98d9dfca7ab35d54a62c8ff01074194c54bd0d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 16 Mar 2022 09:58:44 -0700 Subject: [PATCH 1/8] Base of Record type registration --- octodns/record/__init__.py | 85 ++++++++++++++++++++++++++++-------- tests/test_octodns_record.py | 12 +++-- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 61dfb04..95053f2 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -64,7 +64,11 @@ class Delete(Change): return f'Delete {self.existing}' -class ValidationError(Exception): +class RecordException(Exception): + pass + + +class ValidationError(RecordException): @classmethod def build_message(cls, fqdn, reasons): @@ -80,6 +84,18 @@ class ValidationError(Exception): class Record(EqualityTupleMixin): log = getLogger('Record') + _CLASSES = {} + + @classmethod + def register_type(cls, _type, _class): + existing = cls._CLASSES.get(_type, None) + if existing: + module = existing.__module__ + name = existing.__name__ + msg = f'Type "{_type}" already registered by {module}.{name}' + raise RecordException(msg) + cls._CLASSES[_type] = _class + @classmethod def new(cls, zone, name, data, source=None, lenient=False): name = str(name) @@ -89,24 +105,7 @@ class Record(EqualityTupleMixin): except KeyError: raise Exception(f'Invalid record {fqdn}, missing type') try: - _class = { - 'A': ARecord, - 'AAAA': AaaaRecord, - 'ALIAS': AliasRecord, - 'CAA': CaaRecord, - 'CNAME': CnameRecord, - 'DNAME': DnameRecord, - 'LOC': LocRecord, - 'MX': MxRecord, - 'NAPTR': NaptrRecord, - 'NS': NsRecord, - 'PTR': PtrRecord, - 'SPF': SpfRecord, - 'SRV': SrvRecord, - 'SSHFP': SshfpRecord, - 'TXT': TxtRecord, - 'URLFWD': UrlfwdRecord, - }[_type] + _class = cls._CLASSES[_type] except KeyError: raise Exception(f'Unknown record type: "{_type}"') reasons = _class.validate(name, fqdn, data) @@ -804,11 +803,17 @@ class ARecord(_DynamicMixin, _GeoMixin, Record): _value_type = Ipv4List +Record.register_type('A', ARecord) + + class AaaaRecord(_DynamicMixin, _GeoMixin, Record): _type = 'AAAA' _value_type = Ipv6List +Record.register_type('AAAA', AaaaRecord) + + class AliasValue(_TargetValue): pass @@ -826,6 +831,9 @@ class AliasRecord(_ValueMixin, Record): return reasons +Record.register_type('ALIAS', AliasRecord) + + class CaaValue(EqualityTupleMixin): # https://tools.ietf.org/html/rfc6844#page-5 @@ -877,6 +885,9 @@ class CaaRecord(_ValuesMixin, Record): _value_type = CaaValue +Record.register_type('CAA', CaaRecord) + + class CnameRecord(_DynamicMixin, _ValueMixin, Record): _type = 'CNAME' _value_type = CnameValue @@ -890,11 +901,17 @@ class CnameRecord(_DynamicMixin, _ValueMixin, Record): return reasons +Record.register_type('CNAME', CnameRecord) + + class DnameRecord(_DynamicMixin, _ValueMixin, Record): _type = 'DNAME' _value_type = DnameValue +Record.register_type('DNAME', DnameRecord) + + class LocValue(EqualityTupleMixin): # TODO: work out how to do defaults per RFC @@ -1071,6 +1088,9 @@ class LocRecord(_ValuesMixin, Record): _value_type = LocValue +Record.register_type('LOC', LocRecord) + + class MxValue(EqualityTupleMixin): @classmethod @@ -1141,6 +1161,9 @@ class MxRecord(_ValuesMixin, Record): _value_type = MxValue +Record.register_type('MX', MxRecord) + + class NaptrValue(EqualityTupleMixin): VALID_FLAGS = ('S', 'A', 'U', 'P') @@ -1219,6 +1242,9 @@ class NaptrRecord(_ValuesMixin, Record): _value_type = NaptrValue +Record.register_type('NAPTR', NaptrRecord) + + class _NsValue(object): @classmethod @@ -1246,6 +1272,9 @@ class NsRecord(_ValuesMixin, Record): _value_type = _NsValue +Record.register_type('NS', NsRecord) + + class PtrValue(_TargetValue): @classmethod @@ -1279,6 +1308,9 @@ class PtrRecord(_ValuesMixin, Record): return self.values[0] +Record.register_type('PTR', PtrRecord) + + class SshfpValue(EqualityTupleMixin): VALID_ALGORITHMS = (1, 2, 3, 4) VALID_FINGERPRINT_TYPES = (1, 2) @@ -1343,6 +1375,9 @@ class SshfpRecord(_ValuesMixin, Record): _value_type = SshfpValue +Record.register_type('SSHFP', SshfpRecord) + + class _ChunkedValuesMixin(_ValuesMixin): CHUNK_SIZE = 255 _unescaped_semicolon_re = re.compile(r'\w;') @@ -1392,6 +1427,9 @@ class SpfRecord(_ChunkedValuesMixin, Record): _value_type = _ChunkedValue +Record.register_type('SPF', SpfRecord) + + class SrvValue(EqualityTupleMixin): @classmethod @@ -1474,6 +1512,9 @@ class SrvRecord(_ValuesMixin, Record): return reasons +Record.register_type('SRV', SrvRecord) + + class _TxtValue(_ChunkedValue): pass @@ -1483,6 +1524,9 @@ class TxtRecord(_ChunkedValuesMixin, Record): _value_type = _TxtValue +Record.register_type('TXT', TxtRecord) + + class UrlfwdValue(EqualityTupleMixin): VALID_CODES = (301, 302) VALID_MASKS = (0, 1, 2) @@ -1558,3 +1602,6 @@ class UrlfwdValue(EqualityTupleMixin): class UrlfwdRecord(_ValuesMixin, Record): _type = 'URLFWD' _value_type = UrlfwdValue + + +Record.register_type('URLFWD', UrlfwdRecord) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index f8819a6..5ce1d0c 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -10,9 +10,9 @@ from unittest import TestCase from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \ CaaValue, CnameRecord, DnameRecord, Create, Delete, GeoValue, LocRecord, \ LocValue, MxRecord, MxValue, NaptrRecord, NaptrValue, NsRecord, \ - PtrRecord, Record, SshfpRecord, SshfpValue, SpfRecord, SrvRecord, \ - SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, ValidationError, \ - _Dynamic, _DynamicPool, _DynamicRule + PtrRecord, Record, RecordException, SshfpRecord, SshfpValue, SpfRecord, \ + SrvRecord, SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, \ + ValidationError, _Dynamic, _DynamicPool, _DynamicRule from octodns.zone import Zone from helpers import DynamicProvider, GeoProvider, SimpleProvider @@ -21,6 +21,12 @@ from helpers import DynamicProvider, GeoProvider, SimpleProvider class TestRecord(TestCase): zone = Zone('unit.tests.', []) + def test_registration(self): + with self.assertRaises(RecordException) as ctx: + Record.register_type('A', None) + self.assertEqual('Type "A" already registered by ' + 'octodns.record.ARecord', str(ctx.exception)) + def test_lowering(self): record = ARecord(self.zone, 'MiXeDcAsE', { 'ttl': 30, From ea5000e191d804d6a26c0637a2059960d24ab41d Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 16 Mar 2022 12:26:29 -0700 Subject: [PATCH 2/8] Real testing of register_type --- tests/test_octodns_record.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 5ce1d0c..97ea3e3 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -12,7 +12,8 @@ from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \ LocValue, MxRecord, MxValue, NaptrRecord, NaptrValue, NsRecord, \ PtrRecord, Record, RecordException, SshfpRecord, SshfpValue, SpfRecord, \ SrvRecord, SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, \ - ValidationError, _Dynamic, _DynamicPool, _DynamicRule + ValidationError, _Dynamic, _DynamicPool, _DynamicRule, _NsValue, \ + _ValuesMixin from octodns.zone import Zone from helpers import DynamicProvider, GeoProvider, SimpleProvider @@ -27,6 +28,18 @@ class TestRecord(TestCase): self.assertEqual('Type "A" already registered by ' 'octodns.record.ARecord', str(ctx.exception)) + class AaRecord(_ValuesMixin, Record): + _type = 'AA' + _value_type = _NsValue + + Record.register_type('AA', AaRecord) + aa = Record.new(self.zone, 'registered', { + 'ttl': 360, + 'type': 'AA', + 'value': 'does.not.matter.', + }) + self.assertEqual(AaRecord, aa.__class__) + def test_lowering(self): record = ARecord(self.zone, 'MiXeDcAsE', { 'ttl': 30, From 5827e23d22639fbe678c51944bc1c5b52b790b33 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Fri, 18 Mar 2022 19:05:00 -0700 Subject: [PATCH 3/8] Simplify Record.register_type. Values?Mixin public --- octodns/record/__init__.py | 84 ++++++++++++++++++------------------ tests/test_octodns_record.py | 8 ++-- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 95053f2..474d48c 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -87,7 +87,9 @@ class Record(EqualityTupleMixin): _CLASSES = {} @classmethod - def register_type(cls, _type, _class): + def register_type(cls, _class, _type=None): + if _type is None: + _type = _class._type existing = cls._CLASSES.get(_type, None) if existing: module = existing.__module__ @@ -283,11 +285,11 @@ class GeoValue(EqualityTupleMixin): "{self.subdivision_code} {self.values}'" -class _ValuesMixin(object): +class ValuesMixin(object): @classmethod def validate(cls, name, fqdn, data): - reasons = super(_ValuesMixin, cls).validate(name, fqdn, data) + reasons = super(ValuesMixin, cls).validate(name, fqdn, data) values = data.get('values', data.get('value', [])) @@ -296,7 +298,7 @@ class _ValuesMixin(object): return reasons def __init__(self, zone, name, data, source=None): - super(_ValuesMixin, self).__init__(zone, name, data, source=source) + super(ValuesMixin, self).__init__(zone, name, data, source=source) try: values = data['values'] except KeyError: @@ -306,10 +308,10 @@ class _ValuesMixin(object): def changes(self, other, target): if self.values != other.values: return Update(self, other) - return super(_ValuesMixin, self).changes(other, target) + return super(ValuesMixin, self).changes(other, target) def _data(self): - ret = super(_ValuesMixin, self)._data() + ret = super(ValuesMixin, self)._data() if len(self.values) > 1: values = [getattr(v, 'data', v) for v in self.values if v] if len(values) > 1: @@ -329,7 +331,7 @@ class _ValuesMixin(object): return f"<{klass} {self._type} {self.ttl}, {self.fqdn}, ['{values}']>" -class _GeoMixin(_ValuesMixin): +class _GeoMixin(ValuesMixin): ''' Adds GeoDNS support to a record. @@ -380,26 +382,26 @@ class _GeoMixin(_ValuesMixin): return super(_GeoMixin, self).__repr__() -class _ValueMixin(object): +class ValueMixin(object): @classmethod def validate(cls, name, fqdn, data): - reasons = super(_ValueMixin, cls).validate(name, fqdn, data) + reasons = super(ValueMixin, cls).validate(name, fqdn, data) reasons.extend(cls._value_type.validate(data.get('value', None), cls._type)) return reasons def __init__(self, zone, name, data, source=None): - super(_ValueMixin, self).__init__(zone, name, data, source=source) + super(ValueMixin, self).__init__(zone, name, data, source=source) self.value = self._value_type.process(data['value']) def changes(self, other, target): if self.value != other.value: return Update(self, other) - return super(_ValueMixin, self).changes(other, target) + return super(ValueMixin, self).changes(other, target) def _data(self): - ret = super(_ValueMixin, self)._data() + ret = super(ValueMixin, self)._data() if self.value: ret['value'] = getattr(self.value, 'data', self.value) return ret @@ -803,7 +805,7 @@ class ARecord(_DynamicMixin, _GeoMixin, Record): _value_type = Ipv4List -Record.register_type('A', ARecord) +Record.register_type(ARecord) class AaaaRecord(_DynamicMixin, _GeoMixin, Record): @@ -811,14 +813,14 @@ class AaaaRecord(_DynamicMixin, _GeoMixin, Record): _value_type = Ipv6List -Record.register_type('AAAA', AaaaRecord) +Record.register_type(AaaaRecord) class AliasValue(_TargetValue): pass -class AliasRecord(_ValueMixin, Record): +class AliasRecord(ValueMixin, Record): _type = 'ALIAS' _value_type = AliasValue @@ -831,7 +833,7 @@ class AliasRecord(_ValueMixin, Record): return reasons -Record.register_type('ALIAS', AliasRecord) +Record.register_type(AliasRecord) class CaaValue(EqualityTupleMixin): @@ -880,15 +882,15 @@ class CaaValue(EqualityTupleMixin): return f'{self.flags} {self.tag} "{self.value}"' -class CaaRecord(_ValuesMixin, Record): +class CaaRecord(ValuesMixin, Record): _type = 'CAA' _value_type = CaaValue -Record.register_type('CAA', CaaRecord) +Record.register_type(CaaRecord) -class CnameRecord(_DynamicMixin, _ValueMixin, Record): +class CnameRecord(_DynamicMixin, ValueMixin, Record): _type = 'CNAME' _value_type = CnameValue @@ -901,15 +903,15 @@ class CnameRecord(_DynamicMixin, _ValueMixin, Record): return reasons -Record.register_type('CNAME', CnameRecord) +Record.register_type(CnameRecord) -class DnameRecord(_DynamicMixin, _ValueMixin, Record): +class DnameRecord(_DynamicMixin, ValueMixin, Record): _type = 'DNAME' _value_type = DnameValue -Record.register_type('DNAME', DnameRecord) +Record.register_type(DnameRecord) class LocValue(EqualityTupleMixin): @@ -1083,12 +1085,12 @@ class LocValue(EqualityTupleMixin): f"{self.precision_horz:.2f}m {self.precision_vert:.2f}m'" -class LocRecord(_ValuesMixin, Record): +class LocRecord(ValuesMixin, Record): _type = 'LOC' _value_type = LocValue -Record.register_type('LOC', LocRecord) +Record.register_type(LocRecord) class MxValue(EqualityTupleMixin): @@ -1156,12 +1158,12 @@ class MxValue(EqualityTupleMixin): return f"'{self.preference} {self.exchange}'" -class MxRecord(_ValuesMixin, Record): +class MxRecord(ValuesMixin, Record): _type = 'MX' _value_type = MxValue -Record.register_type('MX', MxRecord) +Record.register_type(MxRecord) class NaptrValue(EqualityTupleMixin): @@ -1237,12 +1239,12 @@ class NaptrValue(EqualityTupleMixin): f"\"{regexp}\" {self.replacement}'" -class NaptrRecord(_ValuesMixin, Record): +class NaptrRecord(ValuesMixin, Record): _type = 'NAPTR' _value_type = NaptrValue -Record.register_type('NAPTR', NaptrRecord) +Record.register_type(NaptrRecord) class _NsValue(object): @@ -1267,12 +1269,12 @@ class _NsValue(object): return values -class NsRecord(_ValuesMixin, Record): +class NsRecord(ValuesMixin, Record): _type = 'NS' _value_type = _NsValue -Record.register_type('NS', NsRecord) +Record.register_type(NsRecord) class PtrValue(_TargetValue): @@ -1297,7 +1299,7 @@ class PtrValue(_TargetValue): return [super(PtrValue, cls).process(v) for v in values] -class PtrRecord(_ValuesMixin, Record): +class PtrRecord(ValuesMixin, Record): _type = 'PTR' _value_type = PtrValue @@ -1308,7 +1310,7 @@ class PtrRecord(_ValuesMixin, Record): return self.values[0] -Record.register_type('PTR', PtrRecord) +Record.register_type(PtrRecord) class SshfpValue(EqualityTupleMixin): @@ -1370,15 +1372,15 @@ class SshfpValue(EqualityTupleMixin): return f"'{self.algorithm} {self.fingerprint_type} {self.fingerprint}'" -class SshfpRecord(_ValuesMixin, Record): +class SshfpRecord(ValuesMixin, Record): _type = 'SSHFP' _value_type = SshfpValue -Record.register_type('SSHFP', SshfpRecord) +Record.register_type(SshfpRecord) -class _ChunkedValuesMixin(_ValuesMixin): +class _ChunkedValuesMixin(ValuesMixin): CHUNK_SIZE = 255 _unescaped_semicolon_re = re.compile(r'\w;') @@ -1427,7 +1429,7 @@ class SpfRecord(_ChunkedValuesMixin, Record): _value_type = _ChunkedValue -Record.register_type('SPF', SpfRecord) +Record.register_type(SpfRecord) class SrvValue(EqualityTupleMixin): @@ -1498,7 +1500,7 @@ class SrvValue(EqualityTupleMixin): return f"'{self.priority} {self.weight} {self.port} {self.target}'" -class SrvRecord(_ValuesMixin, Record): +class SrvRecord(ValuesMixin, Record): _type = 'SRV' _value_type = SrvValue _name_re = re.compile(r'^(\*|_[^\.]+)\.[^\.]+') @@ -1512,7 +1514,7 @@ class SrvRecord(_ValuesMixin, Record): return reasons -Record.register_type('SRV', SrvRecord) +Record.register_type(SrvRecord) class _TxtValue(_ChunkedValue): @@ -1524,7 +1526,7 @@ class TxtRecord(_ChunkedValuesMixin, Record): _value_type = _TxtValue -Record.register_type('TXT', TxtRecord) +Record.register_type(TxtRecord) class UrlfwdValue(EqualityTupleMixin): @@ -1599,9 +1601,9 @@ class UrlfwdValue(EqualityTupleMixin): f'{self.masking} {self.query}' -class UrlfwdRecord(_ValuesMixin, Record): +class UrlfwdRecord(ValuesMixin, Record): _type = 'URLFWD' _value_type = UrlfwdValue -Record.register_type('URLFWD', UrlfwdRecord) +Record.register_type(UrlfwdRecord) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py index 97ea3e3..fd3f70f 100644 --- a/tests/test_octodns_record.py +++ b/tests/test_octodns_record.py @@ -13,7 +13,7 @@ from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \ PtrRecord, Record, RecordException, SshfpRecord, SshfpValue, SpfRecord, \ SrvRecord, SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, \ ValidationError, _Dynamic, _DynamicPool, _DynamicRule, _NsValue, \ - _ValuesMixin + ValuesMixin from octodns.zone import Zone from helpers import DynamicProvider, GeoProvider, SimpleProvider @@ -24,15 +24,15 @@ class TestRecord(TestCase): def test_registration(self): with self.assertRaises(RecordException) as ctx: - Record.register_type('A', None) + Record.register_type(None, 'A') self.assertEqual('Type "A" already registered by ' 'octodns.record.ARecord', str(ctx.exception)) - class AaRecord(_ValuesMixin, Record): + class AaRecord(ValuesMixin, Record): _type = 'AA' _value_type = _NsValue - Record.register_type('AA', AaRecord) + Record.register_type(AaRecord) aa = Record.new(self.zone, 'registered', { 'ttl': 360, 'type': 'AA', From a96dff480c162139991d030d4743ec3c9034fed9 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Mar 2022 14:29:39 -0700 Subject: [PATCH 4/8] Documentation for dynamic Record type registration --- docs/records.md | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/records.md b/docs/records.md index f210846..182a109 100644 --- a/docs/records.md +++ b/docs/records.md @@ -124,10 +124,10 @@ If you'd like to enable lenience for a whole zone you can do so with the followi #### Restrict Record manipulations -OctoDNS currently provides the ability to limit the number of updates/deletes on +OctoDNS currently provides the ability to limit the number of updates/deletes on DNS records by configuring a percentage of allowed operations as a threshold. -If left unconfigured, suitable defaults take over instead. In the below example, -the Dyn provider is configured with limits of 40% on both update and +If left unconfigured, suitable defaults take over instead. In the below example, +the Dyn provider is configured with limits of 40% on both update and delete operations over all the records present. ````yaml @@ -136,3 +136,42 @@ dyn: update_pcent_threshold: 0.4 delete_pcent_threshold: 0.4 ```` + +## Provider specific record types + +### Creating and registering + +octoDNS has support for provider specific record types through a dynamic type registration system. This functionality is powered by `Route.register_type` and can be used as follows. + +```python +class _SpecificValue(object): + ... + +class SomeProviderSpecificRecord(ValuesMixin, Record): + _type = 'SomeProvider/SPECIFIC' + _value_type = _SpecificValue + +Record.register_type(SomeProviderSpecificRecord) +``` + +Have a look in [octodns.record](/octodns/record/__init__.py) for examples of how records are implemented. `NsRecord` and `_NsValue` are fairly simple examples to start with. You can also take a look at [`Route53Provider`'s `Route53Provider/ALIAS` type](https://github.com/octodns/octodns-route53/blob/main/octodns_route53/record.py). + +In general this support is intended for record types that only make sense for a single provider. If multiple providers have a similar record it may make sense to implement it in octoDNS core. + +### Naming + +By convention the record type should be prefix with the provider class, e.g. `Route53Provider` followed by a `/` and an all-caps record type name `ALIAS`, e.g. `Route53Provider/ALIAS`. + +### YamlProvider support + +Once the type is registered `YamlProvider` will automatically gain support for it and they can be included in your zone yaml files. + +```yaml +alias: + type: Route53Provider/ALIAS + values: + - name: www + type: A + - name: www + type: AAAA +``` From a0f9668e8317266acb222fb8b10c131ab96bf902 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 27 Mar 2022 07:05:55 -0700 Subject: [PATCH 5/8] Remove extranious None default from get --- octodns/record/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octodns/record/__init__.py b/octodns/record/__init__.py index 474d48c..6fe3f90 100644 --- a/octodns/record/__init__.py +++ b/octodns/record/__init__.py @@ -90,7 +90,7 @@ class Record(EqualityTupleMixin): def register_type(cls, _class, _type=None): if _type is None: _type = _class._type - existing = cls._CLASSES.get(_type, None) + existing = cls._CLASSES.get(_type) if existing: module = existing.__module__ name = existing.__name__ From 9313b5f233619ff77756ce3e373f70527e5af480 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 27 Mar 2022 07:10:15 -0700 Subject: [PATCH 6/8] CHANGELOG entry for Record.register_type --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed95a32..bbe6c26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ just standardizes what they are doing as many other providers appear to need to do so, but weren't. There was an ordering before, but it was essentially arbitrarily picked. +* Record.register_type added so that providers can register custom record + types, see [docs/records.md](docs/records.md) for more information ## v0.9.16 - 2022-03-04 - Manage the root of the problem From d207df9e40a0c9deb8dee38d9b6b1b809d98ec18 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 27 Mar 2022 07:34:08 -0700 Subject: [PATCH 7/8] Implement pep440 style public and local version numbers --- script/release | 3 +++ setup.py | 22 +++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/script/release b/script/release index 975aac2..ea2543b 100755 --- a/script/release +++ b/script/release @@ -16,6 +16,9 @@ if [ ! -f "$ACTIVATE" ]; then fi . "$ACTIVATE" +# Set so that setup.py will create a public release style version number +export OCTODNS_RELEASE=1 + VERSION="$(grep __VERSION__ "$ROOT/octodns/__init__.py" | sed -e "s/.* = '//" -e "s/'$//")" git tag -s "v$VERSION" -m "Release $VERSION" diff --git a/setup.py b/setup.py index 4e5d631..2aa6407 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,9 @@ #!/usr/bin/env python -try: - from StringIO import StringIO -except ImportError: - from io import StringIO +from io import StringIO +from os import environ from os.path import dirname, join +from subprocess import CalledProcessError, check_output import octodns try: @@ -55,6 +54,19 @@ def long_description(): return buf.getvalue() +def version(): + # pep440 style public & local version numbers + if environ.get('OCTODNS_RELEASE', False): + # public + return octodns.__VERSION__ + try: + sha = check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8')[:8] + except (CalledProcessError, FileNotFoundError): + sha = 'unknown' + # local + return f'{octodns.__VERSION__}+{sha}' + + tests_require = ( 'pytest>=6.2.5', 'pytest-cov>=3.0.0', @@ -94,5 +106,5 @@ setup( python_requires='>=3.6', tests_require=tests_require, url='https://github.com/octodns/octodns', - version=octodns.__VERSION__, + version=version(), ) From 45123a4c812ce494ab93fbbdf523ac3a6a28da10 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 27 Mar 2022 07:42:35 -0700 Subject: [PATCH 8/8] Split out cibuild-setup-py --- .github/workflows/main.yml | 12 ++++++++++++ script/cibuild | 12 ------------ script/cibuild-setup-py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100755 script/cibuild-setup-py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c082948..76decff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,3 +24,15 @@ jobs: - name: CI Build run: | ./script/cibuild + setup-py: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + architecture: x64 + - name: CI setup.py + run: | + ./script/cibuild-setup-py diff --git a/script/cibuild b/script/cibuild index 2826596..77d7a2c 100755 --- a/script/cibuild +++ b/script/cibuild @@ -26,16 +26,4 @@ echo "## lint ################################################################## script/lint echo "## tests/coverage ##############################################################" script/coverage -echo "## validate setup.py build #####################################################" -python setup.py build -echo "## validate setup.py install ###################################################" -deactivate -TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) -python3 -m venv $TMP_DIR -. "$TMP_DIR/bin/activate" -python setup.py install -octodns-sync --help -echo "## validate tests can run against installed code ###############################" -pip install pytest pytest-network -pytest --disable-network echo "## complete ####################################################################" diff --git a/script/cibuild-setup-py b/script/cibuild-setup-py new file mode 100755 index 0000000..49f8409 --- /dev/null +++ b/script/cibuild-setup-py @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." + +echo "## create test venv ############################################################" +TMP_DIR=$(mktemp -d -t ci-XXXXXXXXXX) +python3 -m venv $TMP_DIR +. "$TMP_DIR/bin/activate" +echo "## environment & versions ######################################################" +python --version +pip --version +echo "## validate setup.py build #####################################################" +python setup.py build +echo "## validate setup.py install ###################################################" +python setup.py install +echo "## validate tests can run against installed code ###############################" +pip install pytest pytest-network +pytest --disable-network +echo "## complete ####################################################################"