Repository: ansible-collections/community.aws

Issue:
Add route53 GeoLocation support
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
Moving [issue](https://github.com/ansible/ansible/issues/24834) from Ansible repository.
Add route53 GeoLocation support

##### ISSUE TYPE
- Feature Idea
Add change_resource_record_sets method to support route53 GeoLocation. 
This functionality is available in [salt](https://docs.saltstack.com/en/develop/ref/states/all/salt.states.boto3_route53.html)

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
Either update/upgrade route53 module, or create a new module to support adding/changing recourd sets.

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Files identified in the description:
* [`plugins/modules/route53.py`](https://github.com/ansible-collections/community.aws/blob/main/plugins/modules/route53.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jillr @jimbydamonk @s-hertel @tremble @wimnat
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @markuman
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/route53.py b/plugins/modules/route53.py
--- a/plugins/modules/route53.py
+++ b/plugins/modules/route53.py
@@ -108,6 +108,30 @@
         latency-based routing
       - Mutually exclusive with I(weight) and I(failover).
     type: str
+  geo_location:
+    description:
+      - Allows to control how Amazon Route 53 responds to DNS queries based on the geographic origin of the query.
+      - Two geolocation resource record sets that specify same geographic location cannot be created.
+      - Non-geolocation resource record sets that have the same values for the Name and Type elements as geolocation
+        resource record sets cannot be created.
+    suboptions:
+      continent_code:
+        description:
+          - The two-letter code for the continent.
+          - Specifying I(continent_code) with either I(country_code) or I(subdivision_code) returns an InvalidInput error.
+        type: str
+      country_code:
+        description:
+          - The two-letter code for a country.
+          - Amazon Route 53 uses the two-letter country codes that are specified in ISO standard 3166-1 alpha-2 .
+        type: str
+      subdivision_code:
+        description:
+          - The two-letter code for a state of the United States.
+          - To specify I(subdivision_code), I(country_code) must be set to C(US).
+        type: str
+    type: dict
+    version_added: 3.3.0
   health_check:
     description:
       - Health check to associate with this record
@@ -166,6 +190,12 @@
       returned: always
       type: str
       sample: PRIMARY
+    geo_location:
+      description: geograpic location based on which Route53 resonds to DNS queries.
+      returned: when configured
+      type: dict
+      sample: { continent_code: "NA", country_code: "US", subdivision_code: "CA" }
+      version_added: 3.3.0
     health_check:
       description: health_check associated with this record.
       returned: always
@@ -350,6 +380,29 @@
       - 0 issue "ca.example.net"
       - 0 issuewild ";"
       - 0 iodef "mailto:security@example.com"
+- name: Create a record with geo_location - country_code
+  community.aws.route53:
+    state: present
+    zone: '{{ zone_one }}'
+    record: 'geo-test.{{ zone_one }}'
+    identifier: "geohost@www"
+    type: A
+    value: 1.1.1.1
+    ttl: 30
+    geo_location:
+      country_code: US
+- name: Create a record with geo_location - subdivision code
+  community.aws.route53:
+    state: present
+    zone: '{{ zone_one }}'
+    record: 'geo-test.{{ zone_one }}'
+    identifier: "geohost@www"
+    type: A
+    value: 1.1.1.1
+    ttl: 30
+    geo_location:
+      country_code: US
+      subdivision_code: TX
 '''
 
 from operator import itemgetter
@@ -495,6 +548,12 @@ def main():
         identifier=dict(type='str'),
         weight=dict(type='int'),
         region=dict(type='str'),
+        geo_location=dict(type='dict',
+                          options=dict(
+                              continent_code=dict(type="str"),
+                              country_code=dict(type="str"),
+                              subdivision_code=dict(type="str")),
+                          required=False),
         health_check=dict(type='str'),
         failover=dict(type='str', choices=['PRIMARY', 'SECONDARY']),
         vpc_id=dict(type='str'),
@@ -518,11 +577,12 @@ def main():
             ('failover', 'region', 'weight'),
             ('alias', 'ttl'),
         ],
-        # failover, region and weight require identifier
+        # failover, region, weight and geo_location require identifier
         required_by=dict(
             failover=('identifier',),
             region=('identifier',),
             weight=('identifier',),
+            geo_location=('identifier'),
         ),
     )
 
@@ -557,6 +617,7 @@ def main():
     vpc_id_in = module.params.get('vpc_id')
     wait_in = module.params.get('wait')
     wait_timeout_in = module.params.get('wait_timeout')
+    geo_location = module.params.get('geo_location')
 
     if zone_in[-1:] != '.':
         zone_in += "."
@@ -567,8 +628,8 @@ def main():
     if command_in == 'create' or command_in == 'delete':
         if alias_in and len(value_in) != 1:
             module.fail_json(msg="parameter 'value' must contain a single dns name for alias records")
-        if (weight_in is None and region_in is None and failover_in is None) and identifier_in is not None:
-            module.fail_json(msg="You have specified identifier which makes sense only if you specify one of: weight, region or failover.")
+        if not any([weight_in, region_in, failover_in, geo_location]) and identifier_in is not None:
+            module.fail_json(msg="You have specified identifier which makes sense only if you specify one of: weight, region, geo_location or failover.")
 
     retry_decorator = AWSRetry.jittered_backoff(
         retries=MAX_AWS_RETRIES,
@@ -604,6 +665,30 @@ def main():
         'HealthCheckId': health_check_in,
         'SetIdentifier': identifier_in,
     })
+
+    if geo_location:
+        continent_code = geo_location.get('continent_code')
+        country_code = geo_location.get('country_code')
+        subdivision_code = geo_location.get('subdivision_code')
+
+        if continent_code and (country_code or subdivision_code):
+            module.fail_json(changed=False, msg='While using geo_location, continent_code is mutually exclusive with country_code and subdivision_code.')
+
+        if not any([continent_code, country_code, subdivision_code]):
+            module.fail_json(changed=False, msg='To use geo_location please specify either continent_code, country_code, or subdivision_code.')
+
+        if geo_location.get('subdivision_code') and geo_location.get('country_code').lower() != 'us':
+            module.fail_json(changed=False, msg='To use subdivision_code, you must specify country_code as US.')
+
+        # Build geo_location suboptions specification
+        resource_record_set['GeoLocation'] = {}
+        if continent_code:
+            resource_record_set['GeoLocation']['ContinentCode'] = continent_code
+        if country_code:
+            resource_record_set['GeoLocation']['CountryCode'] = country_code
+        if subdivision_code:
+            resource_record_set['GeoLocation']['SubdivisionCode'] = subdivision_code
+
     if command_in == 'delete' and aws_record is not None:
         resource_record_set['TTL'] = aws_record.get('TTL')
         if not resource_record_set['ResourceRecords']:


Test patch:
diff --git a/tests/integration/targets/route53/tasks/main.yml b/tests/integration/targets/route53/tasks/main.yml
--- a/tests/integration/targets/route53/tasks/main.yml
+++ b/tests/integration/targets/route53/tasks/main.yml
@@ -635,7 +635,314 @@
         - weighted_record is not failed
         - weighted_record is not changed
 
+#Test Geo Location - Continent Code
+  - name: Create a record with geo_location - continent_code (check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-1.{{ zone_one }}'
+      identifier: "geohost1@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        continent_code: NA
+    check_mode: true
+    register: create_geo_continent_check_mode
+  - assert:
+      that:
+        - create_geo_continent_check_mode is changed
+        - create_geo_continent_check_mode is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_continent_check_mode.resource_actions'
+
+  - name: Create a record with geo_location - continent_code
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-1.{{ zone_one }}'
+      identifier: "geohost1@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        continent_code: NA
+    register: create_geo_continent
+  # Get resulting A record and geo_location parameters are applied
+  - name: 'get Route53 A record information'
+    route53_info:
+      type: A
+      query: record_sets
+      hosted_zone_id: '{{ z1.zone_id }}'
+      start_record_name: 'geo-test-1.{{ zone_one }}'
+      max_items: 1
+    register: result
+
+  - assert:
+      that:
+        - create_geo_continent is changed
+        - create_geo_continent is not failed
+        - '"route53:ChangeResourceRecordSets" in create_geo_continent.resource_actions'
+        - result.ResourceRecordSets[0].GeoLocation.ContinentCode == "NA"
+
+  - name: Create a record with geo_location - continent_code (idempotency)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-1.{{ zone_one }}'
+      identifier: "geohost1@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        continent_code: NA
+    register: create_geo_continent_idem
+  - assert:
+      that:
+        - create_geo_continent_idem is not changed
+        - create_geo_continent_idem is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_continent_idem.resource_actions'
+
+  - name: Create a record with geo_location - continent_code (idempotency - check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-1.{{ zone_one }}'
+      identifier: "geohost1@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        continent_code: NA
+    check_mode: true
+    register: create_geo_continent_idem_check
+
+  - assert:
+      that:
+        - create_geo_continent_idem_check is not changed
+        - create_geo_continent_idem_check is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_continent_idem_check.resource_actions'
+
+#Test Geo Location - Country Code
+  - name: Create a record with geo_location - country_code (check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-2.{{ zone_one }}'
+      identifier: "geohost2@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+    check_mode: true
+    register: create_geo_country_check_mode
+  - assert:
+      that:
+        - create_geo_country_check_mode is changed
+        - create_geo_country_check_mode is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_country_check_mode.resource_actions'
+
+  - name: Create a record with geo_location - country_code
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-2.{{ zone_one }}'
+      identifier: "geohost2@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+    register: create_geo_country
+  # Get resulting A record and geo_location parameters are applied
+  - name: 'get Route53 A record information'
+    route53_info:
+      type: A
+      query: record_sets
+      hosted_zone_id: '{{ z1.zone_id }}'
+      start_record_name: 'geo-test-2.{{ zone_one }}'
+      max_items: 1
+    register: result
+  - assert:
+      that:
+        - create_geo_country is changed
+        - create_geo_country is not failed
+        - '"route53:ChangeResourceRecordSets" in create_geo_country.resource_actions'
+        - result.ResourceRecordSets[0].GeoLocation.CountryCode == "US"
+
+  - name: Create a record with geo_location - country_code (idempotency)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-2.{{ zone_one }}'
+      identifier: "geohost2@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+    register: create_geo_country_idem
+  - assert:
+      that:
+        - create_geo_country_idem is not changed
+        - create_geo_country_idem is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_country_idem.resource_actions'
+
+  - name: Create a record with geo_location - country_code (idempotency - check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-2.{{ zone_one }}'
+      identifier: "geohost2@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+    check_mode: true
+    register: create_geo_country_idem_check
+
+  - assert:
+      that:
+        - create_geo_country_idem_check is not changed
+        - create_geo_country_idem_check is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_country_idem_check.resource_actions'
+
+#Test Geo Location - Subdivision Code
+  - name: Create a record with geo_location - subdivision_code (check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-3.{{ zone_one }}'
+      identifier: "geohost3@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+        subdivision_code: TX
+    check_mode: true
+    register: create_geo_subdivision_check_mode
+  - assert:
+      that:
+        - create_geo_subdivision_check_mode is changed
+        - create_geo_subdivision_check_mode is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_subdivision_check_mode.resource_actions'
+
+  - name: Create a record with geo_location - subdivision_code
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-3.{{ zone_one }}'
+      identifier: "geohost3@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+        subdivision_code: TX
+    register: create_geo_subdivision
+  # Get resulting A record and geo_location parameters are applied
+  - name: 'get Route53 A record information'
+    route53_info:
+      type: A
+      query: record_sets
+      hosted_zone_id: '{{ z1.zone_id }}'
+      start_record_name: 'geo-test-3.{{ zone_one }}'
+      max_items: 1
+    register: result
+  - assert:
+      that:
+        - create_geo_subdivision is changed
+        - create_geo_subdivision is not failed
+        - '"route53:ChangeResourceRecordSets" in create_geo_subdivision.resource_actions'
+        - result.ResourceRecordSets[0].GeoLocation.CountryCode == "US"
+        - result.ResourceRecordSets[0].GeoLocation.SubdivisionCode == "TX"
+
+  - name: Create a record with geo_location - subdivision_code (idempotency)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-3.{{ zone_one }}'
+      identifier: "geohost3@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+        subdivision_code: TX
+    register: create_geo_subdivision_idem
+  - assert:
+      that:
+        - create_geo_subdivision_idem is not changed
+        - create_geo_subdivision_idem is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_subdivision_idem.resource_actions'
+
+  - name: Create a record with geo_location - subdivision_code (idempotency - check_mode)
+    route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'geo-test-3.{{ zone_one }}'
+      identifier: "geohost3@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+        subdivision_code: TX
+    check_mode: true
+    register: create_geo_subdivision_idem_check
+
+  - assert:
+      that:
+        - create_geo_subdivision_idem_check is not changed
+        - create_geo_subdivision_idem_check is not failed
+        - '"route53:ChangeResourceRecordSets" not in create_geo_subdivision_idem_check.resource_actions'
+
+#Cleanup------------------------------------------------------
+
   always:
+
+  - name: delete a record with geo_location - continent_code
+    route53:
+      state: absent
+      zone: '{{ zone_one }}'
+      record: 'geo-test-1.{{ zone_one }}'
+      identifier: "geohost1@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        continent_code: NA
+    ignore_errors: true
+
+  - name: delete a record with geo_location - country_code
+    route53:
+      state: absent
+      zone: '{{ zone_one }}'
+      record: 'geo-test-2.{{ zone_one }}'
+      identifier: "geohost2@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+    ignore_errors: true
+
+  - name: delete a record with geo_location - subdivision_code
+    route53:
+      state: absent
+      zone: '{{ zone_one }}'
+      record: 'geo-test-3.{{ zone_one }}'
+      identifier: "geohost3@www"
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+      geo_location:
+        country_code: US
+        subdivision_code: TX
+    ignore_errors: true
+
   - route53_info:
       query: record_sets
       hosted_zone_id: '{{ z1.zone_id }}'
@@ -737,7 +1044,7 @@
       state: absent
       zone: '{{ zone_one }}'
     register: delete_one
-    ignore_errors: yes
+    ignore_errors: true
     retries: 10
     until: delete_one is not failed
 
@@ -746,7 +1053,7 @@
       state: absent
       zone: '{{ zone_two }}'
     register: delete_two
-    ignore_errors: yes
+    ignore_errors: True
     retries: 10
     until: delete_two is not failed