1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

Add custom host & path support to Route53 Geo healthchecks

- This reworks the CallerReference structure for Route53 health checks in a
  non-backwards compatible way. This means records will create new healthchecks
  for themselves which should be fine.
- Since we're pre 1.0, support has NOT been added to cleanup the old
  healthchecks. That could be done reasonably easy, BUT we'd have to keep that
  around forever. The hope is that the new ref format/usage will prevent this
  problem going forward since enough info exists in the ref to identify things
  fully. :fingers_crossed:
- healthcheck GC is much cleaner and more robust thanks to ^
- overall the healthcheck management code is a bit easier to follow and more
  robust now.
This commit is contained in:
Ross McFarland
2017-06-15 09:57:40 -07:00
parent 670d7eef17
commit 4761da9c33
2 changed files with 74 additions and 30 deletions

View File

@@ -156,7 +156,7 @@ class Route53Provider(BaseProvider):
# This should be bumped when there are underlying changes made to the # This should be bumped when there are underlying changes made to the
# health check config. # health check config.
HEALTH_CHECK_VERSION = '0000' HEALTH_CHECK_VERSION = '0001'
def __init__(self, id, access_key_id, secret_access_key, max_changes=1000, def __init__(self, id, access_key_id, secret_access_key, max_changes=1000,
*args, **kwargs): *args, **kwargs):
@@ -420,6 +420,13 @@ class Route53Provider(BaseProvider):
# We've got a cached version use it # We've got a cached version use it
return self._health_checks return self._health_checks
def _health_check_equivilent(self, host, path, health_check,
first_value=None):
config = health_check['HealthCheckConfig']
return host == config['FullyQualifiedDomainName'] and \
path == config['ResourcePath'] and \
(first_value is None or first_value == config['IPAddress'])
def _get_health_check_id(self, record, ident, geo, create): def _get_health_check_id(self, record, ident, geo, create):
# fqdn & the first value are special, we use them to match up health # fqdn & the first value are special, we use them to match up health
# checks to their records. Route53 health checks check a single ip and # checks to their records. Route53 health checks check a single ip and
@@ -431,20 +438,20 @@ class Route53Provider(BaseProvider):
'first_value=%s', fqdn, record._type, ident, 'first_value=%s', fqdn, record._type, ident,
first_value) first_value)
# health check host can't end with a . healthcheck_host = record.healthcheck_host
host = fqdn[:-1] healthcheck_path = record.healthcheck_path
# we're looking for a healthcheck with the current version & our record # we're looking for a healthcheck with the current version & our record
# type, we'll ignore anything else # type, we'll ignore anything else
expected_version_and_type = '{}:{}:'.format(self.HEALTH_CHECK_VERSION, expected_ref = '{}:{}:{}:'.format(self.HEALTH_CHECK_VERSION,
record._type) record._type, record.name)
for id, health_check in self.health_checks.items(): for id, health_check in self.health_checks.items():
if not health_check['CallerReference'] \ if not health_check['CallerReference'].startswith(expected_ref):
.startswith(expected_version_and_type):
# not a version & type match, ignore # not a version & type match, ignore
continue continue
config = health_check['HealthCheckConfig'] if self._health_check_equivilent(healthcheck_host,
if host == config['FullyQualifiedDomainName'] and \ healthcheck_path, health_check,
first_value == config['IPAddress']: first_value=first_value):
# this is the health check we're looking for # this is the health check we're looking for
return id return id
@@ -456,16 +463,16 @@ class Route53Provider(BaseProvider):
config = { config = {
'EnableSNI': True, 'EnableSNI': True,
'FailureThreshold': 6, 'FailureThreshold': 6,
'FullyQualifiedDomainName': host, 'FullyQualifiedDomainName': healthcheck_host,
'IPAddress': first_value, 'IPAddress': first_value,
'MeasureLatency': True, 'MeasureLatency': True,
'Port': 443, 'Port': 443,
'RequestInterval': 10, 'RequestInterval': 10,
'ResourcePath': '/_dns', 'ResourcePath': healthcheck_path,
'Type': 'HTTPS', 'Type': 'HTTPS',
} }
ref = '{}:{}:{}'.format(self.HEALTH_CHECK_VERSION, record._type, ref = '{}:{}:{}:{}'.format(self.HEALTH_CHECK_VERSION, record._type,
uuid4().hex[:16]) record.name, uuid4().hex[:16])
resp = self._conn.create_health_check(CallerReference=ref, resp = self._conn.create_health_check(CallerReference=ref,
HealthCheckConfig=config) HealthCheckConfig=config)
health_check = resp['HealthCheck'] health_check = resp['HealthCheck']
@@ -473,11 +480,14 @@ class Route53Provider(BaseProvider):
# store the new health check so that we'll be able to find it in the # store the new health check so that we'll be able to find it in the
# future # future
self._health_checks[id] = health_check self._health_checks[id] = health_check
self.log.info('_get_health_check_id: created id=%s, host=%s, ' self.log.info('_get_health_check_id: created id=%s, host=%s, path=%s'
'first_value=%s', id, host, first_value) 'first_value=%s', id, healthcheck_host, healthcheck_path,
first_value)
return id return id
def _gc_health_checks(self, record, new): def _gc_health_checks(self, record, new):
if record._type not in ('A', 'AAAA'):
return
self.log.debug('_gc_health_checks: record=%s', record) self.log.debug('_gc_health_checks: record=%s', record)
# Find the health checks we're using for the new route53 records # Find the health checks we're using for the new route53 records
in_use = set() in_use = set()
@@ -488,14 +498,12 @@ class Route53Provider(BaseProvider):
# Now we need to run through ALL the health checks looking for those # Now we need to run through ALL the health checks looking for those
# that apply to this record, deleting any that do and are no longer in # that apply to this record, deleting any that do and are no longer in
# use # use
host = record.fqdn[:-1] expected_re = re.compile(r'^\d\d\d\d:{}:{}:'
.format(record._type, record.name))
for id, health_check in self.health_checks.items(): for id, health_check in self.health_checks.items():
config = health_check['HealthCheckConfig'] ref = health_check['CallerReference']
_type = health_check['CallerReference'].split(':', 2)[1] if expected_re.match(ref) and id not in in_use:
# if host and the pulled out type match it applies # this is a health check for this record, but not one we're
if host == config['FullyQualifiedDomainName'] and \
_type == record._type and id not in in_use:
# this is a health check for our fqdn & type but not one we're
# planning to use going forward # planning to use going forward
self.log.info('_gc_health_checks: deleting id=%s', id) self.log.info('_gc_health_checks: deleting id=%s', id)
self._conn.delete_health_check(HealthCheckId=id) self._conn.delete_health_check(HealthCheckId=id)
@@ -598,7 +606,11 @@ class Route53Provider(BaseProvider):
# b/c of a health check version bump # b/c of a health check version bump
self.log.debug('_extra_changes: inspecting=%s, %s', record.fqdn, self.log.debug('_extra_changes: inspecting=%s, %s', record.fqdn,
record._type) record._type)
healthcheck_host = record.healthcheck_host
healthcheck_path = record.healthcheck_path
fqdn = record.fqdn fqdn = record.fqdn
# loop through all the r53 rrsets # loop through all the r53 rrsets
for rrset in self._load_records(zone_id): for rrset in self._load_records(zone_id):
if fqdn != rrset['Name'] or record._type != rrset['Type']: if fqdn != rrset['Name'] or record._type != rrset['Type']:
@@ -611,12 +623,15 @@ class Route53Provider(BaseProvider):
# we expect a healtcheck now # we expect a healtcheck now
try: try:
health_check_id = rrset['HealthCheckId'] health_check_id = rrset['HealthCheckId']
caller_ref = \ health_check = self.health_checks[health_check_id]
self.health_checks[health_check_id]['CallerReference'] caller_ref = health_check['CallerReference']
if caller_ref.startswith(self.HEALTH_CHECK_VERSION): if caller_ref.startswith(self.HEALTH_CHECK_VERSION):
# it has the right health check if self._health_check_equivilent(healthcheck_host,
continue healthcheck_path,
except KeyError: health_check):
# it has the right health check
continue
except (IndexError, KeyError):
# no health check id or one that isn't the right version # no health check id or one that isn't the right version
pass pass
# no good, doesn't have the right health check, needs an update # no good, doesn't have the right health check, needs an update

View File

@@ -81,7 +81,7 @@ class TestRoute53Provider(TestCase):
record = Record.new(expected, name, data) record = Record.new(expected, name, data)
expected.add_record(record) expected.add_record(record)
caller_ref = '{}:A:1324'.format(Route53Provider.HEALTH_CHECK_VERSION) caller_ref = '{}:A::1324'.format(Route53Provider.HEALTH_CHECK_VERSION)
health_checks = [{ health_checks = [{
'Id': '42', 'Id': '42',
'CallerReference': caller_ref, 'CallerReference': caller_ref,
@@ -89,6 +89,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '4.2.3.4', 'IPAddress': '4.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}, { }, {
@@ -98,6 +99,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '5.2.3.4', 'IPAddress': '5.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 42, 'HealthCheckVersion': 42,
}, { }, {
@@ -107,6 +109,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '5.2.3.4', 'IPAddress': '5.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}, { }, {
@@ -116,6 +119,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '7.2.3.4', 'IPAddress': '7.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}, { }, {
@@ -126,6 +130,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '7.2.3.4', 'IPAddress': '7.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}] }]
@@ -639,6 +644,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '4.2.3.4', 'IPAddress': '4.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}, { }, {
@@ -648,6 +654,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '9.2.3.4', 'IPAddress': '9.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}] }]
@@ -667,6 +674,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '8.2.3.4', 'IPAddress': '8.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}] }]
@@ -712,6 +720,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '4.2.3.4', 'IPAddress': '4.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}, { }, {
@@ -721,6 +730,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '4.2.3.4', 'IPAddress': '4.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}] }]
@@ -736,6 +746,7 @@ class TestRoute53Provider(TestCase):
'FailureThreshold': 6, 'FailureThreshold': 6,
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '4.2.3.4', 'IPAddress': '4.2.3.4',
'ResourcePath': '/_dns',
'MeasureLatency': True, 'MeasureLatency': True,
'Port': 443, 'Port': 443,
'RequestInterval': 10, 'RequestInterval': 10,
@@ -995,6 +1006,7 @@ class TestRoute53Provider(TestCase):
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'unit.tests',
'IPAddress': '2.2.3.4', 'IPAddress': '2.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}], }],
@@ -1093,8 +1105,9 @@ class TestRoute53Provider(TestCase):
'CallerReference': self.caller_ref, 'CallerReference': self.caller_ref,
'HealthCheckConfig': { 'HealthCheckConfig': {
'Type': 'HTTPS', 'Type': 'HTTPS',
'FullyQualifiedDomainName': 'unit.tests', 'FullyQualifiedDomainName': 'a.unit.tests',
'IPAddress': '2.2.3.4', 'IPAddress': '2.2.3.4',
'ResourcePath': '/_dns',
}, },
'HealthCheckVersion': 2, 'HealthCheckVersion': 2,
}], }],
@@ -1106,6 +1119,22 @@ class TestRoute53Provider(TestCase):
self.assertEquals(0, len(extra)) self.assertEquals(0, len(extra))
stubber.assert_no_pending_responses() stubber.assert_no_pending_responses()
# change b/c of healthcheck path
record._octodns['healthcheck'] = {
'path': '/_ready'
}
extra = provider._extra_changes(existing, [])
self.assertEquals(1, len(extra))
stubber.assert_no_pending_responses()
# change b/c of healthcheck host
record._octodns['healthcheck'] = {
'host': 'foo.bar.io'
}
extra = provider._extra_changes(existing, [])
self.assertEquals(1, len(extra))
stubber.assert_no_pending_responses()
def test_route_53_record(self): def test_route_53_record(self):
# Just make sure it doesn't blow up # Just make sure it doesn't blow up
_Route53Record('foo.unit.tests.', 'A', 30).__repr__() _Route53Record('foo.unit.tests.', 'A', 30).__repr__()