Repository: AnalogJ/lexicon

Issue:
Route53: Public and Private Zones can't be distinguished.
I've been testing out lexicon for updating DNS records via Route53, and I have a Public and Private Zone with the same domain name.
I noticed that lexicon is only searching for the domain name by name, so in my case, my internal zone was the first created and so it's the only thing lexicon itself finds for my domain name.

I was going to have it update a record for my home IP address for dynamic IP issues, but what's happening is it is only updating the Private zone's record. I've specified --identifier with the ZoneID of the Public Zone, but that is not working either.

I didn't even have a record for home.mydomain.com in my Private Zone, and it ended up creating the record just to fullfill the update. I do see in the output of lexicon both private and public zones, including the <PrivateZone>true|false</PrivateZone> specifically identifying it as private or not.

I'd like to be able to update both, differently as needed.


Discussion:
I have the same problem.

I think the best option would be to implement the "--identifier" option for Route53.

The create then should look like:
`lexicon route53 create www.example.com TXT --identifier="route53 ZoneID" --name="_acme-challenge.www.example.com." --content="challenge token"`

Like the delete example of cloudflare
`lexicon cloudflare delete www.example.com TXT --identifier="cloudflare record id"`

I've made a fork and I hope to have time this week to make a pull request.
@Stollie @erenfro Would the delegated domain work done in #136 help you guys? 
I'm using delegated domain to get "example.staging.example.org" data into the right Hosted Zone.
But r53_client.list_hosted_zones_by_name outputs:

| Domain name | Type | Record set count  | ZoneID|
|-----|----|-----|-----|
| staging.example.org. | Private | 7  | KFAOE |
| staging.example.org. | Public | 7  | ZAIA9 |

And lexicon will pick the first, this Hosted zone cannot be accessed by for example Let’s Encrypt .


Resolving patch:
diff --git a/lexicon/providers/route53.py b/lexicon/providers/route53.py
--- a/lexicon/providers/route53.py
+++ b/lexicon/providers/route53.py
@@ -19,6 +19,7 @@ def ProviderParser(subparser):
     """Specify arguments for AWS Route 53 Lexicon Provider."""
     subparser.add_argument("--auth-access-key", help="specify ACCESS_KEY used to authenticate")
     subparser.add_argument("--auth-access-secret", help="specify ACCESS_SECRET used authenticate")
+    subparser.add_argument("--private-zone", help="indicates what kind of hosted zone to use, if true, use only private zones, if false, use only public zones")
 
     #TODO: these are only required for testing, we should figure out a way to remove them & update the integration tests
     # to dynamically populate the auth credentials that are required.
@@ -82,6 +83,7 @@ def __init__(self, options, engine_overrides=None):
         """Initialize AWS Route 53 DNS provider."""
         super(Provider, self).__init__(options, engine_overrides)
         self.domain_id = None
+        self.private_zone = options.get('private_zone', None)
         # instantiate the client
         self.r53_client = boto3.client(
             'route53',
@@ -89,6 +91,20 @@ def __init__(self, options, engine_overrides=None):
             aws_secret_access_key=self.options.get('auth_access_secret', self.options.get('auth_token'))
         )
 
+    def filter_zone(self, hz):
+        if self.private_zone is not None:
+            if hz['Config']['PrivateZone'] != self.str2bool(self.private_zone):
+                return False
+
+        if hz['Name'] != '{0}.'.format(self.options['domain']):
+            return False
+
+        return True
+
+    @staticmethod
+    def str2bool(input_string):
+        return input_string.lower() in ('true', 'yes')
+
     def authenticate(self):
         """Determine the hosted zone id for the domain."""
         try:
@@ -97,7 +113,7 @@ def authenticate(self):
             ]
             hosted_zone = next(
                 hz for hz in hosted_zones
-                if hz['Name'] == '{0}.'.format(self.options['domain'])
+                if self.filter_zone(hz)
             )
             self.domain_id = hosted_zone['Id']
         except StopIteration:


Test patch:
diff --git a/tests/providers/test_route53.py b/tests/providers/test_route53.py
--- a/tests/providers/test_route53.py
+++ b/tests/providers/test_route53.py
@@ -2,8 +2,7 @@
 import unittest
 import pytest
 from lexicon.providers.route53 import Provider
-from integration_tests import IntegrationTests
-
+from integration_tests import IntegrationTests, provider_vcr
 
 class Route53ProviderTests(unittest.TestCase, IntegrationTests):
     """Route53 Proivder Tests."""
@@ -16,6 +15,22 @@ def _filter_headers(self):
         """Sensitive headers to be filtered."""
         return ['Authorization']
 
+    def test_Provider_authenticate_private_zone_only(self):
+        with provider_vcr.use_cassette(super(Route53ProviderTests, self)._cassette_path('IntegrationTests/test_Provider_authenticate.yaml'), filter_headers=super(Route53ProviderTests, self)._filter_headers(), filter_query_parameters=super(Route53ProviderTests, self)._filter_query_parameters(), filter_post_data_parameters=super(Route53ProviderTests, self)._filter_post_data_parameters()):
+            options = super(Route53ProviderTests, self)._test_options()
+            options['private_zone'] = 'true'
+            provider = self.Provider(options, super(Route53ProviderTests, self)._test_engine_overrides())
+            with pytest.raises(Exception):
+                provider.authenticate()
+
+    def test_Provider_authenticate_private_zone_false(self):
+        with provider_vcr.use_cassette(super(Route53ProviderTests, self)._cassette_path('IntegrationTests/test_Provider_authenticate.yaml'), filter_headers=super(Route53ProviderTests, self)._filter_headers(), filter_query_parameters=super(Route53ProviderTests, self)._filter_query_parameters(), filter_post_data_parameters=super(Route53ProviderTests, self)._filter_post_data_parameters()):
+            options = super(Route53ProviderTests, self)._test_options()
+            options['private_zone'] = 'false'
+            provider = self.Provider(options, super(Route53ProviderTests, self)._test_engine_overrides())
+            provider.authenticate()
+            assert provider.domain_id is not None
+
     @pytest.mark.skip(reason="route 53 dns records don't have ids")
     def test_Provider_when_calling_delete_record_by_identifier_should_remove_record(self):
         return
@@ -23,4 +38,4 @@ def test_Provider_when_calling_delete_record_by_identifier_should_remove_record(
     # TODO: this should be enabled
     @pytest.mark.skip(reason="regenerating auth keys required")
     def test_Provider_when_calling_update_record_should_modify_record_name_specified(self):
-        return
\ No newline at end of file
+        return