Repository: ansible-collections/amazon.aws

Issue:
Lambda execute stack trace f o r m a t t e d   w i t h   e x t r a   s p a c e
### Summary

When executing a Lambda function which fails, the output has spaces between each character.

There's a `" ".join()` call in the module, expecting a list of strings but getting just a string.

Note that in python a string is also an iterable of strings.

So `" ".join("abc")` returns `'a b c'`.

I think the cause is here:

https://github.com/ansible-collections/amazon.aws/blob/f399fb763b419e9a57bac1045f7af7340f39fcc1/plugins/modules/lambda_execute.py#L265

If `line` is a string, that would explain the behavior.

The documentation [here](https://docs.aws.amazon.com/lambda/latest/dg/python-exceptions.html) shows that the stack trace for Python lambdas is already formatted as a single string.

Note that I suspect this response format is specific to the runtime of the lambda. Some languages don't even have tracebacks. (e.g. Go typically has one error string, and two line numbers.)
So I wonder how this works for other languages.

### Issue Type

Bug Report

### Component Name

lambda_execute

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.13.9]
  config file = /home/ec2-user/.ansible.cfg
  configured module search path = ['/home/ec2-user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/ec2-user/.pyenv/versions/3.8.11/lib/python3.8/site-packages/ansible
  ansible collection location = /home/ec2-user/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/ec2-user/.pyenv/versions/3.8.11/bin/ansible
  python version = 3.8.11 (default, Sep  7 2022, 04:17:12) [GCC 7.3.1 20180712 (Red Hat 7.3.1-15)]
  jinja version = 3.1.2
  libyaml = True
```


### Collection Versions

```console (paste below)
$ ansible-galaxy collection list
$ ansible-galaxy collection list

# /home/ec2-user/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
amazon.aws        5.4.0  
ansible.netcommon 4.1.0  
ansible.utils     2.8.0  
community.aws     5.0.0  
community.crypto  2.9.0  
community.general 6.6.0  

# /home/ec2-user/.pyenv/versions/3.8.11/lib/python3.8/site-packages/ansible_collections
Collection                    Version
----------------------------- -------
amazon.aws                    3.5.0  
ansible.netcommon             3.1.3  
ansible.posix                 1.4.0  
ansible.utils                 2.7.0  
ansible.windows               1.12.0 
arista.eos                    5.0.1  
awx.awx                       21.8.0 
azure.azcollection            1.14.0 
check_point.mgmt              2.3.0  
chocolatey.chocolatey         1.3.1  
cisco.aci                     2.3.0  
cisco.asa                     3.1.0  
cisco.dnac                    6.6.0  
cisco.intersight              1.0.20 
cisco.ios                     3.3.2  
cisco.iosxr                   3.3.1  
cisco.ise                     2.5.8  
cisco.meraki                  2.11.0 
cisco.mso                     2.1.0  
cisco.nso                     1.0.3  
cisco.nxos                    3.2.0  
cisco.ucs                     1.8.0  
cloud.common                  2.1.2  
cloudscale_ch.cloud           2.2.2  
community.aws                 3.6.0  
community.azure               1.1.0  
community.ciscosmb            1.0.5  
community.crypto              2.8.1  
community.digitalocean        1.22.0 
community.dns                 2.4.0  
community.docker              2.7.1  
community.fortios             1.0.0  
community.general             5.8.0  
community.google              1.0.0  
community.grafana             1.5.3  
community.hashi_vault         3.4.0  
community.hrobot              1.6.0  
community.libvirt             1.2.0  
community.mongodb             1.4.2  
community.mysql               3.5.1  
community.network             4.0.1  
community.okd                 2.2.0  
community.postgresql          2.3.0  
community.proxysql            1.4.0  
community.rabbitmq            1.2.3  
community.routeros            2.3.1  
community.sap                 1.0.0  
community.sap_libs            1.3.0  
community.skydive             1.0.0  
community.sops                1.4.1  
community.vmware              2.10.1 
community.windows             1.11.1 
community.zabbix              1.8.0  
containers.podman             1.9.4  
cyberark.conjur               1.2.0  
cyberark.pas                  1.0.14 
dellemc.enterprise_sonic      1.1.2  
dellemc.openmanage            5.5.0  
dellemc.os10                  1.1.1  
dellemc.os6                   1.0.7  
dellemc.os9                   1.0.4  
f5networks.f5_modules         1.20.0 
fortinet.fortimanager         2.1.6  
fortinet.fortios              2.1.7  
frr.frr                       2.0.0  
gluster.gluster               1.0.2  
google.cloud                  1.0.2  
hetzner.hcloud                1.8.2  
hpe.nimble                    1.1.4  
ibm.qradar                    2.1.0  
ibm.spectrum_virtualize       1.10.0 
infinidat.infinibox           1.3.7  
infoblox.nios_modules         1.4.0  
inspur.ispim                  1.2.0  
inspur.sm                     2.3.0  
junipernetworks.junos         3.1.0  
kubernetes.core               2.3.2  
lowlydba.sqlserver            1.0.4  
mellanox.onyx                 1.0.0  
netapp.aws                    21.7.0 
netapp.azure                  21.10.0
netapp.cloudmanager           21.21.0
netapp.elementsw              21.7.0 
netapp.ontap                  21.24.1
netapp.storagegrid            21.11.1
netapp.um_info                21.8.0 
netapp_eseries.santricity     1.3.1  
netbox.netbox                 3.8.1  
ngine_io.cloudstack           2.2.4  
ngine_io.exoscale             1.0.0  
ngine_io.vultr                1.1.2  
openstack.cloud               1.10.0 
openvswitch.openvswitch       2.1.0  
ovirt.ovirt                   2.3.1  
purestorage.flasharray        1.14.0 
purestorage.flashblade        1.10.0 
purestorage.fusion            1.1.1  
sensu.sensu_go                1.13.1 
servicenow.servicenow         1.0.6  
splunk.es                     2.1.0  
t_systems_mms.icinga_director 1.31.4 
theforeman.foreman            3.7.0  
vmware.vmware_rest            2.2.0  
vultr.cloud                   1.3.0  
vyos.vyos                     3.0.1  
wti.remote                    1.0.4 
```


### AWS SDK versions

```console (paste below)
$ pip3 show boto boto3 botocore                                                                                                                                                                           
WARNING: Package(s) not found: boto
Name: boto3
Version: 1.26.90
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/ec2-user/.pyenv/versions/3.8.11/lib/python3.8/site-packages
Requires: botocore, jmespath, s3transfer
Required-by: awswrangler, credstash, moto, redshift-connector
---
Name: botocore
Version: 1.29.120
Summary: Low-level, data-driven core of boto 3.
Home-page: https://github.com/boto/botocore
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/ec2-user/.pyenv/versions/3.8.11/lib/python3.8/site-packages
Requires: jmespath, python-dateutil, urllib3
Required-by: aws-xray-sdk, awscli, awswrangler, boto3, moto, redshift-connector, s3transfer
```

(Note that I run with `-e ansible_python_interpreter=$(which python3)`)

### Configuration

```console (paste below)
$ ansible-config dump --only-changed
ANSIBLE_PIPELINING(/home/ec2-user/.ansible.cfg) = True
DEFAULT_LOCAL_TMP(/home/ec2-user/.ansible.cfg) = /dev/shm/ansible/tmp_local/ansible-local-30944ml1b7p1u
DEFAULT_STDOUT_CALLBACK(/home/ec2-user/.ansible.cfg) = yaml
INTERPRETER_PYTHON(/home/ec2-user/.ansible.cfg) = /usr/bin/python3
```


### OS / Environment

Amazon Linux 2

### Steps to Reproduce

Here's a MWE that deploys a Lambda via CloudFormation.
If you have an existing lambda which fails, just use the last task in the playbook.

`playbook.yaml`
```
---
- hosts: localhost
  connection: local
  vars:
    aws_region: ap-southeast-2
  tasks:
  
  - name: deploy Lambda
    cloudformation:
      stack_name: test-ansible-fail
      state: "present"
      region: "{{ aws_region }}"
      template_body: "{{ lookup('file', 'template.yaml') }}"
      on_create_failure: "DELETE"
    register: stack

  - execute_lambda:
      name: "{{ stack['stack_outputs']['LambdaArn'] }}" 
      region: "{{ aws_region }}"
      wait: yes
      tail_log: yes
```

```
AWSTemplateFormatVersion: 2010-09-09
Description:
  This stack includes a lambda for Ansible testing purposes
Parameters:
  LambdaName:
    Type: String
    Default: test-failer-ansible
Resources:

  Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Code: 
        ZipFile: |
          def main(event, context):
              print("Here is some output")
              other_func_a()
          def other_func_a():
              other_func_b()
          def other_func_b():
              assert False, "deliberate failure"

      Description: Deliberately fail to test Ansible error handling
      FunctionName: !Ref LambdaName
      Handler: index.main
      PackageType: Zip
      Role: !GetAtt LambdaRole.Arn
      Runtime: python3.9
      
  LambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      AssumeRolePolicyDocument: # which AWS service can use this role
        Statement:
          - Action:
            - "sts:AssumeRole"
            Effect: Allow
            Principal:
              Service:
              - "lambda.amazonaws.com"
      RoleName: !Ref LambdaName

Outputs:
  LambdaArn:
    Value: !GetAtt Lambda.Arn
  
```

Run with:

```
ansible-playbook  playbook.yaml  -e ansible_python_interpreter=$(which python3)
```

### Expected Results

```
TASK [execute_lambda] **************************************************************************************************************************************************************************************
fatal: [127.0.0.1]: FAILED! => changed=false 
  msg: |-
    Function executed, but there was an error in the Lambda function. Message: deliberate failure, Type: AssertionError, Stack Trace:
      File "/var/task/index.py", line 3, in main
        other_func_a()
      File "/var/task/index.py", line 5, in other_func_a
        other_func_b()
      File "/var/task/index.py", line 7, in other_func_b
        assert False, "deliberate failure"
```

### Actual Results

```
TASK [execute_lambda] **************************************************************************************************************************************************************************************
fatal: [127.0.0.1]: FAILED! => changed=false 
  msg: |-
    Function executed, but there was an error in the Lambda function. Message: deliberate failure, Type: AssertionError, Stack Trace:     F i l e   " / v a r / t a s k / i n d e x . p y " ,   l i n e   3 ,   i n   m a i n
             o t h e r _ f u n c _ a ( )
  
        F i l e   " / v a r / t a s k / i n d e x . p y " ,   l i n e   5 ,   i n   o t h e r _ f u n c _ a
             o t h e r _ f u n c _ b ( )
  
        F i l e   " / v a r / t a s k / i n d e x . p y " ,   l i n e   7 ,   i n   o t h e r _ f u n c _ b
             a s s e r t   F a l s e ,   " d e l i b e r a t e   f a i l u r e "
```


### Code of Conduct

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


Discussion:
@mdavis-xyz Thanks for filing an issue. Would you be willing to submit a PR for this?
Yes I can try. Obviously this isn't a super urgent bug to fix. I might not get around to it for a few weeks.
(Every time I contribute to Ansible AWS modules, it takes an hour to figure out how to install from source and run tests, and 5 minutes to write the patch.)

Do I need to write unit tests to ensure this is fixed? Or is that not necessary for something cosmetic like this?
We can probably skip a test for this one.

Resolving patch:
diff --git a/plugins/modules/lambda_execute.py b/plugins/modules/lambda_execute.py
--- a/plugins/modules/lambda_execute.py
+++ b/plugins/modules/lambda_execute.py
@@ -252,17 +252,16 @@ def main():
                 [results['output'].get('stackTrace'), results['output'].get('errorMessage')]):
             # AWS sends back stack traces and error messages when a function failed
             # in a RequestResponse (synchronous) context.
-            template = ("Function executed, but there was an error in the Lambda function. "
-                        "Message: {errmsg}, Type: {type}, Stack Trace: {trace}")
+            template = (
+                "Function executed, but there was an error in the Lambda function. "
+                "Message: {errmsg}, Type: {type}, Stack Trace: {trace}"
+            )
+
             error_data = {
                 # format the stacktrace sent back as an array into a multiline string
-                'trace': '\n'.join(
-                    [' '.join([
-                        str(x) for x in line  # cast line numbers to strings
-                    ]) for line in results.get('output', {}).get('stackTrace', [])]
-                ),
-                'errmsg': results['output'].get('errorMessage'),
-                'type': results['output'].get('errorType')
+                "trace": "\n".join(results.get("output", {}).get("stackTrace", [])),
+                "errmsg": results["output"].get("errorMessage"),
+                "type": results["output"].get("errorType"),
             }
             module.fail_json(msg=template.format(**error_data), result=results)
 


Test patch: