Repository: ansible-collections/amazon.aws

Issue:
Inconsistent IAM profile regexes with some hard-coded partitions
### Summary

This came up in a community forum, in some modules where we check IAM profiles we have regexes to check for valid ARNs.  We use slightly different regexes in different modules, with some hard-coding the `aws` partition (which means they won't work in GovCloud or China).  We should move this logic to a module_util and add unit tests to ensure all currently-existing partitions are supported.

Compare:
```
/src/ansible_collections/amazon/aws$ grep -r '[\\w+=,.@-]+' plugins/
plugins/module_utils/iam.py:            m = re.search(r"arn:(aws(-([a-z\-]+))?):iam::([0-9]{12,32}):\w+/", except_msg)
plugins/modules/ec2_instance.py:    if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn):
````
`ec2_instance` will not match `arn:aws-us-gov:iam::0123456789:role/foo-role`, but the regex used in the `module_utils.iam` utility should.

Profile name is a work around, but we can predict that this will be frustrating for users.

### Issue Type

Bug Report

### Component Name

ec2_instance

### 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

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

```


### Expected Results

consistency between modules

### Actual Results

```console (paste below)

```


### Code of Conduct

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


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

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 --->
cc @ryansb @s-hertel @tremble @wimnat
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/module_utils/core.py b/plugins/module_utils/core.py
--- a/plugins/module_utils/core.py
+++ b/plugins/module_utils/core.py
@@ -440,6 +440,28 @@ def _boto3_handler(obj):
         return obj
 
 
+def parse_aws_arn(arn):
+    """
+    The following are the general formats for ARNs.
+        arn:partition:service:region:account-id:resource-id
+        arn:partition:service:region:account-id:resource-type/resource-id
+        arn:partition:service:region:account-id:resource-type:resource-id
+    The specific formats depend on the resource.
+    The ARNs for some resources omit the Region, the account ID, or both the Region and the account ID.
+    """
+    m = re.search(r"arn:(aws(-([a-z\-]+))?):(\w+):([a-z0-9\-]*):(\d*):(.*)", arn)
+    if m is None:
+        return None
+    result = dict()
+    result.update(dict(partition=m.group(1)))
+    result.update(dict(service=m.group(4)))
+    result.update(dict(region=m.group(5)))
+    result.update(dict(account_id=m.group(6)))
+    result.update(dict(resource=m.group(7)))
+
+    return result
+
+
 def normalize_boto3_result(result):
     """
     Because Boto3 returns datetime objects where it knows things are supposed to
diff --git a/plugins/module_utils/iam.py b/plugins/module_utils/iam.py
--- a/plugins/module_utils/iam.py
+++ b/plugins/module_utils/iam.py
@@ -4,8 +4,6 @@
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
-import re
-
 try:
     import botocore
 except ImportError:
@@ -15,6 +13,7 @@
 
 from .ec2 import AWSRetry
 from .core import is_boto3_error_code
+from .core import parse_aws_arn
 
 
 def get_aws_account_id(module):
@@ -54,14 +53,14 @@ def get_aws_account_info(module):
                 except_msg = to_native(e.message)
             except AttributeError:
                 except_msg = to_native(e)
-            m = re.search(r"arn:(aws(-([a-z\-]+))?):iam::([0-9]{12,32}):\w+/", except_msg)
-            if m is None:
+            result = parse_aws_arn(except_msg)
+            if result is None or result['service'] != 'iam':
                 module.fail_json_aws(
                     e,
                     msg="Failed to get AWS account information, Try allowing sts:GetCallerIdentity or iam:GetUser permissions."
                 )
-            account_id = m.group(4)
-            partition = m.group(1)
+            account_id = result.get('account_id')
+            partition = result.get('partition')
         except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:  # pylint: disable=duplicate-except
             module.fail_json_aws(
                 e,
diff --git a/plugins/modules/ec2_instance.py b/plugins/modules/ec2_instance.py
--- a/plugins/modules/ec2_instance.py
+++ b/plugins/modules/ec2_instance.py
@@ -904,6 +904,7 @@
 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.core import is_boto3_error_message
+from ansible_collections.amazon.aws.plugins.module_utils.core import parse_aws_arn
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags
@@ -1703,7 +1704,8 @@ def pretty_instance(i):
 
 
 def determine_iam_role(name_or_arn):
-    if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn):
+    result = parse_aws_arn(name_or_arn)
+    if result and result['service'] == 'iam' and result['resource'].startswith('instance-profile/'):
         return name_or_arn
     iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff())
     try:


Test patch:
diff --git a/tests/unit/module_utils/core/test_parse_aws_arn.py b/tests/unit/module_utils/core/test_parse_aws_arn.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/module_utils/core/test_parse_aws_arn.py
@@ -0,0 +1,45 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import parse_aws_arn
+
+
+arn_data = [
+    ("arn:aws-us-gov:iam::0123456789:role/foo-role",
+     dict(partition="aws-us-gov", account_id="0123456789", resource="role/foo-role", service="iam", region="")
+     ),
+    ("arn:aws:iam::123456789012:user/dev/*",
+     dict(partition="aws", account_id="123456789012", resource="user/dev/*", service='iam', region="")
+     ),
+    ("arn:aws:iam::123456789012:user:test",
+     dict(partition="aws", account_id="123456789012", resource="user:test", service="iam", region="")
+     ),
+    ("arn:aws-cn:iam::123456789012:user:test",
+     dict(partition="aws-cn", account_id="123456789012", resource="user:test", service="iam", region="")
+     ),
+    ("arn:aws:iam::123456789012:user",
+     dict(partition="aws", account_id="123456789012", resource="user", service="iam", region="")
+     ),
+    ("arn:aws:s3:::my_corporate_bucket/*",
+     dict(partition="aws", account_id="", resource="my_corporate_bucket/*", service="s3", region="")
+     ),
+    ("arn:aws:s3:::my_corporate_bucket/Development/*",
+     dict(partition="aws", account_id="", resource="my_corporate_bucket/Development/*", service="s3", region="")
+     ),
+    ("arn:aws:rds:es-east-1:000000000000:snapshot:rds:my-db-snapshot",
+     dict(partition="aws", account_id="000000000000", resource="snapshot:rds:my-db-snapshot", service="rds", region="es-east-1")
+     ),
+    ("arn:aws:cloudformation:us-east-1:012345678901:changeSet/Ansible-StackName-c6884247ede41eb0",
+     dict(partition="aws", account_id="012345678901", resource="changeSet/Ansible-StackName-c6884247ede41eb0", service="cloudformation", region="us-east-1")
+     ),
+    ("test:aws:iam::123456789012:user", None),
+    ("arn:aws::us-east-1:123456789012:user", None),
+]
+
+
+@pytest.mark.parametrize("input_params, output_params", arn_data)
+def test_parse_aws_iam_arn(input_params, output_params):
+
+    assert parse_aws_arn(input_params) == output_params