diff options
Diffstat (limited to 'ansible_collections/amazon')
25 files changed, 1302 insertions, 180 deletions
diff --git a/ansible_collections/amazon/aws/.github/workflows/docs-pr.yml b/ansible_collections/amazon/aws/.github/workflows/docs-pr.yml index 8cc39d8f5..9158b038f 100644 --- a/ansible_collections/amazon/aws/.github/workflows/docs-pr.yml +++ b/ansible_collections/amazon/aws/.github/workflows/docs-pr.yml @@ -21,6 +21,7 @@ jobs: intersphinx-links: | community_aws:https://ansible-collections.github.io/community.aws/branch/main/ ansible_devel:https://docs.ansible.com/ansible-core/devel/ + artifact-name: ${{ github.event.repository.name }}_validate_docs_${{ github.event.pull_request.head.sha }} build-docs: diff --git a/ansible_collections/amazon/aws/CHANGELOG.rst b/ansible_collections/amazon/aws/CHANGELOG.rst index 3e5dc1c2c..219d962b4 100644 --- a/ansible_collections/amazon/aws/CHANGELOG.rst +++ b/ansible_collections/amazon/aws/CHANGELOG.rst @@ -4,6 +4,27 @@ amazon.aws Release Notes .. contents:: Topics +v7.5.0 +====== + +Release Summary +--------------- + +This release includes a new feature for the ``iam_user_info`` module, bugfixes for the ``cloudwatchlogs_log_group_info`` and ``s3_object`` modules and the inventory plugins, and some internal refactoring of ``module_utils``. + +Minor Changes +------------- + +- iam_user_info - Add ``login_profile`` to return info that is get from a user, to know if they can login from AWS console (https://github.com/ansible-collections/amazon.aws/pull/2012). +- module_utils.iam - refactored normalization functions to use ``boto3_resource_to_ansible_dict()`` and ``boto3_resource_list_to_ansible_dict()`` (https://github.com/ansible-collections/amazon.aws/pull/2006). +- module_utils.transformations - add ``boto3_resource_to_ansible_dict()`` and ``boto3_resource_list_to_ansible_dict()`` helpers (https://github.com/ansible-collections/amazon.aws/pull/2006). + +Bugfixes +-------- + +- cloudwatchlogs_log_group_info - Implement exponential backoff when making API calls to prevent throttling exceptions (https://github.com/ansible-collections/amazon.aws/issues/2011). +- plugin_utils.inventory - Ensure templated options in lookup plugins are converted (https://github.com/ansible-collections/amazon.aws/issues/1955). +- s3_object - Fix the issue when copying an object with overriding metadata. (https://github.com/ansible-collections/amazon.aws/issues/1991). v7.4.0 ====== @@ -232,7 +253,6 @@ Release Summary This release is the last planned minor release of ``amazon.aws`` prior to the release of 7.0.0. It includes documentation fixes as well as minor changes and bug fixes for the ``ec2_ami`` and ``elb_application_lb_info`` modules. - Minor Changes ------------- @@ -564,7 +584,6 @@ Release Summary This release brings few bugfixes. - Bugfixes -------- @@ -585,7 +604,6 @@ Release Summary This release contains a number of bugfixes, new features and new modules. This is the last planned minor release prior to the release of version 6.0.0. - Minor Changes ------------- @@ -680,7 +698,6 @@ Release Summary A minor release containing bugfixes for the ``ec2_eni_info`` module and the ``aws_rds`` inventory plugin, as well as improvements to the ``rds_instance`` module. - Minor Changes ------------- @@ -904,7 +921,6 @@ Release Summary This release contains a minor bugfix for the ``ec2_vol`` module, some minor work on the ``ec2_key`` module, and various documentation fixes. This is the last planned release of the 4.x series. - Minor Changes ------------- @@ -944,7 +960,6 @@ The amazon.aws 4.3.0 release includes a number of minor bug fixes and improvemen Following the release of amazon.aws 5.0.0, backports to the 4.x series will be limited to security issues and bugfixes. - Minor Changes ------------- @@ -1099,7 +1114,6 @@ Release Summary Following the release of amazon.aws 5.0.0, 3.5.0 is a bugfix release and the final planned release for the 3.x series. - Minor Changes ------------- diff --git a/ansible_collections/amazon/aws/FILES.json b/ansible_collections/amazon/aws/FILES.json index 1f9947ab7..61352c6b6 100644 --- a/ansible_collections/amazon/aws/FILES.json +++ b/ansible_collections/amazon/aws/FILES.json @@ -123,7 +123,7 @@ "name": ".github/workflows/docs-pr.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "befbd2d31f5509f704e57c06b07c42fa7867dd353ab3d24856eb865cf8d44b00", + "chksum_sha256": "12f439dd44738a38b1468b670158eaf719539b91f0570d2b9a2f0565ef1de659", "format": 1 }, { @@ -221,7 +221,7 @@ "name": "changelogs/changelog.yaml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8484f733ac3da28af9d97a0f85ecc2f3d601009ed6d55d02f8a3cac6dc32eea9", + "chksum_sha256": "fab623a9b576e9d450f5285c5ad77eed36f30882a396eeba97046f4b8fdbf3cd", "format": 1 }, { @@ -256,7 +256,7 @@ "name": "docs/docsite/rst/CHANGELOG.rst", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "b3a5af02bc807a9248c3820f9f07c6ce0fbf5f75f22613ae3e79a795d34165fc", + "chksum_sha256": "f60899f9e09f217d9c8963676ddad7d070ce9233e0f32c02b96ad1839ec3cd9f", "format": 1 }, { @@ -543,7 +543,7 @@ "name": "plugins/module_utils/common.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c2056cf9ef583ee29ac1165aa03e5cf165830ed6d1703c275c4c9d154222f3c3", + "chksum_sha256": "cef7b396d560a646961755d2a54c7131e553dfe26fbb26e04be073cce5bb0095", "format": 1 }, { @@ -599,7 +599,7 @@ "name": "plugins/module_utils/iam.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8aaa38e784250525c884b936370b2db5ff61b84fcd62a30c239a5e3dc8e20ca3", + "chksum_sha256": "1cd5d5532049e4afd2858a35480cd173f72c4ed9174c67bb26186e47fe183ba5", "format": 1 }, { @@ -662,7 +662,7 @@ "name": "plugins/module_utils/transformation.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "77e7b561643de0ed96b958af3ec6694edae360c8fb4e51ffa4144dace02063fe", + "chksum_sha256": "882308fe4ef2b74fcb41dd09b0e6575d47e2f3d6a3f2164d8dc1706cec213f0c", "format": 1 }, { @@ -851,7 +851,7 @@ "name": "plugins/modules/cloudwatchlogs_log_group_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "00a40c6593d6cc582205943f247a33d7621e4ffa49410c9476e6fefc4e800ccd", + "chksum_sha256": "03d947455d91f77a833dd705fc05d2882a62786e1e09968f7cc8b668964d56f6", "format": 1 }, { @@ -1243,7 +1243,7 @@ "name": "plugins/modules/iam_user_info.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "92d2d3f21e43a53f4e25ed497c76d2935ed533b8bdbf9aa5af41a4a8c27e2cb7", + "chksum_sha256": "11b3a159e2c6542b861c94c5d5f1eae8375f1e0e4ecef7ad413932254f6a3157", "format": 1 }, { @@ -1446,7 +1446,7 @@ "name": "plugins/modules/s3_object.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "36bf675910b9b5a9a932fc90c1ceb4f2c54d90a191bf9c2cb8a47cd5ebea032f", + "chksum_sha256": "47610c1c778b26def1d5c3aef8c5f6d2089537445ae420d1da5f1afb4a0b8600", "format": 1 }, { @@ -1495,7 +1495,7 @@ "name": "plugins/plugin_utils/inventory.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1c652b1e2733fe92f7d95ea6f76bed638fbed402e7ffe15753e896328869f4b3", + "chksum_sha256": "2bbe05440920c867bd4b8935a0a97963211b74149ca6c83d9990f9b6a5e4e6ae", "format": 1 }, { @@ -2531,7 +2531,7 @@ "name": "tests/integration/targets/ec2_ami/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9433c585f807cca76bb83c1cbe0a5d0a85966ead2ca30dde4cefd71be3cf155f", + "chksum_sha256": "919ee94ad149f90556ea20402e2393253157946b2d0e32a71957a4f58cfa6018", "format": 1 }, { @@ -4302,7 +4302,7 @@ "name": "tests/integration/targets/ec2_security_group/tasks/multi_nested_target.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "be781072f2d349ec5c93cbb05e40ef71846d2f4b5884ad297a736d3b0c72e542", + "chksum_sha256": "79aa1549464410bff8ff9f0272780d8b408a370d088100617656975024b6fef2", "format": 1 }, { @@ -6115,7 +6115,7 @@ "name": "tests/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c031a3d0f6a2c17914bedc3a7b37c475cbd55420cb7ae7fe4c6f24855bb5565d", + "chksum_sha256": "44bab5285a394bde857e65e324b15039d248a20c9a05b5eec45eac2ce312550e", "format": 1 }, { @@ -6259,6 +6259,13 @@ "format": 1 }, { + "name": "tests/integration/targets/inventory_aws_ec2/templates/config.ini.j2", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "777e02f32a471e59d05fc66ef26716aaaa368085f991f179866d8f38a7d44aae", + "format": 1 + }, + { "name": "tests/integration/targets/inventory_aws_ec2/templates/inventory.yml.j2", "ftype": "file", "chksum_type": "sha256", @@ -6332,7 +6339,7 @@ "name": "tests/integration/targets/inventory_aws_ec2/templates/inventory_with_template.yml.j2", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "156031d22e1ae780ea2012e96afd68679bd27ee0cfe4bec4b03bf556f5477375", + "chksum_sha256": "a15b10d8b1dc85d869d6df0054d44d95b68d53d5cda6c9372dc86d72e260a00d", "format": 1 }, { @@ -9447,7 +9454,7 @@ "name": "tests/integration/targets/s3_object/tasks/copy_object.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9b412470d4ef414efef1f407f84c72f1685f77c6ef2551517cdfa4cd6ab1515d", + "chksum_sha256": "a935fc78dc9973eac71decd7308d4e827c27a7f30b29c7c416f9f676163b4ec7", "format": 1 }, { @@ -9475,7 +9482,7 @@ "name": "tests/integration/targets/s3_object/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c7568cf168c02a65bef0d921cea02304fd568fa6cd93175395bed359e6a14cf7", + "chksum_sha256": "697d66379fe4c4b33f82b1f7ac363f1e25331480201701b23d28494165346043", "format": 1 }, { @@ -9503,7 +9510,7 @@ "name": "tests/integration/targets/s3_object/aliases", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "da6542f9ffbf6dfd96214cc7e7c08e8bd4662a5479a21ad1b3f79ad2b163c9ad", + "chksum_sha256": "e217807ba499d974106cc2132230a411f7564e6c337b0a731a6301c5a1d69619", "format": 1 }, { @@ -10284,6 +10291,13 @@ "format": 1 }, { + "name": "tests/unit/module_utils/iam/test_iam_resource_transforms.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9ba95e45f5d2c2502a8ae6ea6dacc35e340f931a4d4d7fde2064ba0f89018ed0", + "format": 1 + }, + { "name": "tests/unit/module_utils/iam/test_validate_iam_identifiers.py", "ftype": "file", "chksum_type": "sha256", @@ -10452,6 +10466,13 @@ "format": 1 }, { + "name": "tests/unit/module_utils/transformation/test_boto3_resource_to_ansible_dict.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8b27e0ccb712e9bf43897b4226b99318c42e27f344f660326d13699bf1b45c45", + "format": 1 + }, + { "name": "tests/unit/module_utils/transformation/test_map_complex_type.py", "ftype": "file", "chksum_type": "sha256", @@ -10630,7 +10651,7 @@ "name": "tests/unit/plugin_utils/inventory/test_inventory_base.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4668d08e6f30a2aae02c0b27174dc759f4ec554d7207bcf33f879e9dee67720e", + "chksum_sha256": "ebae64f9db5ec8444c35b5d41127f1a5191ed10cd34acb6c89d0e5de2879b33d", "format": 1 }, { @@ -10693,7 +10714,7 @@ "name": "tests/unit/plugins/inventory/test_aws_ec2.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "cbb0ce6de6b22c4d62588d230353de215a4ccb273838b4870c17da8548ad3f16", + "chksum_sha256": "8556d8258e0d0aaeadd02898d1198152c478fa23677ff51990a0e1420f99e482", "format": 1 }, { @@ -11659,7 +11680,7 @@ "name": "CHANGELOG.rst", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "b3a5af02bc807a9248c3820f9f07c6ce0fbf5f75f22613ae3e79a795d34165fc", + "chksum_sha256": "f60899f9e09f217d9c8963676ddad7d070ce9233e0f32c02b96ad1839ec3cd9f", "format": 1 }, { diff --git a/ansible_collections/amazon/aws/MANIFEST.json b/ansible_collections/amazon/aws/MANIFEST.json index 3eb50b454..cd19577e3 100644 --- a/ansible_collections/amazon/aws/MANIFEST.json +++ b/ansible_collections/amazon/aws/MANIFEST.json @@ -2,7 +2,7 @@ "collection_info": { "namespace": "amazon", "name": "aws", - "version": "7.4.0", + "version": "7.5.0", "authors": [ "Ansible (https://github.com/ansible)" ], @@ -25,7 +25,7 @@ "name": "FILES.json", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "524e7534581a787eb78ed1366e0d732b0673fcb3e4b5df5ffae1ca6c92c0ffe5", + "chksum_sha256": "34bdeb6686c662a0a524953b80ae953b67d328542ba5d43bc1e4058c45ed4136", "format": 1 }, "format": 1 diff --git a/ansible_collections/amazon/aws/changelogs/changelog.yaml b/ansible_collections/amazon/aws/changelogs/changelog.yaml index 24f7b8247..587c55a28 100644 --- a/ansible_collections/amazon/aws/changelogs/changelog.yaml +++ b/ansible_collections/amazon/aws/changelogs/changelog.yaml @@ -2692,7 +2692,7 @@ releases: - iam_user_info - the ``path`` parameter has been renamed ``path_prefix`` for consistency with other IAM modules, ``path`` remains as an alias. No change to playbooks is required (https://github.com/ansible-collections/amazon.aws/pull/1933). - release_summary: This release includes new features and a bugfix. + release_summary: This release includes new features and a bugfix. fragments: - 1918-ec2_instance-add-support-to-modify-metadata-options.yml - 20231206-iam_user_path.yml @@ -2777,3 +2777,28 @@ releases: - 20240227-iam-refactor.yml - release_summary.yml release_date: '2024-03-05' + 7.5.0: + changes: + bugfixes: + - cloudwatchlogs_log_group_info - Implement exponential backoff when making + API calls to prevent throttling exceptions (https://github.com/ansible-collections/amazon.aws/issues/2011). + - plugin_utils.inventory - Ensure templated options in lookup plugins are converted + (https://github.com/ansible-collections/amazon.aws/issues/1955). + - s3_object - Fix the issue when copying an object with overriding metadata. + (https://github.com/ansible-collections/amazon.aws/issues/1991). + minor_changes: + - iam_user_info - Add ``login_profile`` to return info that is get from a user, + to know if they can login from AWS console (https://github.com/ansible-collections/amazon.aws/pull/2012). + - module_utils.iam - refactored normalization functions to use ``boto3_resource_to_ansible_dict()`` + and ``boto3_resource_list_to_ansible_dict()`` (https://github.com/ansible-collections/amazon.aws/pull/2006). + - module_utils.transformations - add ``boto3_resource_to_ansible_dict()`` and + ``boto3_resource_list_to_ansible_dict()`` helpers (https://github.com/ansible-collections/amazon.aws/pull/2006). + release_summary: This release includes a new feature for the ``iam_user_info`` + module, bugfixes for the ``cloudwatchlogs_log_group_info`` and ``s3_object`` + modules and the inventory plugins, and some internal refactoring of ``module_utils``. + fragments: + - 2006-normalize-refactor.yml + - 20240314-cloudwatchlogs_log_group_info-fix-throttling-exceptions.yml + - 20240314-s3_object-copy-mode-with-metadata.yml + - 20240321-iam-user-info.yml + release_date: '2024-04-03' diff --git a/ansible_collections/amazon/aws/docs/docsite/rst/CHANGELOG.rst b/ansible_collections/amazon/aws/docs/docsite/rst/CHANGELOG.rst index 3e5dc1c2c..219d962b4 100644 --- a/ansible_collections/amazon/aws/docs/docsite/rst/CHANGELOG.rst +++ b/ansible_collections/amazon/aws/docs/docsite/rst/CHANGELOG.rst @@ -4,6 +4,27 @@ amazon.aws Release Notes .. contents:: Topics +v7.5.0 +====== + +Release Summary +--------------- + +This release includes a new feature for the ``iam_user_info`` module, bugfixes for the ``cloudwatchlogs_log_group_info`` and ``s3_object`` modules and the inventory plugins, and some internal refactoring of ``module_utils``. + +Minor Changes +------------- + +- iam_user_info - Add ``login_profile`` to return info that is get from a user, to know if they can login from AWS console (https://github.com/ansible-collections/amazon.aws/pull/2012). +- module_utils.iam - refactored normalization functions to use ``boto3_resource_to_ansible_dict()`` and ``boto3_resource_list_to_ansible_dict()`` (https://github.com/ansible-collections/amazon.aws/pull/2006). +- module_utils.transformations - add ``boto3_resource_to_ansible_dict()`` and ``boto3_resource_list_to_ansible_dict()`` helpers (https://github.com/ansible-collections/amazon.aws/pull/2006). + +Bugfixes +-------- + +- cloudwatchlogs_log_group_info - Implement exponential backoff when making API calls to prevent throttling exceptions (https://github.com/ansible-collections/amazon.aws/issues/2011). +- plugin_utils.inventory - Ensure templated options in lookup plugins are converted (https://github.com/ansible-collections/amazon.aws/issues/1955). +- s3_object - Fix the issue when copying an object with overriding metadata. (https://github.com/ansible-collections/amazon.aws/issues/1991). v7.4.0 ====== @@ -232,7 +253,6 @@ Release Summary This release is the last planned minor release of ``amazon.aws`` prior to the release of 7.0.0. It includes documentation fixes as well as minor changes and bug fixes for the ``ec2_ami`` and ``elb_application_lb_info`` modules. - Minor Changes ------------- @@ -564,7 +584,6 @@ Release Summary This release brings few bugfixes. - Bugfixes -------- @@ -585,7 +604,6 @@ Release Summary This release contains a number of bugfixes, new features and new modules. This is the last planned minor release prior to the release of version 6.0.0. - Minor Changes ------------- @@ -680,7 +698,6 @@ Release Summary A minor release containing bugfixes for the ``ec2_eni_info`` module and the ``aws_rds`` inventory plugin, as well as improvements to the ``rds_instance`` module. - Minor Changes ------------- @@ -904,7 +921,6 @@ Release Summary This release contains a minor bugfix for the ``ec2_vol`` module, some minor work on the ``ec2_key`` module, and various documentation fixes. This is the last planned release of the 4.x series. - Minor Changes ------------- @@ -944,7 +960,6 @@ The amazon.aws 4.3.0 release includes a number of minor bug fixes and improvemen Following the release of amazon.aws 5.0.0, backports to the 4.x series will be limited to security issues and bugfixes. - Minor Changes ------------- @@ -1099,7 +1114,6 @@ Release Summary Following the release of amazon.aws 5.0.0, 3.5.0 is a bugfix release and the final planned release for the 3.x series. - Minor Changes ------------- diff --git a/ansible_collections/amazon/aws/plugins/module_utils/common.py b/ansible_collections/amazon/aws/plugins/module_utils/common.py index 673915725..41ba80231 100644 --- a/ansible_collections/amazon/aws/plugins/module_utils/common.py +++ b/ansible_collections/amazon/aws/plugins/module_utils/common.py @@ -4,7 +4,7 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) AMAZON_AWS_COLLECTION_NAME = "amazon.aws" -AMAZON_AWS_COLLECTION_VERSION = "7.4.0" +AMAZON_AWS_COLLECTION_VERSION = "7.5.0" _collection_info_context = { diff --git a/ansible_collections/amazon/aws/plugins/module_utils/iam.py b/ansible_collections/amazon/aws/plugins/module_utils/iam.py index 430823f3b..56920d53e 100644 --- a/ansible_collections/amazon/aws/plugins/module_utils/iam.py +++ b/ansible_collections/amazon/aws/plugins/module_utils/iam.py @@ -4,7 +4,6 @@ # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) import re -from copy import deepcopy try: import botocore @@ -12,17 +11,20 @@ except ImportError: pass # Modules are responsible for handling this. from ansible.module_utils._text import to_native -from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from .arn import parse_aws_arn from .arn import validate_aws_arn from .botocore import is_boto3_error_code -from .botocore import normalize_boto3_result from .errors import AWSErrorHandler from .exceptions import AnsibleAWSError from .retries import AWSRetry from .tagging import ansible_dict_to_boto3_tag_list -from .tagging import boto3_tag_list_to_ansible_dict +from .transformation import AnsibleAWSResource +from .transformation import AnsibleAWSResourceList +from .transformation import BotoResource +from .transformation import BotoResourceList +from .transformation import boto3_resource_list_to_ansible_dict +from .transformation import boto3_resource_to_ansible_dict class AnsibleIAMError(AnsibleAWSError): @@ -198,66 +200,6 @@ def get_iam_managed_policy_version(client, arn, version): return client.get_policy_version(PolicyArn=arn, VersionId=version)["PolicyVersion"] -def normalize_iam_mfa_device(device): - """Converts IAM MFA Device from the CamelCase boto3 format to the snake_case Ansible format""" - if not device: - return device - camel_device = camel_dict_to_snake_dict(device) - camel_device["tags"] = boto3_tag_list_to_ansible_dict(device.pop("Tags", [])) - return camel_device - - -def normalize_iam_mfa_devices(devices): - """Converts a list of IAM MFA Devices from the CamelCase boto3 format to the snake_case Ansible format""" - if not devices: - return [] - devices = [normalize_iam_mfa_device(d) for d in devices] - return devices - - -def normalize_iam_user(user): - """Converts IAM users from the CamelCase boto3 format to the snake_case Ansible format""" - if not user: - return user - camel_user = camel_dict_to_snake_dict(user) - camel_user["tags"] = boto3_tag_list_to_ansible_dict(user.pop("Tags", [])) - return camel_user - - -def normalize_iam_policy(policy): - """Converts IAM policies from the CamelCase boto3 format to the snake_case Ansible format""" - if not policy: - return policy - camel_policy = camel_dict_to_snake_dict(policy) - camel_policy["tags"] = boto3_tag_list_to_ansible_dict(policy.get("Tags", [])) - return camel_policy - - -def normalize_iam_group(group): - """Converts IAM Groups from the CamelCase boto3 format to the snake_case Ansible format""" - if not group: - return group - camel_group = camel_dict_to_snake_dict(normalize_boto3_result(group)) - return camel_group - - -def normalize_iam_access_key(access_key): - """Converts IAM access keys from the CamelCase boto3 format to the snake_case Ansible format""" - if not access_key: - return access_key - camel_key = camel_dict_to_snake_dict(normalize_boto3_result(access_key)) - return camel_key - - -def normalize_iam_access_keys(access_keys): - """Converts a list of IAM access keys from the CamelCase boto3 format to the snake_case Ansible format""" - if not access_keys: - return [] - access_keys = [normalize_iam_access_key(k) for k in access_keys] - sorted_keys = sorted(access_keys, key=lambda d: d.get("create_date", None)) - return sorted_keys - - def convert_managed_policy_names_to_arns(client, policy_names): if all(validate_aws_arn(policy, service="iam") for policy in policy_names if policy is not None): return policy_names @@ -386,47 +328,6 @@ def list_iam_instance_profiles(client, name=None, prefix=None, role=None): return _list_iam_instance_profiles(client) -def normalize_iam_instance_profile(profile, _v7_compat=False): - """ - Converts a boto3 format IAM instance profile into "Ansible" format - - _v7_compat is deprecated and will be removed in release after 2025-05-01 DO NOT USE. - """ - - new_profile = camel_dict_to_snake_dict(deepcopy(profile)) - if profile.get("Roles"): - new_profile["roles"] = [normalize_iam_role(role, _v7_compat=_v7_compat) for role in profile.get("Roles")] - if profile.get("Tags"): - new_profile["tags"] = boto3_tag_list_to_ansible_dict(profile.get("Tags")) - else: - new_profile["tags"] = {} - new_profile["original"] = profile - return new_profile - - -def normalize_iam_role(role, _v7_compat=False): - """ - Converts a boto3 format IAM instance role into "Ansible" format - - _v7_compat is deprecated and will be removed in release after 2025-05-01 DO NOT USE. - """ - - new_role = camel_dict_to_snake_dict(deepcopy(role)) - if role.get("InstanceProfiles"): - new_role["instance_profiles"] = [ - normalize_iam_instance_profile(profile, _v7_compat=_v7_compat) for profile in role.get("InstanceProfiles") - ] - if role.get("AssumeRolePolicyDocument"): - if _v7_compat: - # new_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument") - new_role["assume_role_policy_document_raw"] = role.get("AssumeRolePolicyDocument") - else: - new_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument") - - new_role["tags"] = boto3_tag_list_to_ansible_dict(role.get("Tags", [])) - return new_role - - @IAMErrorHandler.common_error_handler("tag instance profile") @AWSRetry.jittered_backoff() def tag_iam_instance_profile(client, name, tags): @@ -497,3 +398,83 @@ def validate_iam_identifiers(resource_type, name=None, path=None): return path_problem return None + + +def normalize_iam_mfa_device(device: BotoResource) -> AnsibleAWSResource: + """Converts IAM MFA Device from the CamelCase boto3 format to the snake_case Ansible format""" + # MFA Devices don't support Tags (as of 1.34.52) + return boto3_resource_to_ansible_dict(device) + + +def normalize_iam_mfa_devices(devices: BotoResourceList) -> AnsibleAWSResourceList: + """Converts a list of IAM MFA Devices from the CamelCase boto3 format to the snake_case Ansible format""" + # MFA Devices don't support Tags (as of 1.34.52) + return boto3_resource_list_to_ansible_dict(devices) + + +def normalize_iam_user(user: BotoResource) -> AnsibleAWSResource: + """Converts IAM users from the CamelCase boto3 format to the snake_case Ansible format""" + return boto3_resource_to_ansible_dict(user) + + +def normalize_iam_policy(policy: BotoResource) -> AnsibleAWSResource: + """Converts IAM policies from the CamelCase boto3 format to the snake_case Ansible format""" + return boto3_resource_to_ansible_dict(policy) + + +def normalize_iam_group(group: BotoResource) -> AnsibleAWSResource: + """Converts IAM Groups from the CamelCase boto3 format to the snake_case Ansible format""" + # Groups don't support Tags (as of 1.34.52) + return boto3_resource_to_ansible_dict(group, force_tags=False) + + +def normalize_iam_access_key(access_key: BotoResource) -> AnsibleAWSResource: + """Converts IAM access keys from the CamelCase boto3 format to the snake_case Ansible format""" + # Access Keys don't support Tags (as of 1.34.52) + return boto3_resource_to_ansible_dict(access_key, force_tags=False) + + +def normalize_iam_access_keys(access_keys: BotoResourceList) -> AnsibleAWSResourceList: + """Converts a list of IAM access keys from the CamelCase boto3 format to the snake_case Ansible format""" + # Access Keys don't support Tags (as of 1.34.52) + if not access_keys: + return access_keys + access_keys = boto3_resource_list_to_ansible_dict(access_keys, force_tags=False) + return sorted(access_keys, key=lambda d: d.get("create_date", None)) + + +def normalize_iam_instance_profile(profile: BotoResource) -> AnsibleAWSResource: + """ + Converts a boto3 format IAM instance profile into "Ansible" format + + _v7_compat is deprecated and will be removed in release after 2025-05-01 DO NOT USE. + """ + transforms = {"Roles": _normalize_iam_roles} + transformed_profile = boto3_resource_to_ansible_dict(profile, nested_transforms=transforms) + return transformed_profile + + +def normalize_iam_role(role: BotoResource, _v7_compat: bool = False) -> AnsibleAWSResource: + """ + Converts a boto3 format IAM instance role into "Ansible" format + + _v7_compat is deprecated and will be removed in release after 2025-05-01 DO NOT USE. + """ + transforms = {"InstanceProfiles": _normalize_iam_instance_profiles} + ignore_list = [] if _v7_compat else ["AssumeRolePolicyDocument"] + transformed_role = boto3_resource_to_ansible_dict(role, nested_transforms=transforms, ignore_list=ignore_list) + if _v7_compat and role.get("AssumeRolePolicyDocument"): + transformed_role["assume_role_policy_document_raw"] = role["AssumeRolePolicyDocument"] + return transformed_role + + +def _normalize_iam_instance_profiles(profiles: BotoResourceList) -> AnsibleAWSResourceList: + if not profiles: + return profiles + return [normalize_iam_instance_profile(p) for p in profiles] + + +def _normalize_iam_roles(roles: BotoResourceList) -> AnsibleAWSResourceList: + if not roles: + return roles + return [normalize_iam_role(r) for r in roles] diff --git a/ansible_collections/amazon/aws/plugins/module_utils/transformation.py b/ansible_collections/amazon/aws/plugins/module_utils/transformation.py index 708736fc0..a5bc23607 100644 --- a/ansible_collections/amazon/aws/plugins/module_utils/transformation.py +++ b/ansible_collections/amazon/aws/plugins/module_utils/transformation.py @@ -28,9 +28,26 @@ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from copy import deepcopy +from typing import Any +from typing import Callable +from typing import Mapping +from typing import Optional +from typing import Sequence +from typing import Union + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict from ansible.module_utils.six import integer_types from ansible.module_utils.six import string_types +from .botocore import normalize_boto3_result +from .tagging import boto3_tag_list_to_ansible_dict + +BotoResource = Union[None, Mapping[str, Any]] +BotoResourceList = Union[None, Sequence[Mapping[str, Any]]] +AnsibleAWSResource = Union[None, Mapping[str, Any]] +AnsibleAWSResourceList = Union[None, Sequence[Mapping[str, Any]]] + def ansible_dict_to_boto3_filter_list(filters_dict): """Convert an Ansible dict of filters to list of dicts that boto3 can use @@ -133,3 +150,82 @@ def scrub_none_parameters(parameters, descend_into_lists=True): clean_parameters[k] = v return clean_parameters + + +def _perform_nested_transforms( + resource: Mapping[str, Any], + nested_transforms: Optional[Mapping[str, Callable]], +) -> Mapping[str, Any]: + if not nested_transforms: + return resource + + for k, transform in nested_transforms.items(): + if k in resource: + resource[k] = transform(resource[k]) + + return resource + + +def boto3_resource_to_ansible_dict( + resource: BotoResource, + transform_tags: bool = True, + force_tags: bool = True, + normalize: bool = True, + ignore_list: Optional[Sequence[str]] = None, + nested_transforms: Optional[Mapping[str, Callable]] = None, +) -> AnsibleAWSResource: + """ + Transforms boto3-style (CamelCase) resource to the ansible-style (snake_case). + + :param resource: a dictionary representing the resource + :param transform_tags: whether or not to perform "tag list" to "dictionary" conversion on the "Tags" key + :param normalize: whether resources should be passed through .botocore.normalize_boto3_result + :param ignore_list: a list of keys, the contents of which should not be transformed + :param nested_transforms: a mapping of keys to Callable, the Callable will only be passed the value for the key + in the resource dictionary + :return: dictionary representing the transformed resource + """ + if not resource: + return resource + ignore_list = ignore_list or [] + nested_transforms = nested_transforms or {} + + transformed_resource = deepcopy(resource) + if normalize: + transformed_resource = normalize_boto3_result(transformed_resource) + transformed_resource = _perform_nested_transforms(transformed_resource, nested_transforms) + ignore_list = [*ignore_list, *nested_transforms] + camel_resource = camel_dict_to_snake_dict(transformed_resource, ignore_list=ignore_list) + if transform_tags and "Tags" in resource: + camel_resource["tags"] = boto3_tag_list_to_ansible_dict(resource["Tags"]) + if force_tags and "Tags" not in resource: + camel_resource["tags"] = {} + + return camel_resource + + +def boto3_resource_list_to_ansible_dict( + resource_list: BotoResourceList, + transform_tags: bool = True, + force_tags: bool = True, + normalize: bool = True, + ignore_list: Optional[Sequence[str]] = None, + nested_transforms: Optional[Mapping[str, Callable]] = None, +) -> AnsibleAWSResourceList: + """ + Transforms a list of boto3-style (CamelCase) resources to the ansible-style (snake_case). + + :param resource_list: a list of dictionaries representing the resources + :param transform_tags: whether or not to perform "tag list" to "dictionary" conversion on the "Tags" key + :param normalize: whether resources should be passed through .botocore.normalize_boto3_result() + :param ignore_list: a list of keys, the contents of which should not be transformed + :param nested_transforms: a mapping of keys to Callable, the Callable will only be passed the value for the key + in the resource dictionary + :return: list of dictionaries representing the transformed resources + """ + if not resource_list: + return resource_list + return [ + boto3_resource_to_ansible_dict(resource, transform_tags, force_tags, normalize, ignore_list, nested_transforms) + for resource in resource_list + ] diff --git a/ansible_collections/amazon/aws/plugins/modules/cloudwatchlogs_log_group_info.py b/ansible_collections/amazon/aws/plugins/modules/cloudwatchlogs_log_group_info.py index 0cfe22e22..453d268d5 100644 --- a/ansible_collections/amazon/aws/plugins/modules/cloudwatchlogs_log_group_info.py +++ b/ansible_collections/amazon/aws/plugins/modules/cloudwatchlogs_log_group_info.py @@ -82,6 +82,18 @@ from ansible.module_utils.common.dict_transformations import camel_dict_to_snake from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry + + +@AWSRetry.exponential_backoff() +def list_tags_log_group_with_backoff(client, log_group_name): + return client.list_tags_log_group(logGroupName=log_group_name) + + +@AWSRetry.exponential_backoff() +def describe_log_groups_with_backoff(client, **kwargs): + paginator = client.get_paginator("describe_log_groups") + return paginator.paginate(**kwargs).build_full_result() def describe_log_group(client, log_group_name, module): @@ -89,15 +101,14 @@ def describe_log_group(client, log_group_name, module): if log_group_name: params["logGroupNamePrefix"] = log_group_name try: - paginator = client.get_paginator("describe_log_groups") - desc_log_group = paginator.paginate(**params).build_full_result() + desc_log_group = describe_log_groups_with_backoff(client, **params) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg=f"Unable to describe log group {log_group_name}") for log_group in desc_log_group["logGroups"]: log_group_name = log_group["logGroupName"] try: - tags = client.list_tags_log_group(logGroupName=log_group_name) + tags = list_tags_log_group_with_backoff(client, log_group_name) except is_boto3_error_code("AccessDeniedException"): tags = {} module.warn(f"Permission denied listing tags for log group {log_group_name}") diff --git a/ansible_collections/amazon/aws/plugins/modules/iam_user_info.py b/ansible_collections/amazon/aws/plugins/modules/iam_user_info.py index 259d26803..2ddbe1d5a 100644 --- a/ansible_collections/amazon/aws/plugins/modules/iam_user_info.py +++ b/ansible_collections/amazon/aws/plugins/modules/iam_user_info.py @@ -103,14 +103,27 @@ iam_users: type: dict returned: if user exists sample: '{"Env": "Prod"}' + login_profile: + description: Detailed login profile information if the user has access to log in from AWS default console. Returns an empty object {} if no access. + returned: always + type: dict + sample: {"create_date": "2024-03-20T12:50:56+00:00", "password_reset_required": false, "user_name": "i_am_a_user"} """ from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError +from ansible_collections.amazon.aws.plugins.module_utils.iam import IAMErrorHandler from ansible_collections.amazon.aws.plugins.module_utils.iam import get_iam_group from ansible_collections.amazon.aws.plugins.module_utils.iam import get_iam_user from ansible_collections.amazon.aws.plugins.module_utils.iam import list_iam_users from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_user from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry + + +@IAMErrorHandler.list_error_handler("get login profile", {}) +@AWSRetry.jittered_backoff() +def check_console_access(connection, user_name): + return connection.get_login_profile(UserName=user_name)["LoginProfile"] def _list_users(connection, name, group, path): @@ -136,6 +149,8 @@ def _list_users(connection, name, group, path): def list_users(connection, name, group, path): users = _list_users(connection, name, group, path) users = [u for u in users if u is not None] + for user in users: + user["LoginProfile"] = check_console_access(connection, user["UserName"]) return [normalize_iam_user(user) for user in users] @@ -147,7 +162,9 @@ def main(): ) module = AnsibleAWSModule( - argument_spec=argument_spec, mutually_exclusive=[["group", "path_prefix"]], supports_check_mode=True + argument_spec=argument_spec, + mutually_exclusive=[["group", "path_prefix"]], + supports_check_mode=True, ) name = module.params.get("name") diff --git a/ansible_collections/amazon/aws/plugins/modules/s3_object.py b/ansible_collections/amazon/aws/plugins/modules/s3_object.py index 2c4ebe9c3..2cd897c89 100644 --- a/ansible_collections/amazon/aws/plugins/modules/s3_object.py +++ b/ansible_collections/amazon/aws/plugins/modules/s3_object.py @@ -315,7 +315,9 @@ EXAMPLES = r""" object: /my/desired/key.txt src: /usr/local/myfile.txt mode: put - metadata: 'Content-Encoding=gzip,Cache-Control=no-cache' + metadata: + Content-Encoding: gzip + Cache-Control: no-cache - name: PUT/upload with custom headers amazon.aws.s3_object: @@ -1314,6 +1316,11 @@ def copy_object_to_bucket(module, s3, bucket, obj, encrypt, metadata, validate, metadata, ) ) + if metadata: + # 'MetadataDirective' Specifies whether the metadata is copied from the source object or replaced + # with metadata that's provided in the request. The default value is 'COPY', therefore when user + # specifies a metadata we should set it to 'REPLACE' + params.update({"MetadataDirective": "REPLACE"}) s3.copy_object(aws_retry=True, **params) put_object_acl(module, s3, bucket, obj) # Tags diff --git a/ansible_collections/amazon/aws/plugins/plugin_utils/inventory.py b/ansible_collections/amazon/aws/plugins/plugin_utils/inventory.py index 144f77a7a..b0e47f7ef 100644 --- a/ansible_collections/amazon/aws/plugins/plugin_utils/inventory.py +++ b/ansible_collections/amazon/aws/plugins/plugin_utils/inventory.py @@ -33,7 +33,10 @@ class AWSInventoryBase(BaseInventoryPlugin, Constructable, Cacheable, AWSPluginB "secret_key", "session_token", "profile", - "iam_role_name", + "endpoint_url", + "assume_role_arn", + "region", + "regions", ) def __init__(self, templar, options): @@ -48,20 +51,21 @@ class AWSInventoryBase(BaseInventoryPlugin, Constructable, Cacheable, AWSPluginB def get(self, *args): value = self.original_options.get(*args) - if not value: - return value - if args[0] not in self.TEMPLATABLE_OPTIONS: - return value - if not self.templar.is_template(value): + if ( + not value + or not self.templar + or args[0] not in self.TEMPLATABLE_OPTIONS + or not self.templar.is_template(value) + ): return value return self.templar.template(variable=value, disable_lookups=False) def get_options(self, *args): - original_options = super().get_options(*args) - if not self.templar: - return original_options - return self.TemplatedOptions(self.templar, original_options) + return self.TemplatedOptions(self.templar, super().get_options(*args)) + + def get_option(self, option, hostvars=None): + return self.TemplatedOptions(self.templar, {option: super().get_option(option, hostvars)}).get(option) def __init__(self): super().__init__() @@ -109,8 +113,7 @@ class AWSInventoryBase(BaseInventoryPlugin, Constructable, Cacheable, AWSPluginB } def _set_frozen_credentials(self): - options = self.get_options() - iam_role_arn = options.get("assume_role_arn") + iam_role_arn = self.get_option("assume_role_arn") if iam_role_arn: self._freeze_iam_role(iam_role_arn) @@ -136,10 +139,9 @@ class AWSInventoryBase(BaseInventoryPlugin, Constructable, Cacheable, AWSPluginB return None def _boto3_regions(self, service): - options = self.get_options() - - if options.get("regions"): - return options.get("regions") + regions = self.get_option("regions") + if regions: + return regions # boto3 has hard coded lists of available regions for resources, however this does bit-rot # As such we try to query the service, and fall back to ec2 for a list of regions @@ -149,7 +151,7 @@ class AWSInventoryBase(BaseInventoryPlugin, Constructable, Cacheable, AWSPluginB return regions # fallback to local list hardcoded in boto3 if still no regions - session = _boto3_session(options.get("profile")) + session = _boto3_session(self.get_option("profile")) regions = session.get_available_regions(service) if not regions: diff --git a/ansible_collections/amazon/aws/tests/integration/targets/ec2_ami/tasks/main.yml b/ansible_collections/amazon/aws/tests/integration/targets/ec2_ami/tasks/main.yml index a9289b3c1..267e52abb 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/ec2_ami/tasks/main.yml +++ b/ansible_collections/amazon/aws/tests/integration/targets/ec2_ami/tasks/main.yml @@ -708,8 +708,8 @@ tags: Name: "{{ ec2_ami_name }}_permissions" launch_permissions: - org_arns: [arn:aws:organizations::123456789012:organization/o-123ab4cdef] - org_unit_arns: [arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5exampld] + org_arns: ["arn:aws:organizations::123456789012:organization/o-123ab4cdef"] + org_unit_arns: ["arn:aws:organizations::123456789012:ou/o-123example/ou-1234-5exampld"] register: permissions_update_result - name: Get ami info diff --git a/ansible_collections/amazon/aws/tests/integration/targets/ec2_security_group/tasks/multi_nested_target.yml b/ansible_collections/amazon/aws/tests/integration/targets/ec2_security_group/tasks/multi_nested_target.yml index dcb7ac7bb..02057003a 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/ec2_security_group/tasks/multi_nested_target.yml +++ b/ansible_collections/amazon/aws/tests/integration/targets/ec2_security_group/tasks/multi_nested_target.yml @@ -12,7 +12,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -38,7 +38,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -66,7 +66,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -92,7 +92,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -117,7 +117,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -142,7 +142,7 @@ to_port: 8182 cidr_ipv6: - 64:ff9b::/96 - - [2620::/32] + - ["2620::/32"] - proto: tcp ports: 5665 cidr_ip: @@ -167,7 +167,7 @@ from_port: 8182 to_port: 8182 cidr_ipv6: - - [2620::/32, 64:ff9b::/96] + - ["2620::/32", "64:ff9b::/96"] - proto: tcp ports: 5665 cidr_ip: @@ -190,8 +190,8 @@ from_port: 8182 to_port: 8182 cidr_ipv6: - - [2620::/32, 64:ff9b::/96] - - [2001:DB8:A0B:12F0::1/64] + - ["2620::/32", "64:ff9b::/96"] + - ["2001:DB8:A0B:12F0::1/64"] - proto: tcp ports: 5665 cidr_ip: diff --git a/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml index 232911d24..282ca43ee 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml +++ b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml @@ -9,3 +9,8 @@ ansible.builtin.copy: dest: ../test.aws_ec2.yml content: "{{ lookup('template', template_name) }}" + + - name: write ini configuration + ansible.builtin.copy: + dest: ../config.ini + content: "{{ lookup('template', '../templates/config.ini.j2') }}" diff --git a/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/config.ini.j2 b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/config.ini.j2 new file mode 100644 index 000000000..f7320a7fb --- /dev/null +++ b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/config.ini.j2 @@ -0,0 +1,3 @@ +[ansible-test] + +region = {{ aws_region }}
\ No newline at end of file diff --git a/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_template.yml.j2 b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_template.yml.j2 index 44a132c1c..dee7422a9 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_template.yml.j2 +++ b/ansible_collections/amazon/aws/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_template.yml.j2 @@ -5,7 +5,7 @@ secret_key: '{{ aws_secret_key }}' session_token: '{{ security_token }}' {% endif %} regions: -- '{{ aws_region }}' +- '{{ '{{ lookup("ansible.builtin.ini", "region", section="ansible-test", file="config.ini") }}' }}' filters: tag:Name: - '{{ resource_prefix }}' diff --git a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/aliases b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/aliases index d34fac48d..2a1c5ccb6 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/aliases +++ b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/aliases @@ -1,3 +1,4 @@ cloud/aws aws_s3 s3_object_info +time=12m diff --git a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/copy_object.yml b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/copy_object.yml index 9ae36b952..994733d81 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/copy_object.yml +++ b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/copy_object.yml @@ -1,5 +1,12 @@ --- -- block: +- vars: + withmeta_data: + something: exists + version: "1.0.2" + metacopy_data: + name: metacopy + version: "1.0.3" + block: - name: define bucket name used for tests ansible.builtin.set_fact: copy_bucket: @@ -142,6 +149,68 @@ - result is not changed - result.msg == "Key this_key_does_not_exist.txt does not exist in bucket "+copy_bucket.src+"." + # Copy with metadata + - name: Set fact for bucket name + ansible.builtin.set_fact: + bucket_name: "{{ copy_bucket.dst }}" + + - name: Create test bucket + amazon.aws.s3_bucket: + name: "{{ bucket_name }}" + state: present + + - name: Create test object + amazon.aws.s3_object: + bucket: "{{ bucket_name }}" + object: nometa + mode: put + content: "some content" + + - name: Copy and add metadata + amazon.aws.s3_object: + bucket: "{{ bucket_name }}" + object: metacopy + mode: copy + copy_src: + bucket: "{{ bucket_name }}" + object: nometa + metadata: "{{ metacopy_data }}" + + - name: Create test object with metadata + amazon.aws.s3_object: + bucket: "{{ bucket_name }}" + object: withmeta + mode: put + content: "another content" + metadata: "{{ withmeta_data }}" + + - name: Copy and preserve metadata + amazon.aws.s3_object: + bucket: "{{ bucket_name }}" + object: copywithmeta + mode: copy + copy_src: + bucket: "{{ bucket_name }}" + object: withmeta + + - name: Get objects info + amazon.aws.s3_object_info: + bucket_name: "{{ bucket_name }}" + object_name: "{{ item }}" + loop: + - nometa + - metacopy + - withmeta + - copywithmeta + register: obj_info + + - assert: + that: + - obj_info.results | selectattr('item', 'equalto', 'nometa') | map(attribute='object_info.0.object_data.metadata') | first == {} + - obj_info.results | selectattr('item', 'equalto', 'withmeta') | map(attribute='object_info.0.object_data.metadata') | first == withmeta_data + - obj_info.results | selectattr('item', 'equalto', 'metacopy') | map(attribute='object_info.0.object_data.metadata') | first == metacopy_data + - obj_info.results | selectattr('item', 'equalto', 'copywithmeta') | map(attribute='object_info.0.object_data.metadata') | first == withmeta_data + always: - ansible.builtin.include_tasks: delete_bucket.yml with_items: diff --git a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/main.yml b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/main.yml index ed65fe31f..7a8a585de 100644 --- a/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/main.yml +++ b/ansible_collections/amazon/aws/tests/integration/targets/s3_object/tasks/main.yml @@ -837,8 +837,6 @@ that: - binary_files.results[0].stat.checksum == binary_files.results[1].stat.checksum - - ansible.builtin.include_tasks: copy_object.yml - - ansible.builtin.include_tasks: copy_object_acl_disabled_bucket.yml - name: Run tagging tests block: # ============================================================ @@ -1074,6 +1072,8 @@ - (result.tags | length) == 0 - ansible.builtin.include_tasks: copy_recursively.yml + - ansible.builtin.include_tasks: copy_object.yml + - ansible.builtin.include_tasks: copy_object_acl_disabled_bucket.yml always: - name: delete temporary files file: diff --git a/ansible_collections/amazon/aws/tests/unit/module_utils/iam/test_iam_resource_transforms.py b/ansible_collections/amazon/aws/tests/unit/module_utils/iam/test_iam_resource_transforms.py new file mode 100644 index 000000000..28090f993 --- /dev/null +++ b/ansible_collections/amazon/aws/tests/unit/module_utils/iam/test_iam_resource_transforms.py @@ -0,0 +1,583 @@ +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +import dateutil + +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_access_key +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_access_keys +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_group +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_mfa_device +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_mfa_devices +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_policy +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_role +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_user + +# The various normalize_ functions are based upon ..transformation.boto3_resource_to_ansible_dict +# As such these tests will be relatively light touch. + +example_date1_txt = "2020-12-30T00:00:00.000Z" +example_date2_txt = "2021-04-26T01:23:58.000Z" +example_date1_iso = "2020-12-30T00:00:00+00:00" +example_date2_iso = "2021-04-26T01:23:58+00:00" +example_date1 = dateutil.parser.parse(example_date1_txt) +example_date2 = dateutil.parser.parse(example_date2_txt) + + +class TestIamResourceToAnsibleDict: + def setup_method(self): + pass + + def test_normalize_iam_mfa_device(self): + INPUT = { + "UserName": "ExampleUser", + "SerialNumber": "arn:aws:iam::123456789012:mfa/ExampleUser", + "EnableDate": example_date1, + } + OUTPUT = { + "user_name": "ExampleUser", + "serial_number": "arn:aws:iam::123456789012:mfa/ExampleUser", + "enable_date": example_date1_iso, + "tags": {}, + } + + assert OUTPUT == normalize_iam_mfa_device(INPUT) + + def test_normalize_iam_mfa_devices(self): + INPUT = [ + { + "UserName": "ExampleUser", + "SerialNumber": "arn:aws:iam::123456789012:mfa/ExampleUser", + "EnableDate": example_date1, + } + ] + OUTPUT = [ + { + "user_name": "ExampleUser", + "serial_number": "arn:aws:iam::123456789012:mfa/ExampleUser", + "enable_date": example_date1_iso, + "tags": {}, + } + ] + + assert OUTPUT == normalize_iam_mfa_devices(INPUT) + + def test_normalize_iam_user(self): + INPUT = { + "Path": "/MyPath/", + "UserName": "ExampleUser", + "UserId": "AIDU12345EXAMPLE12345", + "Arn": "arn:aws:iam::123456789012:user/MyPath/ExampleUser", + "CreateDate": example_date1, + "PasswordLastUsed": example_date2, + "PermissionsBoundary": { + "PermissionsBoundaryType": "PermissionsBoundaryPolicy", + "PermissionsBoundaryArn": "arn:aws:iam::123456789012:policy/ExamplePolicy", + }, + "Tags": [ + {"Key": "MyKey", "Value": "Example Value"}, + ], + } + + OUTPUT = { + "path": "/MyPath/", + "user_name": "ExampleUser", + "user_id": "AIDU12345EXAMPLE12345", + "arn": "arn:aws:iam::123456789012:user/MyPath/ExampleUser", + "create_date": example_date1_iso, + "password_last_used": example_date2_iso, + "permissions_boundary": { + "permissions_boundary_type": "PermissionsBoundaryPolicy", + "permissions_boundary_arn": "arn:aws:iam::123456789012:policy/ExamplePolicy", + }, + "tags": {"MyKey": "Example Value"}, + } + + assert OUTPUT == normalize_iam_user(INPUT) + + def test_normalize_iam_policy(self): + INPUT = { + "PolicyName": "AnsibleIntegratation-CI-ApplicationServices", + "PolicyId": "ANPA12345EXAMPLE12345", + "Arn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-ApplicationServices", + "Path": "/examples/", + "DefaultVersionId": "v6", + "AttachmentCount": 2, + "PermissionsBoundaryUsageCount": 0, + "IsAttachable": True, + "CreateDate": example_date1, + "UpdateDate": example_date2, + "Tags": [ + {"Key": "MyKey", "Value": "Example Value"}, + ], + } + + OUTPUT = { + "policy_name": "AnsibleIntegratation-CI-ApplicationServices", + "policy_id": "ANPA12345EXAMPLE12345", + "arn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-ApplicationServices", + "path": "/examples/", + "default_version_id": "v6", + "attachment_count": 2, + "permissions_boundary_usage_count": 0, + "is_attachable": True, + "create_date": example_date1_iso, + "update_date": example_date2_iso, + "tags": {"MyKey": "Example Value"}, + } + + assert OUTPUT == normalize_iam_policy(INPUT) + + def test_normalize_iam_group(self): + INPUT = { + "Users": [ + { + "Path": "/", + "UserName": "ansible_test", + "UserId": "AIDA12345EXAMPLE12345", + "Arn": "arn:aws:iam::123456789012:user/ansible_test", + "CreateDate": example_date1, + "PasswordLastUsed": example_date2, + } + ], + "Group": { + "Path": "/", + "GroupName": "ansible-integration-ci", + "GroupId": "AGPA01234EXAMPLE01234", + "Arn": "arn:aws:iam::123456789012:group/ansible-integration-ci", + "CreateDate": example_date1, + }, + "AttachedPolicies": [ + { + "PolicyName": "AnsibleIntegratation-CI-Paas", + "PolicyArn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-Paas", + }, + { + "PolicyName": "AnsibleIntegratation-CI-ApplicationServices", + "PolicyArn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-ApplicationServices", + }, + ], + } + + OUTPUT = { + "users": [ + { + "path": "/", + "user_name": "ansible_test", + "user_id": "AIDA12345EXAMPLE12345", + "arn": "arn:aws:iam::123456789012:user/ansible_test", + "create_date": example_date1_iso, + "password_last_used": example_date2_iso, + } + ], + "group": { + "path": "/", + "group_name": "ansible-integration-ci", + "group_id": "AGPA01234EXAMPLE01234", + "arn": "arn:aws:iam::123456789012:group/ansible-integration-ci", + "create_date": example_date1_iso, + }, + "attached_policies": [ + { + "policy_name": "AnsibleIntegratation-CI-Paas", + "policy_arn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-Paas", + }, + { + "policy_name": "AnsibleIntegratation-CI-ApplicationServices", + "policy_arn": "arn:aws:iam::123456789012:policy/AnsibleIntegratation-CI-ApplicationServices", + }, + ], + } + + assert OUTPUT == normalize_iam_group(INPUT) + + def test_normalize_access_key(self): + INPUT = { + "UserName": "ansible_test", + "AccessKeyId": "AKIA12345EXAMPLE1234", + "Status": "Active", + "CreateDate": example_date1, + } + + OUTPUT = { + "user_name": "ansible_test", + "access_key_id": "AKIA12345EXAMPLE1234", + "status": "Active", + "create_date": example_date1_iso, + } + + assert OUTPUT == normalize_iam_access_key(INPUT) + + def test_normalize_access_keys(self): + INPUT = [ + { + "UserName": "ansible_test", + "AccessKeyId": "AKIA12345EXAMPLE1234", + "Status": "Active", + "CreateDate": example_date1, + }, + { + "UserName": "ansible_test", + "AccessKeyId": "AKIA01234EXAMPLE4321", + "Status": "Active", + "CreateDate": example_date2, + }, + ] + + OUTPUT = [ + { + "access_key_id": "AKIA12345EXAMPLE1234", + "create_date": example_date1_iso, + "status": "Active", + "user_name": "ansible_test", + }, + { + "access_key_id": "AKIA01234EXAMPLE4321", + "create_date": example_date2_iso, + "status": "Active", + "user_name": "ansible_test", + }, + ] + + assert OUTPUT == normalize_iam_access_keys(INPUT) + + # Switch order to test that they're sorted by Creation Date + INPUT = [ + { + "UserName": "ansible_test", + "AccessKeyId": "AKIA12345EXAMPLE1234", + "Status": "Active", + "CreateDate": example_date2, + }, + { + "UserName": "ansible_test", + "AccessKeyId": "AKIA01234EXAMPLE4321", + "Status": "Active", + "CreateDate": example_date1, + }, + ] + + OUTPUT = [ + { + "access_key_id": "AKIA01234EXAMPLE4321", + "create_date": example_date1_iso, + "status": "Active", + "user_name": "ansible_test", + }, + { + "access_key_id": "AKIA12345EXAMPLE1234", + "create_date": example_date2_iso, + "status": "Active", + "user_name": "ansible_test", + }, + ] + + assert OUTPUT == normalize_iam_access_keys(INPUT) + + def test_normalize_role(self): + INPUT = { + "Arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "AssumeRolePolicyDocument": { + "Statement": [ + {"Action": "sts:AssumeRole", "Effect": "Deny", "Principal": {"Service": "ec2.amazonaws.com"}} + ], + "Version": "2012-10-17", + }, + "CreateDate": example_date1, + "Description": "Ansible Test Role (updated) ansible-test-76640355", + "InlinePolicies": ["inline-policy-a", "inline-policy-b"], + "InstanceProfiles": [ + { + "Arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-76640355", + "CreateDate": example_date2, + "InstanceProfileId": "AIPA12345EXAMPLE12345", + "InstanceProfileName": "ansible-test-76640355", + "Path": "/", + "Roles": [ + { + "Arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "CreateDate": example_date1, + "Path": "/", + "RoleId": "AROA12345EXAMPLE12345", + "RoleName": "ansible-test-76640355", + # XXX Bug in iam_role_info - Tags should have been in here. + "Tags": [{"Key": "TagB", "Value": "ValueB"}], + } + ], + "Tags": [{"Key": "TagA", "Value": "Value A"}], + } + ], + "ManagedPolicies": [ + { + "PolicyArn": "arn:aws:iam::123456789012:policy/ansible-test-76640355", + "PolicyName": "ansible-test-76640355", + } + ], + "MaxSessionDuration": 43200, + "Path": "/", + "RoleId": "AROA12345EXAMPLE12345", + "RoleLastUsed": {}, + "RoleName": "ansible-test-76640355", + "Tags": [{"Key": "TagB", "Value": "ValueB"}], + } + + OUTPUT = { + "arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "assume_role_policy_document": { + "Statement": [ + {"Action": "sts:AssumeRole", "Effect": "Deny", "Principal": {"Service": "ec2.amazonaws.com"}} + ], + "Version": "2012-10-17", + }, + "create_date": example_date1_iso, + "description": "Ansible Test Role (updated) ansible-test-76640355", + "inline_policies": ["inline-policy-a", "inline-policy-b"], + "instance_profiles": [ + { + "arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-76640355", + "create_date": example_date2_iso, + "instance_profile_id": "AIPA12345EXAMPLE12345", + "instance_profile_name": "ansible-test-76640355", + "path": "/", + "roles": [ + { + "arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "assume_role_policy_document": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "create_date": example_date1_iso, + "path": "/", + "role_id": "AROA12345EXAMPLE12345", + "role_name": "ansible-test-76640355", + "tags": {"TagB": "ValueB"}, + } + ], + "tags": {"TagA": "Value A"}, + } + ], + "managed_policies": [ + { + "policy_arn": "arn:aws:iam::123456789012:policy/ansible-test-76640355", + "policy_name": "ansible-test-76640355", + } + ], + "max_session_duration": 43200, + "path": "/", + "role_id": "AROA12345EXAMPLE12345", + "role_last_used": {}, + "role_name": "ansible-test-76640355", + "tags": {"TagB": "ValueB"}, + } + + assert OUTPUT == normalize_iam_role(INPUT) + + def test_normalize_role_compat(self): + INPUT = { + "Arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "AssumeRolePolicyDocument": { + "Statement": [ + {"Action": "sts:AssumeRole", "Effect": "Deny", "Principal": {"Service": "ec2.amazonaws.com"}} + ], + "Version": "2012-10-17", + }, + "CreateDate": example_date1, + "Description": "Ansible Test Role (updated) ansible-test-76640355", + "InlinePolicies": ["inline-policy-a", "inline-policy-b"], + "InstanceProfiles": [ + { + "Arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-76640355", + "CreateDate": example_date2, + "InstanceProfileId": "AIPA12345EXAMPLE12345", + "InstanceProfileName": "ansible-test-76640355", + "Path": "/", + "Roles": [ + { + "Arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "CreateDate": example_date1, + "Path": "/", + "RoleId": "AROA12345EXAMPLE12345", + "RoleName": "ansible-test-76640355", + # XXX Bug in iam_role_info - Tags should have been in here. + "Tags": [{"Key": "TagB", "Value": "ValueB"}], + } + ], + "Tags": [{"Key": "TagA", "Value": "Value A"}], + } + ], + "ManagedPolicies": [ + { + "PolicyArn": "arn:aws:iam::123456789012:policy/ansible-test-76640355", + "PolicyName": "ansible-test-76640355", + } + ], + "MaxSessionDuration": 43200, + "Path": "/", + "RoleId": "AROA12345EXAMPLE12345", + "RoleLastUsed": {}, + "RoleName": "ansible-test-76640355", + "Tags": [{"Key": "TagB", "Value": "ValueB"}], + } + + OUTPUT = { + "arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "assume_role_policy_document": { + "statement": [ + {"action": "sts:AssumeRole", "effect": "Deny", "principal": {"service": "ec2.amazonaws.com"}} + ], + "version": "2012-10-17", + }, + "assume_role_policy_document_raw": { + "Statement": [ + {"Action": "sts:AssumeRole", "Effect": "Deny", "Principal": {"Service": "ec2.amazonaws.com"}} + ], + "Version": "2012-10-17", + }, + "create_date": example_date1_iso, + "description": "Ansible Test Role (updated) ansible-test-76640355", + "inline_policies": ["inline-policy-a", "inline-policy-b"], + "instance_profiles": [ + { + "arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-76640355", + "create_date": example_date2_iso, + "instance_profile_id": "AIPA12345EXAMPLE12345", + "instance_profile_name": "ansible-test-76640355", + "path": "/", + "roles": [ + { + "arn": "arn:aws:iam::123456789012:role/ansible-test-76640355", + "assume_role_policy_document": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "create_date": example_date1_iso, + "path": "/", + "role_id": "AROA12345EXAMPLE12345", + "role_name": "ansible-test-76640355", + "tags": {"TagB": "ValueB"}, + } + ], + "tags": {"TagA": "Value A"}, + } + ], + "managed_policies": [ + { + "policy_arn": "arn:aws:iam::123456789012:policy/ansible-test-76640355", + "policy_name": "ansible-test-76640355", + } + ], + "max_session_duration": 43200, + "path": "/", + "role_id": "AROA12345EXAMPLE12345", + "role_last_used": {}, + "role_name": "ansible-test-76640355", + "tags": {"TagB": "ValueB"}, + } + + assert OUTPUT == normalize_iam_role(INPUT, _v7_compat=True) + + def test_normalize_instance_profile(self): + INPUT = { + "Arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-40050922/ansible-test-40050922", + "CreateDate": example_date1, + "InstanceProfileId": "AIPA12345EXAMPLE12345", + "InstanceProfileName": "ansible-test-40050922", + "Path": "/ansible-test-40050922/", + "Roles": [ + { + "Arn": "arn:aws:iam::123456789012:role/ansible-test-40050922/ansible-test-40050922", + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "CreateDate": example_date2, + "Path": "/ansible-test-40050922/", + "RoleId": "AROA12345EXAMPLE12345", + "RoleName": "ansible-test-40050922", + "Tags": [{"Key": "TagC", "Value": "ValueC"}], + } + ], + "Tags": [ + {"Key": "Key with Spaces", "Value": "Value with spaces"}, + {"Key": "snake_case_key", "Value": "snake_case_value"}, + {"Key": "CamelCaseKey", "Value": "CamelCaseValue"}, + {"Key": "pascalCaseKey", "Value": "pascalCaseValue"}, + ], + } + + OUTPUT = { + "arn": "arn:aws:iam::123456789012:instance-profile/ansible-test-40050922/ansible-test-40050922", + "create_date": "2020-12-30T00:00:00+00:00", + "instance_profile_id": "AIPA12345EXAMPLE12345", + "instance_profile_name": "ansible-test-40050922", + "path": "/ansible-test-40050922/", + "roles": [ + { + "arn": "arn:aws:iam::123456789012:role/ansible-test-40050922/ansible-test-40050922", + "assume_role_policy_document": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Deny", + "Principal": {"Service": "ec2.amazonaws.com"}, + } + ], + "Version": "2012-10-17", + }, + "create_date": "2021-04-26T01:23:58+00:00", + "path": "/ansible-test-40050922/", + "role_id": "AROA12345EXAMPLE12345", + "role_name": "ansible-test-40050922", + "tags": {"TagC": "ValueC"}, + } + ], + "tags": { + "CamelCaseKey": "CamelCaseValue", + "Key with Spaces": "Value with spaces", + "pascalCaseKey": "pascalCaseValue", + "snake_case_key": "snake_case_value", + }, + } + + assert OUTPUT == normalize_iam_instance_profile(INPUT) diff --git a/ansible_collections/amazon/aws/tests/unit/module_utils/transformation/test_boto3_resource_to_ansible_dict.py b/ansible_collections/amazon/aws/tests/unit/module_utils/transformation/test_boto3_resource_to_ansible_dict.py new file mode 100644 index 000000000..89a0a837c --- /dev/null +++ b/ansible_collections/amazon/aws/tests/unit/module_utils/transformation/test_boto3_resource_to_ansible_dict.py @@ -0,0 +1,140 @@ +# (c) 2017 Red Hat Inc. +# +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from copy import deepcopy +from unittest.mock import sentinel + +import dateutil +import pytest + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.transformation import boto3_resource_to_ansible_dict + +example_date_txt = "2020-12-30T00:00:00.000Z" +example_date_iso = "2020-12-30T00:00:00+00:00" +example_date = dateutil.parser.parse(example_date_txt) + +EXAMPLE_BOTO3 = [ + None, + {}, + {"ExampleDate": example_date}, + {"ExampleTxtDate": example_date_txt}, + {"Tags": [{"Key": "MyKey", "Value": "MyValue"}, {"Key": "Normal case", "Value": "Normal Value"}]}, + { + "Name": "ExampleResource", + "ExampleDate": example_date, + "Tags": [{"Key": "MyKey", "Value": "MyValue"}, {"Key": "Normal case", "Value": "Normal Value"}], + }, + {"ExampleNested": {"ExampleKey": "Example Value"}}, +] + +EXAMPLE_DICT = [ + None, + {}, + {"example_date": example_date_iso, "tags": {}}, + {"example_txt_date": example_date_txt, "tags": {}}, + {"tags": {"MyKey": "MyValue", "Normal case": "Normal Value"}}, + { + "name": "ExampleResource", + "example_date": example_date_iso, + "tags": {"MyKey": "MyValue", "Normal case": "Normal Value"}, + }, + {"example_nested": {"example_key": "Example Value"}, "tags": {}}, +] + +TEST_DATA = zip(EXAMPLE_BOTO3, EXAMPLE_DICT) + +NESTED_DATA = {"sentinal": sentinel.MY_VALUE} + + +def do_transform_nested(resource): + return {"sentinal": sentinel.MY_VALUE} + + +class TestBoto3ResourceToAnsibleDict: + def setup_method(self): + pass + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_default_conversion(self, input_params, output_params): + # Test default behaviour + assert boto3_resource_to_ansible_dict(input_params) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_normalize(self, input_params, output_params): + # Test with normalize explicitly enabled + assert boto3_resource_to_ansible_dict(input_params, normalize=True) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_no_normalize(self, input_params, output_params): + # Test with normalize explicitly disabled + expected_value = deepcopy(output_params) + if input_params and "ExampleDate" in input_params: + expected_value["example_date"] = example_date + assert expected_value == boto3_resource_to_ansible_dict(input_params, normalize=False) + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_no_skip(self, input_params, output_params): + # Test with ignore_list explicitly set to [] + assert boto3_resource_to_ansible_dict(input_params, ignore_list=[]) == output_params + assert boto3_resource_to_ansible_dict(input_params, ignore_list=["NotUsed"]) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_skip(self, input_params, output_params): + # Test with ignore_list explicitly set + expected_value = deepcopy(output_params) + if input_params and "ExampleNested" in input_params: + expected_value["example_nested"] = input_params["ExampleNested"] + assert expected_value == boto3_resource_to_ansible_dict(input_params, ignore_list=["ExampleNested"]) + assert expected_value == boto3_resource_to_ansible_dict(input_params, ignore_list=["NotUsed", "ExampleNested"]) + assert expected_value == boto3_resource_to_ansible_dict(input_params, ignore_list=["ExampleNested", "NotUsed"]) + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_tags(self, input_params, output_params): + # Test with transform_tags explicitly enabled + assert boto3_resource_to_ansible_dict(input_params, transform_tags=True) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_no_tags(self, input_params, output_params): + # Test with transform_tags explicitly disabled + expected_value = deepcopy(output_params) + if input_params and "Tags" in input_params: + camel_tags = camel_dict_to_snake_dict({"tags": input_params["Tags"]}) + expected_value.update(camel_tags) + assert expected_value == boto3_resource_to_ansible_dict(input_params, transform_tags=False) + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_no_nested(self, input_params, output_params): + # Test with transform_nested explicitly set to an empty dictionary + assert boto3_resource_to_ansible_dict(input_params, nested_transforms={}) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_nested(self, input_params, output_params): + # Test with a custom transformation of nested resources + transform_map = {"ExampleNested": do_transform_nested} + expected_value = deepcopy(output_params) + + actual_value = boto3_resource_to_ansible_dict(input_params, nested_transforms=transform_map) + + if input_params and "ExampleNested" in input_params: + assert actual_value["example_nested"] == NESTED_DATA + del actual_value["example_nested"] + del expected_value["example_nested"] + + assert expected_value == actual_value + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_force_tags(self, input_params, output_params): + # Test with force_tags explicitly enabled + assert boto3_resource_to_ansible_dict(input_params, force_tags=True) == output_params + + @pytest.mark.parametrize("input_params, output_params", deepcopy(TEST_DATA)) + def test_no_force_tags(self, input_params, output_params): + # Test with force_tags explicitly enabled + expected_value = deepcopy(output_params) + if input_params and "Tags" not in input_params: + del expected_value["tags"] + assert boto3_resource_to_ansible_dict(input_params, force_tags=False) == expected_value diff --git a/ansible_collections/amazon/aws/tests/unit/plugin_utils/inventory/test_inventory_base.py b/ansible_collections/amazon/aws/tests/unit/plugin_utils/inventory/test_inventory_base.py index 32eb3f7ab..4da5792a8 100644 --- a/ansible_collections/amazon/aws/tests/unit/plugin_utils/inventory/test_inventory_base.py +++ b/ansible_collections/amazon/aws/tests/unit/plugin_utils/inventory/test_inventory_base.py @@ -3,6 +3,7 @@ # This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +import re from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch @@ -11,6 +12,8 @@ from unittest.mock import sentinel import pytest import ansible.plugins.inventory as base_inventory +from ansible.errors import AnsibleError +from ansible.module_utils.six import string_types import ansible_collections.amazon.aws.plugins.plugin_utils.inventory as utils_inventory @@ -65,3 +68,131 @@ def test_inventory_verify_file(monkeypatch, filename, result): assert inventory_plugin.verify_file(filename) is result base_verify.return_value = False assert inventory_plugin.verify_file(filename) is False + + +class AwsUnitTestTemplar: + def __init__(self, config): + self.config = config + + def is_template_string(self, key): + m = re.findall("{{([ ]*[a-zA-Z0-9_]*[ ]*)}}", key) + return bool(m) + + def is_template(self, data): + if isinstance(data, string_types): + return self.is_template_string(data) + elif isinstance(data, (list, tuple)): + for v in data: + if self.is_template(v): + return True + elif isinstance(data, dict): + for k in data: + if self.is_template(k) or self.is_template(data[k]): + return True + return False + + def template(self, variable, disable_lookups): + for k, v in self.config.items(): + variable = re.sub("{{([ ]*%s[ ]*)}}" % k, v, variable) + if self.is_template_string(variable): + m = re.findall("{{([ ]*[a-zA-Z0-9_]*[ ]*)}}", variable) + raise AnsibleError(f"Missing variables: {','.join([k.replace(' ', '') for k in m])}") + return variable + + +@pytest.fixture +def aws_inventory_base(): + inventory = utils_inventory.AWSInventoryBase() + inventory._options = {} + inventory.templar = None + return inventory + + +@pytest.mark.parametrize( + "option,value", + [ + ("access_key", "amazon_ansible_access_key_001"), + ("secret_key", "amazon_ansible_secret_key_890"), + ("session_token", None), + ("use_ssm_inventory", False), + ("This_field_is_undefined", None), + ("assume_role_arn", "arn:aws:iam::123456789012:role/ansible-test-inventory"), + ("region", "us-east-2"), + ], +) +def test_inventory_get_options_without_templar(aws_inventory_base, mocker, option, value): + inventory_options = { + "access_key": "amazon_ansible_access_key_001", + "secret_key": "amazon_ansible_secret_key_890", + "endpoint": "http//ansible.amazon.com", + "assume_role_arn": "arn:aws:iam::123456789012:role/ansible-test-inventory", + "region": "us-east-2", + "use_ssm_inventory": False, + } + aws_inventory_base._options = inventory_options + + super_get_options_patch = mocker.patch( + "ansible_collections.amazon.aws.plugins.plugin_utils.inventory.BaseInventoryPlugin.get_options" + ) + super_get_options_patch.return_value = aws_inventory_base._options + + options = aws_inventory_base.get_options() + assert value == options.get(option) + + +@pytest.mark.parametrize( + "option,value,error", + [ + ("access_key", "amazon_ansible_access_key_001", None), + ("session_token", None, None), + ("use_ssm_inventory", "{{ aws_inventory_use_ssm }}", None), + ("This_field_is_undefined", None, None), + ("region", "us-east-1", None), + ("profile", None, "Missing variables: ansible_version"), + ], +) +def test_inventory_get_options_with_templar(aws_inventory_base, mocker, option, value, error): + inventory_options = { + "access_key": "amazon_ansible_access_key_001", + "profile": "ansbile_{{ ansible_os }}_{{ ansible_version }}", + "endpoint": "{{ aws_endpoint }}", + "region": "{{ aws_region_country }}-east-{{ aws_region_id }}", + "use_ssm_inventory": "{{ aws_inventory_use_ssm }}", + } + aws_inventory_base._options = inventory_options + templar_config = { + "ansible_os": "RedHat", + "aws_region_country": "us", + "aws_region_id": "1", + "aws_endpoint": "http//ansible.amazon.com", + } + aws_inventory_base.templar = AwsUnitTestTemplar(templar_config) + + super_get_options_patch = mocker.patch( + "ansible_collections.amazon.aws.plugins.plugin_utils.inventory.BaseInventoryPlugin.get_options" + ) + super_get_options_patch.return_value = aws_inventory_base._options + + super_get_option_patch = mocker.patch( + "ansible_collections.amazon.aws.plugins.plugin_utils.inventory.BaseInventoryPlugin.get_option" + ) + super_get_option_patch.side_effect = lambda x, hostvars=None: aws_inventory_base._options.get(x) + + if error: + # test using get_options() + with pytest.raises(AnsibleError) as exc: + options = aws_inventory_base.get_options() + options.get(option) + assert error == str(exc.value) + + # test using get_option() + with pytest.raises(AnsibleError) as exc: + aws_inventory_base.get_option(option) + assert error == str(exc.value) + else: + # test using get_options() + options = aws_inventory_base.get_options() + assert value == options.get(option) + + # test using get_option() + assert value == aws_inventory_base.get_option(option) diff --git a/ansible_collections/amazon/aws/tests/unit/plugins/inventory/test_aws_ec2.py b/ansible_collections/amazon/aws/tests/unit/plugins/inventory/test_aws_ec2.py index 8cced1662..e33b78c51 100644 --- a/ansible_collections/amazon/aws/tests/unit/plugins/inventory/test_aws_ec2.py +++ b/ansible_collections/amazon/aws/tests/unit/plugins/inventory/test_aws_ec2.py @@ -240,6 +240,7 @@ def test_get_tag_hostname(preference, instance, expected): ) def test_inventory_build_include_filters(inventory, _options, expected): inventory._options = _options + inventory.templar = None assert inventory.build_include_filters() == expected |