Repository: ansible-collections/amazon.aws

Issue:
aws_ssm look does not support endpoint_url
### Summary

**what was tried**

using the [aws_ssm lookup](https://github.com/ansible-collections/amazon.aws/blob/main/plugins/lookup/aws_ssm.py) against a mock ssm parameter in localstack currently will not work.

```
my_cool_param: "{{ lookup('aws_ssm', '/namespace/my_cool_param' ) }}"
```

**proposed fix**

Add a configurable endpoint_url parameter into the [boto3 Session client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html).

**ideal outcome**

1. an option that can be passed to the `aws_ssm` ansible lookup: 

```
my_cool_param: "{{ lookup('aws_ssm', '/namespace/my_cool_param', aws_endpoint_url=http://localhost:4566 ) }}"
```

2. An environment variable it also accept can accept

```
export AWS_ENDPOINT_URL=http://localhost:4566
```

### Issue Type

Feature Idea

### Component Name

aws_ssm lookup

### Additional Information


the new feature would allow ansible users to use a custom endpoint url rather than the default aws endpoints. 
localstack may be used then for example

```yaml (paste below)
my_cool_param: "{{ lookup('aws_ssm', '/namespace/my_cool_param', aws_endpoint_url=http://localhost:4566 ) }}"
```


### Code of Conduct

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


Discussion:
Files identified in the description:
* [`lib/ansible/plugins/lookup`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/lib/ansible/plugins/lookup)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/plugins/lookup/aws_ssm.py b/plugins/lookup/aws_ssm.py
--- a/plugins/lookup/aws_ssm.py
+++ b/plugins/lookup/aws_ssm.py
@@ -67,6 +67,10 @@
     type: string
     choices: ['error', 'skip', 'warn']
     version_added: 2.0.0
+  endpoint:
+    description: Use a custom endpoint when connecting to SSM service.
+    type: string
+    version_added: 3.3.0
 extends_documentation_fragment:
 - amazon.aws.aws_boto3
 '''
@@ -135,6 +139,8 @@
 from ansible.utils.display import Display
 from ansible.module_utils.six import string_types
 
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_conn
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_aws_connection_info
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import HAS_BOTO3
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
 from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
@@ -142,31 +148,11 @@
 display = Display()
 
 
-def _boto3_conn(region, credentials):
-    if 'boto_profile' in credentials:
-        boto_profile = credentials.pop('boto_profile')
-    else:
-        boto_profile = None
-
-    try:
-        connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region, **credentials)
-    except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError):
-        if boto_profile:
-            try:
-                connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region)
-            # FIXME: we should probably do better passing on of the error information
-            except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError):
-                raise AnsibleError("Insufficient credentials found.")
-        else:
-            raise AnsibleError("Insufficient credentials found.")
-    return connection
-
-
 class LookupModule(LookupBase):
     def run(self, terms, variables=None, boto_profile=None, aws_profile=None,
             aws_secret_key=None, aws_access_key=None, aws_security_token=None, region=None,
             bypath=False, shortnames=False, recursive=False, decrypt=True, on_missing="error",
-            on_denied="error"):
+            on_denied="error", endpoint=None):
         '''
             :arg terms: a list of lookups to run.
                 e.g. ['parameter_name', 'parameter_name_too' ]
@@ -180,6 +166,7 @@ def run(self, terms, variables=None, boto_profile=None, aws_profile=None,
             :kwarg recursive: Set to True to recurse below the path (requires bypath=True)
             :kwarg on_missing: Action to take if the SSM parameter is missing
             :kwarg on_denied: Action to take if access to the SSM parameter is denied
+            :kwarg endpoint: Endpoint for SSM client
             :returns: A list of parameter values or a list of dictionaries if bypath=True.
         '''
 
@@ -195,16 +182,36 @@ def run(self, terms, variables=None, boto_profile=None, aws_profile=None,
         ret = []
         ssm_dict = {}
 
-        credentials = {}
+        self.params = variables
+
+        cli_region, cli_endpoint, cli_boto_params = get_aws_connection_info(self, boto3=True)
+
+        if region:
+            cli_region = region
+
+        if endpoint:
+            cli_endpoint = endpoint
+
+        # For backward  compatibility
+        if aws_access_key:
+            cli_boto_params.update({'aws_access_key_id': aws_access_key})
+        if aws_secret_key:
+            cli_boto_params.update({'aws_secret_access_key': aws_secret_key})
+        if aws_security_token:
+            cli_boto_params.update({'aws_session_token': aws_security_token})
+        if boto_profile:
+            cli_boto_params.update({'profile_name': boto_profile})
         if aws_profile:
-            credentials['boto_profile'] = aws_profile
-        else:
-            credentials['boto_profile'] = boto_profile
-        credentials['aws_secret_access_key'] = aws_secret_key
-        credentials['aws_access_key_id'] = aws_access_key
-        credentials['aws_session_token'] = aws_security_token
+            cli_boto_params.update({'profile_name': aws_profile})
+
+        cli_boto_params.update(dict(
+            conn_type='client',
+            resource='ssm',
+            region=cli_region,
+            endpoint=cli_endpoint,
+        ))
 
-        client = _boto3_conn(region, credentials)
+        client = boto3_conn(module=self, **cli_boto_params)
 
         ssm_dict['WithDecryption'] = decrypt
 


Test patch:
diff --git a/tests/unit/plugins/lookup/test_aws_ssm.py b/tests/unit/plugins/lookup/test_aws_ssm.py
--- a/tests/unit/plugins/lookup/test_aws_ssm.py
+++ b/tests/unit/plugins/lookup/test_aws_ssm.py
@@ -18,9 +18,11 @@
 
 # Make coding more python3-ish
 from __future__ import (absolute_import, division, print_function)
+
 __metaclass__ = type
 
 import pytest
+from unittest.mock import ANY
 from copy import copy
 
 from ansible.errors import AnsibleError
@@ -119,8 +121,16 @@ def test_lookup_variable(mocker):
     assert(isinstance(retval, list))
     assert(len(retval) == 1)
     assert(retval[0] == "simplevalue")
-    boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey',
-                                           aws_secret_access_key="notasecret", aws_session_token=None)
+    boto3_client_double.assert_called_with(
+        'ssm',
+        region_name='eu-west-1',
+        aws_access_key_id='notakey',
+        aws_secret_access_key="notasecret",
+        aws_session_token=None,
+        endpoint_url=None,
+        config=ANY,
+        verify=None,
+    )
 
 
 def test_path_lookup_variable(mocker):
@@ -140,8 +150,16 @@ def test_path_lookup_variable(mocker):
     retval = lookup.run(["/testpath"], {}, **args)
     assert(retval[0]["/testpath/won"] == "simple_value_won")
     assert(retval[0]["/testpath/too"] == "simple_value_too")
-    boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey',
-                                           aws_secret_access_key="notasecret", aws_session_token=None)
+    boto3_client_double.assert_called_with(
+        'ssm',
+        region_name='eu-west-1',
+        aws_access_key_id='notakey',
+        aws_secret_access_key="notasecret",
+        aws_session_token=None,
+        endpoint_url=None,
+        config=ANY,
+        verify=None,
+    )
     get_paginator_fn.assert_called_with('get_parameters_by_path')
     paginator.paginate.assert_called_with(Path="/testpath", Recursive=True, WithDecryption=True)
     paginator.paginate.return_value.build_full_result.assert_called_with()