From 9a4a0d3fddbb8907f17000c997af159c838f28cb Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Wed, 4 Sep 2024 15:37:37 +0200 Subject: [PATCH] iam_role - makes EntityAlreadyExists error a warning and fixes bug when not creating a profile fixes: #2102 fixes: #2281 --- .../2281-iam_role-support-not-creating.yml | 4 + plugins/modules/iam_role.py | 31 +- .../iam_role/tasks/instance_profile.yml | 292 ++++++++++++++++++ .../targets/iam_role/tasks/main.yml | 1 + 4 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 changelogs/fragments/2281-iam_role-support-not-creating.yml create mode 100644 tests/integration/targets/iam_role/tasks/instance_profile.yml diff --git a/changelogs/fragments/2281-iam_role-support-not-creating.yml b/changelogs/fragments/2281-iam_role-support-not-creating.yml new file mode 100644 index 00000000000..d77ad54969e --- /dev/null +++ b/changelogs/fragments/2281-iam_role-support-not-creating.yml @@ -0,0 +1,4 @@ +--- +bugfixes: +- iam_role - fixes issue where IAM instance profiles were created when ``create_instance_profile`` was set to ``false`` (https://github.com/ansible-collections/amazon.aws/issues/2281). +- iam_role - fixes ``EntityAlreadyExists`` exception when ``create_instance_profile`` was set to ``false`` and the instance profile already existed (https://github.com/ansible-collections/amazon.aws/issues/2102). diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py index f50b1b5fd39..77912164987 100644 --- a/plugins/modules/iam_role.py +++ b/plugins/modules/iam_role.py @@ -80,15 +80,17 @@ type: str create_instance_profile: description: - - Creates an IAM instance profile along with the role. + - If no IAM instance profile with the same O(name) exists, setting O(create_instance_profile=True) + will create an IAM instance profile along with the role. - This option has been deprecated and will be removed in a release after 2026-05-01. The M(amazon.aws.iam_instance_profile) module can be used to manage instance profiles. - Defaults to V(True) type: bool delete_instance_profile: description: - - When O(delete_instance_profile=true) and O(state=absent) deleting a role will also delete the instance - profile created with the same O(name) as the role. + - When O(delete_instance_profile=true) and O(state=absent) deleting a role will also delete an + instance profile with the same O(name) as the role, but only if the instance profile is + associated with the role. - Only applies when O(state=absent). - This option has been deprecated and will be removed in a release after 2026-05-01. The M(amazon.aws.iam_instance_profile) module can be used to manage instance profiles. @@ -294,6 +296,10 @@ from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags +class AnsibleIAMAlreadyExistsError(AnsibleIAMError): + pass + + @IAMErrorHandler.common_error_handler("wait for role creation") def wait_iam_exists(client, check_mode, role_name, wait, wait_timeout): if check_mode or wait: @@ -535,8 +541,11 @@ def create_or_update_role(module, client, role_name, create_instance_profile): wait_iam_exists(client, check_mode, role_name, wait, wait_timeout) if create_instance_profile: - changed |= create_instance_profiles(client, check_mode, role_name, path) - wait_iam_exists(client, check_mode, role_name, wait, wait_timeout) + try: + changed |= create_instance_profiles(client, check_mode, role_name, path) + wait_iam_exists(client, check_mode, role_name, wait, wait_timeout) + except AnsibleIAMAlreadyExistsError as e: + module.warn(f"profile {role_name} already exists and will not be updated") changed |= update_managed_policies(client, module.check_mode, role_name, managed_policies, purge_policies) wait_iam_exists(client, check_mode, role_name, wait, wait_timeout) @@ -551,12 +560,15 @@ def create_or_update_role(module, client, role_name, create_instance_profile): def create_instance_profiles(client, check_mode, role_name, path): # Fetch existing Profiles - instance_profiles = list_iam_instance_profiles(client, role=role_name) - + role_profiles = list_iam_instance_profiles(client, role=role_name) # Profile already exists - if any(p["InstanceProfileName"] == role_name for p in instance_profiles): + if any(p["InstanceProfileName"] == role_name for p in role_profiles): return False + named_profile = list_iam_instance_profiles(client, name=role_name) + if named_profile: + raise AnsibleIAMAlreadyExistsError(f"profile {role_name} already exists") + if check_mode: return True @@ -730,7 +742,8 @@ def main(): state = module.params.get("state") role_name = module.params.get("name") - create_profile = module.params.get("create_instance_profile") or True + create_profile = module.params.get("create_instance_profile") + create_profile = True if create_profile is None else create_profile delete_profile = module.params.get("delete_instance_profile") or False try: diff --git a/tests/integration/targets/iam_role/tasks/instance_profile.yml b/tests/integration/targets/iam_role/tasks/instance_profile.yml new file mode 100644 index 00000000000..df59d7fbbd2 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/instance_profile.yml @@ -0,0 +1,292 @@ +--- + +- block: + # Ensure profile doesn't already exist (from an old test) + - name: Delete Instance Profile + amazon.aws.iam_instance_profile: + state: absent + name: "{{ test_role }}" + + ################################################################## + + # Profile doesn't exist, don't create + - name: Minimal IAM Role without instance profile (without existing profile) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: false + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile doesn't exist + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 0 + + # Profile doesn't exist, do delete + - name: Remove IAM Role and profile (with non-existent profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: true + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile doesn't exist + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 0 + + ################################################################## + + # Profile doesn't exist, do create + - name: Minimal IAM Role with instance profile (without existing profile) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: true + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + # Profile does exist, don't delete + - name: Remove IAM Role and not profile (with existent profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: false + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + ################################################################## + + # Profile does exist, do create + - name: Minimal IAM Role with instance profile (profile exists) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: true + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + + # Profile does exist, don't delete + - name: Remove IAM Role and don't delete profile (with existent profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: false + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + ################################################################## + # No create profile - profile already exists + + # Profile does exist, don't create + - name: Minimal IAM Role without instance profile (profile exists) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: false + register: iam_role + + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + - name: Attach Role to profile + amazon.aws.iam_instance_profile: + name: "{{ test_role }}" + role: "{{ test_role }}" + register: iam_instance_profile + + - ansible.builtin.assert: + that: + - iam_instance_profile is changed + + # Profile does exist, do delete + - name: Remove IAM Role and delete profile (with existent profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: true + register: iam_role + + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 0 + + ################################################################## + + # Profile doesn't exist, don't create + - name: Minimal IAM Role without instance profile (without existing profile) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: false + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile doesn't exist + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 0 + + # Profile doesn't exist, don't delete + - name: Remove IAM Role and profile (with non-existent profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: false + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile doesn't exist + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 0 + + ################################################################## + + # Profile doesn't exist, do create + - name: Minimal IAM Role with instance profile (profile does not exist) + amazon.aws.iam_role: + name: "{{ test_role }}" + create_instance_profile: true + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:CreateInstanceProfile" in iam_role.resource_actions' + + - name: Decouple Instance Profile from role + amazon.aws.iam_instance_profile: + name: "{{ test_role }}" + role: "" + register: iam_instance_profile + + - ansible.builtin.assert: + that: + - iam_instance_profile is changed + + # Detached profile exists, we shouldn't delete it. + - name: Remove IAM Role and "delete" profile (with detached profile) + amazon.aws.iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: true + register: iam_role + - ansible.builtin.assert: + that: + - iam_role is changed + - '"iam:DeleteInstanceProfile" not in iam_role.resource_actions' + + - name: Verify instance profile exists + amazon.aws.iam_instance_profile_info: + name: "{{ test_role }}" + register: iam_instance_profile + - ansible.builtin.assert: + that: + - iam_instance_profile.iam_instance_profiles | length == 1 + + ################################################################## + # Delete profile + + - name: Delete Instance Profile + amazon.aws.iam_instance_profile: + state: absent + name: "{{ test_role }}" + register: iam_instance_profile + + - ansible.builtin.assert: + that: + - iam_instance_profile is changed + + always: + - name: Delete Instance Profiles + amazon.aws.iam_instance_profile: + state: absent + name: "{{ test_role }}" + ignore_errors: true diff --git a/tests/integration/targets/iam_role/tasks/main.yml b/tests/integration/targets/iam_role/tasks/main.yml index 21e25d9e344..6221cb7eced 100644 --- a/tests/integration/targets/iam_role/tasks/main.yml +++ b/tests/integration/targets/iam_role/tasks/main.yml @@ -46,6 +46,7 @@ # =================================================================== # Rapid Role Creation and deletion + - ansible.builtin.include_tasks: instance_profile.yml - ansible.builtin.include_tasks: creation_deletion.yml - ansible.builtin.include_tasks: max_session_update.yml - ansible.builtin.include_tasks: description_update.yml