Repository: ansible-collections/amazon.aws

Issue:
Regression: cloudwatchevent_rule doesn't properly support JSON input_templates
### Summary

I reported this same bug over in the [community.aws collection](https://github.com/ansible-collections/community.aws/issues/1348) last year.  It seems to have returned [in this code](https://github.com/ansible-collections/amazon.aws/blob/main/plugins/modules/cloudwatchevent_rule.py#L444).

Please see the [previous issue report](https://github.com/ansible-collections/community.aws/issues/1348) for a detailed reproduction example.

### Issue Type

Bug Report

### Component Name

cloudwatchevent_rule

### Ansible Version

```console (paste below)
$ ansible --version

```


### Collection Versions

```console (paste below)
$ ansible-galaxy collection list
```


### AWS SDK versions

```console (paste below)
$ pip show boto boto3 botocore
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed

```


### OS / Environment

_No response_

### Steps to Reproduce


Here is a working example using the AWS CLI:

```
eventbridge_target.json
{
   "Rule": "myRule",
   "Targets": [{
      ...
      "InputTransformer": {
         "InputPathsMap": {
            "s3Key": "$.detail.object.key"
         },
         "InputTemplate": "{\"containerOverrides\": [{\"name\": \"thrivSportsJob-stage_jobmg10\", \"environment\": [{\"name\": \"S3_KEY\",\"value\": <s3Key>}]}]}"
      }
   }]
}
```

Which can be executed with a `aws events put-targets --cli-input-json file://eventbridge_target.json`

However, when parsing the target configuration, the input is wrapped in a string:
```
                target_request['InputTransformer']['InputTemplate'] = '"{0}"'.format(
                    target['input_template']
                )
```

AWS will not route an event with an invalid transformer template. So the input for a JSON template must begin with a `{`, not a `"`. My [solution](https://github.com/alalonde/community.aws/commit/33e57f964687c09e81de238f936fc5b63f95bd76) was to remove the string wrapping, AND prepend the template with a space, as [recommended here](https://stackoverflow.com/a/32014283/331791). This results in:

```yaml
      -  name: Define trigger input template
         set_fact:
            trigger_input_template: |
               {
                  "containerOverrides": [{
                     "name": "{{container_name}}",
                     "environment": [{
                        "name": "S3_KEY",
                        "value": <s3Key>
                     }]
                  }]
               }
      -  name: Create rule in EventBridge
         community.aws.cloudwatchevent_rule:
            ...
            targets:
               -  id: "myJob-{{env}}"
                  ...
                  input_paths_map:
                     s3Key: "$.detail.object.key"
                  input_template: " {{trigger_input_template}}"
                  ...
```

### Expected Results

I expected the input template to be wrapped by `{ ... }` but was instead `"{ ... }"`

![image](https://user-images.githubusercontent.com/1402582/178361885-d1ad3ff8-3071-4316-b6c0-822e629a07c9.png)



### Actual Results

```console (paste below)

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:

Resolving patch:
diff --git a/plugins/modules/cloudwatchevent_rule.py b/plugins/modules/cloudwatchevent_rule.py
--- a/plugins/modules/cloudwatchevent_rule.py
+++ b/plugins/modules/cloudwatchevent_rule.py
@@ -204,6 +204,14 @@ def _format_json(json_string):
         return str(json.dumps(json_string))
 
 
+def _validate_json(s):
+    try:
+        json.loads(s)
+        return True
+    except json.JSONDecodeError:
+        return False
+
+
 class CloudWatchEventRule:
     def __init__(
         self, module, name, client, schedule_expression=None, event_pattern=None, description=None, role_arn=None
@@ -228,7 +236,7 @@ def describe(self):
             botocore.exceptions.ClientError,
         ) as e:  # pylint: disable=duplicate-except
             self.module.fail_json_aws(e, msg=f"Could not describe rule {self.name}")
-        return self._snakify(rule_info)
+        return camel_dict_to_snake_dict(rule_info)
 
     def put(self, enabled=True):
         """Creates or updates the rule in AWS"""
@@ -291,7 +299,7 @@ def list_targets(self):
             botocore.exceptions.ClientError,
         ) as e:  # pylint: disable=duplicate-except
             self.module.fail_json_aws(e, msg=f"Could not find target for rule {self.name}")
-        return self._snakify(targets)["targets"]
+        return camel_dict_to_snake_dict(targets)["targets"]
 
     def put_targets(self, targets):
         """Creates or updates the provided targets on the rule in AWS"""
@@ -342,10 +350,6 @@ def _targets_request(self, targets):
             targets_request.append(target_request)
         return targets_request
 
-    def _snakify(self, dict):
-        """Converts camel case to snake case"""
-        return camel_dict_to_snake_dict(dict)
-
 
 class CloudWatchEventRuleManager:
     RULE_FIELDS = ["name", "event_pattern", "schedule_expression", "description", "role_arn"]
@@ -441,11 +445,18 @@ def _targets_to_put(self):
                 # The remote_targets contain quotes, so add
                 # quotes to temp
                 val = t["input_transformer"]["input_template"]
-                t["input_transformer"]["input_template"] = '"' + val + '"'
+                # list_targets_by_rule return input_template as string
+                # if existing value is string "<instance> is in state <state>", it returns '"<instance> is in state <state>"'
+                # if existing value is <JSON>, it returns '<JSON>'
+                # therefore add quotes to provided input_template value only if it is not a JSON
+                valid_json = _validate_json(val)
+                if not valid_json:
+                    t["input_transformer"]["input_template"] = '"' + val + '"'
             temp.append(scrub_none_parameters(t))
         self.targets = temp
-
-        return [t for t in self.targets if t not in remote_targets]
+        # remote_targets is snakified output of client.list_targets_by_rule()
+        # therefore snakified version of t should be compared to avoid wrong result of below conditional
+        return [t for t in self.targets if camel_dict_to_snake_dict(t) not in remote_targets]
 
     def _remote_target_ids_to_remove(self):
         """Returns a list of targets that need to be removed remotely"""


Test patch:
diff --git a/tests/integration/targets/cloudwatchevent_rule/tasks/main.yml b/tests/integration/targets/cloudwatchevent_rule/tasks/main.yml
--- a/tests/integration/targets/cloudwatchevent_rule/tasks/main.yml
+++ b/tests/integration/targets/cloudwatchevent_rule/tasks/main.yml
@@ -7,6 +7,10 @@
       region: "{{ aws_region }}"
 
   block:
+
+    - name: Run tests for testing json input_template
+      ansible.builtin.import_tasks: test_json_input_template.yml
+
     - name: Create SNS topic
       community.aws.sns_topic:
         name: TestSNSTopic
diff --git a/tests/integration/targets/cloudwatchevent_rule/tasks/test_json_input_template.yml b/tests/integration/targets/cloudwatchevent_rule/tasks/test_json_input_template.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/cloudwatchevent_rule/tasks/test_json_input_template.yml
@@ -0,0 +1,76 @@
+---
+- name: Run tests for json input_template
+  block:
+
+    - name: Create SNS topic
+      community.aws.sns_topic:
+        name: TestSNSTopic-Json
+        state: present
+        display_name: Test SNS Topic
+      register: sns_topic_output
+
+    - name: Define JSON input_template
+      ansible.builtin.set_fact:
+        json_input_template: |
+          {
+            "instance" : "<instance>",
+            "state": "<state>"
+          }
+
+    - name: Create cloudwatch event rule with input transformer
+      amazon.aws.cloudwatchevent_rule:
+        name: "{{ input_transformer_event_name }}-Json"
+        description: Event rule with input transformer configuration
+        state: present
+        event_pattern: '{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change Notification"],"detail":{"state":["pending"]}}'
+        targets:
+          - id: "{{ sns_topic_output.sns_topic.name }}"
+            arn: "{{ sns_topic_output.sns_topic.topic_arn }}"
+            input_transformer:
+              input_paths_map:
+                instance: $.detail.instance-id
+                state: $.detail.state
+              input_template: "{{ json_input_template }}"
+      register: event_rule_input_transformer_output
+
+    - name: Assert that input transformer event rule was created
+      ansible.builtin.assert:
+        that:
+          - event_rule_input_transformer_output.changed
+
+    - name: Assert that event rule is created with a valid json value for input_template
+      ansible.builtin.assert:
+        that:
+          - event_rule_input_transformer_output.targets[0].input_transformer.input_template | from_json
+
+    - name: Create cloudwatch event rule with input transformer (idempotent)
+      amazon.aws.cloudwatchevent_rule:
+        name: "{{ input_transformer_event_name }}-Json"
+        description: Event rule with input transformer configuration
+        state: present
+        event_pattern: '{"source":["aws.ec2"],"detail-type":["EC2 Instance State-change Notification"],"detail":{"state":["pending"]}}'
+        targets:
+          - id: "{{ sns_topic_output.sns_topic.name }}"
+            arn: "{{ sns_topic_output.sns_topic.topic_arn }}"
+            input_transformer:
+              input_paths_map:
+                instance: $.detail.instance-id
+                state: $.detail.state
+              input_template: "{{ json_input_template }}"
+      register: event_rule_input_transformer_output
+
+  always:
+    - name: Assert that no changes were made to the rule
+      ansible.builtin.assert:
+        that:
+          - event_rule_input_transformer_output is not changed
+
+    - name: Delete input transformer CloudWatch event rules
+      amazon.aws.cloudwatchevent_rule:
+        name: "{{ input_transformer_event_name }}-Json"
+        state: absent
+
+    - name: Delete SNS topic
+      community.aws.sns_topic:
+        name: TestSNSTopic-Json
+        state: absent
\ No newline at end of file