Repository: ansible-collections/community.aws

Issue:
Parameter validation logic in ecs_task does not work as intended
##### SUMMARY
ecs_task attempts to validate parameters using an `and` statement.

For example:
```python
if 'task_definition' not in module.params and module.params['task_definition'] is None:
    module.fail_json(msg="To stop a task, a task definition must be specified")
```
This will not evaluate to true even if `task_definition` not provided because `module.params['task_definiton'] = None` by default. Which means the condition `'task_definition' not in module.params` is always false.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
ecs_task

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.10.3
  config file = None
  configured module search path = ['/home/mjmayer/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/mjmayer/.local/lib/python3.8/site-packages/ansible
  executable location = /home/mjmayer/.local/bin/ansible
  python version = 3.8.6 (default, Sep 25 2020, 09:36:53) [GCC 10.2.0]
```

##### CONFIGURATION
```paste below

```

##### OS / ENVIRONMENT
N/A


##### STEPS TO REPRODUCE

<!--- Paste example playbooks or commands between quotes below -->
```yaml
    - name: stop running tasks
      ecs_task:
        operation: stop
        cluster: "{{ cluster_name }}"
        task: "{{ ecs_task }}"
        aws_access_key: "{{ aws_key }}"
        aws_secret_key: "{{ aws_secret }}"
        region: "{{ region }}"
```

##### EXPECTED RESULTS

Error indicatin that the `task_definition` parameter is missing.


##### ACTUAL RESULTS

```paste below
The full traceback is:
Traceback (most recent call last):
  File "/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py", line 249, in <module>
    _ansiballz_main()
  File "/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py", line 239, in _ansiballz_main
    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
  File "/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py", line 110, in invoke_module
    runpy.run_module(mod_name='ansible_collections.community.aws.plugins.modules.ecs_task', init_globals=None, run_name='__main__', alter_sys=True)
  File "/usr/lib/python3.8/runpy.py", line 207, in run_module
    return _run_module_code(code, init_globals, run_name, mod_spec)
  File "/usr/lib/python3.8/runpy.py", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py", line 443, in <module>
  File "/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py", line 390, in main
  File "/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py", line 251, in list_tasks
  File "/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py", line 648, in _make_api_call
    request_dict = self._convert_to_request_dict(
  File "/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py", line 696, in _convert_to_request_dict
    request_dict = self._serializer.serialize_to_request(
  File "/home/mjmayer/.local/lib/python3.8/site-packages/botocore/validate.py", line 297, in serialize_to_request
    raise ParamValidationError(report=report.generate_report())
botocore.exceptions.ParamValidationError: Parameter validation failed:
Invalid type for parameter family, value: None, type: <class 'NoneType'>, valid types: <class 'str'>
failed: [localhost] (item=arn:aws:ecs:us-west-2:989838493125:task/ait-ecs_cluster-np/b7fa41f566ed47a895035b9536bc88a7) => {
    "ansible_loop_var": "item",
    "changed": false,
    "item": "arn:aws:ecs:us-west-2:989838493125:task/ait-ecs_cluster-np/b7fa41f566ed47a895035b9536bc88a7",
    "module_stderr": "Traceback (most recent call last):\n  File \"/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py\", line 249, in <module>\n    _ansiballz_main()\n  File \"/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py\", line 239, in _ansiballz_main\n    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\n  File \"/home/mjmayer/.ansible/tmp/ansible-tmp-1612908699.9970727-2470578-69922562765386/AnsiballZ_ecs_task.py\", line 110, in invoke_module\n    runpy.run_module(mod_name='ansible_collections.community.aws.plugins.modules.ecs_task', init_globals=None, run_name='__main__', alter_sys=True)\n  File \"/usr/lib/python3.8/runpy.py\", line 207, in run_module\n    return _run_module_code(code, init_globals, run_name, mod_spec)\n  File \"/usr/lib/python3.8/runpy.py\", line 97, in _run_module_code\n    _run_code(code, mod_globals, init_globals,\n  File \"/usr/lib/python3.8/runpy.py\", line 87, in _run_code\n    exec(code, run_globals)\n  File \"/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py\", line 443, in <module>\n  File \"/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py\", line 390, in main\n  File \"/tmp/ansible_ecs_task_payload_1abc2nbk/ansible_ecs_task_payload.zip/ansible_collections/community/aws/plugins/modules/ecs_task.py\", line 251, in list_tasks\n  File \"/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n  File \"/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py\", line 648, in _make_api_call\n    request_dict = self._convert_to_request_dict(\n  File \"/home/mjmayer/.local/lib/python3.8/site-packages/botocore/client.py\", line 696, in _convert_to_request_dict\n    request_dict = self._serializer.serialize_to_request(\n  File \"/home/mjmayer/.local/lib/python3.8/site-packages/botocore/validate.py\", line 297, in serialize_to_request\n    raise ParamValidationError(report=report.generate_report())\nbotocore.exceptions.ParamValidationError: Parameter validation failed:\nInvalid type for parameter family, value: None, type: <class 'NoneType'>, valid types: <class 'str'>\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
    "rc": 1
}
```



Discussion:

Resolving patch:
diff --git a/plugins/modules/ecs_task.py b/plugins/modules/ecs_task.py
--- a/plugins/modules/ecs_task.py
+++ b/plugins/modules/ecs_task.py
@@ -19,17 +19,20 @@
     operation:
         description:
             - Which task operation to execute.
+            - When I(operation=run) I(task_definition) must be set.
+            - When I(operation=start) both I(task_definition) and I(container_instances) must be set.
+            - When I(operation=stop) both I(task_definition) and I(task) must be set.
         required: True
         choices: ['run', 'start', 'stop']
         type: str
     cluster:
         description:
             - The name of the cluster to run the task on.
-        required: False
+        required: True
         type: str
     task_definition:
         description:
-            - The task definition to start or run.
+            - The task definition to start, run or stop.
         required: False
         type: str
     overrides:
@@ -44,7 +47,7 @@
         type: int
     task:
         description:
-            - The task to stop.
+            - The ARN of the task to stop.
         required: False
         type: str
     container_instances:
@@ -332,7 +335,7 @@ def ecs_api_handles_network_configuration(self):
 def main():
     argument_spec = dict(
         operation=dict(required=True, choices=['run', 'start', 'stop']),
-        cluster=dict(required=False, type='str'),  # R S P
+        cluster=dict(required=True, type='str'),  # R S P
         task_definition=dict(required=False, type='str'),  # R* S*
         overrides=dict(required=False, type='dict'),  # R S
         count=dict(required=False, type='int'),  # R
@@ -345,28 +348,26 @@ def main():
     )
 
     module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True,
-                              required_if=[('launch_type', 'FARGATE', ['network_configuration'])])
+                              required_if=[
+                                  ('launch_type', 'FARGATE', ['network_configuration']),
+                                  ('operation', 'run', ['task_definition']),
+                                  ('operation', 'start', [
+                                      'task_definition',
+                                      'container_instances'
+                                  ]),
+                                  ('operation', 'stop', ['task_definition', 'task']),
+                              ])
 
     # Validate Inputs
     if module.params['operation'] == 'run':
-        if 'task_definition' not in module.params and module.params['task_definition'] is None:
-            module.fail_json(msg="To run a task, a task_definition must be specified")
         task_to_list = module.params['task_definition']
         status_type = "RUNNING"
 
     if module.params['operation'] == 'start':
-        if 'task_definition' not in module.params and module.params['task_definition'] is None:
-            module.fail_json(msg="To start a task, a task_definition must be specified")
-        if 'container_instances' not in module.params and module.params['container_instances'] is None:
-            module.fail_json(msg="To start a task, container instances must be specified")
         task_to_list = module.params['task']
         status_type = "RUNNING"
 
     if module.params['operation'] == 'stop':
-        if 'task' not in module.params and module.params['task'] is None:
-            module.fail_json(msg="To stop a task, a task must be specified")
-        if 'task_definition' not in module.params and module.params['task_definition'] is None:
-            module.fail_json(msg="To stop a task, a task definition must be specified")
         task_to_list = module.params['task_definition']
         status_type = "STOPPED"
 


Test patch: