From 1f3b0d843538242c706d318924935b6491133ae7 Mon Sep 17 00:00:00 2001 From: abikouo Date: Fri, 15 Sep 2023 16:52:28 +0200 Subject: [PATCH 1/5] Promote modules iam_role and iam_role_info and apply deprecation --- ...915_migrate_iam_role_and_iam_role_info.yml | 7 + meta/runtime.yml | 2 + plugins/modules/iam_role.py | 754 ++++++++++++++++++ plugins/modules/iam_role_info.py | 277 +++++++ tests/integration/targets/iam_role/aliases | 9 + .../targets/iam_role/defaults/main.yml | 6 + .../targets/iam_role/files/deny-all-a.json | 13 + .../targets/iam_role/files/deny-all-b.json | 13 + .../targets/iam_role/files/deny-all.json | 12 + .../targets/iam_role/files/deny-assume.json | 10 + .../targets/iam_role/meta/main.yml | 1 + .../iam_role/tasks/boundary_policy.yml | 94 +++ .../iam_role/tasks/complex_role_creation.yml | 131 +++ .../iam_role/tasks/creation_deletion.yml | 404 ++++++++++ .../iam_role/tasks/description_update.yml | 148 ++++ .../iam_role/tasks/inline_policy_update.yml | 48 ++ .../targets/iam_role/tasks/main.yml | 119 +++ .../iam_role/tasks/max_session_update.yml | 71 ++ .../iam_role/tasks/parameter_checks.yml | 90 +++ .../targets/iam_role/tasks/policy_update.yml | 250 ++++++ .../targets/iam_role/tasks/role_removal.yml | 65 ++ .../targets/iam_role/tasks/tags_update.yml | 341 ++++++++ 22 files changed, 2865 insertions(+) create mode 100644 changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml create mode 100644 plugins/modules/iam_role.py create mode 100644 plugins/modules/iam_role_info.py create mode 100644 tests/integration/targets/iam_role/aliases create mode 100644 tests/integration/targets/iam_role/defaults/main.yml create mode 100644 tests/integration/targets/iam_role/files/deny-all-a.json create mode 100644 tests/integration/targets/iam_role/files/deny-all-b.json create mode 100644 tests/integration/targets/iam_role/files/deny-all.json create mode 100644 tests/integration/targets/iam_role/files/deny-assume.json create mode 100644 tests/integration/targets/iam_role/meta/main.yml create mode 100644 tests/integration/targets/iam_role/tasks/boundary_policy.yml create mode 100644 tests/integration/targets/iam_role/tasks/complex_role_creation.yml create mode 100644 tests/integration/targets/iam_role/tasks/creation_deletion.yml create mode 100644 tests/integration/targets/iam_role/tasks/description_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/inline_policy_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/main.yml create mode 100644 tests/integration/targets/iam_role/tasks/max_session_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/parameter_checks.yml create mode 100644 tests/integration/targets/iam_role/tasks/policy_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/role_removal.yml create mode 100644 tests/integration/targets/iam_role/tasks/tags_update.yml diff --git a/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml b/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml new file mode 100644 index 00000000000..100bbc93c88 --- /dev/null +++ b/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml @@ -0,0 +1,7 @@ +breaking_changes: +- iam_role - The module has been migrated from the ``community.aws`` collection. + Playbooks using the Fully Qualified Collection Name for this module should be updated + to use ``amazon.aws.iam_role`` (https://github.com/ansible-collections/amazon.aws/pull/1756). +- iam_role_info - The module has been migrated from the ``community.aws`` collection. + Playbooks using the Fully Qualified Collection Name for this module should be updated + to use ``amazon.aws.iam_role_info`` (https://github.com/ansible-collections/amazon.aws/pull/1756). diff --git a/meta/runtime.yml b/meta/runtime.yml index c627df5be2b..fb09ff6ee8d 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -70,6 +70,8 @@ action_groups: - execute_lambda - iam_instance_profile - iam_instance_profile_info + - iam_role + - iam_role_info - iam_policy - iam_policy_info - iam_user diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py new file mode 100644 index 00000000000..f995af1d6bf --- /dev/null +++ b/plugins/modules/iam_role.py @@ -0,0 +1,754 @@ +#!/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_role +version_added: 1.0.0 +short_description: Manage AWS IAM roles +description: + - Manage AWS IAM roles. +author: + - "Rob White (@wimnat)" +options: + path: + description: + - The path to the role. For more information about paths, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). + default: "/" + type: str + name: + description: + - The name of the role to create. + required: true + type: str + description: + description: + - Provides a description of the role. + type: str + boundary: + description: + - The ARN of an IAM managed policy to use to restrict the permissions this role can pass on to IAM roles/users that it creates. + - Boundaries cannot be set on Instance Profiles, as such if this option is specified then I(create_instance_profile) must be C(false). + - This is intended for roles/users that have permissions to create new IAM objects. + - For more information on boundaries, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). + aliases: [boundary_policy_arn] + type: str + assume_role_policy_document: + description: + - The trust relationship policy document that grants an entity permission to assume the role. + - This parameter is required when I(state=present). + type: json + managed_policies: + description: + - A list of managed policy ARNs, managed policy ARNs or friendly names. + - To remove all policies set I(purge_polices=true) and I(managed_policies=[None]). + - To embed an inline policy, use M(amazon.aws.iam_policy). + aliases: ['managed_policy'] + type: list + elements: str + max_session_duration: + description: + - The maximum duration (in seconds) of a session when assuming the role. + - Valid values are between 1 and 12 hours (3600 and 43200 seconds). + type: int + purge_policies: + description: + - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. + type: bool + aliases: ['purge_policy', 'purge_managed_policies'] + default: true + state: + description: + - Create or remove the IAM role. + default: present + choices: [ present, absent ] + type: str + create_instance_profile: + description: + - Creates an IAM instance profile along with the role. + default: true + type: bool + delete_instance_profile: + description: + - When I(delete_instance_profile=true) and I(state=absent) deleting a role will also delete the instance + profile created with the same I(name) as the role. + - Only applies when I(state=absent). + default: false + type: bool + wait_timeout: + description: + - How long (in seconds) to wait for creation / update to complete. + default: 120 + type: int + wait: + description: + - When I(wait=True) the module will wait for up to I(wait_timeout) seconds + for IAM role creation before returning. + default: True + type: bool +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.tags + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create a role with description and tags + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + description: This is My New Role + tags: + env: dev + +- name: "Create a role and attach a managed policy called 'PowerUserAccess'" + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + managed_policies: + - arn:aws:iam::aws:policy/PowerUserAccess + +- name: Keep the role created above but remove all managed policies + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + managed_policies: [] + +- name: Delete the role + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file', 'policy.json') }}" + state: absent + +""" +RETURN = r""" +iam_role: + description: dictionary containing the IAM Role data + returned: success + type: complex + contains: + path: + description: the path to the role + type: str + returned: always + sample: / + role_name: + description: the friendly name that identifies the role + type: str + returned: always + sample: myrole + role_id: + description: the stable and unique string identifying the role + type: str + returned: always + sample: ABCDEFF4EZ4ABCDEFV4ZC + arn: + description: the Amazon Resource Name (ARN) specifying the role + type: str + returned: always + sample: "arn:aws:iam::1234567890:role/mynewrole" + create_date: + description: the date and time, in ISO 8601 date-time format, when the role was created + type: str + returned: always + sample: "2016-08-14T04:36:28+00:00" + assume_role_policy_document: + description: + - the policy that grants an entity permission to assume the role + type: dict + returned: always + sample: { + 'statement': [ + { + 'action': 'sts:AssumeRole', + 'effect': 'Allow', + 'principal': { + 'service': 'ec2.amazonaws.com' + }, + 'sid': '' + } + ], + 'version': '2012-10-17' + } + assume_role_policy_document_raw: + description: the policy that grants an entity permission to assume the role + type: dict + returned: always + version_added: 5.3.0 + sample: { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'Service': 'ec2.amazonaws.com' + }, + 'Sid': '' + } + ], + 'Version': '2012-10-17' + } + + attached_policies: + description: a list of dicts containing the name and ARN of the managed IAM policies attached to the role + type: list + returned: always + sample: [ + { + 'policy_arn': 'arn:aws:iam::aws:policy/PowerUserAccess', + 'policy_name': 'PowerUserAccess' + } + ] + tags: + description: role tags + type: dict + returned: always + sample: '{"Env": "Prod"}' +""" + +import json + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.arn import validate_aws_arn +from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.policy import compare_policies +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +@AWSRetry.jittered_backoff() +def _list_policies(client): + paginator = client.get_paginator("list_policies") + return paginator.paginate().build_full_result()["Policies"] + + +def wait_iam_exists(module, client): + if module.check_mode: + return + if not module.params.get("wait"): + return + + role_name = module.params.get("name") + wait_timeout = module.params.get("wait_timeout") + + delay = min(wait_timeout, 5) + max_attempts = wait_timeout // delay + + try: + waiter = client.get_waiter("role_exists") + waiter.wait( + WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts}, + RoleName=role_name, + ) + except botocore.exceptions.WaiterError as e: + module.fail_json_aws(e, msg="Timeout while waiting on IAM role creation") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed while waiting on IAM role creation") + + +def convert_friendly_names_to_arns(module, client, policy_names): + if all(validate_aws_arn(policy, service="iam") for policy in policy_names if policy is not None): + return policy_names + + allpolicies = {} + policies = _list_policies(client) + + for policy in policies: + allpolicies[policy["PolicyName"]] = policy["Arn"] + allpolicies[policy["Arn"]] = policy["Arn"] + try: + return [allpolicies[policy] for policy in policy_names if policy is not None] + except KeyError as e: + module.fail_json_aws(e, msg="Couldn't find policy") + + +def attach_policies(module, client, policies_to_attach, role_name): + if module.check_mode and policies_to_attach: + return True + + changed = False + for policy_arn in policies_to_attach: + try: + client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn, aws_retry=True) + changed = True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to attach policy {policy_arn} to role {role_name}") + return changed + + +def remove_policies(module, client, policies_to_remove, role_name): + if module.check_mode and policies_to_remove: + return True + + changed = False + for policy in policies_to_remove: + try: + client.detach_role_policy(RoleName=role_name, PolicyArn=policy, aws_retry=True) + changed = True + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to detach policy {policy} from {role_name}") + return changed + + +def remove_inline_policies(module, client, role_name): + current_inline_policies = get_inline_policy_list(module, client, role_name) + for policy in current_inline_policies: + try: + client.delete_role_policy(RoleName=role_name, PolicyName=policy, aws_retry=True) + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to delete policy {policy} embedded in {role_name}") + + +def generate_create_params(module): + params = dict() + params["Path"] = module.params.get("path") + params["RoleName"] = module.params.get("name") + params["AssumeRolePolicyDocument"] = module.params.get("assume_role_policy_document") + if module.params.get("description") is not None: + params["Description"] = module.params.get("description") + if module.params.get("max_session_duration") is not None: + params["MaxSessionDuration"] = module.params.get("max_session_duration") + if module.params.get("boundary") is not None: + params["PermissionsBoundary"] = module.params.get("boundary") + if module.params.get("tags") is not None: + params["Tags"] = ansible_dict_to_boto3_tag_list(module.params.get("tags")) + + return params + + +def create_basic_role(module, client): + """ + Perform the Role creation. + Assumes tests for the role existing have already been performed. + """ + if module.check_mode: + module.exit_json(changed=True) + + try: + params = generate_create_params(module) + role = client.create_role(aws_retry=True, **params) + # 'Description' is documented as key of the role returned by create_role + # but appears to be an AWS bug (the value is not returned using the AWS CLI either). + # Get the role after creating it. + role = get_role_with_backoff(module, client, params["RoleName"]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to create role") + + return role + + +def update_role_assumed_policy(module, client, role_name, target_assumed_policy, current_assumed_policy): + # Check Assumed Policy document + if target_assumed_policy is None or not compare_policies(current_assumed_policy, json.loads(target_assumed_policy)): + return False + + if module.check_mode: + return True + + try: + client.update_assume_role_policy(RoleName=role_name, PolicyDocument=target_assumed_policy, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update assume role policy for role {role_name}") + return True + + +def update_role_description(module, client, role_name, target_description, current_description): + # Check Description update + if target_description is None or current_description == target_description: + return False + + if module.check_mode: + return True + + try: + client.update_role(RoleName=role_name, Description=target_description, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update description for role {role_name}") + return True + + +def update_role_max_session_duration(module, client, role_name, target_duration, current_duration): + # Check MaxSessionDuration update + if target_duration is None or current_duration == target_duration: + return False + + if module.check_mode: + return True + + try: + client.update_role(RoleName=role_name, MaxSessionDuration=target_duration, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update maximum session duration for role {role_name}") + return True + + +def update_role_permissions_boundary( + module, client, role_name, target_permissions_boundary, current_permissions_boundary +): + # Check PermissionsBoundary + if target_permissions_boundary is None or target_permissions_boundary == current_permissions_boundary: + return False + + if module.check_mode: + return True + + if target_permissions_boundary == "": + try: + client.delete_role_permissions_boundary(RoleName=role_name, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to remove permission boundary for role {role_name}") + else: + try: + client.put_role_permissions_boundary( + RoleName=role_name, PermissionsBoundary=target_permissions_boundary, aws_retry=True + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update permission boundary for role {role_name}") + return True + + +def update_managed_policies(module, client, role_name, managed_policies, purge_policies): + # Check Managed Policies + if managed_policies is None: + return False + + # Get list of current attached managed policies + current_attached_policies = get_attached_policy_list(module, client, role_name) + current_attached_policies_arn_list = [policy["PolicyArn"] for policy in current_attached_policies] + + if len(managed_policies) == 1 and managed_policies[0] is None: + managed_policies = [] + + policies_to_remove = set(current_attached_policies_arn_list) - set(managed_policies) + policies_to_attach = set(managed_policies) - set(current_attached_policies_arn_list) + + changed = False + if purge_policies and policies_to_remove: + if module.check_mode: + return True + else: + changed |= remove_policies(module, client, policies_to_remove, role_name) + + if policies_to_attach: + if module.check_mode: + return True + else: + changed |= attach_policies(module, client, policies_to_attach, role_name) + + return changed + + +def create_or_update_role(module, client): + role_name = module.params.get("name") + assumed_policy = module.params.get("assume_role_policy_document") + create_instance_profile = module.params.get("create_instance_profile") + description = module.params.get("description") + duration = module.params.get("max_session_duration") + path = module.params.get("path") + permissions_boundary = module.params.get("boundary") + purge_tags = module.params.get("purge_tags") + tags = ansible_dict_to_boto3_tag_list(module.params.get("tags")) if module.params.get("tags") else None + purge_policies = module.params.get("purge_policies") + managed_policies = module.params.get("managed_policies") + if managed_policies: + # Attempt to list the policies early so we don't leave things behind if we can't find them. + managed_policies = convert_friendly_names_to_arns(module, client, managed_policies) + + changed = False + + # Get role + role = get_role(module, client, role_name) + + # If role is None, create it + if role is None: + role = create_basic_role(module, client) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + changed = True + else: + # Role exists - get current attributes + current_assumed_policy = role.get("AssumeRolePolicyDocument") + current_description = role.get("Description") + current_duration = role.get("MaxSessionDuration") + current_permissions_boundary = role.get("PermissionsBoundary", {}).get("PermissionsBoundaryArn", "") + + # Update attributes + changed |= update_role_tags(module, client, role_name, tags, purge_tags) + changed |= update_role_assumed_policy(module, client, role_name, assumed_policy, current_assumed_policy) + changed |= update_role_description(module, client, role_name, description, current_description) + changed |= update_role_max_session_duration(module, client, role_name, duration, current_duration) + changed |= update_role_permissions_boundary( + module, client, role_name, permissions_boundary, current_permissions_boundary + ) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + if create_instance_profile: + changed |= create_instance_profiles(module, client, role_name, path) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + changed |= update_managed_policies(module, client, role_name, managed_policies, purge_policies) + wait_iam_exists(module, client) + + # Get the role again + role = get_role(module, client, role_name) + role["AttachedPolicies"] = get_attached_policy_list(module, client, role_name) + role["tags"] = get_role_tags(module, client) + + camel_role = camel_dict_to_snake_dict(role, ignore_list=["tags", "AssumeRolePolicyDocument"]) + camel_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument", {}) + camel_role["assume_role_policy_document_raw"] = camel_role["assume_role_policy_document"] + module.exit_json(changed=changed, iam_role=camel_role) + + +def create_instance_profiles(module, client, role_name, path): + # Fetch existing Profiles + try: + instance_profiles = client.list_instance_profiles_for_role(RoleName=role_name, aws_retry=True)[ + "InstanceProfiles" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + + # Profile already exists + if any(p["InstanceProfileName"] == role_name for p in instance_profiles): + return False + + if module.check_mode: + return True + + # Make sure an instance profile is created + try: + client.create_instance_profile(InstanceProfileName=role_name, Path=path, aws_retry=True) + except is_boto3_error_code("EntityAlreadyExists"): + # If the profile already exists, no problem, move on. + # Implies someone's changing things at the same time... + return False + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to create instance profile for role {role_name}") + + # And attach the role to the profile + try: + client.add_role_to_instance_profile(InstanceProfileName=role_name, RoleName=role_name, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to attach role {role_name} to instance profile {role_name}") + + return True + + +def remove_instance_profiles(module, client, role_name): + delete_profiles = module.params.get("delete_instance_profile") + + try: + instance_profiles = client.list_instance_profiles_for_role(aws_retry=True, RoleName=role_name)[ + "InstanceProfiles" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + + # Remove the role from the instance profile(s) + for profile in instance_profiles: + profile_name = profile["InstanceProfileName"] + try: + if not module.check_mode: + client.remove_role_from_instance_profile( + aws_retry=True, InstanceProfileName=profile_name, RoleName=role_name + ) + if profile_name == role_name: + if delete_profiles: + try: + client.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to remove instance profile {profile_name}") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to remove role {role_name} from instance profile {profile_name}") + + +def destroy_role(module, client): + role_name = module.params.get("name") + role = get_role(module, client, role_name) + + if role is None: + module.exit_json(changed=False) + + if not module.check_mode: + # Before we try to delete the role we need to remove any + # - attached instance profiles + # - attached managed policies + # - embedded inline policies + remove_instance_profiles(module, client, role_name) + update_managed_policies(module, client, role_name, [], True) + remove_inline_policies(module, client, role_name) + try: + client.delete_role(aws_retry=True, RoleName=role_name) + except is_boto3_error_code("NoSuchEntityException"): + module.exit_json(changed=False) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Unable to delete role") + + module.exit_json(changed=True) + + +def get_role_with_backoff(module, client, name): + try: + return AWSRetry.jittered_backoff(catch_extra_error_codes=["NoSuchEntity"])(client.get_role)(RoleName=name)[ + "Role" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to get role {name}") + + +def get_role(module, client, name): + try: + return client.get_role(RoleName=name, aws_retry=True)["Role"] + except is_boto3_error_code("NoSuchEntity"): + return None + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to get role {name}") + + +def get_attached_policy_list(module, client, name): + try: + return client.list_attached_role_policies(RoleName=name, aws_retry=True)["AttachedPolicies"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}") + + +def get_inline_policy_list(module, client, name): + try: + return client.list_role_policies(RoleName=name, aws_retry=True)["PolicyNames"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}") + + +def get_role_tags(module, client): + role_name = module.params.get("name") + try: + return boto3_tag_list_to_ansible_dict(client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list tags for role {role_name}") + + +def update_role_tags(module, client, role_name, new_tags, purge_tags): + if new_tags is None: + return False + new_tags = boto3_tag_list_to_ansible_dict(new_tags) + + try: + existing_tags = boto3_tag_list_to_ansible_dict( + client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError, KeyError): + existing_tags = {} + + tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, new_tags, purge_tags=purge_tags) + + if not module.check_mode: + try: + if tags_to_remove: + client.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) + if tags_to_add: + client.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to set tags for role {role_name}") + + changed = bool(tags_to_add) or bool(tags_to_remove) + return changed + + +def main(): + argument_spec = dict( + name=dict(type="str", required=True), + path=dict(type="str", default="/"), + assume_role_policy_document=dict(type="json"), + managed_policies=dict(type="list", aliases=["managed_policy"], elements="str"), + max_session_duration=dict(type="int"), + state=dict(type="str", choices=["present", "absent"], default="present"), + description=dict(type="str"), + boundary=dict(type="str", aliases=["boundary_policy_arn"]), + create_instance_profile=dict(type="bool", default=True), + delete_instance_profile=dict(type="bool", default=False), + purge_policies=dict(default=True, type="bool", aliases=["purge_policy", "purge_managed_policies"]), + tags=dict(type="dict", aliases=["resource_tags"]), + purge_tags=dict(type="bool", default=True), + wait=dict(type="bool", default=True), + wait_timeout=dict(default=120, type="int"), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + required_if=[("state", "present", ["assume_role_policy_document"])], + supports_check_mode=True, + ) + + if module.params.get("boundary"): + if module.params.get("create_instance_profile"): + module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.") + if not validate_aws_arn(module.params.get("boundary"), service="iam"): + module.fail_json(msg="Boundary policy must be an ARN") + if module.params.get("max_session_duration"): + max_session_duration = module.params.get("max_session_duration") + if max_session_duration < 3600 or max_session_duration > 43200: + module.fail_json(msg="max_session_duration must be between 1 and 12 hours (3600 and 43200 seconds)") + if module.params.get("path"): + path = module.params.get("path") + if not path.endswith("/") or not path.startswith("/"): + module.fail_json(msg="path must begin and end with /") + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + state = module.params.get("state") + + if state == "present": + create_or_update_role(module, client) + elif state == "absent": + destroy_role(module, client) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/iam_role_info.py b/plugins/modules/iam_role_info.py new file mode 100644 index 00000000000..ce40b4237d1 --- /dev/null +++ b/plugins/modules/iam_role_info.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: iam_role_info +version_added: 1.0.0 +short_description: Gather information on IAM roles +description: + - Gathers information about IAM roles. +author: + - "Will Thames (@willthames)" +options: + name: + description: + - Name of a role to search for. + - Mutually exclusive with I(path_prefix). + aliases: + - role_name + type: str + path_prefix: + description: + - Prefix of role to restrict IAM role search for. + - Mutually exclusive with I(name). + type: str +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +- name: find all existing IAM roles + amazon.aws.iam_role_info: + register: result + +- name: describe a single role + amazon.aws.iam_role_info: + name: MyIAMRole + +- name: describe all roles matching a path prefix + amazon.aws.iam_role_info: + path_prefix: /application/path +""" + +RETURN = r""" +iam_roles: + description: List of IAM roles + returned: always + type: complex + contains: + arn: + description: Amazon Resource Name for IAM role. + returned: always + type: str + sample: arn:aws:iam::123456789012:role/AnsibleTestRole + assume_role_policy_document: + description: + - The policy that grants an entity permission to assume the role. + returned: always + type: dict + assume_role_policy_document_raw: + description: The policy document describing what can assume the role. + returned: always + type: dict + version_added: 5.3.0 + create_date: + description: Date IAM role was created. + returned: always + type: str + sample: '2017-10-23T00:05:08+00:00' + inline_policies: + description: List of names of inline policies. + returned: always + type: list + sample: [] + managed_policies: + description: List of attached managed policies. + returned: always + type: complex + contains: + policy_arn: + description: Amazon Resource Name for the policy. + returned: always + type: str + sample: arn:aws:iam::123456789012:policy/AnsibleTestEC2Policy + policy_name: + description: Name of managed policy. + returned: always + type: str + sample: AnsibleTestEC2Policy + instance_profiles: + description: List of attached 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/AnsibleTestEC2Policy + create_date: + description: Date instance profile was created. + returned: always + type: str + sample: '2017-10-23T00:05:08+00:00' + instance_profile_id: + description: Amazon Identifier for the instance profile. + returned: always + type: str + sample: AROAII7ABCD123456EFGH + 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: [] + path: + description: Path of role. + returned: always + type: str + sample: / + role_id: + description: Amazon Identifier for the role. + returned: always + type: str + sample: AROAII7ABCD123456EFGH + role_name: + description: Name of the role. + returned: always + type: str + sample: AnsibleTestRole + tags: + description: Role tags. + type: dict + returned: always + sample: '{"Env": "Prod"}' +""" + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +@AWSRetry.jittered_backoff() +def list_iam_roles_with_backoff(client, **kwargs): + paginator = client.get_paginator("list_roles") + return paginator.paginate(**kwargs).build_full_result() + + +@AWSRetry.jittered_backoff() +def list_iam_role_policies_with_backoff(client, role_name): + paginator = client.get_paginator("list_role_policies") + return paginator.paginate(RoleName=role_name).build_full_result()["PolicyNames"] + + +@AWSRetry.jittered_backoff() +def list_iam_attached_role_policies_with_backoff(client, role_name): + paginator = client.get_paginator("list_attached_role_policies") + return paginator.paginate(RoleName=role_name).build_full_result()["AttachedPolicies"] + + +@AWSRetry.jittered_backoff() +def list_iam_instance_profiles_for_role_with_backoff(client, role_name): + paginator = client.get_paginator("list_instance_profiles_for_role") + return paginator.paginate(RoleName=role_name).build_full_result()["InstanceProfiles"] + + +def describe_iam_role(module, client, role): + name = role["RoleName"] + try: + role["InlinePolicies"] = list_iam_role_policies_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get inline policies for role {name}") + try: + role["ManagedPolicies"] = list_iam_attached_role_policies_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get managed policies for role {name}") + try: + role["InstanceProfiles"] = list_iam_instance_profiles_for_role_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get instance profiles for role {name}") + try: + role["tags"] = boto3_tag_list_to_ansible_dict(role["Tags"]) + del role["Tags"] + except KeyError: + role["tags"] = {} + return role + + +def describe_iam_roles(module, client): + name = module.params["name"] + path_prefix = module.params["path_prefix"] + if name: + try: + roles = [client.get_role(RoleName=name, aws_retry=True)["Role"]] + except is_boto3_error_code("NoSuchEntity"): + return [] + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Couldn't get IAM role {name}") + else: + params = dict() + if path_prefix: + if not path_prefix.startswith("/"): + path_prefix = "/" + path_prefix + if not path_prefix.endswith("/"): + path_prefix = path_prefix + "/" + params["PathPrefix"] = path_prefix + try: + roles = list_iam_roles_with_backoff(client, **params)["Roles"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't list IAM roles") + return [normalize_role(describe_iam_role(module, client, role)) for role in roles] + + +def normalize_profile(profile): + new_profile = camel_dict_to_snake_dict(profile) + if profile.get("Roles"): + profile["roles"] = [normalize_role(role) for role in profile.get("Roles")] + return new_profile + + +def normalize_role(role): + new_role = camel_dict_to_snake_dict(role, ignore_list=["tags", "AssumeRolePolicyDocument"]) + new_role["assume_role_policy_document"] = new_role.pop("AssumeRolePolicyDocument", {}) + new_role["assume_role_policy_document_raw"] = new_role["assume_role_policy_document"] + if role.get("InstanceProfiles"): + role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")] + return new_role + + +def main(): + """ + Module action handler + """ + argument_spec = dict( + name=dict(aliases=["role_name"]), + path_prefix=dict(), + ) + + 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_roles=describe_iam_roles(module, client)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/iam_role/aliases b/tests/integration/targets/iam_role/aliases new file mode 100644 index 00000000000..483c861158c --- /dev/null +++ b/tests/integration/targets/iam_role/aliases @@ -0,0 +1,9 @@ +# reason: missing-policy +# It should be possible to test iam_role by limiting which policies can be +# attached to the roles. +# Careful review is needed prior to adding this to the main CI. +unsupported + +cloud/aws + +iam_role_info diff --git a/tests/integration/targets/iam_role/defaults/main.yml b/tests/integration/targets/iam_role/defaults/main.yml new file mode 100644 index 00000000000..d496c421636 --- /dev/null +++ b/tests/integration/targets/iam_role/defaults/main.yml @@ -0,0 +1,6 @@ +--- +test_role: '{{ resource_prefix }}-role' +test_path: '/{{ resource_prefix }}/' +safe_managed_policy: 'AWSDenyAll' +custom_policy_name: '{{ resource_prefix }}-denyall' +boundary_policy: 'arn:aws:iam::aws:policy/AWSDenyAll' diff --git a/tests/integration/targets/iam_role/files/deny-all-a.json b/tests/integration/targets/iam_role/files/deny-all-a.json new file mode 100644 index 00000000000..ae62fd1975d --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all-a.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyA" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-all-b.json b/tests/integration/targets/iam_role/files/deny-all-b.json new file mode 100644 index 00000000000..3a4704a46ab --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all-b.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyB" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-all.json b/tests/integration/targets/iam_role/files/deny-all.json new file mode 100644 index 00000000000..3d324b9b9c6 --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-assume.json b/tests/integration/targets/iam_role/files/deny-assume.json new file mode 100644 index 00000000000..73e87715862 --- /dev/null +++ b/tests/integration/targets/iam_role/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_role/meta/main.yml b/tests/integration/targets/iam_role/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/iam_role/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/iam_role/tasks/boundary_policy.yml b/tests/integration/targets/iam_role/tasks/boundary_policy.yml new file mode 100644 index 00000000000..89a983f1564 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/boundary_policy.yml @@ -0,0 +1,94 @@ +--- +- name: "Create minimal role with no boundary policy" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: "Configure Boundary Policy (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + boundary: "{{ boundary_policy }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Configure Boundary Policy" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + boundary: "{{ boundary_policy }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: "Configure Boundary Policy (no change) - check mode" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + boundary: "{{ boundary_policy }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Configure Boundary Policy (no change)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + boundary: "{{ boundary_policy }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after adding boundary policy" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + +- name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is changed \ No newline at end of file diff --git a/tests/integration/targets/iam_role/tasks/complex_role_creation.yml b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml new file mode 100644 index 00000000000..c23234ebf1f --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml @@ -0,0 +1,131 @@ +--- +- name: "Complex IAM Role (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: "{{ boundary_policy }}" + create_instance_profile: no + description: "Ansible Test Role {{ resource_prefix }}" + managed_policy: + - "{{ safe_managed_policy }}" + - "{{ custom_policy_name }}" + max_session_duration: 43200 + path: "{{ test_path }}" + tags: + TagA: "ValueA" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after Complex Role creation in check_mode" + iam_role_info: + name: "{{ test_role }}" + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: "Complex IAM Role" + iam_role: + name: "{{ test_role }}" + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: "{{ boundary_policy }}" + create_instance_profile: no + description: "Ansible Test Role {{ resource_prefix }}" + managed_policy: + - "{{ safe_managed_policy }}" + - "{{ custom_policy_name }}" + max_session_duration: 43200 + path: "{{ test_path }}" + tags: + TagA: "ValueA" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role" + test_path + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 2 + - iam_role.iam_role.max_session_duration == 43200 + - iam_role.iam_role.path == test_path + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: "Complex IAM role (no change) - check mode" + iam_role: + name: "{{ test_role }}" + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: "{{ boundary_policy }}" + create_instance_profile: no + description: "Ansible Test Role {{ resource_prefix }}" + managed_policy: + - "{{ safe_managed_policy }}" + - "{{ custom_policy_name }}" + max_session_duration: 43200 + path: "{{ test_path }}" + tags: + TagA: "ValueA" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Complex IAM role (no change)" + iam_role: + name: "{{ test_role }}" + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: "{{ boundary_policy }}" + create_instance_profile: no + description: "Ansible Test Role {{ resource_prefix }}" + managed_policy: + - "{{ safe_managed_policy }}" + - "{{ custom_policy_name }}" + max_session_duration: 43200 + path: "{{ test_path }}" + tags: + TagA: "ValueA" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after Role creation" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == test_path + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "ValueA" diff --git a/tests/integration/targets/iam_role/tasks/creation_deletion.yml b/tests/integration/targets/iam_role/tasks/creation_deletion.yml new file mode 100644 index 00000000000..0579a6d3430 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/creation_deletion.yml @@ -0,0 +1,404 @@ +--- +- name: Try running some rapid fire create/delete tests + block: + - name: "Minimal IAM Role without instance profile (rapid)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + + - name: "Minimal IAM Role without instance profile (rapid)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role_again + + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + + - name: "Remove IAM Role (rapid)" + iam_role: + state: absent + name: "{{ test_role }}" + register: iam_role + + - name: "Remove IAM Role (rapid)" + iam_role: + state: absent + name: "{{ test_role }}" + register: iam_role_again + + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + + - name: "Minimal IAM Role without instance profile (rapid)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + + - name: "Remove IAM Role (rapid)" + iam_role: + state: absent + name: "{{ test_role }}" + + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is changed + +# =================================================================== +# Role Creation +# (without Instance profile) +- name: "iam_role_info before Role creation (no args)" + iam_role_info: + register: role_info + +- assert: + that: + - role_info is succeeded + +- name: "iam_role_info before Role creation (search for test role)" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: "Minimal IAM Role (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after Role creation in check_mode" + iam_role_info: + name: "{{ test_role }}" + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: "Minimal IAM Role without instance profile" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in iam_role.iam_role' + - '"assume_role_policy_document_raw" in iam_role.iam_role' + - iam_role.iam_role.assume_role_policy_document_raw == assume_deny_policy + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: "Minimal IAM Role without instance profile (no change) - check mode" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Minimal IAM Role without instance profile (no change)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: no + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after Role creation" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].assume_role_policy_document_raw == assume_deny_policy + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after Role deletion" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +# ------------------------------------------------------------------------------------------ + +# (with path) +- name: "Minimal IAM Role with path (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + path: "{{ test_path }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is changed + +- name: "Minimal IAM Role with path" + iam_role: + name: "{{ test_role }}" + path: "{{ test_path }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role" + test_path + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '{{ test_path }}' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: "Minimal IAM Role with path (no change) - check mode" + iam_role: + name: "{{ test_role }}" + path: "{{ test_path }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Minimal IAM Role with path (no change)" + iam_role: + name: "{{ test_role }}" + path: "{{ test_path }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after Role creation" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + test_path + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '{{ test_path }}' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: "iam_role_info after Role creation (searching a path)" + iam_role_info: + path_prefix: "{{ test_path }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + test_path + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].path == '{{ test_path }}' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + path: "{{ test_path }}" + # If we don't delete the existing profile it'll be reused (with the path) + # by the test below. + delete_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after Role deletion" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +# ------------------------------------------------------------------------------------------ + +# (with Instance profile) +- name: "Minimal IAM Role with instance profile - check mode" + iam_role: + name: "{{ test_role }}" + create_instance_profile: yes + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is changed + +- name: "Minimal IAM Role with instance profile" + iam_role: + name: "{{ test_role }}" + create_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - 'iam_role.iam_role.arn.startswith("arn")' + - 'iam_role.iam_role.arn.endswith("role/" + test_role )' + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: "Minimal IAM Role wth instance profile (no change) - check mode" + iam_role: + name: "{{ test_role }}" + create_instance_profile: yes + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Minimal IAM Role wth instance profile (no change)" + iam_role: + name: "{{ test_role }}" + create_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after Role creation" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/description_update.yml b/tests/integration/targets/iam_role/tasks/description_update.yml new file mode 100644 index 00000000000..85f5e1f56a3 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/description_update.yml @@ -0,0 +1,148 @@ +--- +- name: "Add Description (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role {{ resource_prefix }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Add Description" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role {{ resource_prefix }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + +- name: "Add Description (no change) - check mode" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role {{ resource_prefix }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Add Description (no change)" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role {{ resource_prefix }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + +- name: "iam_role_info after adding Description" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +# ------------------------------------------------------------------------------------------ + +- name: "Update Description (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role (updated) {{ resource_prefix }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Update Description" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role (updated) {{ resource_prefix }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix }}' + +- name: "Update Description (no change) - check mode" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role (updated) {{ resource_prefix }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Update Description (no change)" + iam_role: + name: "{{ test_role }}" + description: "Ansible Test Role (updated) {{ resource_prefix }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix }}' + +- name: "iam_role_info after updating Description" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/inline_policy_update.yml b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml new file mode 100644 index 00000000000..d364d87d79f --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml @@ -0,0 +1,48 @@ +--- +- name: "Attach inline policy a" + iam_policy: + state: present + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-a" + policy_json: '{{ lookup("file", "deny-all-a.json") }}' + +- name: "Attach inline policy b" + iam_policy: + state: present + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-b" + policy_json: '{{ lookup("file", "deny-all-b.json") }}' + +- name: "iam_role_info after attaching inline policies (using iam_policy)" + iam_role_info: + name: "{{ test_role }}" + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 2 + - '"inline-policy-a" in role_info.iam_roles[0].inline_policies' + - '"inline-policy-b" in role_info.iam_roles[0].inline_policies' + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" diff --git a/tests/integration/targets/iam_role/tasks/main.yml b/tests/integration/targets/iam_role/tasks/main.yml new file mode 100644 index 00000000000..821a683eb53 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/main.yml @@ -0,0 +1,119 @@ +--- +# Tests for iam_role and iam_role_info +# +# Tests: +# - Minimal Role creation +# - Role deletion +# - Fetching a specific role +# - Creating roles w/ and w/o instance profiles +# - Creating roles w/ a path +# - Updating Max Session Duration +# - Updating Description +# - Managing list of managed policies +# - Managing list of inline policies (for testing _info) +# - Managing boundary policy +# +# Notes: +# - Only tests *documented* return values ( RESULT.iam_role ) +# - There are some known timing issues with boto3 returning before actions +# complete in the case of problems with "changed" status it's worth enabling +# the standard_pauses and paranoid_pauses options as a first step in debugging + + +- name: "Setup AWS connection info" + module_defaults: + group/aws: + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + session_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + iam_role: + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + collections: + - amazon.aws + - community.general + block: + - set_fact: + assume_deny_policy: '{{ lookup("file", "deny-assume.json") | from_json }}' + # =================================================================== + # Parameter Checks + - include_tasks: parameter_checks.yml + + # =================================================================== + # Supplemental resource pre-creation + - name: "Create Safe IAM Managed Policy" + iam_managed_policy: + state: present + policy_name: "{{ custom_policy_name }}" + policy_description: "A safe (deny-all) managed policy" + policy: "{{ lookup('file', 'deny-all.json') }}" + register: create_managed_policy + + - assert: + that: + - create_managed_policy is succeeded + + # =================================================================== + # Rapid Role Creation and deletion + - include_tasks: creation_deletion.yml + + # =================================================================== + # Max Session Duration Manipulation + - include_tasks: max_session_update.yml + + # =================================================================== + # Description Manipulation + - include_tasks: description_update.yml + + # =================================================================== + # Tag Manipulation + - include_tasks: tags_update.yml + + # =================================================================== + # Policy Manipulation + - include_tasks: policy_update.yml + + # =================================================================== + # Inline Policy (test _info behavior) + - include_tasks: inline_policy_update.yml + + # =================================================================== + # Role Removal + - include_tasks: role_removal.yml + + # =================================================================== + # Boundary Policy (requires create_instance_profile: no) + - include_tasks: boundary_policy.yml + + # =================================================================== + # Complex role Creation + - include_tasks: complex_role_creation.yml + + always: + # =================================================================== + # Cleanup + + - name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + ignore_errors: true + + - name: "Remove IAM Role (with path)" + iam_role: + state: absent + name: "{{ test_role }}" + path: "{{ test_path }}" + delete_instance_profile: yes + ignore_errors: true + + - name: "iam_role_info after Role deletion" + iam_role_info: + name: "{{ test_role }}" + ignore_errors: true + + - name: "Remove test managed policy" + iam_managed_policy: + state: absent + policy_name: "{{ custom_policy_name }}" diff --git a/tests/integration/targets/iam_role/tasks/max_session_update.yml b/tests/integration/targets/iam_role/tasks/max_session_update.yml new file mode 100644 index 00000000000..8ad3641be62 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/max_session_update.yml @@ -0,0 +1,71 @@ +--- +- name: "Update Max Session Duration (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + max_session_duration: 43200 + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Update Max Session Duration" + iam_role: + name: "{{ test_role }}" + max_session_duration: 43200 + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.max_session_duration == 43200 + +- name: "Update Max Session Duration (no change)" + iam_role: + name: "{{ test_role }}" + max_session_duration: 43200 + register: iam_role + +- assert: + that: + - iam_role is not changed + +- name: "Update Max Session Duration (no change) - check mode" + iam_role: + name: "{{ test_role }}" + max_session_duration: 43200 + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "iam_role_info after updating Max Session Duration" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/parameter_checks.yml b/tests/integration/targets/iam_role/tasks/parameter_checks.yml new file mode 100644 index 00000000000..57df5436afc --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/parameter_checks.yml @@ -0,0 +1,90 @@ +--- +# Parameter Checks +- name: "Friendly message when creating an instance profile and adding a boundary profile" + iam_role: + name: "{{ test_role }}" + boundary: "{{ boundary_policy }}" + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"boundary policy" in iam_role.msg' + - '"create_instance_profile" in iam_role.msg' + - '"false" in iam_role.msg' + +- name: "Friendly message when boundary profile is not an ARN" + iam_role: + name: "{{ test_role }}" + boundary: "AWSDenyAll" + create_instance_profile: no + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"Boundary policy" in iam_role.msg' + - '"ARN" in iam_role.msg' + +- name: 'Friendly message when "present" without assume_role_policy_document' + module_defaults: { iam_role: {} } + iam_role: + name: "{{ test_role }}" + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - 'iam_role.msg.startswith("state is present but all of the following are missing")' + - '"assume_role_policy_document" in iam_role.msg' + +- name: "Maximum Session Duration needs to be between 1 and 12 hours" + iam_role: + name: "{{ test_role }}" + max_session_duration: 3599 + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + +- name: "Maximum Session Duration needs to be between 1 and 12 hours" + iam_role: + name: "{{ test_role }}" + max_session_duration: 43201 + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + +- name: "Role Paths must start with /" + iam_role: + name: "{{ test_role }}" + path: "test/" + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' + +- name: "Role Paths must end with /" + iam_role: + name: "{{ test_role }}" + path: "/test" + register: iam_role + ignore_errors: yes + +- assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' diff --git a/tests/integration/targets/iam_role/tasks/policy_update.yml b/tests/integration/targets/iam_role/tasks/policy_update.yml new file mode 100644 index 00000000000..a822edf74b6 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/policy_update.yml @@ -0,0 +1,250 @@ +--- +- name: "Add Managed Policy (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ safe_managed_policy }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Add Managed Policy" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ safe_managed_policy }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: "Add Managed Policy (no change) - check mode" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ safe_managed_policy }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Add Managed Policy (no change)" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ safe_managed_policy }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after adding Managed Policy" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +- name: "Update Managed Policy without purge (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ custom_policy_name }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Update Managed Policy without purge" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: "Update Managed Policy without purge (no change) - check mode" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Update Managed Policy without purge (no change)" + iam_role: + name: "{{ test_role }}" + purge_policies: no + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after updating Managed Policy without purge" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +# Managed Policies are purged by default +- name: "Update Managed Policy with purge (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + managed_policy: + - "{{ custom_policy_name }}" + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Update Managed Policy with purge" + iam_role: + name: "{{ test_role }}" + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: "Update Managed Policy with purge (no change) - check mode" + iam_role: + name: "{{ test_role }}" + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Update Managed Policy with purge (no change)" + iam_role: + name: "{{ test_role }}" + managed_policy: + - "{{ custom_policy_name }}" + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: "iam_role_info after updating Managed Policy with purge" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" diff --git a/tests/integration/targets/iam_role/tasks/role_removal.yml b/tests/integration/targets/iam_role/tasks/role_removal.yml new file mode 100644 index 00000000000..ebcfd54530a --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/role_removal.yml @@ -0,0 +1,65 @@ +--- +- name: "Remove IAM Role (CHECK MODE)" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after deleting role in check mode" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + +- name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "iam_role_info after deleting role" + iam_role_info: + name: "{{ test_role }}" + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: "Remove IAM Role (should be gone already) - check mode" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Remove IAM Role (should be gone already)" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + register: iam_role + +- assert: + that: + - iam_role is not changed diff --git a/tests/integration/targets/iam_role/tasks/tags_update.yml b/tests/integration/targets/iam_role/tasks/tags_update.yml new file mode 100644 index 00000000000..5eadd9fdf7e --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/tags_update.yml @@ -0,0 +1,341 @@ +--- +- name: "Add Tag (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + tags: + TagA: ValueA + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Add Tag" + iam_role: + name: "{{ test_role }}" + tags: + TagA: ValueA + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.tags | length == 1 + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "ValueA" + +- name: "Add Tag (no change) - check mode" + iam_role: + name: "{{ test_role }}" + tags: + TagA: ValueA + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Add Tag (no change)" + iam_role: + name: "{{ test_role }}" + tags: + TagA: ValueA + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "ValueA" + +- name: "iam_role_info after adding Tags" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "ValueA" + +# ------------------------------------------------------------------------------------------ + +- name: "Update Tag (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + tags: + TagA: AValue + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Update Tag" + iam_role: + name: "{{ test_role }}" + tags: + TagA: AValue + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "AValue" + +- name: "Update Tag (no change) - check mode" + iam_role: + name: "{{ test_role }}" + tags: + TagA: AValue + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Update Tag (no change)" + iam_role: + name: "{{ test_role }}" + tags: + TagA: AValue + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "AValue" + +- name: "iam_role_info after updating Tag" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "AValue" + +# ------------------------------------------------------------------------------------------ + +- name: "Add second Tag without purge (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + purge_tags: no + tags: + TagB: ValueB + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Add second Tag without purge" + iam_role: + name: "{{ test_role }}" + purge_tags: no + tags: + TagB: ValueB + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: "Add second Tag without purge (no change) - check mode" + iam_role: + name: "{{ test_role }}" + purge_tags: no + tags: + TagB: ValueB + register: iam_role + check_mode: yes + +- assert: + that: + - iam_role is not changed + +- name: "Add second Tag without purge (no change)" + iam_role: + name: "{{ test_role }}" + purge_tags: no + tags: + TagB: ValueB + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: "iam_role_info after adding second Tag without purge" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 2 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "AValue" + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +- name: "Purge first tag (CHECK MODE)" + iam_role: + name: "{{ test_role }}" + purge_tags: yes + tags: + TagB: ValueB + check_mode: yes + register: iam_role + +- assert: + that: + - iam_role is changed + +- name: "Purge first tag" + iam_role: + name: "{{ test_role }}" + purge_tags: yes + tags: + TagB: ValueB + register: iam_role + +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: "Purge first tag (no change) - check mode" + iam_role: + name: "{{ test_role }}" + purge_tags: yes + tags: + TagB: ValueB + register: iam_role + +- assert: + that: + - iam_role is not changed + +- name: "Purge first tag (no change)" + iam_role: + name: "{{ test_role }}" + purge_tags: yes + tags: + TagB: ValueB + register: iam_role + +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: "iam_role_info after purging first Tag" + iam_role_info: + name: "{{ test_role }}" + register: role_info + +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - 'role_info.iam_roles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - 'role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix }}"' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - 'role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")' + - 'role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + test_role)' + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" not in role_info.iam_roles[0].tags' + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" From 10f88ea7d89c5d31c374e7051adf10f8e3f3789f Mon Sep 17 00:00:00 2001 From: abikouo Date: Fri, 15 Sep 2023 16:56:58 +0200 Subject: [PATCH 2/5] update changelog --- .../fragments/20230915_migrate_iam_role_and_iam_role_info.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml b/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml index 100bbc93c88..b297ffcbfb0 100644 --- a/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml +++ b/changelogs/fragments/20230915_migrate_iam_role_and_iam_role_info.yml @@ -1,7 +1,7 @@ breaking_changes: - iam_role - The module has been migrated from the ``community.aws`` collection. Playbooks using the Fully Qualified Collection Name for this module should be updated - to use ``amazon.aws.iam_role`` (https://github.com/ansible-collections/amazon.aws/pull/1756). + to use ``amazon.aws.iam_role`` (https://github.com/ansible-collections/amazon.aws/pull/1757). - iam_role_info - The module has been migrated from the ``community.aws`` collection. Playbooks using the Fully Qualified Collection Name for this module should be updated - to use ``amazon.aws.iam_role_info`` (https://github.com/ansible-collections/amazon.aws/pull/1756). + to use ``amazon.aws.iam_role_info`` (https://github.com/ansible-collections/amazon.aws/pull/1757). From a7888687e6a656530f1642aeea912c5c7423cc63 Mon Sep 17 00:00:00 2001 From: abikouo Date: Fri, 15 Sep 2023 17:30:28 +0200 Subject: [PATCH 3/5] fix some minor issue --- plugins/modules/iam_role_info.py | 2 +- .../targets/iam_role/tasks/creation_deletion.yml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/modules/iam_role_info.py b/plugins/modules/iam_role_info.py index ce40b4237d1..317ee25937b 100644 --- a/plugins/modules/iam_role_info.py +++ b/plugins/modules/iam_role_info.py @@ -246,7 +246,7 @@ def normalize_profile(profile): def normalize_role(role): new_role = camel_dict_to_snake_dict(role, ignore_list=["tags", "AssumeRolePolicyDocument"]) - new_role["assume_role_policy_document"] = new_role.pop("AssumeRolePolicyDocument", {}) + new_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument", {}) new_role["assume_role_policy_document_raw"] = new_role["assume_role_policy_document"] if role.get("InstanceProfiles"): role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")] diff --git a/tests/integration/targets/iam_role/tasks/creation_deletion.yml b/tests/integration/targets/iam_role/tasks/creation_deletion.yml index 0579a6d3430..78d60cb7a0d 100644 --- a/tests/integration/targets/iam_role/tasks/creation_deletion.yml +++ b/tests/integration/targets/iam_role/tasks/creation_deletion.yml @@ -107,6 +107,7 @@ - 'iam_role.iam_role.arn.endswith("role/" + test_role )' - '"assume_role_policy_document" in iam_role.iam_role' - '"assume_role_policy_document_raw" in iam_role.iam_role' + - iam_role.iam_role.assume_role_policy_document == assume_deny_policy - iam_role.iam_role.assume_role_policy_document_raw == assume_deny_policy - iam_role.iam_role.attached_policies | length == 0 - iam_role.iam_role.max_session_duration == 3600 @@ -152,6 +153,7 @@ - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' - '"create_date" in role_info.iam_roles[0]' - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].assume_role_policy_document == assume_deny_policy - role_info.iam_roles[0].assume_role_policy_document_raw == assume_deny_policy - role_info.iam_roles[0].inline_policies | length == 0 - role_info.iam_roles[0].instance_profiles | length == 0 @@ -212,6 +214,7 @@ - 'iam_role.iam_role.arn.endswith("role" + test_path + test_role )' # Would be nice to test the contents... - '"assume_role_policy_document" in iam_role.iam_role' + - '"assume_role_policy_document_raw" in iam_role.iam_role' - iam_role.iam_role.attached_policies | length == 0 - iam_role.iam_role.max_session_duration == 3600 - iam_role.iam_role.path == '{{ test_path }}' @@ -253,6 +256,7 @@ - 'role_info.iam_roles[0].arn.startswith("arn")' - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' - '"create_date" in role_info.iam_roles[0]' - '"description" not in role_info.iam_roles[0]' - role_info.iam_roles[0].inline_policies | length == 0 @@ -280,6 +284,7 @@ - 'role_info.iam_roles[0].arn.startswith("arn")' - 'role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )' - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' - '"create_date" in role_info.iam_roles[0]' - '"description" not in role_info.iam_roles[0]' - role_info.iam_roles[0].inline_policies | length == 0 @@ -347,6 +352,7 @@ - 'iam_role.iam_role.arn.endswith("role/" + test_role )' # Would be nice to test the contents... - '"assume_role_policy_document" in iam_role.iam_role' + - '"assume_role_policy_document_raw" in iam_role.iam_role' - iam_role.iam_role.attached_policies | length == 0 - iam_role.iam_role.max_session_duration == 3600 - iam_role.iam_role.path == '/' @@ -388,6 +394,7 @@ - 'role_info.iam_roles[0].arn.startswith("arn")' - 'role_info.iam_roles[0].arn.endswith("role/" + test_role )' - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' - '"create_date" in role_info.iam_roles[0]' - '"description" not in role_info.iam_roles[0]' - role_info.iam_roles[0].inline_policies | length == 0 From bee50d8ef8d7b21145cc5a1ea2a758dd64407b67 Mon Sep 17 00:00:00 2001 From: abikouo Date: Mon, 18 Sep 2023 17:57:47 +0200 Subject: [PATCH 4/5] adding some unit tests --- plugins/modules/iam_role.py | 127 +++++---- plugins/modules/iam_role_info.py | 33 ++- .../plugins/modules/test_iam_role_info.py | 245 ++++++++++++++++++ 3 files changed, 334 insertions(+), 71 deletions(-) create mode 100644 tests/unit/plugins/modules/test_iam_role_info.py diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py index f995af1d6bf..02ee63f66e2 100644 --- a/plugins/modules/iam_role.py +++ b/plugins/modules/iam_role.py @@ -7,7 +7,7 @@ DOCUMENTATION = r""" --- module: iam_role -version_added: 1.0.0 +version_added: 7.0.0 short_description: Manage AWS IAM roles description: - Manage AWS IAM roles. @@ -180,7 +180,6 @@ description: the policy that grants an entity permission to assume the role type: dict returned: always - version_added: 5.3.0 sample: { 'Statement': [ { @@ -238,6 +237,17 @@ def _list_policies(client): return paginator.paginate().build_full_result()["Policies"] +def _wait_iam_role(client, role_name, wait_timeout): + delay = min(wait_timeout, 5) + max_attempts = wait_timeout // delay + + waiter = client.get_waiter("role_exists") + waiter.wait( + WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts}, + RoleName=role_name, + ) + + def wait_iam_exists(module, client): if module.check_mode: return @@ -247,15 +257,8 @@ def wait_iam_exists(module, client): role_name = module.params.get("name") wait_timeout = module.params.get("wait_timeout") - delay = min(wait_timeout, 5) - max_attempts = wait_timeout // delay - try: - waiter = client.get_waiter("role_exists") - waiter.wait( - WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts}, - RoleName=role_name, - ) + _wait_iam_role(client, role_name, wait_timeout) except botocore.exceptions.WaiterError as e: module.fail_json_aws(e, msg="Timeout while waiting on IAM role creation") except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: @@ -532,25 +535,35 @@ def create_or_update_role(module, client): module.exit_json(changed=changed, iam_role=camel_role) -def create_instance_profiles(module, client, role_name, path): - # Fetch existing Profiles +def list_instance_profiles_for_role(module, client, name): try: - instance_profiles = client.list_instance_profiles_for_role(RoleName=role_name, aws_retry=True)[ - "InstanceProfiles" - ] + return client.list_instance_profiles_for_role(RoleName=name, aws_retry=True)["InstanceProfiles"] except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {name}") - # Profile already exists - if any(p["InstanceProfileName"] == role_name for p in instance_profiles): - return False - if module.check_mode: - return True +def delete_instance_profile(module, client, name): + try: + client.delete_instance_profile(InstanceProfileName=name, aws_retry=True) + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to remove instance profile {name}") - # Make sure an instance profile is created + +def remove_role_from_instance_profile(module, client, role_name, profile_name): + try: + client.remove_role_from_instance_profile(aws_retry=True, InstanceProfileName=profile_name, RoleName=role_name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to remove role {role_name} from instance profile {profile_name}") + + +def create_instance_profile(module, client, name, path): try: - client.create_instance_profile(InstanceProfileName=role_name, Path=path, aws_retry=True) + client.create_instance_profile(InstanceProfileName=name, Path=path, aws_retry=True) except is_boto3_error_code("EntityAlreadyExists"): # If the profile already exists, no problem, move on. # Implies someone's changing things at the same time... @@ -559,48 +572,48 @@ def create_instance_profiles(module, client, role_name, path): botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError, ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Unable to create instance profile for role {role_name}") + module.fail_json_aws(e, msg=f"Unable to create instance profile for role {name}") - # And attach the role to the profile + +def add_role_to_instance_profile(module, client, name): try: - client.add_role_to_instance_profile(InstanceProfileName=role_name, RoleName=role_name, aws_retry=True) + client.add_role_to_instance_profile(InstanceProfileName=name, RoleName=name, aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Unable to attach role {role_name} to instance profile {role_name}") + module.fail_json_aws(e, msg=f"Unable to attach role {name} to instance profile {name}") - return True +def create_instance_profiles(module, client, role_name, path): + # Fetch existing Profiles + instance_profiles = list_instance_profiles_for_role(module, client, role_name) -def remove_instance_profiles(module, client, role_name): - delete_profiles = module.params.get("delete_instance_profile") + # Profile already exists + if any(p["InstanceProfileName"] == role_name for p in instance_profiles): + return False - try: - instance_profiles = client.list_instance_profiles_for_role(aws_retry=True, RoleName=role_name)[ - "InstanceProfiles" - ] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + if module.check_mode: + return True - # Remove the role from the instance profile(s) - for profile in instance_profiles: - profile_name = profile["InstanceProfileName"] - try: - if not module.check_mode: - client.remove_role_from_instance_profile( - aws_retry=True, InstanceProfileName=profile_name, RoleName=role_name - ) - if profile_name == role_name: - if delete_profiles: - try: - client.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) - except is_boto3_error_code("NoSuchEntityException"): - pass - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Unable to remove instance profile {profile_name}") - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg=f"Unable to remove role {role_name} from instance profile {profile_name}") + # Make sure an instance profile is created + create_instance_profile(module, client, role_name, path) + + # And attach the role to the profile + add_role_to_instance_profile(module, client, role_name) + + return True + + +def remove_instance_profiles(module, client, role_name): + if not module.check_mode: + delete_profiles = module.params.get("delete_instance_profile") + instance_profiles = list_instance_profiles_for_role(module, client, role_name) + + # Remove the role from the instance profile(s) + for profile in instance_profiles: + profile_name = profile["InstanceProfileName"] + remove_role_from_instance_profile(module, client, role_name, profile_name) + if profile_name == role_name: + if delete_profiles: + delete_instance_profile(module, client, profile_name) def destroy_role(module, client): diff --git a/plugins/modules/iam_role_info.py b/plugins/modules/iam_role_info.py index 317ee25937b..872bdb4f6c8 100644 --- a/plugins/modules/iam_role_info.py +++ b/plugins/modules/iam_role_info.py @@ -7,7 +7,7 @@ DOCUMENTATION = r""" --- module: iam_role_info -version_added: 1.0.0 +version_added: 7.0.0 short_description: Gather information on IAM roles description: - Gathers information about IAM roles. @@ -66,7 +66,6 @@ description: The policy document describing what can assume the role. returned: always type: dict - version_added: 5.3.0 create_date: description: Date IAM role was created. returned: always @@ -187,6 +186,18 @@ def list_iam_instance_profiles_for_role_with_backoff(client, role_name): return paginator.paginate(RoleName=role_name).build_full_result()["InstanceProfiles"] +def get_role(module, client, name): + try: + return [client.get_role(RoleName=name, aws_retry=True)["Role"]] + except is_boto3_error_code("NoSuchEntity"): + return [] + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Couldn't get IAM role {name}") + + def describe_iam_role(module, client, role): name = role["RoleName"] try: @@ -213,15 +224,7 @@ def describe_iam_roles(module, client): name = module.params["name"] path_prefix = module.params["path_prefix"] if name: - try: - roles = [client.get_role(RoleName=name, aws_retry=True)["Role"]] - except is_boto3_error_code("NoSuchEntity"): - return [] - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Couldn't get IAM role {name}") + roles = get_role(module, client, name) else: params = dict() if path_prefix: @@ -240,16 +243,18 @@ def describe_iam_roles(module, client): def normalize_profile(profile): new_profile = camel_dict_to_snake_dict(profile) if profile.get("Roles"): - profile["roles"] = [normalize_role(role) for role in profile.get("Roles")] + new_profile["roles"] = [normalize_role(role) for role in profile.get("Roles")] + del new_profile["Roles"] return new_profile def normalize_role(role): new_role = camel_dict_to_snake_dict(role, ignore_list=["tags", "AssumeRolePolicyDocument"]) - new_role["assume_role_policy_document"] = role.get("AssumeRolePolicyDocument", {}) + new_role["assume_role_policy_document"] = role.pop("AssumeRolePolicyDocument", {}) new_role["assume_role_policy_document_raw"] = new_role["assume_role_policy_document"] if role.get("InstanceProfiles"): - role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")] + new_role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")] + del new_role["InstanceProfiles"] return new_role diff --git a/tests/unit/plugins/modules/test_iam_role_info.py b/tests/unit/plugins/modules/test_iam_role_info.py new file mode 100644 index 00000000000..71b23d65a1b --- /dev/null +++ b/tests/unit/plugins/modules/test_iam_role_info.py @@ -0,0 +1,245 @@ +# +# (c) 2022 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) + +import pytest +from botocore.exceptions import BotoCoreError + +from unittest.mock import MagicMock, call, patch +from ansible_collections.amazon.aws.plugins.modules import iam_role_info + + +mod_list_iam_roles = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.list_iam_roles_with_backoff" +mod_list_iam_role_policies = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role_info.list_iam_role_policies_with_backoff" +) +mod_list_iam_attached_policies = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role_info.list_iam_attached_role_policies_with_backoff" +) +mod_list_iam_instance_profiles_for_role = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role_info.list_iam_instance_profiles_for_role_with_backoff" +) +mod_boto3_tag_list_to_ansible_dict = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role_info.boto3_tag_list_to_ansible_dict" +) +mod_normalize_role = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.normalize_role" +mod_normalize_profile = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.normalize_profile" +mod_camel_dict_to_snake_dict = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.camel_dict_to_snake_dict" +mod_get_role = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.get_role" +mod_describe_iam_role = "ansible_collections.amazon.aws.plugins.modules.iam_role_info.describe_iam_role" + + +def raise_botocore_exception(): + return BotoCoreError(error="failed", operation="Unexpected error while calling botocore api") + + +@pytest.mark.parametrize("list_iam_policies_status", [True, False]) +@pytest.mark.parametrize("list_iam_attached_policies_status", [True, False]) +@pytest.mark.parametrize("list_iam_instance_profiles_for_role_status", [True, False]) +@patch(mod_list_iam_role_policies) +@patch(mod_list_iam_attached_policies) +@patch(mod_list_iam_instance_profiles_for_role) +@patch(mod_boto3_tag_list_to_ansible_dict) +def test_describe_iam_role_with_iam_policies_error( + m_boto3_tag_list_to_ansible_dict, + m_list_iam_instance_profiles_for_role, + m_list_iam_attached_policies, + m_list_iam_role_policies, + list_iam_policies_status, + list_iam_attached_policies_status, + list_iam_instance_profiles_for_role_status, +): + client = MagicMock() + module = MagicMock() + module.fail_json_aws.side_effect = SystemExit(1) + + iam_policies = { + "PolicyNames": [ + "policy-1", + ] + } + iam_attached_policies = { + "AttachedPolicies": [ + {"PolicyName": "policy-1", "PolicyArn": "iam:policy:arn:xxx:xxx:xxx"}, + ] + } + iam_instance_profiles = {"InstanceProfiles": ["instance-profile-1"]} + + has_failure = False + if list_iam_policies_status: + m_list_iam_role_policies.return_value = iam_policies + else: + has_failure = True + m_list_iam_role_policies.side_effect = raise_botocore_exception() + + if list_iam_attached_policies_status: + m_list_iam_attached_policies.return_value = iam_attached_policies + else: + has_failure = True + m_list_iam_attached_policies.side_effect = raise_botocore_exception() + module.fail_json_aws.side_effect = SystemExit(1) + + if list_iam_instance_profiles_for_role_status: + m_list_iam_instance_profiles_for_role.return_value = iam_instance_profiles + else: + has_failure = True + m_list_iam_instance_profiles_for_role.side_effect = raise_botocore_exception() + module.fail_json_aws.side_effect = SystemExit(1) + + m_boto3_tag_list_to_ansible_dict.side_effect = lambda x: x + + role_name = "ansible-test-role" + role_tags = { + "Environment": "Dev", + "Phase": "Units", + } + test_role = { + "RoleName": role_name, + "Tags": role_tags, + } + + if has_failure: + with pytest.raises(SystemExit): + iam_role_info.describe_iam_role(module, client, test_role) + module.fail_json_aws.assert_called_once() + # validate that each function has at most 1 call + assert m_list_iam_role_policies.call_count <= 1 + assert m_list_iam_attached_policies.call_count <= 1 + assert m_list_iam_instance_profiles_for_role.call_count <= 1 + # validate function call with expected parameters + if m_list_iam_role_policies.call_count == 1: + m_list_iam_role_policies.assert_called_once_with(client, role_name) + if m_list_iam_attached_policies.call_count == 1: + m_list_iam_attached_policies.assert_called_once_with(client, role_name) + if m_list_iam_instance_profiles_for_role.call_count == 1: + m_list_iam_instance_profiles_for_role.assert_called_once_with(client, role_name) + else: + # Everything went well + expected_role = { + "RoleName": role_name, + "InlinePolicies": iam_policies, + "ManagedPolicies": iam_attached_policies, + "InstanceProfiles": iam_instance_profiles, + "tags": role_tags, + } + assert expected_role == iam_role_info.describe_iam_role(module, client, test_role) + m_list_iam_role_policies.assert_called_once_with(client, role_name) + m_list_iam_attached_policies.assert_called_once_with(client, role_name) + m_list_iam_instance_profiles_for_role.assert_called_once_with(client, role_name) + m_boto3_tag_list_to_ansible_dict.assert_called_once_with(role_tags) + + +@patch(mod_normalize_role) +@patch(mod_camel_dict_to_snake_dict) +def test_normalize_profile(m_camel_dict_to_snake_dict, m_normalize_role): + m_camel_dict_to_snake_dict.side_effect = lambda x: x + m_normalize_role.side_effect = lambda x: {"RoleName": x} + + profile = {"Roles": ["role-1", "role-2"]} + expected = {"roles": [{"RoleName": "role-1"}, {"RoleName": "role-2"}]} + assert expected == iam_role_info.normalize_profile(profile) + m_camel_dict_to_snake_dict.assert_called_once_with(profile) + m_normalize_role.assert_has_calls([call("role-1"), call("role-2")]) + + +@patch(mod_normalize_profile) +@patch(mod_camel_dict_to_snake_dict) +def test_normalize_role(m_camel_dict_to_snake_dict, m_normalize_profile): + m_camel_dict_to_snake_dict.side_effect = lambda x, **kwargs: x + m_normalize_profile.side_effect = lambda x: x + + role_policy_document = { + "Statement": [{"Action": "sts:AssumeRole", "Effect": "Deny", "Principal": {"Service": "ec2.amazonaws.com"}}], + "Version": "2012-10-17", + } + role_tags = { + "Environment": "Dev", + "Phase": "Units", + } + role = { + "AssumeRolePolicyDocument": role_policy_document, + "tags": role_tags, + "InstanceProfiles": [ + "profile-1", + "profile-2", + ], + } + expected = { + "assume_role_policy_document": role_policy_document, + "assume_role_policy_document_raw": role_policy_document, + "tags": role_tags, + "instance_profiles": [ + "profile-1", + "profile-2", + ], + } + + assert expected == iam_role_info.normalize_role(role) + m_camel_dict_to_snake_dict.assert_called_once_with(role, ignore_list=["tags", "AssumeRolePolicyDocument"]) + m_normalize_profile.assert_has_calls([call("profile-1"), call("profile-2")]) + + +@patch(mod_get_role) +@patch(mod_list_iam_roles) +@patch(mod_normalize_role) +@patch(mod_describe_iam_role) +def test_describe_iam_roles_with_name(m_describe_iam_role, m_normalize_role, m_list_iam_roles, m_get_role): + role_name = "ansible-test-role" + + client = MagicMock() + module = MagicMock() + module.params = { + "name": role_name, + "path_prefix": "path prefix", + } + + m_get_role.return_value = [{"RoleName": role_name}] + expected = {"role_name": role_name, "instance_profiles": ["profile-1", "profile-2"]} + m_describe_iam_role.return_value = expected + m_normalize_role.return_value = expected + + assert [expected] == iam_role_info.describe_iam_roles(module, client) + m_get_role.assert_called_once_with(module, client, role_name) + m_list_iam_roles.assert_not_called() + + m_describe_iam_role.assert_called_once_with(module, client, {"RoleName": role_name}) + m_normalize_role.assert_called_once_with(expected) + + +@pytest.mark.parametrize( + "path_prefix", + [ + "ansible-prefix", + "ansible-prefix/", + "/ansible-prefix", + "/ansible-prefix/", + ], +) +@patch(mod_get_role) +@patch(mod_list_iam_roles) +@patch(mod_normalize_role) +@patch(mod_describe_iam_role) +def test_describe_iam_roles_with_path_prefix( + m_describe_iam_role, m_normalize_role, m_list_iam_roles, m_get_role, path_prefix +): + client = MagicMock() + role = MagicMock() + module = MagicMock() + module.params = { + "name": None, + "path_prefix": path_prefix, + } + + m_list_iam_roles.return_value = {"Roles": [role]} + + m_describe_iam_role.side_effect = lambda m, c, r: r + m_normalize_role.side_effect = lambda x: x + + assert [role] == iam_role_info.describe_iam_roles(module, client) + m_get_role.assert_not_called() + m_list_iam_roles.assert_called_once_with(client, PathPrefix="/ansible-prefix/") + + m_describe_iam_role.assert_called_once_with(module, client, role) + m_normalize_role.assert_called_once_with(role) From 84e4c7f18ab9051e8ecbf6111de85c6e6bc7386f Mon Sep 17 00:00:00 2001 From: abikouo Date: Mon, 18 Sep 2023 18:02:39 +0200 Subject: [PATCH 5/5] missing file --- tests/unit/plugins/modules/test_iam_role.py | 598 ++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 tests/unit/plugins/modules/test_iam_role.py diff --git a/tests/unit/plugins/modules/test_iam_role.py b/tests/unit/plugins/modules/test_iam_role.py new file mode 100644 index 00000000000..dac64ef1946 --- /dev/null +++ b/tests/unit/plugins/modules/test_iam_role.py @@ -0,0 +1,598 @@ +# +# (c) 2022 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) + +import pytest +from botocore.exceptions import BotoCoreError, WaiterError, ClientError + +from unittest.mock import MagicMock, call, patch, ANY +from ansible_collections.amazon.aws.plugins.modules import iam_role + + +mod__list_policies = "ansible_collections.amazon.aws.plugins.modules.iam_role._list_policies" +mod__wait_iam_role = "ansible_collections.amazon.aws.plugins.modules.iam_role._wait_iam_role" +mod_validate_aws_arn = "ansible_collections.amazon.aws.plugins.modules.iam_role.validate_aws_arn" +mod_get_inline_policy_list = "ansible_collections.amazon.aws.plugins.modules.iam_role.get_inline_policy_list" +mod_ansible_dict_to_boto3_tag_list = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role.ansible_dict_to_boto3_tag_list" +) +mod_generate_create_params = "ansible_collections.amazon.aws.plugins.modules.iam_role.generate_create_params" +mod_get_role_with_backoff = "ansible_collections.amazon.aws.plugins.modules.iam_role.get_role_with_backoff" +mod_remove_instance_profiles = "ansible_collections.amazon.aws.plugins.modules.iam_role.remove_instance_profiles" +mod_update_managed_policies = "ansible_collections.amazon.aws.plugins.modules.iam_role.update_managed_policies" +mod_remove_inline_policies = "ansible_collections.amazon.aws.plugins.modules.iam_role.remove_inline_policies" +mod_get_role = "ansible_collections.amazon.aws.plugins.modules.iam_role.get_role" +mod_list_instance_profiles_for_role = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role.list_instance_profiles_for_role" +) +mod_remove_role_from_instance_profile = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role.remove_role_from_instance_profile" +) +mod_delete_instance_profile = "ansible_collections.amazon.aws.plugins.modules.iam_role.delete_instance_profile" +mod_create_instance_profile = "ansible_collections.amazon.aws.plugins.modules.iam_role.create_instance_profile" +mod_add_role_to_instance_profile = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role.add_role_to_instance_profile" +) +mod_convert_friendly_names_to_arns = ( + "ansible_collections.amazon.aws.plugins.modules.iam_role.convert_friendly_names_to_arns" +) + + +@patch(mod__wait_iam_role) +def test_wait_iam_exists_check_mode_or_parameter_not_set(m__wait_iam_role): + module = MagicMock() + client = MagicMock() + + module.check_mode = False + module.params = {"wait_timeout": 10} + + m__wait_iam_role.side_effect = SystemExit(1) + + # Test with module parameter not set + iam_role.wait_iam_exists(module, client) + m__wait_iam_role.assert_not_called() + + # Test with check_mode=true + module.check_mode = True + iam_role.wait_iam_exists(module, client) + m__wait_iam_role.assert_not_called() + + +@patch(mod__wait_iam_role) +def test_wait_iam_exists_waiter_error(m__wait_iam_role): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.fail_json_aws.side_effect = SystemExit(1) + module.check_mode = False + wait_timeout = 10 + module.params = {"name": role_name, "wait": True, "wait_timeout": wait_timeout} + waiter_err = WaiterError( + name="IAMCreationError", + reason="Waiter encountered an unexpected error", + last_response=None, + ) + m__wait_iam_role.side_effect = waiter_err + + with pytest.raises(SystemExit): + iam_role.wait_iam_exists(module, client) + m__wait_iam_role.assert_called_once_with(client, role_name, wait_timeout) + module.fail_json_aws.assert_called_once_with(waiter_err, msg="Timeout while waiting on IAM role creation") + + +@patch(mod__list_policies) +@patch(mod_validate_aws_arn) +def test_convert_friendly_names_to_arns_with_valid_iam_arn(m_validate_aws_arn, m__list_policies): + m_validate_aws_arn.side_effect = lambda *ag, **kw: True + m__list_policies.side_effect = SystemExit(1) + + module = MagicMock() + client = MagicMock() + policy_names = [None, "policy-1"] + + assert iam_role.convert_friendly_names_to_arns(module, client, policy_names) == policy_names + m_validate_aws_arn.assert_called_once_with("policy-1", service="iam") + m__list_policies.assert_not_called() + + +@pytest.mark.parametrize( + "policy_names", + [ + ["AWSEC2SpotServiceRolePolicy", "AllowXRayPutTraceSegments"], + ["AWSEC2SpotServiceRolePolicy", "AllowXRayPutTraceSegments", "ThisPolicyDoesNotExists"], + ], +) +@patch(mod__list_policies) +@patch(mod_validate_aws_arn) +def test_convert_friendly_names_to_arns(m_validate_aws_arn, m__list_policies, policy_names): + m_validate_aws_arn.side_effect = lambda *ag, **kw: False + module = MagicMock() + module.fail_json_aws.side_effect = SystemExit(1) + client = MagicMock() + + test_policies = [ + { + "Arn": "arn:aws:iam::aws:policy/aws-service-role/AWSEC2SpotServiceRolePolicy", + "PolicyName": "AWSEC2SpotServiceRolePolicy", + }, + { + "Arn": "arn:aws:iam::aws:policy/aws-service-role/AWSServiceRoleForAmazonEKSNodegroup", + "PolicyName": "AWSServiceRoleForAmazonEKSNodegroup", + }, + { + "Arn": "arn:aws:iam::966509639900:policy/AllowXRayPutTraceSegments", + "PolicyName": "AllowXRayPutTraceSegments", + }, + ] + test_policies_names = [policy["PolicyName"] for policy in test_policies] + m__list_policies.return_value = test_policies + if any(policy not in test_policies_names for policy in policy_names if policy is not None): + with pytest.raises(SystemExit): + iam_role.convert_friendly_names_to_arns(module, client, policy_names) + module.fail_json_aws.assert_called_once_with(ANY, msg="Couldn't find policy") + else: + + def _get_policy_arn(policy): + for item in test_policies: + if item.get("PolicyName") == policy: + return item.get("Arn") + + expected = [_get_policy_arn(policy) for policy in policy_names if policy is not None] + assert iam_role.convert_friendly_names_to_arns(module, client, policy_names) == expected + m__list_policies.assert_called_once_with(client) + + +def test_attach_policies(): + module = MagicMock() + client = MagicMock() + + module.fail_json_aws.side_effect = SystemExit(1) + role_name = "ansible-test-role" + + # Test: check_mode=true and policies_to_attach = [] + module.check_mode = True + assert not iam_role.attach_policies(module, client, [], role_name) + client.attach_role_policy.assert_not_called() + + # Test: check_mode=true and policies_to_attach != [] + module.check_mode = True + assert iam_role.attach_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + client.attach_role_policy.assert_not_called() + + # Test: check_mode=false and policies_to_attach != [] + module.check_mode = False + assert iam_role.attach_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + client.attach_role_policy.assert_has_calls( + [ + call(RoleName=role_name, PolicyArn="policy-1", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-2", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-3", aws_retry=True), + ] + ) + + # Test: client.attach_role_policy raised botocore exception + error = BotoCoreError(error="AttachRolePolicy", operation="Failed to attach policy to IAM role") + client.attach_role_policy.side_effect = error + with pytest.raises(SystemExit): + iam_role.attach_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + module.fail_json_aws.assert_called_once_with(error, msg=f"Unable to attach policy policy-1 to role {role_name}") + + +def test_remove_policies(): + module = MagicMock() + client = MagicMock() + + module.fail_json_aws.side_effect = SystemExit(1) + role_name = "ansible-test-role" + + # Test: check_mode=true and policies_to_remove = [] + module.check_mode = True + assert not iam_role.remove_policies(module, client, [], role_name) + client.detach_role_policy.assert_not_called() + + # Test: check_mode=true and policies_to_remove != [] + module.check_mode = True + assert iam_role.remove_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + client.detach_role_policy.assert_not_called() + + # Test: check_mode=false and policies_to_attach != [] + module.check_mode = False + assert iam_role.remove_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + client.detach_role_policy.assert_has_calls( + [ + call(RoleName=role_name, PolicyArn="policy-1", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-2", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-3", aws_retry=True), + ] + ) + + # Test: client.attach_role_policy raised botocore exception + error = BotoCoreError(error="DetachRolePolicy", operation="Failed to detach policy to IAM role") + client.detach_role_policy.side_effect = error + with pytest.raises(SystemExit): + iam_role.remove_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + module.fail_json_aws.assert_called_once_with(error, msg=f"Unable to detach policy policy-1 from {role_name}") + + # Test: client.attach_role_policy raised botocore error 'NoSuchEntityException' + nosuch_entity_err = ClientError( + {"Error": {"Code": "NoSuchEntityException"}}, + "DetachRolePolicy", + ) + client.detach_role_policy.side_effect = ( + lambda *args, **kw: nosuch_entity_err if kw.get("PolicyArn") == "policy-2" else True + ) + assert iam_role.remove_policies(module, client, ["policy-1", "policy-2", "policy-3"], role_name) + client.detach_role_policy.assert_has_calls( + [ + call(RoleName=role_name, PolicyArn="policy-1", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-2", aws_retry=True), + call(RoleName=role_name, PolicyArn="policy-3", aws_retry=True), + ] + ) + + +@patch(mod_get_inline_policy_list) +def test_remove_inline_policies(m_get_inline_policy_list): + role_name = "ansible-test-role" + module = MagicMock() + client = MagicMock() + + m_get_inline_policy_list.return_value = ["policy-1", "policy-2", "policy-3"] + nosuch_entity_err = ClientError( + {"Error": {"Code": "NoSuchEntityException"}}, + "DetachRolePolicy", + ) + client.detach_role_policy.side_effect = ( + lambda *args, **kw: nosuch_entity_err if kw.get("PolicyArn") == "policy-2" else True + ) + iam_role.remove_inline_policies(module, client, role_name) + client.delete_role_policy.assert_has_calls( + [ + call(RoleName=role_name, PolicyName="policy-1", aws_retry=True), + call(RoleName=role_name, PolicyName="policy-2", aws_retry=True), + call(RoleName=role_name, PolicyName="policy-3", aws_retry=True), + ] + ) + + +@patch(mod_ansible_dict_to_boto3_tag_list) +def test_generate_create_params(m_ansible_dict_to_boto3_tag_list): + module = MagicMock() + path = MagicMock() + name = MagicMock() + policy_document = MagicMock() + description = MagicMock() + max_session_duration = MagicMock() + boundary = MagicMock() + tags = MagicMock() + module.params = { + "path": path, + "name": name, + "assume_role_policy_document": policy_document, + "description": description, + "max_session_duration": max_session_duration, + "boundary": boundary, + "tags": tags, + } + expected = { + "Path": path, + "RoleName": name, + "AssumeRolePolicyDocument": policy_document, + "Description": description, + "MaxSessionDuration": max_session_duration, + "PermissionsBoundary": boundary, + "Tags": tags, + } + + m_ansible_dict_to_boto3_tag_list.return_value = tags + assert iam_role.generate_create_params(module) == expected + m_ansible_dict_to_boto3_tag_list.assert_called_once_with(tags) + + +@patch(mod_get_role_with_backoff) +@patch(mod_generate_create_params) +def test_create_basic_role_check_mode(m_generate_create_params, m_get_role_with_backoff): + module = MagicMock() + module.exit_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemExit(1) + client = MagicMock() + + module.check_mode = True + with pytest.raises(SystemExit): + iam_role.create_basic_role(module, client) + m_generate_create_params.assert_not_called() + m_get_role_with_backoff.assert_not_called() + + +@patch(mod_get_role_with_backoff) +@patch(mod_generate_create_params) +def test_create_basic_role_with_create_role_error(m_generate_create_params, m_get_role_with_backoff): + role_name = "ansible-test-role" + params = { + "RoleName": role_name, + "Tags": { + "Phase": "dev", + "ansible-test": "units", + }, + } + m_generate_create_params.return_value = params + + module = MagicMock() + module.fail_json_aws.side_effect = SystemExit(1) + client = MagicMock() + + module.check_mode = False + create_role_error = BotoCoreError(error="failed", operation="Not enough permission to create role") + client.create_role.side_effect = create_role_error + with pytest.raises(SystemExit): + iam_role.create_basic_role(module, client) + module.fail_json_aws.assert_called_once_with(create_role_error, msg="Unable to create role") + m_get_role_with_backoff.assert_not_called() + + +@patch(mod_get_role_with_backoff) +@patch(mod_generate_create_params) +def test_create_basic_role_with_get_role_error(m_generate_create_params, m_get_role_with_backoff): + role_name = "ansible-test-role" + params = { + "RoleName": role_name, + "Tags": { + "Phase": "dev", + "ansible-test": "units", + }, + } + m_generate_create_params.return_value = params + module = MagicMock() + module.fail_json_aws.side_effect = SystemExit(1) + client = MagicMock() + + module.check_mode = False + client.create_role.return_value = { + "RoleName": role_name, + } + error = BotoCoreError(error="failed", operation="Unable to get role") + m_get_role_with_backoff.side_effect = error + with pytest.raises(SystemExit): + iam_role.create_basic_role(module, client) + module.fail_json_aws.assert_called_once_with(error, msg="Unable to create role") + client.create_role.assert_called_once_with(aws_retry=True, **params) + + +@patch(mod_get_role_with_backoff) +@patch(mod_generate_create_params) +def test_create_basic_role(m_generate_create_params, m_get_role_with_backoff): + role_name = "ansible-test-role" + params = { + "RoleName": role_name, + "Tags": { + "Phase": "dev", + "ansible-test": "units", + }, + } + m_generate_create_params.return_value = params + module = MagicMock() + module.fail_json_aws.side_effect = SystemExit(1) + client = MagicMock() + + module.check_mode = False + client.create_role.return_value = { + "RoleName": role_name, + } + role = { + "RoleName": role_name, + "Description": "Role created for ansible unit testing", + "Tags": { + "Phase": "dev", + "ansible-test": "units", + }, + } + m_get_role_with_backoff.return_value = role + assert iam_role.create_basic_role(module, client) == role + client.create_role.assert_called_once_with(aws_retry=True, **params) + m_get_role_with_backoff.assert_called_once_with(module, client, role_name) + + +@patch(mod_update_managed_policies) +@patch(mod_remove_inline_policies) +@patch(mod_remove_instance_profiles) +@patch(mod_get_role) +def test_destroy_role_unexisting_role( + m_get_role, m_remove_instance_profiles, m_remove_inline_policies, m_update_managed_policies +): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.params = {"name": role_name} + module.check_mode = False + module.exit_json.side_effect = SystemExit(1) + m_get_role.return_value = None + + with pytest.raises(SystemExit): + iam_role.destroy_role(module, client) + m_get_role.assert_called_once_with(module, client, role_name) + module.exit_json.assert_called_once_with(changed=False) + m_remove_instance_profiles.assert_not_called() + m_remove_inline_policies.assert_not_called() + m_update_managed_policies.assert_not_called() + + +@patch(mod_update_managed_policies) +@patch(mod_remove_inline_policies) +@patch(mod_remove_instance_profiles) +@patch(mod_get_role) +def test_destroy_role_check_mode( + m_get_role, m_remove_instance_profiles, m_remove_inline_policies, m_update_managed_policies +): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.params = {"name": role_name} + module.check_mode = True + module.exit_json.side_effect = SystemExit(1) + m_get_role.return_value = MagicMock() + + with pytest.raises(SystemExit): + iam_role.destroy_role(module, client) + m_get_role.assert_called_once_with(module, client, role_name) + module.exit_json.assert_called_once_with(changed=True) + m_remove_instance_profiles.assert_not_called() + m_remove_inline_policies.assert_not_called() + m_update_managed_policies.assert_not_called() + + +@patch(mod_update_managed_policies) +@patch(mod_remove_inline_policies) +@patch(mod_remove_instance_profiles) +@patch(mod_get_role) +def test_destroy_role(m_get_role, m_remove_instance_profiles, m_remove_inline_policies, m_update_managed_policies): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.params = {"name": role_name} + module.check_mode = False + module.exit_json.side_effect = SystemExit(1) + m_get_role.return_value = MagicMock() + + with pytest.raises(SystemExit): + iam_role.destroy_role(module, client) + m_get_role.assert_called_once_with(module, client, role_name) + module.exit_json.assert_called_once_with(changed=True) + m_remove_instance_profiles.assert_called_once_with(module, client, role_name) + m_remove_inline_policies.assert_called_once_with(module, client, role_name) + m_update_managed_policies.assert_called_once_with(module, client, role_name, [], True) + + +@patch(mod_update_managed_policies) +@patch(mod_remove_inline_policies) +@patch(mod_remove_instance_profiles) +@patch(mod_get_role) +def test_destroy_role_with_deletion_error( + m_get_role, m_remove_instance_profiles, m_remove_inline_policies, m_update_managed_policies +): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.params = {"name": role_name} + module.check_mode = False + module.exit_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemExit(1) + m_get_role.return_value = MagicMock() + + error = BotoCoreError(error="failed", operation="Unable to get role") + client.delete_role.side_effect = error + + with pytest.raises(SystemExit): + iam_role.destroy_role(module, client) + m_get_role.assert_called_once_with(module, client, role_name) + module.exit_json.assert_not_called() + module.fail_json_aws.assert_called_once_with(error, msg="Unable to delete role") + m_remove_instance_profiles.assert_called_once_with(module, client, role_name) + m_remove_inline_policies.assert_called_once_with(module, client, role_name) + m_update_managed_policies.assert_called_once_with(module, client, role_name, [], True) + + +@patch(mod_list_instance_profiles_for_role) +@patch(mod_remove_role_from_instance_profile) +@patch(mod_delete_instance_profile) +def test_remove_instance_profiles_check_mode( + m_delete_instance_profile, m_remove_role_from_instance_profile, m_list_instance_profiles_for_role +): + module = MagicMock() + client = MagicMock() + + role_name = "ansible-test-role" + module.check_mode = True + iam_role.remove_instance_profiles(module, client, role_name) + for m_func in (m_delete_instance_profile, m_remove_role_from_instance_profile, m_list_instance_profiles_for_role): + m_func.assert_not_called() + + +@pytest.mark.parametrize("delete_profiles", [True, False]) +@patch(mod_list_instance_profiles_for_role) +@patch(mod_remove_role_from_instance_profile) +@patch(mod_delete_instance_profile) +def test_remove_instance_profiles_with_delete_profile( + m_delete_instance_profile, m_remove_role_from_instance_profile, m_list_instance_profiles_for_role, delete_profiles +): + module = MagicMock() + client = MagicMock() + + module.params = {"delete_instance_profile": delete_profiles} + module.check_mode = False + role_name = "ansible-test-role" + instance_profiles = [ + {"InstanceProfileName": "instance_profile_1"}, + {"InstanceProfileName": "instance_profile_2"}, + {"InstanceProfileName": role_name}, + ] + m_list_instance_profiles_for_role.return_value = instance_profiles + iam_role.remove_instance_profiles(module, client, role_name) + m_list_instance_profiles_for_role.assert_called_once_with(module, client, role_name) + m_remove_role_from_instance_profile.assert_has_calls( + [call(module, client, role_name, profile["InstanceProfileName"]) for profile in instance_profiles], + any_order=True, + ) + if delete_profiles: + m_delete_instance_profile.assert_called_once_with(module, client, role_name) + else: + m_delete_instance_profile.assert_not_called() + + +@patch(mod_list_instance_profiles_for_role) +@patch(mod_create_instance_profile) +@patch(mod_add_role_to_instance_profile) +def test_create_instance_profiles_with_existing_profile( + m_add_role_to_instance_profile, m_create_instance_profile, m_list_instance_profiles_for_role +): + module = MagicMock() + client = MagicMock() + path = MagicMock() + + role_name = "ansible-test-role" + m_list_instance_profiles_for_role.return_value = [{"InstanceProfileName": role_name}] + assert not iam_role.create_instance_profiles(module, client, role_name, path) + m_add_role_to_instance_profile.assert_not_called() + m_create_instance_profile.assert_not_called() + + +@patch(mod_list_instance_profiles_for_role) +@patch(mod_create_instance_profile) +@patch(mod_add_role_to_instance_profile) +def test_create_instance_profiles_check_mode( + m_add_role_to_instance_profile, m_create_instance_profile, m_list_instance_profiles_for_role +): + module = MagicMock() + client = MagicMock() + path = MagicMock() + + module.check_mode = True + role_name = "ansible-test-role" + m_list_instance_profiles_for_role.return_value = [{"InstanceProfileName": "instance-profile-1"}] + assert iam_role.create_instance_profiles(module, client, role_name, path) + m_add_role_to_instance_profile.assert_not_called() + m_create_instance_profile.assert_not_called() + + +@patch(mod_list_instance_profiles_for_role) +@patch(mod_create_instance_profile) +@patch(mod_add_role_to_instance_profile) +def test_create_instance_profiles( + m_add_role_to_instance_profile, m_create_instance_profile, m_list_instance_profiles_for_role +): + module = MagicMock() + client = MagicMock() + path = MagicMock() + + module.check_mode = False + role_name = "ansible-test-role" + m_list_instance_profiles_for_role.return_value = [{"InstanceProfileName": "instance-profile-1"}] + assert iam_role.create_instance_profiles(module, client, role_name, path) + m_add_role_to_instance_profile.assert_called_once_with(module, client, role_name) + m_create_instance_profile.assert_called_once_with(module, client, role_name, path)