Skip to content

Commit

Permalink
Add argspec validation for targets (#1355) (#1358)
Browse files Browse the repository at this point in the history
[PR #1355/5f0e10e7 backport][stable-4] cloudwatchevent_rule - Add argspec validation for targets

This is a backport of PR #1355 as merged into main (5f0e10e).
SUMMARY
fixes: #201
Targets currently has minimal validation applied.
Because of the way Ansible converts JSON strings to dicts/lists, then back to the Python format string representing the dicts/lists, unless we explicitly define a parameter is a JSON string they get corrupted.
This also moves the new input_paths_map/input_template parameters under input_transformer.  Because we've not released 4.1.0 yet this doesn't cause any breakage.  This will make adding other target parameters simpler further down the road.  (There's a lot that we don't support today)
ISSUE TYPE

Bugfix Pull Request

COMPONENT NAME
cloudwatchevent_rule
ADDITIONAL INFORMATION

Reviewed-by: Mark Chappell <None>
  • Loading branch information
patchback[bot] authored Jul 15, 2022
1 parent 4b94a62 commit 15b1249
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
minor_changes:
- cloudwatchevent_rule - Added ``input_paths_map`` and ``input_template`` parameters to support ``input_transformer`` on CloudWatch event rule (https://github.com/ansible-collections/community.aws/pull/623).
- cloudwatchevent_rule - Added ``targets.input_transformer.input_paths_map`` and ``targets.input_transformer.input_template`` parameters to
support configuring on CloudWatch event rule input transformation (https://github.com/ansible-collections/community.aws/pull/623).
- cloudwatchevent_rule - Applied validation of ``targets`` arguments (https://github.com/ansible-collections/community.aws/issues/201).
122 changes: 73 additions & 49 deletions plugins/modules/cloudwatchevent_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
type: str
event_pattern:
description:
- A string pattern (in valid JSON format) that is used to match against
incoming events to determine if the rule should be triggered.
- A string pattern that is used to match against incoming events to determine if the rule
should be triggered.
required: false
type: str
type: json
state:
description:
- Whether the rule is present (and enabled), disabled, or absent.
Expand Down Expand Up @@ -78,34 +78,34 @@
type: str
description: The ARN of the IAM role to be used for this target when the rule is triggered.
input:
type: str
type: json
description:
- A JSON object that will override the event data when passed to the target.
- If neither I(input) nor I(input_path) nor I(input_paths_map) nor I(input_template)
- A JSON object that will override the event data passed to the target.
- If neither I(input) nor I(input_path) nor I(input_transformer)
is specified, then the entire event is passed to the target in JSON form.
input_path:
type: str
description:
- A JSONPath string (e.g. C($.detail)) that specifies the part of the event data to be
passed to the target.
- If neither I(input) nor I(input_path) nor I(input_paths_map) nor I(input_template)
- If neither I(input) nor I(input_path) nor I(input_transformer)
is specified, then the entire event is passed to the target in JSON form.
input_paths_map:
input_transformer:
type: dict
version_added: 4.1.0
description:
- A dict that specifies the transformation of the event data to
custom input parameters.
- If neither I(input) nor I(input_path) nor I(input_paths_map) nor I(input_template)
is specified, then the entire event is passed to the target in JSON form.
input_template:
type: str
- Settings to support providing custom input to a target based on certain event data.
version_added: 4.1.0
description:
- A string that templates the values input_paths_map extracted from the event data.
It is used to produce the output you want to be sent to the target.
- If neither I(input) nor I(input_path) nor I(input_paths_map) nor I(input_template)
is specified, then the entire event is passed to the target in JSON form.
suboptions:
input_paths_map:
type: dict
description:
- A dict that specifies the transformation of the event data to
custom input parameters.
input_template:
type: json
description:
- A string that templates the values input_paths_map extracted from the event data.
It is used to produce the output you want to be sent to the target.
ecs_parameters:
type: dict
description:
Expand All @@ -114,6 +114,7 @@
task_definition_arn:
type: str
description: The full ARN of the task definition.
required: true
task_count:
type: int
description: The number of tasks to create based on I(task_definition).
Expand Down Expand Up @@ -147,10 +148,11 @@
targets:
- id: MyTargetSnsTopic
arn: arn:aws:sns:us-east-1:123456789012:MySNSTopic
input_paths_map:
instance: "$.detail.instance-id"
state: "$.detail.state"
input_template: "<instance> is in state <state>"
input_transformer:
input_paths_map:
instance: "$.detail.instance-id"
state: "$.detail.state"
input_template: "<instance> is in state <state>"
- community.aws.cloudwatchevent_rule:
name: MyCronTask
Expand All @@ -175,15 +177,28 @@
sample: "[{ 'arn': 'arn:aws:lambda:us-east-1:123456789012:function:MyFunction', 'id': 'MyTargetId' }]"
'''

import json

try:
import botocore
except ImportError:
pass # handled by AnsibleAWSModule

from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict

from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters


def _format_json(json_string):
# When passed a simple string, Ansible doesn't quote it to ensure it's a *quoted* string
try:
json.loads(json_string)
return json_string
except json.decoder.JSONDecodeError:
return str(json.dumps(json_string))


class CloudWatchEventRule(object):
Expand Down Expand Up @@ -307,29 +322,14 @@ def _targets_request(self, targets):
"""Formats each target for the request"""
targets_request = []
for target in targets:
target_request = {
'Id': target['id'],
'Arn': target['arn']
}
if 'input' in target:
target_request['Input'] = target['input']
if 'input_path' in target:
target_request['InputPath'] = target['input_path']
if 'input_paths_map' in target or 'input_template' in target:
target_request['InputTransformer'] = {}
target_request['InputTransformer']['InputPathsMap'] = target['input_paths_map']
target_request['InputTransformer']['InputTemplate'] = '"{0}"'.format(
target['input_template']
)
if 'role_arn' in target:
target_request['RoleArn'] = target['role_arn']
if 'ecs_parameters' in target:
target_request['EcsParameters'] = {}
ecs_parameters = target['ecs_parameters']
if 'task_definition_arn' in target['ecs_parameters']:
target_request['EcsParameters']['TaskDefinitionArn'] = ecs_parameters['task_definition_arn']
if 'task_count' in target['ecs_parameters']:
target_request['EcsParameters']['TaskCount'] = ecs_parameters['task_count']
target_request = scrub_none_parameters(snake_dict_to_camel_dict(target, True))
if target_request.get('Input', None):
target_request['Input'] = _format_json(target_request['Input'])
if target_request.get('InputTransformer', None):
if target_request.get('InputTransformer').get('InputTemplate', None):
target_request['InputTransformer']['InputTemplate'] = _format_json(target_request['InputTransformer']['InputTemplate'])
if target_request.get('InputTransformer').get('InputPathsMap', None):
target_request['InputTransformer']['InputPathsMap'] = target['input_transformer']['input_paths_map']
targets_request.append(target_request)
return targets_request

Expand Down Expand Up @@ -450,15 +450,39 @@ def _remote_state(self):


def main():
target_args = dict(
type='list', elements='dict', default=[],
options=dict(
id=dict(type='str', required=True),
arn=dict(type='str', required=True),
role_arn=dict(type='str'),
input=dict(type='json'),
input_path=dict(type='str'),
input_transformer=dict(
type='dict',
options=dict(
input_paths_map=dict(type='dict'),
input_template=dict(type='json'),
),
),
ecs_parameters=dict(
type='dict',
options=dict(
task_definition_arn=dict(type='str', required=True),
task_count=dict(type='int'),
),
),
),
)
argument_spec = dict(
name=dict(required=True),
schedule_expression=dict(),
event_pattern=dict(),
event_pattern=dict(type='json'),
state=dict(choices=['present', 'disabled', 'absent'],
default='present'),
description=dict(),
role_arn=dict(),
targets=dict(type='list', default=[], elements='dict'),
targets=target_args,
)
module = AnsibleAWSModule(argument_spec=argument_spec)

Expand Down
10 changes: 5 additions & 5 deletions tests/integration/targets/cloudwatchevent_rule/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
name_pattern: "cloudwatch_event_rule"
unique_id: "{{ tiny_prefix }}"
name_pattern: "cloudwatch_event_rule-{{ tiny_prefix }}"

test_event_names:
- "{{ name_pattern }}-{{ unique_id }}-1"
- "{{ name_pattern }}-{{ unique_id }}-2"
- "{{ name_pattern }}-1"
- "{{ name_pattern }}-2"

input_transformer_event_name: "{{ name_pattern }}-{{ unique_id }}-3"
input_transformer_event_name: "{{ name_pattern }}-3"
input_event_name: "{{ name_pattern }}-4"
42 changes: 34 additions & 8 deletions tests/integration/targets/cloudwatchevent_rule/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
register: event_rules_classic_output
loop: "{{ test_event_names }}"

- name: Assert that classic event rules were created
assert:
that:
- event_rules_classic_output.changed
- event_rules_classic_output.msg == "All items completed"

- name: Create cloudwatch event rule with input transformer
cloudwatchevent_rule:
name: "{{ input_transformer_event_name }}"
Expand All @@ -34,17 +40,34 @@
targets:
- id: "{{ sns_topic_output.sns_topic.name }}"
arn: "{{ sns_topic_output.sns_topic.topic_arn }}"
input_paths_map:
instance: "$.detail.instance-id"
state: "$.detail.state"
input_template: "<instance> is in state <state>"
input_transformer:
input_paths_map:
instance: "$.detail.instance-id"
state: "$.detail.state"
input_template: "<instance> is in state <state>"
register: event_rule_input_transformer_output

- name: Assert that classic event rules were created
- name: Assert that input transformer event rule was created
assert:
that:
- event_rules_classic_output.changed
- event_rules_classic_output.msg == "All items completed"
- event_rule_input_transformer_output.changed

- name: Create cloudwatch event rule with inputs
cloudwatchevent_rule:
name: "{{ input_event_name }}"
description: "Event rule with input 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: 'Hello World'
- id: "{{ sns_topic_output.sns_topic.name }}2"
arn: "{{ sns_topic_output.sns_topic.topic_arn }}"
input:
start: 'Hello World'
end: 'Goodbye oh cruel World'
register: event_rule_input_transformer_output

- name: Assert that input transformer event rule was created
assert:
Expand All @@ -61,8 +84,11 @@

- name: Delete input transformer CloudWatch event rules
cloudwatchevent_rule:
name: "{{ input_transformer_event_name }}"
name: "{{ item }}"
state: absent
loop:
- "{{ input_transformer_event_name }}"
- "{{ input_event_name }}"

- name: Delete SNS topic
sns_topic:
Expand Down

0 comments on commit 15b1249

Please sign in to comment.