From 1e7232374218c8b713be300ac18260d232183fa9 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 16 Jun 2023 15:11:34 +0200 Subject: [PATCH 1/3] New modules: iam_instance_profile(_info) See also: #1842 New modules for listing/managing IAM Instance Profiles --- .../fragments/1843-iam_instance_profile.yml | 2 + meta/runtime.yml | 2 + plugins/module_utils/iam.py | 210 ++++++- plugins/modules/iam_instance_profile.py | 346 ++++++++++++ plugins/modules/iam_instance_profile_info.py | 133 +++++ .../targets/iam_instance_profile/aliases | 3 + .../iam_instance_profile/defaults/main.yml | 12 + .../files/deny-assume.json | 10 + .../iam_instance_profile/meta/main.yml | 1 + .../iam_instance_profile/tasks/main.yml | 511 ++++++++++++++++++ .../iam_instance_profile/tasks/tags.yml | 298 ++++++++++ 11 files changed, 1525 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/1843-iam_instance_profile.yml create mode 100644 plugins/modules/iam_instance_profile.py create mode 100644 plugins/modules/iam_instance_profile_info.py create mode 100644 tests/integration/targets/iam_instance_profile/aliases create mode 100644 tests/integration/targets/iam_instance_profile/defaults/main.yml create mode 100644 tests/integration/targets/iam_instance_profile/files/deny-assume.json create mode 100644 tests/integration/targets/iam_instance_profile/meta/main.yml create mode 100644 tests/integration/targets/iam_instance_profile/tasks/main.yml create mode 100644 tests/integration/targets/iam_instance_profile/tasks/tags.yml diff --git a/changelogs/fragments/1843-iam_instance_profile.yml b/changelogs/fragments/1843-iam_instance_profile.yml new file mode 100644 index 0000000000..3cfb18270d --- /dev/null +++ b/changelogs/fragments/1843-iam_instance_profile.yml @@ -0,0 +1,2 @@ +trivial: +- new modules - iam_instance_profile, iam_instance_profile_info diff --git a/meta/runtime.yml b/meta/runtime.yml index 0242673f7e..b4e6d17cd3 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -67,6 +67,8 @@ action_groups: - elb_application_lb_info - elb_classic_lb - execute_lambda + - iam_instance_profile + - iam_instance_profile_info - iam_policy - iam_policy_info - iam_user diff --git a/plugins/module_utils/iam.py b/plugins/module_utils/iam.py index 3b08f1dac4..cde0aff2b9 100644 --- a/plugins/module_utils/iam.py +++ b/plugins/module_utils/iam.py @@ -3,16 +3,73 @@ # Copyright (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from copy import deepcopy + try: import botocore except ImportError: - pass + 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 .retries import AWSRetry -from .botocore import is_boto3_error_code from .arn import parse_aws_arn +from .botocore import is_boto3_error_code +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 + + +class AnsibleIAMError(AnsibleAWSError): + pass + + +@AWSRetry.jittered_backoff() +def _tag_iam_instance_profile(client, **kwargs): + client.tag_instance_profile(**kwargs) + + +@AWSRetry.jittered_backoff() +def _untag_iam_instance_profile(client, **kwargs): + client.untag_instance_profile(**kwargs) + + +@AWSRetry.jittered_backoff() +def _get_iam_instance_profiles(client, **kwargs): + return client.get_instance_profile(**kwargs)["InstanceProfile"] + + +@AWSRetry.jittered_backoff() +def _list_iam_instance_profiles(client, **kwargs): + paginator = client.get_paginator("list_instance_profiles") + return paginator.paginate(**kwargs).build_full_result()["InstanceProfiles"] + + +@AWSRetry.jittered_backoff() +def _list_iam_instance_profiles_for_role(client, **kwargs): + paginator = client.get_paginator("list_instance_profiles_for_role") + return paginator.paginate(**kwargs).build_full_result()["InstanceProfiles"] + + +@AWSRetry.jittered_backoff() +def _create_instance_profile(client, **kwargs): + return client.create_instance_profile(**kwargs) + + +@AWSRetry.jittered_backoff() +def _delete_instance_profile(client, **kwargs): + client.delete_instance_profile(**kwargs) + + +@AWSRetry.jittered_backoff() +def _add_role_to_instance_profile(client, **kwargs): + client.add_role_to_instance_profile(**kwargs) + + +@AWSRetry.jittered_backoff() +def _remove_role_from_instance_profile(client, **kwargs): + client.remove_role_from_instance_profile(**kwargs) def get_aws_account_id(module): @@ -76,3 +133,150 @@ def get_aws_account_info(module): ) return (to_native(account_id), to_native(partition)) + + +def create_iam_instance_profile(client, name, path, tags): + boto3_tags = ansible_dict_to_boto3_tag_list(tags or {}) + try: + result = _create_instance_profile(client, InstanceProfileName=name, Path=path, Tags=boto3_tags) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError(message="Unable to create instance profile", exception=e) + return result["InstanceProfile"] + + +def delete_iam_instance_profile(client, name): + try: + _delete_instance_profile(client, InstanceProfileName=name) + except is_boto3_error_code("NoSuchEntity"): + # Deletion already happened. + return False + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError(message="Unable to delete instance profile", exception=e) + return True + + +def add_role_to_iam_instance_profile(client, profile_name, role_name): + try: + _add_role_to_instance_profile(client, InstanceProfileName=profile_name, RoleName=role_name) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError( + message="Unable to add role to instance profile", + exception=e, + profile_name=profile_name, + role_name=role_name, + ) + return True + + +def remove_role_from_iam_instance_profile(client, profile_name, role_name): + try: + _remove_role_from_instance_profile(client, InstanceProfileName=profile_name, RoleName=role_name) + except is_boto3_error_code("NoSuchEntity"): + # Deletion already happened. + return False + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError( + message="Unable to remove role from instance profile", + exception=e, + profile_name=profile_name, + role_name=role_name, + ) + return True + + +def list_iam_instance_profiles(client, name=None, prefix=None, role=None): + """ + Returns a list of IAM instance profiles in boto3 format. + Profiles need to be converted to Ansible format using normalize_iam_instance_profile before being displayed. + + See also: normalize_iam_instance_profile + """ + try: + if role: + return _list_iam_instance_profiles_for_role(client, RoleName=role) + if name: + # Unlike the others this returns a single result, make this a list with 1 element. + return [_get_iam_instance_profiles(client, InstanceProfileName=name)] + if prefix: + return _list_iam_instance_profiles(client, PathPrefix=prefix) + return _list_iam_instance_profiles(client) + except is_boto3_error_code("NoSuchEntity"): + return [] + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError(message="Unable to list instance profiles", exception=e) + + +def normalize_iam_instance_profile(profile): + """ + Converts a boto3 format IAM instance profile into "Ansible" format + """ + + new_profile = camel_dict_to_snake_dict(deepcopy(profile)) + if profile.get("Roles"): + new_profile["roles"] = [normalize_iam_role(role) 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): + """ + Converts a boto3 format IAM instance role into "Ansible" format + """ + + new_role = camel_dict_to_snake_dict(deepcopy(role)) + if role.get("InstanceProfiles"): + new_role["instance_profiles"] = [ + normalize_iam_instance_profile(profile) for profile in role.get("InstanceProfiles") + ] + if role.get("AssumeRolePolicyDocument"): + new_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument") + if role.get("Tags"): + new_role["tags"] = boto3_tag_list_to_ansible_dict(role.get("Tags")) + else: + new_role["tags"] = {} + new_role["original"] = role + return new_role + + +def tag_iam_instance_profile(client, name, tags): + if not tags: + return + boto3_tags = ansible_dict_to_boto3_tag_list(tags or {}) + try: + result = _tag_iam_instance_profile(client, InstanceProfileName=name, Tags=boto3_tags) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError(message="Unable to tag instance profile", exception=e) + + +def untag_iam_instance_profile(client, name, tags): + if not tags: + return + try: + result = _untag_iam_instance_profile(client, InstanceProfileName=name, TagKeys=tags) + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + raise AnsibleIAMError(message="Unable to untag instance profile", exception=e) diff --git a/plugins/modules/iam_instance_profile.py b/plugins/modules/iam_instance_profile.py new file mode 100644 index 0000000000..181f4ea746 --- /dev/null +++ b/plugins/modules/iam_instance_profile.py @@ -0,0 +1,346 @@ +#!/usr/bin/python +# -*- 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) + +DOCUMENTATION = r""" +--- +module: iam_instance_profile +version_added: 6.2.0 +short_description: manage IAM instance profiles +description: + - Manage IAM instance profiles. +author: + - Mark Chappell (@tremble) +options: + state: + description: + - Desired state of the instance profile. + type: str + choices: ["absent", "present"] + default: "present" + name: + description: + - Name of an instance profile. + aliases: + - instance_profile_name + type: str + required: True + prefix: + description: + - The path prefix for filtering the results. + aliases: ["path_prefix", "path"] + type: str + default: "/" + role: + description: + - The name of the role to attach to the instance profile. + - To remove all roles from the instance profile set I(role=""). + type: str + +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.tags.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +- name: Find all existing IAM instance profiles + amazon.aws.iam_instance_profile_info: + register: result + +- name: Describe a single instance profile + amazon.aws.iam_instance_profile_info: + name: MyIAMProfile + register: result + +- name: Find all IAM instance profiles starting with /some/path/ + amazon.aws.iam_instance_profile_info: + prefile: /some/path/ + register: result +""" + +RETURN = r""" +iam_instance_profile: + description: List of IAM instance profiles + returned: always + type: complex + contains: + arn: + description: Amazon Resource Name for the instance profile. + returned: always + type: str + sample: arn:aws:iam::123456789012:instance-profile/AnsibleTestProfile + create_date: + description: Date instance profile was created. + returned: always + type: str + sample: '2023-01-12T11:18:29+00:00' + instance_profile_id: + description: Amazon Identifier for the instance profile. + returned: always + type: str + sample: AROA12345EXAMPLE54321 + instance_profile_name: + description: Name of instance profile. + returned: always + type: str + sample: AnsibleTestEC2Policy + path: + description: Path of instance profile. + returned: always + type: str + sample: / + roles: + description: List of roles associated with this instance profile. + returned: always + type: list + sample: [] + tags: + description: Instance profile tags. + type: dict + returned: always + sample: '{"Env": "Prod"}' +""" + +from copy import deepcopy + +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry + +from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError +from ansible_collections.amazon.aws.plugins.module_utils.iam import create_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import delete_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import list_iam_instance_profiles +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import remove_role_from_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import add_role_to_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import tag_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.iam import untag_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags + + +def describe_iam_instance_profile(client, name, prefix): + profiles = [] + profiles = list_iam_instance_profiles(client, name=name, prefix=prefix) + + if not profiles: + return None + + return normalize_iam_instance_profile(profiles[0]) + + +def create_instance_profile(client, name, path, tags, check_mode): + if check_mode: + return True, {"instance_profile_name": name, "path": path, "tags": tags or {}, "roles": []} + + profile = create_iam_instance_profile(client, name, path, tags) + return True, normalize_iam_instance_profile(profile) + + +def ensure_tags( + original_profile, + client, + name, + tags, + purge_tags, + check_mode, +): + if tags is None: + return False, original_profile + + original_tags = original_profile.get("tags") or {} + + tags_to_set, tag_keys_to_unset = compare_aws_tags(original_tags, tags, purge_tags) + if not tags_to_set and not tag_keys_to_unset: + return False, original_profile + + new_profile = deepcopy(original_profile) + desired_tags = deepcopy(original_tags) + + for key in tag_keys_to_unset: + desired_tags.pop(key, None) + desired_tags.update(tags_to_set) + new_profile["tags"] = desired_tags + + if not check_mode: + untag_iam_instance_profile(client, name, tag_keys_to_unset) + tag_iam_instance_profile(client, name, tags_to_set) + + return True, new_profile + + +def ensure_role( + original_profile, + client, + name, + role, + check_mode, +): + if role is None: + return False, original_profile + + if role == "" and not original_profile.get("roles"): + return False, original_profile + else: + desired_role = [] + + if original_profile.get("roles") and original_profile.get("roles")[0].get("role_name", None) == role: + return False, original_profile + else: + desired_role = [{"role_name": role}] + + new_profile = deepcopy(original_profile) + new_profile["roles"] = desired_role + + if check_mode: + return True, new_profile + + if original_profile.get("roles"): + # We're changing the role, so we always need to remove the existing one first + remove_role_from_iam_instance_profile(client, name, original_profile["roles"][0]["role_name"]) + if role: + add_role_to_iam_instance_profile(client, name, role) + + return True, new_profile + + +def ensure_present( + original_profile, + client, + name, + path, + tags, + purge_tags, + role, + check_mode, +): + changed = False + if not original_profile: + changed, new_profile = create_instance_profile( + client, + name=name, + path=path, + tags=tags, + check_mode=check_mode, + ) + else: + new_profile = deepcopy(original_profile) + + role_changed, new_profile = ensure_role( + new_profile, + client, + name, + role, + check_mode, + ) + + tags_changed, new_profile = ensure_tags( + new_profile, + client, + name, + tags, + purge_tags, + check_mode, + ) + + changed |= role_changed + changed |= tags_changed + + return changed, new_profile + + +def ensure_absent( + original_profile, + client, + name, + prefix, + check_mode, +): + if not original_profile: + return False + + if check_mode: + return True + + roles = original_profile.get("roles") or [] + for role in roles: + remove_role_from_iam_instance_profile(client, name, role.get("role_name")) + + return delete_iam_instance_profile(client, name) + + +def main(): + """ + Module action handler + """ + argument_spec = dict( + name=dict(aliases=["instance_profile_name"], required=True), + prefix=dict(aliases=["path_prefix", "path"], default="/"), + state=dict(choices=["absent", "present"], default="present"), + tags=dict(aliases=["resource_tags"], type="dict"), + purge_tags=dict(type="bool", default=True), + role=dict(), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + try: + name = module.params["name"] + prefix = module.params["prefix"] + state = module.params["state"] + + original_profile = describe_iam_instance_profile(client, name, prefix) + + if state == "absent": + changed = ensure_absent( + original_profile, + client, + name, + prefix, + module.check_mode, + ) + final_profile = None + else: + changed, final_profile = ensure_present( + original_profile, + client, + name, + prefix, + module.params["tags"], + module.params["purge_tags"], + module.params["role"], + module.check_mode, + ) + + if not module.check_mode: + final_profile = describe_iam_instance_profile(client, name, prefix) + + except AnsibleIAMError as e: + if e.exception: + module.fail_json_aws(e.exception, msg=e.message) + module.fail_json(msg=e.message) + + results = { + "changed": changed, + "iam_instance_profile": final_profile, + } + if changed: + results["diff"] = { + "before": original_profile, + "after": final_profile, + } + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/iam_instance_profile_info.py b/plugins/modules/iam_instance_profile_info.py new file mode 100644 index 0000000000..faddab030f --- /dev/null +++ b/plugins/modules/iam_instance_profile_info.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- 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) + +DOCUMENTATION = r""" +--- +module: iam_instance_profile_info +version_added: 6.2.0 +short_description: gather information on IAM instance profiles +description: + - Gathers information about IAM instance profiles. +author: + - Mark Chappell (@tremble) +options: + name: + description: + - Name of an instance profile to search for. + - Mutually exclusive with I(prefix). + aliases: + - instance_profile_name + type: str + path_prefix: + description: + - The path prefix for filtering the results. + - Mutually exclusive with I(name). + aliases: ["path", "prefix"] + type: str + +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +- name: Find all existing IAM instance profiles + amazon.aws.iam_instance_profile_info: + register: result + +- name: Describe a single instance profile + amazon.aws.iam_instance_profile_info: + name: MyIAMProfile + register: result + +- name: Find all IAM instance profiles starting with /some/path/ + amazon.aws.iam_instance_profile_info: + prefile: /some/path/ + register: result +""" + +RETURN = r""" +iam_instance_profiles: + description: List of IAM instance profiles + returned: always + type: complex + contains: + arn: + description: Amazon Resource Name for the instance profile. + returned: always + type: str + sample: arn:aws:iam::123456789012:instance-profile/AnsibleTestProfile + create_date: + description: Date instance profile was created. + returned: always + type: str + sample: '2023-01-12T11:18:29+00:00' + instance_profile_id: + description: Amazon Identifier for the instance profile. + returned: always + type: str + sample: AROA12345EXAMPLE54321 + instance_profile_name: + description: Name of instance profile. + returned: always + type: str + sample: AnsibleTestEC2Policy + path: + description: Path of instance profile. + returned: always + type: str + sample: / + roles: + description: List of roles associated with this instance profile. + returned: always + type: list + sample: [] +""" + +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry + +from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError +from ansible_collections.amazon.aws.plugins.module_utils.iam import list_iam_instance_profiles +from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_instance_profile +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +def describe_iam_instance_profiles(module, client): + name = module.params["name"] + prefix = module.params["path_prefix"] + profiles = [] + try: + profiles = list_iam_instance_profiles(client, name=name, prefix=prefix) + except AnsibleIAMError as e: + if e.exception: + module.fail_json_aws(e.exception, msg=e.message) + module.fail_json(msg=e.message) + + return [normalize_iam_instance_profile(p) for p in profiles] + + +def main(): + """ + Module action handler + """ + argument_spec = dict( + name=dict(aliases=["instance_profile_name"]), + path_prefix=dict(aliases=["path", "prefix"]), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[["name", "path_prefix"]], + ) + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + module.exit_json(changed=False, iam_instance_profiles=describe_iam_instance_profiles(module, client)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/iam_instance_profile/aliases b/tests/integration/targets/iam_instance_profile/aliases new file mode 100644 index 0000000000..e381149ffe --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/aliases @@ -0,0 +1,3 @@ +cloud/aws + +iam_instance_profile_info diff --git a/tests/integration/targets/iam_instance_profile/defaults/main.yml b/tests/integration/targets/iam_instance_profile/defaults/main.yml new file mode 100644 index 0000000000..28290d5785 --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/defaults/main.yml @@ -0,0 +1,12 @@ +--- +test_profile: '{{ resource_prefix }}-iam-ip' +test_profile_complex: '{{ resource_prefix }}-iam-ip-complex' +test_role: '{{ resource_prefix }}-iam-ipr' +test_path: '/{{ resource_prefix }}-ip/' +safe_managed_policy: 'AWSDenyAll' + +test_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value diff --git a/tests/integration/targets/iam_instance_profile/files/deny-assume.json b/tests/integration/targets/iam_instance_profile/files/deny-assume.json new file mode 100644 index 0000000000..73e8771586 --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/files/deny-assume.json @@ -0,0 +1,10 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { "Service": "ec2.amazonaws.com" }, + "Effect": "Deny" + } + ] +} diff --git a/tests/integration/targets/iam_instance_profile/meta/main.yml b/tests/integration/targets/iam_instance_profile/meta/main.yml new file mode 100644 index 0000000000..32cf5dda7e --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/iam_instance_profile/tasks/main.yml b/tests/integration/targets/iam_instance_profile/tasks/main.yml new file mode 100644 index 0000000000..37159b1cb8 --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/tasks/main.yml @@ -0,0 +1,511 @@ +--- +# Tests for iam_instance_profile and iam_instance_profile_info +# + +- name: "Setup AWS connection info" + module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + collections: + - amazon.aws + - community.general + block: + # =================================================================== + # Prepare + + - name: "Prepare IAM Roles" + iam_role: + state: present + name: "{{ item }}" + path: "{{ test_path }}" + create_instance_profile: True + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + managed_policies: + - "{{ safe_managed_policy }}" + wait: True + loop: + - "{{ test_role }}" + - "{{ test_role }}-2" + + # =================================================================== + # Test + + # =================================================================== + + - name: "Create minimal Instance Profile (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Create minimal Instance Profile" + iam_instance_profile: + name: "{{ test_profile }}" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Create minimal Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Create minimal Instance Profile - Idempotent" + iam_instance_profile: + name: "{{ test_profile }}" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # =================================================================== + + - include_tasks: 'tags.yml' + + # =================================================================== + + - name: "Add role to Instance Profile (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Add role to Instance Profile" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Add role to Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Add role to Instance Profile - Idempotent" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # ===== + + - name: "Replace role on Instance Profile (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}-2" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Replace role on Instance Profile" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}-2" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Replace role on Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}-2" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Replace role on Instance Profile - Idempotent" + iam_instance_profile: + name: "{{ test_profile }}" + role: "{{ test_role }}-2" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # ===== + + - name: "Remove role from Instance Profile (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Remove role from Instance Profile" + iam_instance_profile: + name: "{{ test_profile }}" + role: "" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Remove role from Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + name: "{{ test_profile }}" + role: "" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Remove role from Instance Profile - Idempotent" + iam_instance_profile: + name: "{{ test_profile }}" + role: "" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # =================================================================== + + - name: "Create complex Instance Profile (CHECK)" + iam_instance_profile: + name: "{{ test_profile_complex }}" + role: "{{ test_role }}-2" + path: "{{ test_path }}" + tags: "{{ test_tags }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Create complex Instance Profile" + iam_instance_profile: + name: "{{ test_profile_complex }}" + role: "{{ test_role }}-2" + path: "{{ test_path }}" + tags: "{{ test_tags }}" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Create complex Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + name: "{{ test_profile_complex }}" + role: "{{ test_role }}-2" + path: "{{ test_path }}" + tags: "{{ test_tags }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Create complex Instance Profile - Idempotent" + iam_instance_profile: + name: "{{ test_profile_complex }}" + role: "{{ test_role }}-2" + path: "{{ test_path }}" + tags: "{{ test_tags }}" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # =================================================================== + + - name: "List all Instance Profiles (no filter)" + iam_instance_profile_info: + register: profile_info + + - assert: + that: + - profile_info.iam_instance_profiles | length >= 4 + - '"{{ test_role }}" in profile_names' + - '"{{ test_role }}-2" in profile_names' + - '"{{ test_profile }}" in profile_names' + - '"{{ test_profile_complex }}" in profile_names' + + - '"arn" in complex_profile' + - '"create_date" in complex_profile' + - '"instance_profile_id" in complex_profile' + - '"instance_profile_name" in complex_profile' + - complex_profile.instance_profile_name == test_profile_complex + - '"path" in complex_profile' + - complex_profile.path == test_path + - '"roles" in complex_profile' + - complex_profile.roles | length == 1 + - '"arn" in complex_profile.roles[0]' + - '"assume_role_policy_document" in complex_profile.roles[0]' + - '"create_date" in complex_profile.roles[0]' + - '"path" in complex_profile.roles[0]' + - complex_profile.roles[0].path == test_path + - '"role_id" in complex_profile.roles[0]' + - '"role_name" in complex_profile.roles[0]' + - complex_profile.roles[0].role_name == "{{ test_role }}-2" + vars: + profile_names: '{{ profile_info.iam_instance_profiles | map(attribute="instance_profile_name") }}' + complex_profile: '{{ profile_info.iam_instance_profiles | selectattr("instance_profile_name", "match", test_profile_complex) | first}}' + + - name: "List all Instance Profiles (filter by path)" + iam_instance_profile_info: + path: "{{ test_path }}" + register: profile_info + + - assert: + that: + - profile_info.iam_instance_profiles | length == 3 + - '"{{ test_role }}" in profile_names' + - '"{{ test_role }}-2" in profile_names' + - '"{{ test_profile_complex }}" in profile_names' + + - '"arn" in complex_profile' + - '"create_date" in complex_profile' + - '"instance_profile_id" in complex_profile' + - '"instance_profile_name" in complex_profile' + - complex_profile.instance_profile_name == test_profile_complex + - '"path" in complex_profile' + - complex_profile.path == test_path + - '"roles" in complex_profile' + - complex_profile.roles | length == 1 + - '"arn" in complex_profile.roles[0]' + - '"assume_role_policy_document" in complex_profile.roles[0]' + - '"create_date" in complex_profile.roles[0]' + - '"path" in complex_profile.roles[0]' + - complex_profile.roles[0].path == test_path + - '"role_id" in complex_profile.roles[0]' + - '"role_name" in complex_profile.roles[0]' + - complex_profile.roles[0].role_name == "{{ test_role }}-2" + vars: + profile_names: '{{ profile_info.iam_instance_profiles | map(attribute="instance_profile_name") }}' + complex_profile: '{{ profile_info.iam_instance_profiles | selectattr("instance_profile_name", "match", test_profile_complex) | first}}' + + - name: "List all Instance Profiles (filter by name - complex)" + iam_instance_profile_info: + name: "{{ test_profile_complex }}" + register: profile_info + + - assert: + that: + - profile_info.iam_instance_profiles | length == 1 + - '"{{ test_role }}" in profile_names' + - '"{{ test_role }}-2" in profile_names' + - '"{{ test_profile_complex }}" in profile_names' + + - '"arn" in complex_profile' + - '"create_date" in complex_profile' + - '"instance_profile_id" in complex_profile' + - '"instance_profile_name" in complex_profile' + - complex_profile.instance_profile_name == test_profile_complex + - '"path" in complex_profile' + - complex_profile.path == test_path + - '"tags" in complex_profile' + - complex_profile.tags == test_tags + - '"roles" in complex_profile' + - complex_profile.roles | length == 1 + - '"arn" in complex_profile.roles[0]' + - '"assume_role_policy_document" in complex_profile.roles[0]' + - '"create_date" in complex_profile.roles[0]' + - '"path" in complex_profile.roles[0]' + - complex_profile.roles[0].path == test_path + - '"role_id" in complex_profile.roles[0]' + - '"role_name" in complex_profile.roles[0]' + - complex_profile.roles[0].role_name == "{{ test_role }}-2" + - '"tags" in complex_profile.roles[0]' + - complex_profile.roles[0].tags == {} + vars: + profile_names: '{{ profile_info.iam_instance_profiles | map(attribute="instance_profile_name") }}' + complex_profile: '{{ profile_info.iam_instance_profiles | selectattr("instance_profile_name", "match", test_profile_complex) | first}}' + + - name: "List an Instance Profile (filter by name)" + iam_instance_profile_info: + name: "{{ test_profile }}" + register: profile_info + + - assert: + that: + - profile_info.iam_instance_profiles | length == 1 + - '"arn" in simple_profile' + - '"create_date" in simple_profile' + - '"instance_profile_id" in simple_profile' + - '"instance_profile_name" in simple_profile' + - simple_profile.instance_profile_name == test_profile + - '"path" in simple_profile' + - simple_profile.path == "/" + - '"tags" in simple_profile' + - simple_profile.tags == {} + - '"roles" in simple_profile' + - simple_profile.roles | length == 0 + vars: + simple_profile: '{{ profile_info.iam_instance_profiles[0] }}' + + # =================================================================== + + - name: "Delete minimal Instance Profile (CHECK)" + iam_instance_profile: + state: absent + name: "{{ test_profile }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Delete minimal Instance Profile" + iam_instance_profile: + state: absent + name: "{{ test_profile }}" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Delete minimal Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + state: absent + name: "{{ test_profile }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Delete minimal Instance Profile - Idempotent" + iam_instance_profile: + state: absent + name: "{{ test_profile }}" + register: profile_result + + - assert: + that: + - profile_result is not changed + + # =================================================================== + + - name: "Delete complex Instance Profile (CHECK)" + iam_instance_profile: + state: absent + name: "{{ test_profile_complex }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Delete complex Instance Profile" + iam_instance_profile: + state: absent + name: "{{ test_profile_complex }}" + register: profile_result + + - assert: + that: + - profile_result is changed + + - name: "Delete complex Instance Profile - Idempotent (CHECK)" + iam_instance_profile: + state: absent + name: "{{ test_profile_complex }}" + check_mode: True + register: profile_result + + - assert: + that: + - profile_result is not changed + + - name: "Delete complex Instance Profile - Idempotent" + iam_instance_profile: + state: absent + name: "{{ test_profile_complex }}" + register: profile_result + + - assert: + that: + - profile_result is not changed + + always: + # =================================================================== + # Cleanup + + - name: "iam_instance_profile_info after Role deletion" + iam_instance_profile_info: + ignore_errors: true + + - name: "Delete Instance Profiles" + iam_instance_profile: + state: absent + name: "{{ item }}" + loop: + - "{{ test_profile }}" + - "{{ test_profile_complex }}" + - "{{ test_role }}" + - "{{ test_role }}-2" + + - name: "Remove IAM Roles" + iam_role: + state: absent + name: "{{ item }}" + path: "{{ test_path }}" + delete_instance_profile: yes + ignore_errors: true + loop: + - "{{ test_role }}" + - "{{ test_role }}-2" + + - name: "iam_role_info after Role deletion" + iam_role_info: + path: "{{ test_path }}" + ignore_errors: true diff --git a/tests/integration/targets/iam_instance_profile/tasks/tags.yml b/tests/integration/targets/iam_instance_profile/tasks/tags.yml new file mode 100644 index 0000000000..5185301b8a --- /dev/null +++ b/tests/integration/targets/iam_instance_profile/tasks/tags.yml @@ -0,0 +1,298 @@ +- vars: + first_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + second_tags: + 'New Key with Spaces': Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + third_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + final_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + module_defaults: + amazon.aws.iam_instance_profile: + name: '{{ test_profile }}' + amazon.aws.iam_instance_profile_info: + name: "{{ test_profile }}" + block: + + # ============================================================ + # + + - name: (check) add tags + iam_instance_profile: + tags: '{{ first_tags }}' + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would change + assert: + that: + - tag_profile is changed + + - name: add tags + iam_instance_profile: + tags: '{{ first_tags }}' + state: 'present' + register: tag_profile + + - name: get instance profile facts + iam_instance_profile_info: {} + register: tag_profile_info + + - name: verify the tags were added + assert: + that: + - tag_profile is changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == first_tags + + - name: (check) add tags - IDEMPOTENCY + iam_instance_profile: + tags: '{{ first_tags }}' + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would not change + assert: + that: + - tag_profile is not changed + + - name: add tags - IDEMPOTENCY + iam_instance_profile: + tags: '{{ first_tags }}' + state: 'present' + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: {} + register: tag_profile_info + + - name: verify no change + assert: + that: + - tag_profile is not changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == first_tags + + # ============================================================ + + - name: (check) modify tags with purge + iam_instance_profile: + tags: '{{ second_tags }}' + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would change + assert: + that: + - tag_profile is changed + + - name: modify tags with purge + iam_instance_profile: + tags: '{{ second_tags }}' + state: 'present' + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify the tags were added + assert: + that: + - tag_profile is changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == second_tags + + - name: (check) modify tags with purge - IDEMPOTENCY + iam_instance_profile: + tags: '{{ second_tags }}' + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would not change + assert: + that: + - tag_profile is not changed + + - name: modify tags with purge - IDEMPOTENCY + iam_instance_profile: + tags: '{{ second_tags }}' + state: 'present' + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify no change + assert: + that: + - tag_profile is not changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == second_tags + + # ============================================================ + + - name: (check) modify tags without purge + iam_instance_profile: + tags: '{{ third_tags }}' + state: 'present' + purge_tags: False + register: tag_profile + check_mode: True + + - name: assert would change + assert: + that: + - tag_profile is changed + + - name: modify tags without purge + iam_instance_profile: + tags: '{{ third_tags }}' + state: 'present' + purge_tags: False + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify the tags were added + assert: + that: + - tag_profile is changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == final_tags + + - name: (check) modify tags without purge - IDEMPOTENCY + iam_instance_profile: + tags: '{{ third_tags }}' + state: 'present' + purge_tags: False + register: tag_profile + check_mode: True + + - name: assert would not change + assert: + that: + - tag_profile is not changed + + - name: modify tags without purge - IDEMPOTENCY + iam_instance_profile: + tags: '{{ third_tags }}' + state: 'present' + purge_tags: False + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify no change + assert: + that: + - tag_profile is not changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == final_tags + + # ============================================================ + + - name: (check) No change to tags without setting tags + iam_instance_profile: + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would change + assert: + that: + - tag_profile is not changed + + - name: No change to tags without setting tags + iam_instance_profile: + state: 'present' + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify the tags were added + assert: + that: + - tag_profile is not changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == final_tags + + # ============================================================ + + - name: (check) remove all tags + iam_instance_profile: + tags: {} + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would change + assert: + that: + - tag_profile is changed + + - name: remove all tags + iam_instance_profile: + tags: {} + state: 'present' + register: tag_profile + - name: get instance profile facts + iam_instance_profile_info: + register: tag_profile_info + + - name: verify the tags were added + assert: + that: + - tag_profile is changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == {} + + - name: (check) remove all tags - IDEMPOTENCY + iam_instance_profile: + tags: {} + state: 'present' + register: tag_profile + check_mode: True + + - name: assert would not change + assert: + that: + - tag_profile is not changed + + - name: remove all tags - IDEMPOTENCY + iam_instance_profile: + tags: {} + state: 'present' + register: tag_profile + - name: get instance profile + iam_instance_profile_info: + register: tag_profile_info + + - name: verify no change + assert: + that: + - tag_profile is not changed + - tag_profile_info.iam_instance_profiles[0].instance_profile_name == test_profile + - tag_profile_info.iam_instance_profiles[0].tags == {} From 5e825287e6c60fa903fb10fa913c3ae8c97aea34 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 16 Jun 2023 15:34:54 +0200 Subject: [PATCH 2/3] Disable debugging in integration tests --- .../targets/iam_instance_profile/tasks/main.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/integration/targets/iam_instance_profile/tasks/main.yml b/tests/integration/targets/iam_instance_profile/tasks/main.yml index 37159b1cb8..f628e61711 100644 --- a/tests/integration/targets/iam_instance_profile/tasks/main.yml +++ b/tests/integration/targets/iam_instance_profile/tasks/main.yml @@ -337,8 +337,6 @@ - assert: that: - profile_info.iam_instance_profiles | length == 1 - - '"{{ test_role }}" in profile_names' - - '"{{ test_role }}-2" in profile_names' - '"{{ test_profile_complex }}" in profile_names' - '"arn" in complex_profile' @@ -480,14 +478,15 @@ # =================================================================== # Cleanup - - name: "iam_instance_profile_info after Role deletion" - iam_instance_profile_info: - ignore_errors: true +# - name: "iam_instance_profile_info after Role deletion" +# iam_instance_profile_info: +# ignore_errors: true - name: "Delete Instance Profiles" iam_instance_profile: state: absent name: "{{ item }}" + ignore_errors: true loop: - "{{ test_profile }}" - "{{ test_profile_complex }}" @@ -505,7 +504,7 @@ - "{{ test_role }}" - "{{ test_role }}-2" - - name: "iam_role_info after Role deletion" - iam_role_info: - path: "{{ test_path }}" - ignore_errors: true +# - name: "iam_role_info after Role deletion" +# iam_role_info: +# path: "{{ test_path }}" +# ignore_errors: true From 858a711e0f861266ff6c84335e050f9191d42b65 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 16 Jun 2023 15:42:21 +0200 Subject: [PATCH 3/3] sort imports --- plugins/modules/iam_instance_profile.py | 7 ++----- plugins/modules/iam_instance_profile_info.py | 3 +-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/plugins/modules/iam_instance_profile.py b/plugins/modules/iam_instance_profile.py index 181f4ea746..b34b6b1305 100644 --- a/plugins/modules/iam_instance_profile.py +++ b/plugins/modules/iam_instance_profile.py @@ -107,20 +107,17 @@ from copy import deepcopy -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry - +from ansible_collections.amazon.aws.plugins.module_utils.iam import add_role_to_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError from ansible_collections.amazon.aws.plugins.module_utils.iam import create_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import delete_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import list_iam_instance_profiles from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import remove_role_from_iam_instance_profile -from ansible_collections.amazon.aws.plugins.module_utils.iam import add_role_to_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import tag_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.iam import untag_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags diff --git a/plugins/modules/iam_instance_profile_info.py b/plugins/modules/iam_instance_profile_info.py index faddab030f..6b7f5ed246 100644 --- a/plugins/modules/iam_instance_profile_info.py +++ b/plugins/modules/iam_instance_profile_info.py @@ -88,12 +88,11 @@ sample: [] """ -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry - from ansible_collections.amazon.aws.plugins.module_utils.iam import AnsibleIAMError from ansible_collections.amazon.aws.plugins.module_utils.iam import list_iam_instance_profiles from ansible_collections.amazon.aws.plugins.module_utils.iam import normalize_iam_instance_profile from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry def describe_iam_instance_profiles(module, client):