Hello community, here is the log from the commit of package python-certbot-dns-route53 for openSUSE:Factory checked in at 2020-01-03 17:39:25 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-certbot-dns-route53 (Old) and /work/SRC/openSUSE:Factory/.python-certbot-dns-route53.new.6675 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot-dns-route53" Fri Jan 3 17:39:25 2020 rev:15 rq:760666 version:1.0.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-certbot-dns-route53/python-certbot-dns-route53.changes 2019-11-15 00:29:24.243760314 +0100 +++ /work/SRC/openSUSE:Factory/.python-certbot-dns-route53.new.6675/python-certbot-dns-route53.changes 2020-01-03 17:39:43.907380286 +0100 @@ -1,0 +2,6 @@ +Fri Jan 3 13:18:46 UTC 2020 - Marketa Calabkova <mcalabk...@suse.com> + +- update to version 1.0.0 + * sync with main certbot package + +------------------------------------------------------------------- Old: ---- certbot-dns-route53-0.40.1.tar.gz New: ---- certbot-dns-route53-1.0.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-certbot-dns-route53.spec ++++++ --- /var/tmp/diff_new_pack.LU2AbC/_old 2020-01-03 17:39:44.547380615 +0100 +++ /var/tmp/diff_new_pack.LU2AbC/_new 2020-01-03 17:39:44.547380615 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-certbot-dns-route53 # -# Copyright (c) 2019 SUSE LLC. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,23 +18,24 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-certbot-dns-route53 -Version: 0.40.1 +Version: 1.0.0 Release: 0 Summary: Route53 DNS Authenticator plugin for Certbot License: Apache-2.0 URL: https://github.com/certbot/certbot Source: https://files.pythonhosted.org/packages/source/c/certbot-dns-route53/certbot-dns-route53-%{version}.tar.gz -BuildRequires: %{python_module acme >= 0.25.0} +BuildRequires: %{python_module acme >= 0.29.0} BuildRequires: %{python_module boto3} -BuildRequires: %{python_module certbot >= 0.34.0} +BuildRequires: %{python_module certbot >= 1.0.0} BuildRequires: %{python_module mock} +BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} BuildRequires: %{python_module zope.interface} BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: python-acme >= 0.29.0 Requires: python-boto3 -Requires: python-certbot >= 0.34.0 +Requires: python-certbot >= 1.0.0 Requires: python-zope.interface BuildArch: noarch %python_subpackages ++++++ certbot-dns-route53-0.40.1.tar.gz -> certbot-dns-route53-1.0.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/MANIFEST.in new/certbot-dns-route53-1.0.0/MANIFEST.in --- old/certbot-dns-route53-0.40.1/MANIFEST.in 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/MANIFEST.in 2019-12-03 18:20:30.000000000 +0100 @@ -1,3 +1,6 @@ include LICENSE.txt include README recursive-include docs * +recursive-include tests * +global-exclude __pycache__ +global-exclude *.py[cod] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/PKG-INFO new/certbot-dns-route53-1.0.0/PKG-INFO --- old/certbot-dns-route53-0.40.1/PKG-INFO 2019-11-06 03:25:26.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/PKG-INFO 2019-12-03 18:21:17.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: certbot-dns-route53 -Version: 0.40.1 +Version: 1.0.0 Summary: Route53 DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53/_internal/__init__.py new/certbot-dns-route53-1.0.0/certbot_dns_route53/_internal/__init__.py --- old/certbot-dns-route53-0.40.1/certbot_dns_route53/_internal/__init__.py 1970-01-01 01:00:00.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53/_internal/__init__.py 2019-12-03 18:20:30.000000000 +0100 @@ -0,0 +1 @@ +"""Internal implementation of `~certbot_dns_route53.dns_route53` plugin.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53/_internal/dns_route53.py new/certbot-dns-route53-1.0.0/certbot_dns_route53/_internal/dns_route53.py --- old/certbot-dns-route53-0.40.1/certbot_dns_route53/_internal/dns_route53.py 1970-01-01 01:00:00.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53/_internal/dns_route53.py 2019-12-03 18:20:30.000000000 +0100 @@ -0,0 +1,151 @@ +"""Certbot Route53 authenticator plugin.""" +import collections +import logging +import time + +import boto3 +import zope.interface +from botocore.exceptions import NoCredentialsError, ClientError + +from certbot import errors +from certbot import interfaces +from certbot.plugins import dns_common + +from acme.magic_typing import DefaultDict, List, Dict # pylint: disable=unused-import, no-name-in-module + +logger = logging.getLogger(__name__) + +INSTRUCTIONS = ( + "To use certbot-dns-route53, configure credentials as described at " + "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " # pylint: disable=line-too-long + "and add the necessary permissions for Route53 access.") + +@zope.interface.implementer(interfaces.IAuthenticator) +@zope.interface.provider(interfaces.IPluginFactory) +class Authenticator(dns_common.DNSAuthenticator): + """Route53 Authenticator + + This authenticator solves a DNS01 challenge by uploading the answer to AWS + Route53. + """ + + description = ("Obtain certificates using a DNS TXT record (if you are using AWS Route53 for " + "DNS).") + ttl = 10 + + def __init__(self, *args, **kwargs): + super(Authenticator, self).__init__(*args, **kwargs) + self.r53 = boto3.client("route53") + self._resource_records = collections.defaultdict(list) # type: DefaultDict[str, List[Dict[str, str]]] + + def more_info(self): # pylint: disable=missing-docstring,no-self-use + return "Solve a DNS01 challenge using AWS Route53" + + def _setup_credentials(self): + pass + + def _perform(self, domain, validation_name, validation): # pylint: disable=missing-docstring + pass + + def perform(self, achalls): + self._attempt_cleanup = True + + try: + change_ids = [ + self._change_txt_record("UPSERT", + achall.validation_domain_name(achall.domain), + achall.validation(achall.account_key)) + for achall in achalls + ] + + for change_id in change_ids: + self._wait_for_change(change_id) + except (NoCredentialsError, ClientError) as e: + logger.debug('Encountered error during perform: %s', e, exc_info=True) + raise errors.PluginError("\n".join([str(e), INSTRUCTIONS])) + return [achall.response(achall.account_key) for achall in achalls] + + def _cleanup(self, domain, validation_name, validation): + try: + self._change_txt_record("DELETE", validation_name, validation) + except (NoCredentialsError, ClientError) as e: + logger.debug('Encountered error during cleanup: %s', e, exc_info=True) + + def _find_zone_id_for_domain(self, domain): + """Find the zone id responsible a given FQDN. + + That is, the id for the zone whose name is the longest parent of the + domain. + """ + paginator = self.r53.get_paginator("list_hosted_zones") + zones = [] + target_labels = domain.rstrip(".").split(".") + for page in paginator.paginate(): + for zone in page["HostedZones"]: + if zone["Config"]["PrivateZone"]: + continue + + candidate_labels = zone["Name"].rstrip(".").split(".") + if candidate_labels == target_labels[-len(candidate_labels):]: + zones.append((zone["Name"], zone["Id"])) + + if not zones: + raise errors.PluginError( + "Unable to find a Route53 hosted zone for {0}".format(domain) + ) + + # Order the zones that are suffixes for our desired to domain by + # length, this puts them in an order like: + # ["foo.bar.baz.com", "bar.baz.com", "baz.com", "com"] + # And then we choose the first one, which will be the most specific. + zones.sort(key=lambda z: len(z[0]), reverse=True) + return zones[0][1] + + def _change_txt_record(self, action, validation_domain_name, validation): + zone_id = self._find_zone_id_for_domain(validation_domain_name) + + rrecords = self._resource_records[validation_domain_name] + challenge = {"Value": '"{0}"'.format(validation)} + if action == "DELETE": + # Remove the record being deleted from the list of tracked records + rrecords.remove(challenge) + if rrecords: + # Need to update instead, as we're not deleting the rrset + action = "UPSERT" + else: + # Create a new list containing the record to use with DELETE + rrecords = [challenge] + else: + rrecords.append(challenge) + + response = self.r53.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "certbot-dns-route53 certificate validation " + action, + "Changes": [ + { + "Action": action, + "ResourceRecordSet": { + "Name": validation_domain_name, + "Type": "TXT", + "TTL": self.ttl, + "ResourceRecords": rrecords, + } + } + ] + } + ) + return response["ChangeInfo"]["Id"] + + def _wait_for_change(self, change_id): + """Wait for a change to be propagated to all Route53 DNS servers. + https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html + """ + for unused_n in range(0, 120): + response = self.r53.get_change(Id=change_id) + if response["ChangeInfo"]["Status"] == "INSYNC": + return + time.sleep(5) + raise errors.PluginError( + "Timed out waiting for Route53 change. Current status: %s" % + response["ChangeInfo"]["Status"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53/authenticator.py new/certbot-dns-route53-1.0.0/certbot_dns_route53/authenticator.py --- old/certbot-dns-route53-0.40.1/certbot_dns_route53/authenticator.py 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53/authenticator.py 2019-12-03 18:20:30.000000000 +0100 @@ -1,16 +1,17 @@ -"""Shim around `~certbot_dns_route53.dns_route53` for backwards compatibility.""" +"""Shim around `~certbot_dns_route53._internal.dns_route53` for backwards compatibility.""" import warnings import zope.interface from certbot import interfaces -from certbot_dns_route53 import dns_route53 +from certbot_dns_route53._internal import dns_route53 @zope.interface.implementer(interfaces.IAuthenticator) @zope.interface.provider(interfaces.IPluginFactory) class Authenticator(dns_route53.Authenticator): - """Shim around `~certbot_dns_route53.dns_route53.Authenticator` for backwards compatibility.""" + """Shim around `~certbot_dns_route53._internal.dns_route53.Authenticator` + for backwards compatibility.""" hidden = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53/dns_route53.py new/certbot-dns-route53-1.0.0/certbot_dns_route53/dns_route53.py --- old/certbot-dns-route53-0.40.1/certbot_dns_route53/dns_route53.py 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53/dns_route53.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,151 +0,0 @@ -"""Certbot Route53 authenticator plugin.""" -import collections -import logging -import time - -import boto3 -import zope.interface -from botocore.exceptions import NoCredentialsError, ClientError - -from certbot import errors -from certbot import interfaces -from certbot.plugins import dns_common - -from acme.magic_typing import DefaultDict, List, Dict # pylint: disable=unused-import, no-name-in-module - -logger = logging.getLogger(__name__) - -INSTRUCTIONS = ( - "To use certbot-dns-route53, configure credentials as described at " - "https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials " # pylint: disable=line-too-long - "and add the necessary permissions for Route53 access.") - -@zope.interface.implementer(interfaces.IAuthenticator) -@zope.interface.provider(interfaces.IPluginFactory) -class Authenticator(dns_common.DNSAuthenticator): - """Route53 Authenticator - - This authenticator solves a DNS01 challenge by uploading the answer to AWS - Route53. - """ - - description = ("Obtain certificates using a DNS TXT record (if you are using AWS Route53 for " - "DNS).") - ttl = 10 - - def __init__(self, *args, **kwargs): - super(Authenticator, self).__init__(*args, **kwargs) - self.r53 = boto3.client("route53") - self._resource_records = collections.defaultdict(list) # type: DefaultDict[str, List[Dict[str, str]]] - - def more_info(self): # pylint: disable=missing-docstring,no-self-use - return "Solve a DNS01 challenge using AWS Route53" - - def _setup_credentials(self): - pass - - def _perform(self, domain, validation_name, validation): # pylint: disable=missing-docstring - pass - - def perform(self, achalls): - self._attempt_cleanup = True - - try: - change_ids = [ - self._change_txt_record("UPSERT", - achall.validation_domain_name(achall.domain), - achall.validation(achall.account_key)) - for achall in achalls - ] - - for change_id in change_ids: - self._wait_for_change(change_id) - except (NoCredentialsError, ClientError) as e: - logger.debug('Encountered error during perform: %s', e, exc_info=True) - raise errors.PluginError("\n".join([str(e), INSTRUCTIONS])) - return [achall.response(achall.account_key) for achall in achalls] - - def _cleanup(self, domain, validation_name, validation): - try: - self._change_txt_record("DELETE", validation_name, validation) - except (NoCredentialsError, ClientError) as e: - logger.debug('Encountered error during cleanup: %s', e, exc_info=True) - - def _find_zone_id_for_domain(self, domain): - """Find the zone id responsible a given FQDN. - - That is, the id for the zone whose name is the longest parent of the - domain. - """ - paginator = self.r53.get_paginator("list_hosted_zones") - zones = [] - target_labels = domain.rstrip(".").split(".") - for page in paginator.paginate(): - for zone in page["HostedZones"]: - if zone["Config"]["PrivateZone"]: - continue - - candidate_labels = zone["Name"].rstrip(".").split(".") - if candidate_labels == target_labels[-len(candidate_labels):]: - zones.append((zone["Name"], zone["Id"])) - - if not zones: - raise errors.PluginError( - "Unable to find a Route53 hosted zone for {0}".format(domain) - ) - - # Order the zones that are suffixes for our desired to domain by - # length, this puts them in an order like: - # ["foo.bar.baz.com", "bar.baz.com", "baz.com", "com"] - # And then we choose the first one, which will be the most specific. - zones.sort(key=lambda z: len(z[0]), reverse=True) - return zones[0][1] - - def _change_txt_record(self, action, validation_domain_name, validation): - zone_id = self._find_zone_id_for_domain(validation_domain_name) - - rrecords = self._resource_records[validation_domain_name] - challenge = {"Value": '"{0}"'.format(validation)} - if action == "DELETE": - # Remove the record being deleted from the list of tracked records - rrecords.remove(challenge) - if rrecords: - # Need to update instead, as we're not deleting the rrset - action = "UPSERT" - else: - # Create a new list containing the record to use with DELETE - rrecords = [challenge] - else: - rrecords.append(challenge) - - response = self.r53.change_resource_record_sets( - HostedZoneId=zone_id, - ChangeBatch={ - "Comment": "certbot-dns-route53 certificate validation " + action, - "Changes": [ - { - "Action": action, - "ResourceRecordSet": { - "Name": validation_domain_name, - "Type": "TXT", - "TTL": self.ttl, - "ResourceRecords": rrecords, - } - } - ] - } - ) - return response["ChangeInfo"]["Id"] - - def _wait_for_change(self, change_id): - """Wait for a change to be propagated to all Route53 DNS servers. - https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html - """ - for unused_n in range(0, 120): - response = self.r53.get_change(Id=change_id) - if response["ChangeInfo"]["Status"] == "INSYNC": - return - time.sleep(5) - raise errors.PluginError( - "Timed out waiting for Route53 change. Current status: %s" % - response["ChangeInfo"]["Status"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53/dns_route53_test.py new/certbot-dns-route53-1.0.0/certbot_dns_route53/dns_route53_test.py --- old/certbot-dns-route53-0.40.1/certbot_dns_route53/dns_route53_test.py 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53/dns_route53_test.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,263 +0,0 @@ -"""Tests for certbot_dns_route53.dns_route53.Authenticator""" - -import unittest - -import mock -from botocore.exceptions import NoCredentialsError, ClientError - -from certbot import errors -from certbot.compat import os -from certbot.plugins import dns_test_common -from certbot.plugins.dns_test_common import DOMAIN - - -class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest): - # pylint: disable=protected-access - - def setUp(self): - from certbot_dns_route53.dns_route53 import Authenticator - - super(AuthenticatorTest, self).setUp() - - self.config = mock.MagicMock() - - # Set up dummy credentials for testing - os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key" - os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key" - - self.auth = Authenticator(self.config, "route53") - - def tearDown(self): - # Remove the dummy credentials from env vars - del os.environ["AWS_ACCESS_KEY_ID"] - del os.environ["AWS_SECRET_ACCESS_KEY"] - super(AuthenticatorTest, self).tearDown() - - def test_perform(self): - self.auth._change_txt_record = mock.MagicMock() - self.auth._wait_for_change = mock.MagicMock() - - self.auth.perform([self.achall]) - - self.auth._change_txt_record.assert_called_once_with("UPSERT", - '_acme-challenge.' + DOMAIN, - mock.ANY) - self.assertEqual(self.auth._wait_for_change.call_count, 1) - - def test_perform_no_credentials_error(self): - self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - def test_perform_client_error(self): - self.auth._change_txt_record = mock.MagicMock( - side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - - self.assertRaises(errors.PluginError, - self.auth.perform, - [self.achall]) - - def test_cleanup(self): - self.auth._attempt_cleanup = True - - self.auth._change_txt_record = mock.MagicMock() - - self.auth.cleanup([self.achall]) - - self.auth._change_txt_record.assert_called_once_with("DELETE", - '_acme-challenge.'+DOMAIN, - mock.ANY) - - def test_cleanup_no_credentials_error(self): - self.auth._attempt_cleanup = True - - self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) - - self.auth.cleanup([self.achall]) - - def test_cleanup_client_error(self): - self.auth._attempt_cleanup = True - - self.auth._change_txt_record = mock.MagicMock( - side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) - - self.auth.cleanup([self.achall]) - - -class ClientTest(unittest.TestCase): - # pylint: disable=protected-access - - PRIVATE_ZONE = { - "Id": "BAD-PRIVATE", - "Name": "example.com", - "Config": { - "PrivateZone": True - } - } - - EXAMPLE_NET_ZONE = { - "Id": "BAD-WRONG-TLD", - "Name": "example.net", - "Config": { - "PrivateZone": False - } - } - - EXAMPLE_COM_ZONE = { - "Id": "EXAMPLE", - "Name": "example.com", - "Config": { - "PrivateZone": False - } - } - - FOO_EXAMPLE_COM_ZONE = { - "Id": "FOO", - "Name": "foo.example.com", - "Config": { - "PrivateZone": False - } - } - - def setUp(self): - from certbot_dns_route53.dns_route53 import Authenticator - - super(ClientTest, self).setUp() - - self.config = mock.MagicMock() - - # Set up dummy credentials for testing - os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key" - os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key" - - self.client = Authenticator(self.config, "route53") - - def tearDown(self): - # Remove the dummy credentials from env vars - del os.environ["AWS_ACCESS_KEY_ID"] - del os.environ["AWS_SECRET_ACCESS_KEY"] - super(ClientTest, self).tearDown() - - def test_find_zone_id_for_domain(self): - self.client.r53.get_paginator = mock.MagicMock() - self.client.r53.get_paginator().paginate.return_value = [ - { - "HostedZones": [ - self.EXAMPLE_NET_ZONE, - self.EXAMPLE_COM_ZONE, - ] - } - ] - - result = self.client._find_zone_id_for_domain("foo.example.com") - self.assertEqual(result, "EXAMPLE") - - def test_find_zone_id_for_domain_pagination(self): - self.client.r53.get_paginator = mock.MagicMock() - self.client.r53.get_paginator().paginate.return_value = [ - { - "HostedZones": [ - self.PRIVATE_ZONE, - self.EXAMPLE_COM_ZONE, - ] - }, - { - "HostedZones": [ - self.PRIVATE_ZONE, - self.FOO_EXAMPLE_COM_ZONE, - ] - } - ] - - result = self.client._find_zone_id_for_domain("foo.example.com") - self.assertEqual(result, "FOO") - - def test_find_zone_id_for_domain_no_results(self): - self.client.r53.get_paginator = mock.MagicMock() - self.client.r53.get_paginator().paginate.return_value = [] - - self.assertRaises(errors.PluginError, - self.client._find_zone_id_for_domain, - "foo.example.com") - - def test_find_zone_id_for_domain_no_correct_results(self): - self.client.r53.get_paginator = mock.MagicMock() - self.client.r53.get_paginator().paginate.return_value = [ - { - "HostedZones": [ - self.PRIVATE_ZONE, - self.EXAMPLE_NET_ZONE, - ] - }, - ] - - self.assertRaises(errors.PluginError, - self.client._find_zone_id_for_domain, - "foo.example.com") - - def test_change_txt_record(self): - self.client._find_zone_id_for_domain = mock.MagicMock() - self.client.r53.change_resource_record_sets = mock.MagicMock( - return_value={"ChangeInfo": {"Id": 1}}) - - self.client._change_txt_record("FOO", DOMAIN, "foo") - - call_count = self.client.r53.change_resource_record_sets.call_count - self.assertEqual(call_count, 1) - - def test_change_txt_record_delete(self): - self.client._find_zone_id_for_domain = mock.MagicMock() - self.client.r53.change_resource_record_sets = mock.MagicMock( - return_value={"ChangeInfo": {"Id": 1}}) - - validation = "some-value" - validation_record = {"Value": '"{0}"'.format(validation)} - self.client._resource_records[DOMAIN] = [validation_record] - - self.client._change_txt_record("DELETE", DOMAIN, validation) - - call_count = self.client.r53.change_resource_record_sets.call_count - self.assertEqual(call_count, 1) - call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1] - call_args_batch = call_args["ChangeBatch"]["Changes"][0] - self.assertEqual(call_args_batch["Action"], "DELETE") - self.assertEqual( - call_args_batch["ResourceRecordSet"]["ResourceRecords"], - [validation_record]) - - def test_change_txt_record_multirecord(self): - self.client._find_zone_id_for_domain = mock.MagicMock() - self.client._get_validation_rrset = mock.MagicMock() - self.client._resource_records[DOMAIN] = [ - {"Value": "\"pre-existing-value\""}, - {"Value": "\"pre-existing-value-two\""}, - ] - self.client.r53.change_resource_record_sets = mock.MagicMock( - return_value={"ChangeInfo": {"Id": 1}}) - - self.client._change_txt_record("DELETE", DOMAIN, "pre-existing-value") - - call_count = self.client.r53.change_resource_record_sets.call_count - call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1] - call_args_batch = call_args["ChangeBatch"]["Changes"][0] - self.assertEqual(call_args_batch["Action"], "UPSERT") - self.assertEqual( - call_args_batch["ResourceRecordSet"]["ResourceRecords"], - [{"Value": "\"pre-existing-value-two\""}]) - - self.assertEqual(call_count, 1) - - def test_wait_for_change(self): - self.client.r53.get_change = mock.MagicMock( - side_effect=[{"ChangeInfo": {"Status": "PENDING"}}, - {"ChangeInfo": {"Status": "INSYNC"}}]) - - self.client._wait_for_change(1) - - self.assertTrue(self.client.r53.get_change.called) - - -if __name__ == "__main__": - unittest.main() # pragma: no cover diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/PKG-INFO new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/PKG-INFO --- old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/PKG-INFO 2019-11-06 03:25:26.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/PKG-INFO 2019-12-03 18:21:17.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: certbot-dns-route53 -Version: 0.40.1 +Version: 1.0.0 Summary: Route53 DNS Authenticator plugin for Certbot Home-page: https://github.com/certbot/certbot Author: Certbot Project diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/SOURCES.txt new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/SOURCES.txt --- old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/SOURCES.txt 2019-11-06 03:25:26.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/SOURCES.txt 2019-12-03 18:21:17.000000000 +0100 @@ -5,19 +5,18 @@ setup.py certbot_dns_route53/__init__.py certbot_dns_route53/authenticator.py -certbot_dns_route53/dns_route53.py -certbot_dns_route53/dns_route53_test.py certbot_dns_route53.egg-info/PKG-INFO certbot_dns_route53.egg-info/SOURCES.txt certbot_dns_route53.egg-info/dependency_links.txt certbot_dns_route53.egg-info/entry_points.txt certbot_dns_route53.egg-info/requires.txt certbot_dns_route53.egg-info/top_level.txt +certbot_dns_route53/_internal/__init__.py +certbot_dns_route53/_internal/dns_route53.py docs/.gitignore docs/Makefile docs/api.rst docs/conf.py docs/index.rst docs/make.bat -docs/api/authenticator.rst -docs/api/dns_route53.rst \ No newline at end of file +tests/dns_route53_test.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/entry_points.txt new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/entry_points.txt --- old/certbot-dns-route53-0.40.1/certbot_dns_route53.egg-info/entry_points.txt 2019-11-06 03:25:26.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/certbot_dns_route53.egg-info/entry_points.txt 2019-12-03 18:21:17.000000000 +0100 @@ -1,4 +1,4 @@ [certbot.plugins] certbot-route53:auth = certbot_dns_route53.authenticator:Authenticator -dns-route53 = certbot_dns_route53.dns_route53:Authenticator +dns-route53 = certbot_dns_route53._internal.dns_route53:Authenticator diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/docs/api/authenticator.rst new/certbot-dns-route53-1.0.0/docs/api/authenticator.rst --- old/certbot-dns-route53-0.40.1/docs/api/authenticator.rst 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/docs/api/authenticator.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -:mod:`certbot_dns_route53.authenticator` ----------------------------------------- - -.. automodule:: certbot_dns_route53.authenticator - :members: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/docs/api/dns_route53.rst new/certbot-dns-route53-1.0.0/docs/api/dns_route53.rst --- old/certbot-dns-route53-0.40.1/docs/api/dns_route53.rst 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/docs/api/dns_route53.rst 1970-01-01 01:00:00.000000000 +0100 @@ -1,5 +0,0 @@ -:mod:`certbot_dns_route53.dns_route53` --------------------------------------- - -.. automodule:: certbot_dns_route53.dns_route53 - :members: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/docs/api.rst new/certbot-dns-route53-1.0.0/docs/api.rst --- old/certbot-dns-route53-0.40.1/docs/api.rst 2019-11-06 03:24:51.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/docs/api.rst 2019-12-03 18:20:30.000000000 +0100 @@ -2,7 +2,4 @@ API Documentation ================= -.. toctree:: - :glob: - - api/** +Certbot plugins implement the Certbot plugins API, and do not otherwise have an external API. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/setup.py new/certbot-dns-route53-1.0.0/setup.py --- old/certbot-dns-route53-0.40.1/setup.py 2019-11-06 03:24:52.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/setup.py 2019-12-03 18:20:32.000000000 +0100 @@ -1,7 +1,9 @@ from setuptools import setup from setuptools import find_packages +from setuptools.command.test import test as TestCommand +import sys -version = '0.40.1' +version = '1.0.0' # Remember to update local-oldest-requirements.txt when changing the minimum # acme/certbot version. @@ -14,6 +16,20 @@ 'zope.interface', ] +class PyTest(TestCommand): + user_options = [] + + def initialize_options(self): + TestCommand.initialize_options(self) + self.pytest_args = '' + + def run_tests(self): + import shlex + # import here, cause outside the eggs aren't loaded + import pytest + errno = pytest.main(shlex.split(self.pytest_args)) + sys.exit(errno) + setup( name='certbot-dns-route53', version=version, @@ -51,9 +67,11 @@ keywords=['certbot', 'route53', 'aws'], entry_points={ 'certbot.plugins': [ - 'dns-route53 = certbot_dns_route53.dns_route53:Authenticator', + 'dns-route53 = certbot_dns_route53._internal.dns_route53:Authenticator', 'certbot-route53:auth = certbot_dns_route53.authenticator:Authenticator' ], }, + tests_require=["pytest"], test_suite='certbot_dns_route53', + cmdclass={"test": PyTest}, ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/certbot-dns-route53-0.40.1/tests/dns_route53_test.py new/certbot-dns-route53-1.0.0/tests/dns_route53_test.py --- old/certbot-dns-route53-0.40.1/tests/dns_route53_test.py 1970-01-01 01:00:00.000000000 +0100 +++ new/certbot-dns-route53-1.0.0/tests/dns_route53_test.py 2019-12-03 18:20:30.000000000 +0100 @@ -0,0 +1,263 @@ +"""Tests for certbot_dns_route53._internal.dns_route53.Authenticator""" + +import unittest + +import mock +from botocore.exceptions import NoCredentialsError, ClientError + +from certbot import errors +from certbot.compat import os +from certbot.plugins import dns_test_common +from certbot.plugins.dns_test_common import DOMAIN + + +class AuthenticatorTest(unittest.TestCase, dns_test_common.BaseAuthenticatorTest): + # pylint: disable=protected-access + + def setUp(self): + from certbot_dns_route53._internal.dns_route53 import Authenticator + + super(AuthenticatorTest, self).setUp() + + self.config = mock.MagicMock() + + # Set up dummy credentials for testing + os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key" + os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key" + + self.auth = Authenticator(self.config, "route53") + + def tearDown(self): + # Remove the dummy credentials from env vars + del os.environ["AWS_ACCESS_KEY_ID"] + del os.environ["AWS_SECRET_ACCESS_KEY"] + super(AuthenticatorTest, self).tearDown() + + def test_perform(self): + self.auth._change_txt_record = mock.MagicMock() + self.auth._wait_for_change = mock.MagicMock() + + self.auth.perform([self.achall]) + + self.auth._change_txt_record.assert_called_once_with("UPSERT", + '_acme-challenge.' + DOMAIN, + mock.ANY) + self.assertEqual(self.auth._wait_for_change.call_count, 1) + + def test_perform_no_credentials_error(self): + self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) + + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_perform_client_error(self): + self.auth._change_txt_record = mock.MagicMock( + side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) + + self.assertRaises(errors.PluginError, + self.auth.perform, + [self.achall]) + + def test_cleanup(self): + self.auth._attempt_cleanup = True + + self.auth._change_txt_record = mock.MagicMock() + + self.auth.cleanup([self.achall]) + + self.auth._change_txt_record.assert_called_once_with("DELETE", + '_acme-challenge.'+DOMAIN, + mock.ANY) + + def test_cleanup_no_credentials_error(self): + self.auth._attempt_cleanup = True + + self.auth._change_txt_record = mock.MagicMock(side_effect=NoCredentialsError) + + self.auth.cleanup([self.achall]) + + def test_cleanup_client_error(self): + self.auth._attempt_cleanup = True + + self.auth._change_txt_record = mock.MagicMock( + side_effect=ClientError({"Error": {"Code": "foo"}}, "bar")) + + self.auth.cleanup([self.achall]) + + +class ClientTest(unittest.TestCase): + # pylint: disable=protected-access + + PRIVATE_ZONE = { + "Id": "BAD-PRIVATE", + "Name": "example.com", + "Config": { + "PrivateZone": True + } + } + + EXAMPLE_NET_ZONE = { + "Id": "BAD-WRONG-TLD", + "Name": "example.net", + "Config": { + "PrivateZone": False + } + } + + EXAMPLE_COM_ZONE = { + "Id": "EXAMPLE", + "Name": "example.com", + "Config": { + "PrivateZone": False + } + } + + FOO_EXAMPLE_COM_ZONE = { + "Id": "FOO", + "Name": "foo.example.com", + "Config": { + "PrivateZone": False + } + } + + def setUp(self): + from certbot_dns_route53._internal.dns_route53 import Authenticator + + super(ClientTest, self).setUp() + + self.config = mock.MagicMock() + + # Set up dummy credentials for testing + os.environ["AWS_ACCESS_KEY_ID"] = "dummy_access_key" + os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret_access_key" + + self.client = Authenticator(self.config, "route53") + + def tearDown(self): + # Remove the dummy credentials from env vars + del os.environ["AWS_ACCESS_KEY_ID"] + del os.environ["AWS_SECRET_ACCESS_KEY"] + super(ClientTest, self).tearDown() + + def test_find_zone_id_for_domain(self): + self.client.r53.get_paginator = mock.MagicMock() + self.client.r53.get_paginator().paginate.return_value = [ + { + "HostedZones": [ + self.EXAMPLE_NET_ZONE, + self.EXAMPLE_COM_ZONE, + ] + } + ] + + result = self.client._find_zone_id_for_domain("foo.example.com") + self.assertEqual(result, "EXAMPLE") + + def test_find_zone_id_for_domain_pagination(self): + self.client.r53.get_paginator = mock.MagicMock() + self.client.r53.get_paginator().paginate.return_value = [ + { + "HostedZones": [ + self.PRIVATE_ZONE, + self.EXAMPLE_COM_ZONE, + ] + }, + { + "HostedZones": [ + self.PRIVATE_ZONE, + self.FOO_EXAMPLE_COM_ZONE, + ] + } + ] + + result = self.client._find_zone_id_for_domain("foo.example.com") + self.assertEqual(result, "FOO") + + def test_find_zone_id_for_domain_no_results(self): + self.client.r53.get_paginator = mock.MagicMock() + self.client.r53.get_paginator().paginate.return_value = [] + + self.assertRaises(errors.PluginError, + self.client._find_zone_id_for_domain, + "foo.example.com") + + def test_find_zone_id_for_domain_no_correct_results(self): + self.client.r53.get_paginator = mock.MagicMock() + self.client.r53.get_paginator().paginate.return_value = [ + { + "HostedZones": [ + self.PRIVATE_ZONE, + self.EXAMPLE_NET_ZONE, + ] + }, + ] + + self.assertRaises(errors.PluginError, + self.client._find_zone_id_for_domain, + "foo.example.com") + + def test_change_txt_record(self): + self.client._find_zone_id_for_domain = mock.MagicMock() + self.client.r53.change_resource_record_sets = mock.MagicMock( + return_value={"ChangeInfo": {"Id": 1}}) + + self.client._change_txt_record("FOO", DOMAIN, "foo") + + call_count = self.client.r53.change_resource_record_sets.call_count + self.assertEqual(call_count, 1) + + def test_change_txt_record_delete(self): + self.client._find_zone_id_for_domain = mock.MagicMock() + self.client.r53.change_resource_record_sets = mock.MagicMock( + return_value={"ChangeInfo": {"Id": 1}}) + + validation = "some-value" + validation_record = {"Value": '"{0}"'.format(validation)} + self.client._resource_records[DOMAIN] = [validation_record] + + self.client._change_txt_record("DELETE", DOMAIN, validation) + + call_count = self.client.r53.change_resource_record_sets.call_count + self.assertEqual(call_count, 1) + call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1] + call_args_batch = call_args["ChangeBatch"]["Changes"][0] + self.assertEqual(call_args_batch["Action"], "DELETE") + self.assertEqual( + call_args_batch["ResourceRecordSet"]["ResourceRecords"], + [validation_record]) + + def test_change_txt_record_multirecord(self): + self.client._find_zone_id_for_domain = mock.MagicMock() + self.client._get_validation_rrset = mock.MagicMock() + self.client._resource_records[DOMAIN] = [ + {"Value": "\"pre-existing-value\""}, + {"Value": "\"pre-existing-value-two\""}, + ] + self.client.r53.change_resource_record_sets = mock.MagicMock( + return_value={"ChangeInfo": {"Id": 1}}) + + self.client._change_txt_record("DELETE", DOMAIN, "pre-existing-value") + + call_count = self.client.r53.change_resource_record_sets.call_count + call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1] + call_args_batch = call_args["ChangeBatch"]["Changes"][0] + self.assertEqual(call_args_batch["Action"], "UPSERT") + self.assertEqual( + call_args_batch["ResourceRecordSet"]["ResourceRecords"], + [{"Value": "\"pre-existing-value-two\""}]) + + self.assertEqual(call_count, 1) + + def test_wait_for_change(self): + self.client.r53.get_change = mock.MagicMock( + side_effect=[{"ChangeInfo": {"Status": "PENDING"}}, + {"ChangeInfo": {"Status": "INSYNC"}}]) + + self.client._wait_for_change(1) + + self.assertTrue(self.client.r53.get_change.called) + + +if __name__ == "__main__": + unittest.main() # pragma: no cover