Repository: ansible-collections/community.aws

Issue:
route53 cannot delete A record set
### Summary

Cannot delete A record without or with TTL value
Providing TTL=null results in
> An error occurred (InvalidInput) when calling the ChangeResourceRecordSets operation: Invalid request: Expected exactly one of [AliasTarget, all of [TTL, and ResourceRecords], or TrafficPolicyInstanceId], but found none in Change with [Action=DELETE, Name=<>., Type=A, SetIdentifier=null]

When not providing TTL, module uses default of 3600
Providing default ttl result in
> An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: [Tried to delete resource record set [name='<>.', type='A'] but the rdata provided is invalid]

Relevant blogs: https://blog.ryanhalliday.com/2019/09/route53-aliastarget-invalid-request.html

### Issue Type

Bug Report

### Component Name

route53

### Ansible Version
```console
$ ansible --version
ansible [core 2.11.3] 
  config file = None
  configured module search path = ['/Users/tiger/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.9/site-packages/ansible
  ansible collection location = /Users/tiger/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.7 (default, Sep  3 2021, 12:37:55) [Clang 12.0.5 (clang-1205.0.22.9)]
  jinja version = 3.0.1
  libyaml = True
```

### Collection Versions
```console
$ ansible-galaxy collection list
lections
Collection                    Version
----------------------------- -------
amazon.aws                    1.5.0  
ansible.netcommon             2.2.0  
ansible.posix                 1.2.0  
ansible.utils                 2.3.0  
ansible.windows               1.7.0  
arista.eos                    2.2.0  
awx.awx                       19.2.2 
azure.azcollection            1.7.0  
check_point.mgmt              2.0.0  
chocolatey.chocolatey         1.1.0  
cisco.aci                     2.0.0  
cisco.asa                     2.0.2  
cisco.intersight              1.0.15 
cisco.ios                     2.3.0  
cisco.iosxr                   2.3.0  
cisco.meraki                  2.4.2  
cisco.mso                     1.2.0  
cisco.nso                     1.0.3  
cisco.nxos                    2.4.0  
cisco.ucs                     1.6.0  
cloudscale_ch.cloud           2.2.0  
community.aws                 1.5.0  
community.azure               1.0.0  
community.crypto              1.7.1  
community.digitalocean        1.7.0  
community.docker              1.8.0  
community.fortios             1.0.0  
community.general             3.3.0  
community.google              1.0.0  
community.grafana             1.2.1  
community.hashi_vault         1.3.0  
community.hrobot              1.1.1  
community.kubernetes          1.2.1  
community.kubevirt            1.0.0  
community.libvirt             1.0.1  
community.mongodb             1.2.1  
community.mysql               2.1.0  
community.network             3.0.0  
community.okd                 1.1.2  
community.postgresql          1.3.0  
community.proxysql            1.0.0  
community.rabbitmq            1.0.3  
community.routeros            1.2.0  
community.skydive             1.0.0  
community.sops                1.1.0  
community.vmware              1.11.0 
community.windows             1.5.0  
community.zabbix              1.3.0  
containers.podman             1.6.1  
cyberark.conjur               1.1.0  
cyberark.pas                  1.0.7  
dellemc.enterprise_sonic      1.1.0  
dellemc.openmanage            3.5.0  
dellemc.os10                  1.1.1  
dellemc.os6                   1.0.7  
dellemc.os9                   1.0.4  
f5networks.f5_modules         1.10.1 
fortinet.fortimanager         2.1.2  
fortinet.fortios              2.1.1  
frr.frr                       1.0.3  
gluster.gluster               1.0.1  
google.cloud                  1.0.2  
hetzner.hcloud                1.4.3  
hpe.nimble                    1.1.3  
ibm.qradar                    1.0.3  
infinidat.infinibox           1.2.4  
inspur.sm                     1.2.0  
junipernetworks.junos         2.3.0  
kubernetes.core               1.2.1  
mellanox.onyx                 1.0.0  
netapp.aws                    21.2.0 
netapp.azure                  21.7.0 
netapp.cloudmanager           21.7.0 
netapp.elementsw              21.6.1 
netapp.ontap                  21.7.0 
netapp.um_info                21.6.0 
netapp_eseries.santricity     1.2.13 
netbox.netbox                 3.1.1  
ngine_io.cloudstack           2.1.0  
ngine_io.exoscale             1.0.0  
ngine_io.vultr                1.1.0  
openstack.cloud               1.5.0  
openvswitch.openvswitch       2.0.0  
ovirt.ovirt                   1.5.3  
purestorage.flasharray        1.8.0  
purestorage.flashblade        1.6.0  
sensu.sensu_go                1.11.1 
servicenow.servicenow         1.0.6  
splunk.es                     1.0.2  
t_systems_mms.icinga_director 1.18.0 
theforeman.foreman            2.1.1  
vyos.vyos                     2.3.1  
wti.remote                    1.0.1  
```

### AWS SDK versions
```console
$ pip show boto boto3 botocore
Name: boto
Version: 2.49.0
Summary: Amazon Web Services Library
Home-page: https://github.com/boto/boto/
Author: Mitch Garnaat
Author-email: mitch@garnaat.com
License: MIT
Location: /usr/local/lib/python3.9/site-packages
Requires: 
Required-by: 
---
Name: boto3
Version: 1.14.43
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: UNKNOWN
License: Apache License 2.0
Location: /usr/local/lib/python3.9/site-packages
Requires: botocore, jmespath, s3transfer
Required-by: 
---
Name: botocore
Version: 1.17.43
Summary: Low-level, data-driven core of boto 3.
Home-page: https://github.com/boto/botocore
Author: Amazon Web Services
Author-email: UNKNOWN
License: Apache License 2.0
Location: /usr/local/lib/python3.9/site-packages
Requires: docutils, jmespath, python-dateutil, urllib3
Required-by: boto3, s3transfer
```

### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

macos

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
  - name: Delete openshift api route53 A record
    community.aws.route53:
      state: absent
      aws_access_key: "{{ aws_access_key_id }}"
      aws_secret_key: "{{ aws_secret_access_key }}"
      zone: "{{ guid }}.{{ aws_dns_zone_root }}"
      record: "{{ records.set.record }}"
      value: "{{ records.set.value }}"
      ttl: null
      type: "{{ records.set.type }}"
```
or leave empty ttl.. results in default ttl 3600 used
```yaml
  - name: Delete openshift api route53 A record
    community.aws.route53:
      state: absent
      aws_access_key: "{{ aws_access_key_id }}"
      aws_secret_key: "{{ aws_secret_access_key }}"
      zone: "{{ guid }}.{{ aws_dns_zone_root }}"
      record: "{{ records.set.record }}"
      value: "{{ records.set.value }}"
      type: "{{ records.set.type }}"
```


### Expected Results

A record is deleted without TTL errors

### Actual Results

ttl = null
```console (paste below)
The full traceback is:
Traceback (most recent call last):
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_c4b6fh52/ansible_community.aws.route53_payload.zip/ansible_collections/community/aws/plugins/modules/route53.py", line 652, in main
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_c4b6fh52/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/core.py", line 288, in deciding_wrapper
    return retrying_wrapper(*args, **kwargs)
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_c4b6fh52/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/cloud.py", line 154, in retry_func
    raise e
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_c4b6fh52/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/cloud.py", line 144, in retry_func
    return f(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/botocore/client.py", line 316, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.9/site-packages/botocore/client.py", line 635, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.InvalidInput: An error occurred (InvalidInput) when calling the ChangeResourceRecordSets operation: Invalid request: Expected exactly one of [AliasTarget, all of [TTL, and ResourceRecords], or TrafficPolicyInstanceId], but found none in Change with [Action=DELETE, Name=<>., Type=A, SetIdentifier=null]
fatal: [localhost]: FAILED! => {
    "boto3_version": "1.14.43",
    "botocore_version": "1.17.43",
    "changed": false,
    "error": {
        "code": "InvalidInput",
        "message": "Invalid request: Expected exactly one of [AliasTarget, all of [TTL, and ResourceRecords], or TrafficPolicyInstanceId], but found none in Change with [Action=DELETE, Name=<>., Type=A, SetIdentifier=null]",
        "type": "Sender"
    },
    "invocation": {
        "module_args": {
            "alias": null,
            "alias_evaluate_target_health": false,
            "alias_hosted_zone_id": null,
            "aws_access_key": "<>",
            "aws_ca_bundle": null,
            "aws_config": null,
            "aws_secret_key": "<>",
            "debug_botocore_endpoint_logs": false,
            "ec2_url": null,
            "failover": null,
            "health_check": null,
            "hosted_zone_id": null,
            "identifier": null,
            "overwrite": null,
            "private_zone": false,
            "profile": null,
            "record": "<>.",
            "region": null,
            "retry_interval": 500,
            "security_token": null,
            "state": "absent",
            "ttl": null,
            "type": "A",
            "validate_certs": true,
            "value": [
                "<>"
            ],
            "vpc_id": null,
            "wait": false,
            "wait_timeout": 300,
            "weight": null,
            "zone": "<>"
        }
    },
    "msg": "Failed to update records: An error occurred (InvalidInput) when calling the ChangeResourceRecordSets operation: Invalid request: Expected exactly one of [AliasTarget, all of [TTL, and ResourceRecords], or TrafficPolicyInstanceId], but found none in Change with [Action=DELETE, Name=<>., Type=A, SetIdentifier=null]",
    "response_metadata": {
        "http_headers": {
            "connection": "close",
            "content-length": "507",
            "content-type": "text/xml",
            "date": "Fri, 12 Nov 2021 14:41:35 GMT",
            "x-amzn-requestid": <>
        },
        "http_status_code": 400,
        "request_id": <>,
        "retry_attempts": 0
    }
}
```

ttl left to default
```yaml
The full traceback is:
Traceback (most recent call last):
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_tvoqyvr_/ansible_community.aws.route53_payload.zip/ansible_collections/community/aws/plugins/modules/route53.py", line 652, in main
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_tvoqyvr_/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/core.py", line 288, in deciding_wrapper
    return retrying_wrapper(*args, **kwargs)
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_tvoqyvr_/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/cloud.py", line 154, in retry_func
    raise e
  File "/var/folders/62/wkysd_4n0w57ljl9ycfsd9cc0000gn/T/ansible_community.aws.route53_payload_tvoqyvr_/ansible_community.aws.route53_payload.zip/ansible_collections/amazon/aws/plugins/module_utils/cloud.py", line 144, in retry_func
    return f(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/botocore/client.py", line 316, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/usr/local/lib/python3.9/site-packages/botocore/client.py", line 635, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.InvalidChangeBatch: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: [Tried to delete resource record set [name='<>.', type='A'] but the rdata provided is invalid]
fatal: [localhost]: FAILED! => {
    "boto3_version": "1.14.43",
    "botocore_version": "1.17.43",
    "changed": false,
    "error": {
        "code": "InvalidChangeBatch",
        "message": "[Tried to delete resource record set [name='<>.', type='A'] but the rdata provided is invalid]",
        "type": "Sender"
    },
    "invocation": {
        "module_args": {
            "alias": null,
            "alias_evaluate_target_health": false,
            "alias_hosted_zone_id": null,
            "aws_access_key": "<>",
            "aws_ca_bundle": null,
            "aws_config": null,
            "aws_secret_key": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "debug_botocore_endpoint_logs": false,
            "ec2_url": null,
            "failover": null,
            "health_check": null,
            "hosted_zone_id": null,
            "identifier": null,
            "overwrite": null,
            "private_zone": false,
            "profile": null,
            "record": "<>.",
            "region": null,
            "retry_interval": 500,
            "security_token": null,
            "state": "absent",
            "ttl": 3600,
            "type": "A",
            "validate_certs": true,
            "value": [
                "<>."
            ],
            "vpc_id": null,
            "wait": false,
            "wait_timeout": 300,
            "weight": null,
            "zone": "<>"
        }
    },
    "msg": "Failed to update records: An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: [Tried to delete resource record set [name='<>.', type='A'] but the rdata provided is invalid]",
    "response_metadata": {
        "http_headers": {
            "connection": "close",
            "content-length": "405",
            "content-type": "text/xml",
            "date": "Fri, 12 Nov 2021 14:55:42 GMT",
            "x-amzn-requestid": "<>"
        },
        "http_status_code": 400,
        "request_id": "<>",
        "retry_attempts": 0
    }
}
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Solution would be to make TTL nullable.. and if null.. don't provide TTL in the request.
I can confirm.   
Basically when deleting records, the name and type are sufficient. Everything else does not matter. That's how `community.dns` is handling it imo.

However, just leaving it out makes botocore fail

```
 "msg": "Failed to update records: An error occurred (InvalidInput) when calling the ChangeResourceRecordSets operation: Invalid request: Expected exactly one of [AliasTarget, all of [TTL, and ResourceRecords], or TrafficPolicyInstanceId], but found none in Change with [Action=DELETE, Name=ansible.xn--mitlinuxwrdasnichtpassiert-ohc.de., Type=A, SetIdentifier=null]",
```

So we must keep the TTL of the existing record.  
This is kind of a hotfix. But I need to make some more tests later.

```diff
diff --git a/plugins/modules/route53.py b/plugins/modules/route53.py
index 9640202..e971e18 100644
--- a/plugins/modules/route53.py
+++ b/plugins/modules/route53.py
@@ -596,17 +596,29 @@ def main():
 
     aws_record = get_record(route53, zone_id, record_in, type_in, identifier_in)
 
-    resource_record_set = scrub_none_parameters({
-        'Name': record_in,
-        'Type': type_in,
-        'Weight': weight_in,
-        'Region': region_in,
-        'Failover': failover_in,
-        'TTL': ttl_in,
-        'ResourceRecords': [dict(Value=value) for value in value_in],
-        'HealthCheckId': health_check_in,
-        'SetIdentifier': identifier_in,
-    })
+    if command_in is not 'delete':
+        resource_record_set = scrub_none_parameters({
+            'Name': record_in,
+            'Type': type_in,
+            'Weight': weight_in,
+            'Region': region_in,
+            'Failover': failover_in,
+            'TTL': ttl_in,
+            'ResourceRecords': [dict(Value=value) for value in value_in],
+            'HealthCheckId': health_check_in,
+            'SetIdentifier': identifier_in,
+        })
+    else:
+        resource_record_set = scrub_none_parameters({
+            'Name': record_in,
+            'Type': type_in,
+            'Weight': weight_in,
+            'Region': region_in,
+            'Failover': failover_in,
+            'ResourceRecords': [dict(Value=value) for value in value_in],
+            'HealthCheckId': health_check_in,
+            'TTL': aws_record.get('TTL')
+        })
 
     if alias_in:
         resource_record_set['AliasTarget'] = dict(
```

Resolving patch:
diff --git a/plugins/modules/route53.py b/plugins/modules/route53.py
--- a/plugins/modules/route53.py
+++ b/plugins/modules/route53.py
@@ -69,7 +69,6 @@
   value:
     description:
       - The new value when creating a DNS record.  YAML lists or multiple comma-spaced values are allowed for non-alias records.
-      - When deleting a record all values for the record must be specified or Route 53 will not delete it.
     type: list
     elements: str
   overwrite:
@@ -513,8 +512,6 @@ def main():
         required_if=(
             ('state', 'present', ['value']),
             ('state', 'create', ['value']),
-            ('state', 'absent', ['value']),
-            ('state', 'delete', ['value']),
         ),
         # failover, region and weight are mutually exclusive
         mutually_exclusive=[
@@ -607,6 +604,10 @@ def main():
         'HealthCheckId': health_check_in,
         'SetIdentifier': identifier_in,
     })
+    if command_in == 'delete' and aws_record is not None:
+        resource_record_set['TTL'] = aws_record.get('TTL')
+        if not resource_record_set['ResourceRecords']:
+            resource_record_set['ResourceRecords'] = aws_record.get('ResourceRecords')
 
     if alias_in:
         resource_record_set['AliasTarget'] = dict(


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
@@ -387,6 +387,45 @@
         - wc_a_record is not failed
         - wc_a_record is changed
 
+  - name: create a record with different TTL
+    community.aws.route53:
+      state: present
+      zone: '{{ zone_one }}'
+      record: 'localhost.{{ zone_one }}'
+      type: A
+      value: 127.0.0.1
+      ttl: 30
+    register: ttl30
+  - name: check return values
+    assert:
+      that:
+        - ttl30.diff.resource_record_sets[0].ttl == 30
+        - ttl30 is changed
+
+  - name: delete previous record without mention ttl and value
+    community.aws.route53:
+      state: absent
+      zone: '{{ zone_one }}'
+      record: 'localhost.{{ zone_one }}'
+      type: A
+    register: ttl30
+  - name: check if record is deleted
+    assert:
+      that:
+        - ttl30 is changed
+
+  - name: immutable delete previous record without mention ttl and value
+    community.aws.route53:
+      state: absent
+      zone: '{{ zone_one }}'
+      record: 'localhost.{{ zone_one }}'
+      type: A
+    register: ttl30
+  - name: check if record was deleted
+    assert:
+      that:
+        - ttl30 is not changed
+
   # Tests on zone two (private zone)
   - name: Create A record using zone fqdn
     route53: