From 4b9a83c7c8e9775489e00e7b8937bae4f0af62b3 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 1 Oct 2021 17:16:41 +0200 Subject: [PATCH 01/30] ecs_taskdefinition - add placement_constraints option Signed-off-by: Alina Buzachis --- .../741-ecs_taskdefinition-placement.yml | 2 ++ plugins/modules/ecs_taskdefinition.py | 34 ++++++++++++++++--- .../targets/ecs_cluster/defaults/main.yml | 3 ++ .../targets/ecs_cluster/tasks/full_test.yml | 29 ++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/741-ecs_taskdefinition-placement.yml diff --git a/changelogs/fragments/741-ecs_taskdefinition-placement.yml b/changelogs/fragments/741-ecs_taskdefinition-placement.yml new file mode 100644 index 00000000000..ac96c1af2c0 --- /dev/null +++ b/changelogs/fragments/741-ecs_taskdefinition-placement.yml @@ -0,0 +1,2 @@ +minor_changes: + - ecs_taskdefinition - add ``placement_constraints`` option (https://github.com/ansible-collections/community.aws/pull/741). diff --git a/plugins/modules/ecs_taskdefinition.py b/plugins/modules/ecs_taskdefinition.py index 86319b8bb3c..de02167a050 100644 --- a/plugins/modules/ecs_taskdefinition.py +++ b/plugins/modules/ecs_taskdefinition.py @@ -518,6 +518,22 @@ - If I(launch_type=FARGATE), this field is required and is limited by the CPU. required: false type: str + placement_constraints: + version_added: 2.1.0 + description: + - Placement constraint objects to use for the task. + - You can specify a maximum of 10 constraints per task. + - Task placement constraints are not supported for tasks run on Fargate. + required: false + type: list + elements: dict + suboptions: + type: + description: The type of constraint. + type: str + expression: + description: A cluster query language expression to apply to the constraint. + type: str extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -667,7 +683,7 @@ def describe_task(self, task_name): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: return None - def register_task(self, family, task_role_arn, execution_role_arn, network_mode, container_definitions, volumes, launch_type, cpu, memory): + def register_task(self, family, task_role_arn, execution_role_arn, network_mode, container_definitions, volumes, launch_type, cpu, memory, placement_constraints): validated_containers = [] # Ensures the number parameters are int as required by boto @@ -721,6 +737,8 @@ def register_task(self, family, task_role_arn, execution_role_arn, network_mode, params['requiresCompatibilities'] = [launch_type] if execution_role_arn: params['executionRoleArn'] = execution_role_arn + if placement_constraints: + params['placementConstraints'] = placement_constraints try: response = self.ecs.register_task_definition(aws_retry=True, **params) @@ -780,7 +798,8 @@ def main(): volumes=dict(required=False, type='list', elements='dict'), launch_type=dict(required=False, choices=['EC2', 'FARGATE']), cpu=dict(), - memory=dict(required=False, type='str') + memory=dict(required=False, type='str'), + placement_constraints=dict(required=False, type='list', elements='dict') ) module = AnsibleAWSModule(argument_spec=argument_spec, @@ -801,8 +820,12 @@ def main(): network_mode = module.params['network_mode'] launch_type = module.params['launch_type'] - if launch_type == 'FARGATE' and network_mode != 'awsvpc': - module.fail_json(msg="To use FARGATE launch type, network_mode must be awsvpc") + placement_constraints = module.params['placement_constraints'] + if launch_type == 'FARGATE': + if network_mode != 'awsvpc': + module.fail_json(msg="To use FARGATE launch type, network_mode must be awsvpc") + if placement_constraints: + module.fail_json(msg="Task placement constraints are not supported for tasks run on Fargate") for container in module.params['containers']: if container.get('links') and network_mode == 'awsvpc': @@ -969,7 +992,8 @@ def _task_definition_matches(requested_volumes, requested_containers, requested_ volumes, module.params['launch_type'], module.params['cpu'], - module.params['memory']) + module.params['memory'], + module.params['placement_constraints'],) results['changed'] = True elif module.params['state'] == 'absent': diff --git a/tests/integration/targets/ecs_cluster/defaults/main.yml b/tests/integration/targets/ecs_cluster/defaults/main.yml index 0a34cf0232e..b909b2977af 100644 --- a/tests/integration/targets/ecs_cluster/defaults/main.yml +++ b/tests/integration/targets/ecs_cluster/defaults/main.yml @@ -41,3 +41,6 @@ ecs_fargate_task_containers: - containerPort: "{{ ecs_task_container_port }}" hostPort: "{{ ecs_task_host_port|default(0) }}" #mountPoints: "{{ ecs_task_mount_points|default([]) }}" +ecs_taskdefinition_placement_constraints: + - type: memberOf + expression: 'attribute:ecs.instance-type == t3.micro' diff --git a/tests/integration/targets/ecs_cluster/tasks/full_test.yml b/tests/integration/targets/ecs_cluster/tasks/full_test.yml index fbf50cf05db..cb58ecc74ed 100644 --- a/tests/integration/targets/ecs_cluster/tasks/full_test.yml +++ b/tests/integration/targets/ecs_cluster/tasks/full_test.yml @@ -576,6 +576,35 @@ - name: attempt to get facts from missing task definition ecs_taskdefinition_info: task_definition: "{{ ecs_task_name }}-vpc:{{ ecs_task_definition.taskdefinition.revision + 1}}" + + - name: Create another task definition with placement constraints + ecs_taskdefinition: + containers: "{{ ecs_task_containers }}" + family: "{{ ecs_task_name }}-constraints" + state: present + placement_constraints: "{{ ecs_taskdefinition_placement_constraints }}" + register: ecs_task_definition_constraints + + - name: Check that task definition has been created + assert: + that: + - ecs_task_definition_constraints is changed + - ecs_task_definition_constraints.taskdefinition.placementConstraints[0].type == "{{ ecs_taskdefinition_placement_constraints[0].type }}" + - ecs_task_definition_constraints.taskdefinition.placementConstraints[0].expression == "{{ ecs_taskdefinition_placement_constraints[0].expression }}" + + - name: Remove ecs task definition with placement constraints + ecs_taskdefinition: + containers: "{{ ecs_task_containers }}" + family: "{{ ecs_task_name }}-constraints" + revision: "{{ ecs_task_definition.taskdefinition.revision }}" + state: absent + register: ecs_task_definition_constraints_delete + + - name: Check that task definition has been deleted + assert: + that: + - ecs_task_definition_constraints_delete is changed + # ============================================================ # Begin tests for Fargate From a578f72f0925a90b671adea8784b87f68553d97f Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 8 Oct 2021 14:19:06 +0200 Subject: [PATCH 02/30] Address reviewer's comments Signed-off-by: Alina Buzachis --- plugins/modules/ecs_taskdefinition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/ecs_taskdefinition.py b/plugins/modules/ecs_taskdefinition.py index de02167a050..41a8c9e610f 100644 --- a/plugins/modules/ecs_taskdefinition.py +++ b/plugins/modules/ecs_taskdefinition.py @@ -799,7 +799,7 @@ def main(): launch_type=dict(required=False, choices=['EC2', 'FARGATE']), cpu=dict(), memory=dict(required=False, type='str'), - placement_constraints=dict(required=False, type='list', elements='dict') + placement_constraints=dict(required=False, type='list', elements='dict', options=dict(type=dict(type='str'), expression=dict(type='str')))), ) module = AnsibleAWSModule(argument_spec=argument_spec, From b89d24a9a4fa23c619ec3ac736922d39a619dfac Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Sat, 2 Oct 2021 17:52:09 +0200 Subject: [PATCH 03/30] pep8 fixup - line too long --- plugins/modules/ecs_taskdefinition.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/ecs_taskdefinition.py b/plugins/modules/ecs_taskdefinition.py index 41a8c9e610f..144426670a9 100644 --- a/plugins/modules/ecs_taskdefinition.py +++ b/plugins/modules/ecs_taskdefinition.py @@ -683,7 +683,8 @@ def describe_task(self, task_name): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: return None - def register_task(self, family, task_role_arn, execution_role_arn, network_mode, container_definitions, volumes, launch_type, cpu, memory, placement_constraints): + def register_task(self, family, task_role_arn, execution_role_arn, network_mode, container_definitions, + volumes, launch_type, cpu, memory, placement_constraints): validated_containers = [] # Ensures the number parameters are int as required by boto @@ -799,7 +800,8 @@ def main(): launch_type=dict(required=False, choices=['EC2', 'FARGATE']), cpu=dict(), memory=dict(required=False, type='str'), - placement_constraints=dict(required=False, type='list', elements='dict', options=dict(type=dict(type='str'), expression=dict(type='str')))), + placement_constraints=dict(required=False, type='list', elements='dict', + options=dict(type=dict(type='str'), expression=dict(type='str'))), ) module = AnsibleAWSModule(argument_spec=argument_spec, From 623dee5cee3f664fef5d3667030ad6cd0baba90d Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 14 Oct 2021 10:04:22 +0200 Subject: [PATCH 04/30] docs - Remove references to old (unsupported) versions of Ansible --- plugins/modules/ec2_lc.py | 5 +---- plugins/modules/ecs_taskdefinition.py | 1 - plugins/modules/iam_role.py | 2 +- plugins/modules/route53.py | 3 +-- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/modules/ec2_lc.py b/plugins/modules/ec2_lc.py index 9aaa96538db..2cdf0463863 100644 --- a/plugins/modules/ec2_lc.py +++ b/plugins/modules/ec2_lc.py @@ -21,7 +21,6 @@ notes: - Amazon ASG Autoscaling Launch Configurations are immutable once created, so modifying the configuration after it is changed will not modify the launch configuration on AWS. You must create a new config and assign it to the ASG instead. - - encrypted volumes are supported on versions >= 2.4 author: @@ -188,9 +187,7 @@ EXAMPLES = r''' -# create a launch configuration using an AMI image and instance type as a basis - -- name: note that encrypted volumes are only supported in >= Ansible 2.4 +- name: create a launch configuration with an encrypted volume community.aws.ec2_lc: name: special image_id: ami-XXX diff --git a/plugins/modules/ecs_taskdefinition.py b/plugins/modules/ecs_taskdefinition.py index 86319b8bb3c..c549e8e2c22 100644 --- a/plugins/modules/ecs_taskdefinition.py +++ b/plugins/modules/ecs_taskdefinition.py @@ -471,7 +471,6 @@ network_mode: description: - The Docker networking mode to use for the containers in the task. - - C(awsvpc) mode was added in Ansible 2.5 - Windows containers must use I(network_mode=default), which will utilize docker NAT networking. - Setting I(network_mode=default) for a Linux container will use C(bridge) mode. required: false diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py index f5699edf8b5..356741e4ec5 100644 --- a/plugins/modules/iam_role.py +++ b/plugins/modules/iam_role.py @@ -43,7 +43,7 @@ type: json managed_policies: description: - - A list of managed policy ARNs or, since Ansible 2.4, a list of either managed policy ARNs or friendly names. + - 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(community.aws.iam_policy). aliases: ['managed_policy'] diff --git a/plugins/modules/route53.py b/plugins/modules/route53.py index d4fe99531c0..964020257db 100644 --- a/plugins/modules/route53.py +++ b/plugins/modules/route53.py @@ -19,8 +19,7 @@ options: state: description: - - Specifies the state of the resource record. As of Ansible 2.4, the I(command) option has been changed - to I(state) as default and the choices C(present) and C(absent) have been added, but I(command) still works as well. + - Specifies the state of the resource record. required: true aliases: [ 'command' ] choices: [ 'present', 'absent', 'get', 'create', 'delete' ] From 3ae2b3efa4588ac1284c565d6f73543a555a5edd Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 14 Oct 2021 10:14:21 +0200 Subject: [PATCH 05/30] replace Ansible version deprecations with date deprecations (in line with the module.deprecate date entries) --- plugins/modules/aws_kms.py | 10 +++++----- plugins/modules/cloudfront_info.py | 2 +- plugins/modules/elb_network_lb.py | 4 ++-- plugins/modules/iam_policy.py | 13 +++++++------ plugins/modules/iam_role.py | 4 ++-- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/plugins/modules/aws_kms.py b/plugins/modules/aws_kms.py index 10753f63584..05a520ac94a 100644 --- a/plugins/modules/aws_kms.py +++ b/plugins/modules/aws_kms.py @@ -44,7 +44,7 @@ - (deprecated) Grant or deny access. - Used for modifying the Key Policy rather than modifying a grant and only works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. + - This option has been deprecated, and will be removed in a release after 2021-12-01. Use I(policy) instead. default: grant choices: [ grant, deny ] aliases: @@ -56,7 +56,7 @@ - One of I(policy_role_name) or I(policy_role_arn) are required. - Used for modifying the Key Policy rather than modifying a grant and only works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. + - This option has been deprecated, and will be removed in a release after 2021-12-01. Use I(policy) instead. required: false aliases: - role_name @@ -67,7 +67,7 @@ - One of I(policy_role_name) or I(policy_role_arn) are required. - Used for modifying the Key Policy rather than modifying a grant and only works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. + - This option has been deprecated, and will be removed in a release after 2021-12-01. Use I(policy) instead. type: str required: false aliases: @@ -78,7 +78,7 @@ - Required when I(policy_mode=grant). - Used for modifying the Key Policy rather than modifying a grant and only works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. + - This option has been deprecated, and will be removed in a release after 2021-12-01. Use I(policy) instead. required: false aliases: - grant_types @@ -90,7 +90,7 @@ - Only cleans if changes are being made. - Used for modifying the Key Policy rather than modifying a grant and only works on the default policy created through the AWS Console. - - This option has been deprecated, and will be removed in 2.13. Use I(policy) instead. + - This option has been deprecated, and will be removed in a release after 2021-12-01. Use I(policy) instead. type: bool default: true aliases: diff --git a/plugins/modules/cloudfront_info.py b/plugins/modules/cloudfront_info.py index 767557bf6e9..df42ed0d1ac 100644 --- a/plugins/modules/cloudfront_info.py +++ b/plugins/modules/cloudfront_info.py @@ -173,7 +173,7 @@ # When the module is called as cloudfront_facts, return values are published # in ansible_facts['cloudfront'][] and can be used as follows. -# Note that this is deprecated and will stop working in Ansible 2.13. +# Note that this is deprecated and will stop working in a release after 2021-12-01. - name: Gather facts community.aws.cloudfront_facts: distribution: true diff --git a/plugins/modules/elb_network_lb.py b/plugins/modules/elb_network_lb.py index 47ac7b1d0d7..8de4b7692aa 100644 --- a/plugins/modules/elb_network_lb.py +++ b/plugins/modules/elb_network_lb.py @@ -108,7 +108,7 @@ description: - Create or destroy the load balancer. - The current default is C(absent). However, this behavior is inconsistent with other modules - and as such the default will change to C(present) in 2.14. + and as such the default will change to C(present) in a release after 2022-06-01. To maintain the existing behavior explicitly set I(state=absent). choices: [ 'present', 'absent' ] type: str @@ -452,7 +452,7 @@ def main(): if state is None: # See below, unless state==present we delete. Ouch. module.deprecate('State currently defaults to absent. This is inconsistent with other modules' - ' and the default will be changed to `present` in Ansible 2.14', + ' and the default will be changed to `present` in a release after 2022-06-01', date='2022-06-01', collection_name='community.aws') # Quick check of listeners parameters diff --git a/plugins/modules/iam_policy.py b/plugins/modules/iam_policy.py index 819ed369a31..570c37efa1b 100644 --- a/plugins/modules/iam_policy.py +++ b/plugins/modules/iam_policy.py @@ -36,7 +36,7 @@ description: - The path to the properly json formatted policy file. - Mutually exclusive with I(policy_json). - - This option has been deprecated and will be removed in 2.14. The existing behavior can be + - This option has been deprecated and will be removed in a release after 2022-06-01. The existing behavior can be reproduced by using the I(policy_json) option and reading the file using the lookup plugin. type: str policy_json: @@ -53,9 +53,10 @@ type: str skip_duplicates: description: - - When I(skip_duplicates=true) the module looks for any policies that match the document you pass in. If there is a match it will not make - a new policy object with the same rules. - - The current default is C(true). However, this behavior can be confusing and as such the default will change to C(false) in 2.14. To maintain + - When I(skip_duplicates=true) the module looks for any policies that match the document you pass in. + If there is a match it will not make a new policy object with the same rules. + - The current default is C(true). However, this behavior can be confusing and as such the default will + change to C(false) in a release after 2022-06-01. To maintain the existing behavior explicitly set I(skip_duplicates=true). type: bool @@ -304,13 +305,13 @@ def main(): if (skip_duplicates is None): module.deprecate('The skip_duplicates behaviour has caused confusion and' - ' will be disabled by default in Ansible 2.14', + ' will be disabled by default in a release after 2022-06-01', date='2022-06-01', collection_name='community.aws') skip_duplicates = True if module.params.get('policy_document'): module.deprecate('The policy_document option has been deprecated and' - ' will be removed in Ansible 2.14', + ' will be removed in a release after 2022-06-01', date='2022-06-01', collection_name='community.aws') args = dict( diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py index 356741e4ec5..948358b184e 100644 --- a/plugins/modules/iam_role.py +++ b/plugins/modules/iam_role.py @@ -57,7 +57,7 @@ purge_policies: description: - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. - - By default I(purge_policies=true). In Ansible 2.14 this will be changed to I(purge_policies=false). + - By default I(purge_policies=true). In a release after 2022-06-01 this will be changed to I(purge_policies=false). type: bool aliases: ['purge_policy', 'purge_managed_policies'] state: @@ -621,7 +621,7 @@ def main(): supports_check_mode=True) if module.params.get('purge_policies') is None: - module.deprecate('In Ansible 2.14 the default value of purge_policies will change from true to false.' + module.deprecate('After 2022-06-01 the default value of purge_policies will change from true to false.' ' To maintain the existing behaviour explicitly set purge_policies=true', date='2022-06-01', collection_name='community.aws') if module.params.get('boundary'): From 6e3830cd898675b66bf6409eacf578c6b5de4cf5 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 14 Oct 2021 09:27:08 +0200 Subject: [PATCH 06/30] ec2_win_password - migrate to boto3 --- changelogs/fragments/759-ec2_win_password.yml | 2 ++ plugins/modules/ec2_win_password.py | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 changelogs/fragments/759-ec2_win_password.yml diff --git a/changelogs/fragments/759-ec2_win_password.yml b/changelogs/fragments/759-ec2_win_password.yml new file mode 100644 index 00000000000..a13df0dd63e --- /dev/null +++ b/changelogs/fragments/759-ec2_win_password.yml @@ -0,0 +1,2 @@ +minor_changes: +- ec2_win_password - module updated to use the boto3 AWS SDK (https://github.com/ansible-collections/community.aws/pull/759). diff --git a/plugins/modules/ec2_win_password.py b/plugins/modules/ec2_win_password.py index 3ed0afb79d4..00bd603ed97 100644 --- a/plugins/modules/ec2_win_password.py +++ b/plugins/modules/ec2_win_password.py @@ -55,7 +55,6 @@ requirements: - cryptography -- boto >= 2.49.0 notes: - As of Ansible 2.4, this module requires the python cryptography module rather than the older pycrypto module. @@ -110,11 +109,15 @@ except ImportError: HAS_CRYPTOGRAPHY = False +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + from ansible.module_utils._text import to_bytes from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import HAS_BOTO -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ec2_connect +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry def setup_module_object(): @@ -130,6 +133,14 @@ def setup_module_object(): return module +def _get_password(module, client, instance_id): + try: + data = client.get_password_data(aws_retry=True, InstanceId=instance_id)['PasswordData'] + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg='Failed to get password data') + return data + + def ec2_win_password(module): instance_id = module.params.get('instance_id') key_file = module.params.get('key_file') @@ -144,21 +155,21 @@ def ec2_win_password(module): wait = module.params.get('wait') wait_timeout = module.params.get('wait_timeout') - ec2 = ec2_connect(module) + client = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) if wait: start = datetime.datetime.now() end = start + datetime.timedelta(seconds=wait_timeout) while datetime.datetime.now() < end: - data = ec2.get_password_data(instance_id) + data = _get_password(module, client, instance_id) decoded = b64decode(data) if not decoded: time.sleep(5) else: break else: - data = ec2.get_password_data(instance_id) + data = _get_password(module, client, instance_id) decoded = b64decode(data) if wait and datetime.datetime.now() >= end: @@ -198,9 +209,6 @@ def ec2_win_password(module): def main(): module = setup_module_object() - if not HAS_BOTO: - module.fail_json(msg='Boto required for this module.') - if not HAS_CRYPTOGRAPHY: module.fail_json(msg='cryptography package required for this module.') From e59e034cc1ce6c68528007a0c88024e3e6c11a7d Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Thu, 14 Oct 2021 09:28:28 +0200 Subject: [PATCH 07/30] Clean up docs and make key_file and key_data mutually exclusive as defined by the docs --- plugins/modules/ec2_win_password.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/modules/ec2_win_password.py b/plugins/modules/ec2_win_password.py index 00bd603ed97..7f977360e80 100644 --- a/plugins/modules/ec2_win_password.py +++ b/plugins/modules/ec2_win_password.py @@ -10,10 +10,9 @@ --- module: ec2_win_password version_added: 1.0.0 -short_description: Gets the default administrator password for ec2 windows instances +short_description: Gets the default administrator password for EC2 Windows instances description: - Gets the default administrator password from any EC2 Windows instance. The instance is referenced by its id (e.g. C(i-XXXXXXX)). - - This module has a dependency on python-boto. author: "Rick Mendes (@rickmendes)" options: instance_id: @@ -55,9 +54,6 @@ requirements: - cryptography -notes: - - As of Ansible 2.4, this module requires the python cryptography module rather than the - older pycrypto module. ''' EXAMPLES = ''' @@ -129,7 +125,8 @@ def setup_module_object(): wait=dict(type='bool', default=False, required=False), wait_timeout=dict(default=120, required=False, type='int'), ) - module = AnsibleAWSModule(argument_spec=argument_spec) + mutually_exclusive = [['key_file', 'key_data']] + module = AnsibleAWSModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive) return module From 58e30adf556726b2c3cd78df405b0e9fe4813bbe Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 15 Oct 2021 10:21:05 +0200 Subject: [PATCH 08/30] Slightly rework ec2_win_password unit tests to account for boto3 --- test-requirements.txt | 2 ++ .../plugins/modules/test_ec2_win_password.py | 34 +++++++++++++------ tests/unit/requirements.txt | 1 + 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 77c76b86509..d44dc6b2012 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -14,3 +14,5 @@ netaddr awscli # Used for comparing SSH Public keys to the Amazon fingerprints pycrypto +# Used by ec2_win_password +cryptography diff --git a/tests/unit/plugins/modules/test_ec2_win_password.py b/tests/unit/plugins/modules/test_ec2_win_password.py index c572378ccb1..58082f6cc4a 100644 --- a/tests/unit/plugins/modules/test_ec2_win_password.py +++ b/tests/unit/plugins/modules/test_ec2_win_password.py @@ -17,6 +17,8 @@ string_cipher = base64.b64encode(base64_cipher) ''' +import datetime + from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_text from ansible_collections.community.aws.tests.unit.compat.mock import patch @@ -31,25 +33,37 @@ class TestEc2WinPasswordModule(ModuleTestCase): - @patch('ansible_collections.community.aws.plugins.modules.ec2_win_password.ec2_connect') - def test_decryption(self, mock_connect): - path = fixture_prefix + '/ec2_win_password.pem' + # Future: It would be good to generate this data on the fly and use a + # temporary certificate and password. + PEM_PATH = fixture_prefix + '/ec2_win_password.pem' + UNENCRYPTED_DATA = 'Ansible_AWS_EC2_Win_Password' + ENCRYPTED_DATA = 'L2k1iFiu/TRrjGr6Rwco/T3C7xkWxUw4+YPYpGGOmP3KDdy3hT1' \ + '8RvdDJ2i0e+y7wUcH43DwbRYSlkSyALY/nzjSV9R5NChUyVs3W5' \ + '5oiVuyTKsk0lor8dFJ9z9unq14tScZHvyQ3Nx1ggOtS18S9Pk55q' \ + 'IaCXfx26ucH76VRho=' + INSTANCE_ID = 'i-12345' + + @patch('ansible_collections.community.aws.plugins.modules.s3_bucket_notification.AnsibleAWSModule.client') + def test_decryption(self, mock_client): + + path = self.PEM_PATH with open(path, 'r') as f: pem = to_text(f.read()) with self.assertRaises(AnsibleExitJson) as exec_info: - set_module_args({'instance_id': 'i-12345', - 'key_data': pem + set_module_args({'instance_id': self.INSTANCE_ID, + 'key_data': pem, }) module = setup_module_object() - mock_connect().get_password_data.return_value = 'L2k1iFiu/TRrjGr6Rwco/T3C7xkWxUw4+YPYpGGOmP3KDdy3hT1' \ - '8RvdDJ2i0e+y7wUcH43DwbRYSlkSyALY/nzjSV9R5NChUyVs3W5' \ - '5oiVuyTKsk0lor8dFJ9z9unq14tScZHvyQ3Nx1ggOtS18S9Pk55q' \ - 'IaCXfx26ucH76VRho=' + mock_client().get_password_data.return_value = { + 'InstanceId': self.INSTANCE_ID, + 'PasswordData': self.ENCRYPTED_DATA, + 'Timestamp': datetime.datetime.now(), + } ec2_win_password(module) self.assertEqual( exec_info.exception.args[0]['win_password'], - to_bytes('Ansible_AWS_EC2_Win_Password'), + to_bytes(self.UNENCRYPTED_DATA), ) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 704c73a25b2..4c5b1ce9282 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -4,3 +4,4 @@ boto3 boto placebo +cryptography From 38d414ec7dba34b4ce60d0dae56e42c7256f059e Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 24 Sep 2021 23:45:23 +0100 Subject: [PATCH 09/30] Add support for billing_mode --- plugins/modules/dynamodb_table.py | 74 ++++++++++++++----- .../targets/dynamodb_table/tasks/main.yml | 24 ++++++ 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 86c08f90934..8010a0e024b 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -49,6 +49,12 @@ - Defaults to C('STRING') when creating a new range key. choices: ['STRING', 'NUMBER', 'BINARY'] type: str + billing_mode: + description: + - Controls whether provisoned pr on-demand tables are created. + choices: ['PROVISIONED', 'PAY_PER_REQUEST'] + default: 'PROVISIONED' + type: str read_capacity: description: - Read throughput capacity (units) to provision. @@ -165,6 +171,14 @@ read_capacity: 10 write_capacity: 10 +- name: Create pay-per-request table + community.aws.dynamodb_table: + name: my-table + region: us-east-1 + hash_key_name: id + hash_key_type: STRING + billing_mode: PAY_PER_REQUEST + - name: set index on existing dynamo table community.aws.dynamodb_table: name: my-table @@ -367,7 +381,10 @@ def compatability_results(current_table): if not current_table: return dict() - throughput = current_table.get('provisioned_throughput', {}) + billing_mode = current_table.get('billing_mode') + + if billing_mode == "PROVISIONED": + throughput = current_table.get('provisioned_throughput', {}) primary_indexes = _decode_primary_index(current_table) @@ -394,12 +411,13 @@ def compatability_results(current_table): range_key_name=range_key_name, range_key_type=range_key_type, indexes=indexes, + billing_mode=billing_mode, read_capacity=throughput.get('read_capacity_units', None), + write_capacity=throughput.get('write_capacity_units', None), region=module.region, table_name=current_table.get('table_name', None), table_status=current_table.get('table_status', None), tags=current_table.get('tags', {}), - write_capacity=throughput.get('write_capacity_units', None), ) return compat_results @@ -571,6 +589,13 @@ def _throughput_changes(current_table, params=None): def _generate_global_indexes(): index_exists = dict() indexes = list() + billing_mode = module.params.get('billing_mode') + + if billing_mode == "PROVISIONED": + is_provisioned = True + else: + is_provisioned = False + for index in module.params.get('indexes'): if index.get('type') not in ['global_all', 'global_include', 'global_keys_only']: continue @@ -579,7 +604,7 @@ def _generate_global_indexes(): module.fail_json(msg='Duplicate key {0} in list of global indexes'.format(name)) # Convert the type name to upper case and remove the global_ index['type'] = index['type'].upper()[7:] - index = _generate_index(index) + index = _generate_index(index, include_throughput=is_provisioned) index_exists[name] = True indexes.append(index) @@ -589,6 +614,7 @@ def _generate_global_indexes(): def _generate_local_indexes(): index_exists = dict() indexes = list() + for index in module.params.get('indexes'): index = dict() if index.get('type') not in ['all', 'include', 'keys_only']: @@ -708,20 +734,22 @@ def _local_index_changes(current_table): def _update_table(current_table): changes = dict() additional_global_index_changes = list() - - throughput_changes = _throughput_changes(current_table) - if throughput_changes: - changes['ProvisionedThroughput'] = throughput_changes - - global_index_changes = _global_index_changes(current_table) - if global_index_changes: - changes['GlobalSecondaryIndexUpdates'] = global_index_changes - # Only one index can be changed at a time, pass the first during the - # main update and deal with the others on a slow retry to wait for - # completion - if len(global_index_changes) > 1: - changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] - additional_global_index_changes = global_index_changes[1:] + billing_mode = module.params.get('billing_mode') + + if billing_mode == "PROVISIONED": + throughput_changes = _throughput_changes(current_table) + if throughput_changes: + changes['ProvisionedThroughput'] = throughput_changes + + global_index_changes = _global_index_changes(current_table) + if global_index_changes: + changes['GlobalSecondaryIndexUpdates'] = global_index_changes + # Only one index can be changed at a time, pass the first during the + # main update and deal with the others on a slow retry to wait for + # completion + if len(global_index_changes) > 1: + changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] + additional_global_index_changes = global_index_changes[1:] local_index_changes = _local_index_changes(current_table) if local_index_changes: @@ -818,6 +846,7 @@ def update_table(current_table): def create_table(): table_name = module.params.get('name') hash_key_name = module.params.get('hash_key_name') + billing_mode = module.params.get('billing_mode') tags = ansible_dict_to_boto3_tag_list(module.params.get('tags') or {}) @@ -827,7 +856,9 @@ def create_table(): if module.check_mode: return True - throughput = _generate_throughput() + if billing_mode == "PROVISIONED": + throughput = _generate_throughput() + attributes = _generate_attributes() key_schema = _generate_schema() local_indexes = _generate_local_indexes() @@ -837,13 +868,15 @@ def create_table(): TableName=table_name, AttributeDefinitions=attributes, KeySchema=key_schema, - ProvisionedThroughput=throughput, Tags=tags, + BillingMode=billing_mode # TODO (future) - # BillingMode, # StreamSpecification, # SSESpecification, ) + + if billing_mode == "PROVISIONED": + params['ProvisionedThroughput'] = throughput if local_indexes: params['LocalSecondaryIndexes'] = local_indexes if global_indexes: @@ -919,6 +952,7 @@ def main(): hash_key_type=dict(type='str', choices=KEY_TYPE_CHOICES), range_key_name=dict(type='str'), range_key_type=dict(type='str', choices=KEY_TYPE_CHOICES), + billing_mode=dict(default='PROVISIONED', type='str', choices=['PROVISIONED', 'PAY_PER_REQUEST']), read_capacity=dict(type='int'), write_capacity=dict(type='int'), indexes=dict(default=[], type='list', elements='dict', options=index_options), diff --git a/tests/integration/targets/dynamodb_table/tasks/main.yml b/tests/integration/targets/dynamodb_table/tasks/main.yml index 0cda98bcdbd..d0a9fce3c95 100644 --- a/tests/integration/targets/dynamodb_table/tasks/main.yml +++ b/tests/integration/targets/dynamodb_table/tasks/main.yml @@ -660,6 +660,30 @@ # ============================================== + - name: Create table - pay-per-request + dynamodb_table: + state: present + name: '{{ table_name }}' + hash_key_name: '{{ table_index }}' + hash_key_type: '{{ table_index_type }}' + billing_mode: PAY_PER_REQUEST + register: create_table + + - name: Check results - Create table + assert: + that: + - create_table is successful + - create_table is not changed + - create_table.billing_mode == "PAY_PER_REQUEST" + + - name: Delete table + dynamodb_table: + state: absent + name: '{{ table_name }}' + register: delete_table + + # ============================================== + - name: Create complex table - check_mode dynamodb_table: state: present From 07e42f261d118e4871b63c61636a2230d3a533bf Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 24 Sep 2021 23:56:52 +0100 Subject: [PATCH 10/30] split pay_per_request --- .../targets/dynamodb_table/defaults/main.yml | 16 ++++ .../targets/dynamodb_table/tasks/main.yml | 25 +---- .../tasks/test_pay_per_request.yml | 96 +++++++++++++++++++ 3 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml diff --git a/tests/integration/targets/dynamodb_table/defaults/main.yml b/tests/integration/targets/dynamodb_table/defaults/main.yml index 55c8441bfa6..5618b0fdcc3 100644 --- a/tests/integration/targets/dynamodb_table/defaults/main.yml +++ b/tests/integration/targets/dynamodb_table/defaults/main.yml @@ -27,6 +27,22 @@ indexes: read_capacity: 5 write_capacity: 5 +indexes_pay_per_request: + - name: NamedIndex + type: global_include + hash_key_name: idx + range_key_name: create_time + includes: + - other_field + - other_field2 + - name: AnotherIndex + type: global_all + hash_key_name: foo + range_key_name: bar + includes: + - another_field + - another_field2 + index_updated: - name: NamedIndex type: global_include diff --git a/tests/integration/targets/dynamodb_table/tasks/main.yml b/tests/integration/targets/dynamodb_table/tasks/main.yml index d0a9fce3c95..c18c2a161b1 100644 --- a/tests/integration/targets/dynamodb_table/tasks/main.yml +++ b/tests/integration/targets/dynamodb_table/tasks/main.yml @@ -12,6 +12,7 @@ security_token: '{{ security_token | default(omit) }}' region: '{{ aws_region }}' block: + - include: "test_pay_per_request.yml" # ============================================== @@ -660,30 +661,6 @@ # ============================================== - - name: Create table - pay-per-request - dynamodb_table: - state: present - name: '{{ table_name }}' - hash_key_name: '{{ table_index }}' - hash_key_type: '{{ table_index_type }}' - billing_mode: PAY_PER_REQUEST - register: create_table - - - name: Check results - Create table - assert: - that: - - create_table is successful - - create_table is not changed - - create_table.billing_mode == "PAY_PER_REQUEST" - - - name: Delete table - dynamodb_table: - state: absent - name: '{{ table_name }}' - register: delete_table - - # ============================================== - - name: Create complex table - check_mode dynamodb_table: state: present diff --git a/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml new file mode 100644 index 00000000000..ca7bf567ccc --- /dev/null +++ b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml @@ -0,0 +1,96 @@ +--- +- name: Create table - pay-per-request - check_mode + dynamodb_table: + state: present + name: "{{ table_name }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_index_type }}" + billing_mode: PAY_PER_REQUEST + register: create_table + check_mode: True + +- name: Check results - Create table - check_mode + assert: + that: + - create_table is successful + - create_table is changed + +- name: Create table - pay-per-request + dynamodb_table: + state: present + name: "{{ table_name }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_index_type }}" + billing_mode: PAY_PER_REQUEST + register: create_table + +- name: Check results - Create table + assert: + that: + - create_table is successful + - create_table is not changed + - create_table.billing_mode == "PAY_PER_REQUEST" + +# ============================================== + +- name: Create complex table - check_mode + dynamodb_table: + state: present + name: "{{ table_name }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_index_type }}" + range_key_name: "{{ range_index }}" + range_key_type: "{{ range_index_type }}" + billing_mode: PAY_PER_REQUEST + tags: "{{ tags_default }}" + indexes: "{{ indexes_pay_per_request }}" + register: create_complex_table + check_mode: True + +- name: Check results - Create complex table - check_mode + assert: + that: + - create_complex_table is successful + - create_complex_table is changed + +- name: Create complex table + dynamodb_table: + state: present + name: "{{ table_name }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_index_type }}" + range_key_name: "{{ range_index }}" + range_key_type: "{{ range_index_type }}" + billing_mode: PAY_PER_REQUEST + tags: "{{ tags_default }}" + indexes: "{{ indexes_pay_per_request }}" + register: create_complex_table + +- name: Check results - Create complex table + assert: + that: + - create_complex_table is successful + - create_complex_table is changed + - '"hash_key_name" in create_complex_table' + - '"hash_key_type" in create_complex_table' + - '"indexes" in create_complex_table' + - '"range_key_name" in create_complex_table' + - '"range_key_type" in create_complex_table' + - '"billing_mode" in create_complex_table' + - '"region" in create_complex_table' + - '"table_name" in create_complex_table' + - '"table_status" in create_complex_table' + - '"tags" in create_complex_table' + - create_complex_table.hash_key_name == table_index + - create_complex_table.hash_key_type == table_index_type + - create_complex_table.indexes | length == 2 + - create_complex_table.range_key_name == range_index + - create_complex_table.range_key_type == range_index_type + - create_complex_table.table_name == table_name + - create_complex_table.tags == tags_default + +- name: Delete table + dynamodb_table: + state: absent + name: "{{ table_name }}" + register: delete_table From fdad53d2646c7725cc409c6b1e0a3d4f7446edd7 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Sat, 25 Sep 2021 00:16:05 +0100 Subject: [PATCH 11/30] fix assignment issue --- plugins/modules/dynamodb_table.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 8010a0e024b..2cd380f2107 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -412,14 +412,16 @@ def compatability_results(current_table): range_key_type=range_key_type, indexes=indexes, billing_mode=billing_mode, - read_capacity=throughput.get('read_capacity_units', None), - write_capacity=throughput.get('write_capacity_units', None), region=module.region, table_name=current_table.get('table_name', None), table_status=current_table.get('table_status', None), tags=current_table.get('tags', {}), ) + if billing_mode == "PROVISIONED": + compat_results['read_capacity'] = throughput.get('read_capacity_units', None) + compat_results['write_capacity'] = throughput.get('write_capacity_units', None) + return compat_results From a31259a056598a015be057f49b4a1d7f117068ac Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 11 Oct 2021 17:32:23 +0100 Subject: [PATCH 12/30] fixes & support move between provisioned & pay-per-request --- plugins/modules/dynamodb_table.py | 43 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 2cd380f2107..c0823bfbe8b 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -2,10 +2,6 @@ # Copyright: Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - DOCUMENTATION = r''' --- module: dynamodb_table @@ -455,6 +451,12 @@ def get_dynamodb_table(): table['size'] = table['table_size_bytes'] table['tags'] = tags + # billing_mode_summary is only set if the table is already PAY_PER_REQUEST + if 'billing_mode_summary' in table: + table['billing_mode'] = table['billing_mode_summary']['billing_mode'] + else: + table['billing_mode'] = "PROVISIONED" + # convert indexes into something we can easily search against attributes = table['attribute_definitions'] global_index_map = dict() @@ -736,22 +738,23 @@ def _local_index_changes(current_table): def _update_table(current_table): changes = dict() additional_global_index_changes = list() - billing_mode = module.params.get('billing_mode') - if billing_mode == "PROVISIONED": - throughput_changes = _throughput_changes(current_table) - if throughput_changes: - changes['ProvisionedThroughput'] = throughput_changes - - global_index_changes = _global_index_changes(current_table) - if global_index_changes: - changes['GlobalSecondaryIndexUpdates'] = global_index_changes - # Only one index can be changed at a time, pass the first during the - # main update and deal with the others on a slow retry to wait for - # completion - if len(global_index_changes) > 1: - changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] - additional_global_index_changes = global_index_changes[1:] + throughput_changes = _throughput_changes(current_table) + if throughput_changes: + changes['ProvisionedThroughput'] = throughput_changes + + if current_table.get('billing_mode') != module.params.get('billing_mode'): + changes['BillingMode'] = billing_mode + + global_index_changes = _global_index_changes(current_table) + if global_index_changes: + changes['GlobalSecondaryIndexUpdates'] = global_index_changes + # Only one index can be changed at a time, pass the first during the + # main update and deal with the others on a slow retry to wait for + # completion + if len(global_index_changes) > 1: + changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] + additional_global_index_changes = global_index_changes[1:] local_index_changes = _local_index_changes(current_table) if local_index_changes: @@ -961,7 +964,7 @@ def main(): tags=dict(type='dict'), purge_tags=dict(type='bool', default=True), wait=dict(type='bool', default=True), - wait_timeout=dict(default=120, type='int', aliases=['wait_for_active_timeout']), + wait_timeout=dict(default=300, type='int', aliases=['wait_for_active_timeout']), ) module = AnsibleAWSModule( From d993f22f2664e2c8950e26a07bd71d9dfd4e4705 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 11 Oct 2021 17:42:28 +0100 Subject: [PATCH 13/30] update tests --- .../targets/dynamodb_table/defaults/main.yml | 1 + .../targets/dynamodb_table/tasks/main.yml | 2 + .../tasks/test_pay_per_request.yml | 38 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/tests/integration/targets/dynamodb_table/defaults/main.yml b/tests/integration/targets/dynamodb_table/defaults/main.yml index 5618b0fdcc3..d9e8b6995a0 100644 --- a/tests/integration/targets/dynamodb_table/defaults/main.yml +++ b/tests/integration/targets/dynamodb_table/defaults/main.yml @@ -1,5 +1,6 @@ --- table_name: '{{ resource_prefix }}' +table_name_on_demand: '{{ resource_prefix }}-pay-per-request' table_index: 'id' table_index_type: 'NUMBER' diff --git a/tests/integration/targets/dynamodb_table/tasks/main.yml b/tests/integration/targets/dynamodb_table/tasks/main.yml index c18c2a161b1..a5f3f7a3882 100644 --- a/tests/integration/targets/dynamodb_table/tasks/main.yml +++ b/tests/integration/targets/dynamodb_table/tasks/main.yml @@ -12,6 +12,7 @@ security_token: '{{ security_token | default(omit) }}' region: '{{ aws_region }}' block: + - include: "test_pay_per_request.yml" # ============================================== @@ -907,4 +908,5 @@ dynamodb_table: state: absent name: '{{ table_name }}' + wait: false register: delete_table diff --git a/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml index ca7bf567ccc..c202602e6b4 100644 --- a/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml +++ b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml @@ -2,7 +2,7 @@ - name: Create table - pay-per-request - check_mode dynamodb_table: state: present - name: "{{ table_name }}" + name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" billing_mode: PAY_PER_REQUEST @@ -18,7 +18,7 @@ - name: Create table - pay-per-request dynamodb_table: state: present - name: "{{ table_name }}" + name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" billing_mode: PAY_PER_REQUEST @@ -28,7 +28,7 @@ assert: that: - create_table is successful - - create_table is not changed + - create_table is changed - create_table.billing_mode == "PAY_PER_REQUEST" # ============================================== @@ -36,7 +36,7 @@ - name: Create complex table - check_mode dynamodb_table: state: present - name: "{{ table_name }}" + name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" range_key_name: "{{ range_index }}" @@ -56,7 +56,7 @@ - name: Create complex table dynamodb_table: state: present - name: "{{ table_name }}" + name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" range_key_name: "{{ range_index }}" @@ -86,11 +86,35 @@ - create_complex_table.indexes | length == 2 - create_complex_table.range_key_name == range_index - create_complex_table.range_key_type == range_index_type - - create_complex_table.table_name == table_name + - create_complex_table.table_name == table_name_on_demand - create_complex_table.tags == tags_default +- name: Update complex table billing_mode + dynamodb_table: + state: present + name: "{{ table_name_on_demand }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_index_type }}" + range_key_name: "{{ range_index }}" + range_key_type: "{{ range_index_type }}" + billing_mode: PROVISIONED + read_capacity: 1 + write_capacity: 1 + tags: "{{ tags_default }}" + indexes: "{{ indexes }}" + register: convert_complex_table + +- name: Check results - Update complex table billing_mode + assert: + that: + - convert_complex_table is successful + - convert_complex_table is changed + - '"billing_mode" in convert_complex_table' + - convert_complex_table.billing_mode == "PROVISIONED" + - name: Delete table dynamodb_table: state: absent - name: "{{ table_name }}" + name: "{{ table_name_on_demand }}" + wait: false register: delete_table From bb03d72ce6d9ee6da99a99943d0a3d0424e00fae Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 11 Oct 2021 17:58:16 +0100 Subject: [PATCH 14/30] fix missed update --- plugins/modules/dynamodb_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index c0823bfbe8b..fe4366e3b6e 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -453,7 +453,7 @@ def get_dynamodb_table(): # billing_mode_summary is only set if the table is already PAY_PER_REQUEST if 'billing_mode_summary' in table: - table['billing_mode'] = table['billing_mode_summary']['billing_mode'] + table['billing_mode'] = "PAY_PER_REQUEST" else: table['billing_mode'] = "PROVISIONED" @@ -744,7 +744,7 @@ def _update_table(current_table): changes['ProvisionedThroughput'] = throughput_changes if current_table.get('billing_mode') != module.params.get('billing_mode'): - changes['BillingMode'] = billing_mode + changes['BillingMode'] = module.params.get('billing_mode') global_index_changes = _global_index_changes(current_table) if global_index_changes: From 5dfa6517e7bed1455b76b1881c65b538c131db3c Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 11 Oct 2021 20:40:40 +0100 Subject: [PATCH 15/30] fixes --- plugins/modules/dynamodb_table.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index fe4366e3b6e..b1212cf6647 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -132,7 +132,7 @@ description: - How long (in seconds) to wait for creation / update / deletion to complete. aliases: ['wait_for_active_timeout'] - default: 120 + default: 300 type: int wait: description: @@ -593,12 +593,6 @@ def _throughput_changes(current_table, params=None): def _generate_global_indexes(): index_exists = dict() indexes = list() - billing_mode = module.params.get('billing_mode') - - if billing_mode == "PROVISIONED": - is_provisioned = True - else: - is_provisioned = False for index in module.params.get('indexes'): if index.get('type') not in ['global_all', 'global_include', 'global_keys_only']: @@ -608,7 +602,7 @@ def _generate_global_indexes(): module.fail_json(msg='Duplicate key {0} in list of global indexes'.format(name)) # Convert the type name to upper case and remove the global_ index['type'] = index['type'].upper()[7:] - index = _generate_index(index, include_throughput=is_provisioned) + index = _generate_index(index) index_exists[name] = True indexes.append(index) @@ -666,7 +660,7 @@ def _generate_local_index_map(current_table): return local_index_map -def _generate_index(index, include_throughput=True): +def _generate_index(index): key_schema = _generate_schema(index) throughput = _generate_throughput(index) non_key_attributes = index['includes'] or [] @@ -689,7 +683,8 @@ def _generate_index(index, include_throughput=True): KeySchema=key_schema, Projection=projection, ) - if include_throughput: + + if module.params.get('billing_mode') == "PROVISIONED": idx['ProvisionedThroughput'] = throughput return idx @@ -717,13 +712,15 @@ def _global_index_changes(current_table): # rather than dropping other changes on the floor _current = current_global_index_map[name] _new = global_index_map[name] - change = dict(_throughput_changes(_current, _new)) - if change: - update = dict( - IndexName=name, - ProvisionedThroughput=change, - ) - index_changes.append(dict(Update=update)) + + if module.params.get('billing_mode') == "PROVISIONED": + change = dict(_throughput_changes(_current, _new)) + if change: + update = dict( + IndexName=name, + ProvisionedThroughput=change, + ) + index_changes.append(dict(Update=update)) return index_changes From f1005847d639de939291e4a66fc871129838e8af Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 11 Oct 2021 20:47:53 +0100 Subject: [PATCH 16/30] re-add removal of param --- plugins/modules/dynamodb_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index b1212cf6647..c353ada60c5 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -660,7 +660,7 @@ def _generate_local_index_map(current_table): return local_index_map -def _generate_index(index): +def _generate_index(index, include_throughput=True): key_schema = _generate_schema(index) throughput = _generate_throughput(index) non_key_attributes = index['includes'] or [] From dfc9b7232bd761ef956481e52d9009b0ab0a7b96 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Tue, 12 Oct 2021 09:25:09 +0100 Subject: [PATCH 17/30] re-add removed section --- plugins/modules/dynamodb_table.py | 6 +++++- .../targets/dynamodb_table/defaults/main.yml | 21 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index c353ada60c5..b2621dd79af 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -2,6 +2,10 @@ # Copyright: Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + DOCUMENTATION = r''' --- module: dynamodb_table @@ -684,7 +688,7 @@ def _generate_index(index, include_throughput=True): Projection=projection, ) - if module.params.get('billing_mode') == "PROVISIONED": + if include_throughput and module.params.get('billing_mode') == "PROVISIONED": idx['ProvisionedThroughput'] = throughput return idx diff --git a/tests/integration/targets/dynamodb_table/defaults/main.yml b/tests/integration/targets/dynamodb_table/defaults/main.yml index d9e8b6995a0..96df5538146 100644 --- a/tests/integration/targets/dynamodb_table/defaults/main.yml +++ b/tests/integration/targets/dynamodb_table/defaults/main.yml @@ -1,12 +1,12 @@ --- -table_name: '{{ resource_prefix }}' -table_name_on_demand: '{{ resource_prefix }}-pay-per-request' +table_name: "{{ resource_prefix }}" +table_name_on_demand: "{{ resource_prefix }}-pay-per-request" -table_index: 'id' -table_index_type: 'NUMBER' +table_index: "id" +table_index_type: "NUMBER" -range_index: 'variety' -range_index_type: 'STRING' +range_index: "variety" +range_index_type: "STRING" indexes: - name: NamedIndex @@ -53,13 +53,12 @@ index_updated: type: global_all read_capacity: 4 - tags_default: snake_case_key: snake_case_value camelCaseKey: camelCaseValue PascalCaseKey: PascalCaseValue - 'key with spaces': value with spaces - 'Upper With Spaces': Upper With Spaces + "key with spaces": value with spaces + "Upper With Spaces": Upper With Spaces partial_tags: snake_case_key: snake_case_value @@ -69,5 +68,5 @@ updated_tags: updated_snake_case_key: updated_snake_case_value updatedCamelCaseKey: updatedCamelCaseValue UpdatedPascalCaseKey: UpdatedPascalCaseValue - 'updated key with spaces': updated value with spaces - 'updated Upper With Spaces': Updated Upper With Spaces + "updated key with spaces": updated value with spaces + "updated Upper With Spaces": Updated Upper With Spaces From 4760c6c87dee6ba30b99bc93fdf08d4d85eff9c7 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Tue, 12 Oct 2021 10:31:14 +0100 Subject: [PATCH 18/30] adjust tests can't update primary index --- .../targets/dynamodb_table/tasks/test_pay_per_request.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml index c202602e6b4..059561dea05 100644 --- a/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml +++ b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml @@ -59,8 +59,6 @@ name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" - range_key_name: "{{ range_index }}" - range_key_type: "{{ range_index_type }}" billing_mode: PAY_PER_REQUEST tags: "{{ tags_default }}" indexes: "{{ indexes_pay_per_request }}" @@ -84,8 +82,6 @@ - create_complex_table.hash_key_name == table_index - create_complex_table.hash_key_type == table_index_type - create_complex_table.indexes | length == 2 - - create_complex_table.range_key_name == range_index - - create_complex_table.range_key_type == range_index_type - create_complex_table.table_name == table_name_on_demand - create_complex_table.tags == tags_default @@ -95,8 +91,6 @@ name: "{{ table_name_on_demand }}" hash_key_name: "{{ table_index }}" hash_key_type: "{{ table_index_type }}" - range_key_name: "{{ range_index }}" - range_key_type: "{{ range_index_type }}" billing_mode: PROVISIONED read_capacity: 1 write_capacity: 1 From 84f4e1b3e0520b0378e72efbe09a85ecdb8fae04 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Tue, 12 Oct 2021 11:40:57 +0100 Subject: [PATCH 19/30] multiple indexes can now be updated at once --- plugins/modules/dynamodb_table.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index b2621dd79af..a0eb632be91 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -455,9 +455,10 @@ def get_dynamodb_table(): table['size'] = table['table_size_bytes'] table['tags'] = tags - # billing_mode_summary is only set if the table is already PAY_PER_REQUEST + # billing_mode_summary doesn't always seem to be set but is always set for PAY_PER_REQUEST + # and when updating the billing_mode if 'billing_mode_summary' in table: - table['billing_mode'] = "PAY_PER_REQUEST" + table['billing_mode'] = table['billing_mode_summary']['billing_mode'] else: table['billing_mode'] = "PROVISIONED" @@ -750,12 +751,6 @@ def _update_table(current_table): global_index_changes = _global_index_changes(current_table) if global_index_changes: changes['GlobalSecondaryIndexUpdates'] = global_index_changes - # Only one index can be changed at a time, pass the first during the - # main update and deal with the others on a slow retry to wait for - # completion - if len(global_index_changes) > 1: - changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] - additional_global_index_changes = global_index_changes[1:] local_index_changes = _local_index_changes(current_table) if local_index_changes: From 4fff25cd5b46eb3ff4f34150225467fa2b9ba2b6 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Tue, 12 Oct 2021 14:26:26 +0100 Subject: [PATCH 20/30] indexes indexes --- plugins/modules/dynamodb_table.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index a0eb632be91..299e684b4a1 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -751,6 +751,14 @@ def _update_table(current_table): global_index_changes = _global_index_changes(current_table) if global_index_changes: changes['GlobalSecondaryIndexUpdates'] = global_index_changes + # Only one index can be changed at a time except if changing the billing mode, pass the first during the + # main update and deal with the others on a slow retry to wait for + # completion + + if current_table.get('billing_mode') == module.params.get('billing_mode'): + if len(global_index_changes) > 1: + changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] + additional_global_index_changes = global_index_changes[1:] local_index_changes = _local_index_changes(current_table) if local_index_changes: From c9572de579669563d1d44f9fdb9214d717b7f9b0 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Tue, 12 Oct 2021 21:33:46 +0100 Subject: [PATCH 21/30] PR feedback --- plugins/modules/dynamodb_table.py | 41 ++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 299e684b4a1..a9e19268db5 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -53,7 +53,6 @@ description: - Controls whether provisoned pr on-demand tables are created. choices: ['PROVISIONED', 'PAY_PER_REQUEST'] - default: 'PROVISIONED' type: str read_capacity: description: @@ -383,9 +382,6 @@ def compatability_results(current_table): billing_mode = current_table.get('billing_mode') - if billing_mode == "PROVISIONED": - throughput = current_table.get('provisioned_throughput', {}) - primary_indexes = _decode_primary_index(current_table) hash_key_name = primary_indexes.get('hash_key_name') @@ -419,6 +415,7 @@ def compatability_results(current_table): ) if billing_mode == "PROVISIONED": + throughput = current_table.get('provisioned_throughput', {}) compat_results['read_capacity'] = throughput.get('read_capacity_units', None) compat_results['write_capacity'] = throughput.get('write_capacity_units', None) @@ -595,10 +592,15 @@ def _throughput_changes(current_table, params=None): return dict() -def _generate_global_indexes(): +def _generate_global_indexes(billing_mode): index_exists = dict() indexes = list() + include_throughput = True + + if billing_mode == "PAY_PER_REQUEST": + include_throughput = False + for index in module.params.get('indexes'): if index.get('type') not in ['global_all', 'global_include', 'global_keys_only']: continue @@ -607,7 +609,7 @@ def _generate_global_indexes(): module.fail_json(msg='Duplicate key {0} in list of global indexes'.format(name)) # Convert the type name to upper case and remove the global_ index['type'] = index['type'].upper()[7:] - index = _generate_index(index) + index = _generate_index(index, include_throughput) index_exists[name] = True indexes.append(index) @@ -689,7 +691,7 @@ def _generate_index(index, include_throughput=True): Projection=projection, ) - if include_throughput and module.params.get('billing_mode') == "PROVISIONED": + if include_throughput: idx['ProvisionedThroughput'] = throughput return idx @@ -704,11 +706,19 @@ def _global_index_changes(current_table): current_global_index_map = current_table['_global_index_map'] global_index_map = _generate_global_index_map(current_table) + current_billing_mode = current_table.get('billing_mode') + + include_throughput = True + + if module.params.get('billing_mode', current_billing_mode) == "PAY_PER_REQUEST": + include_throughput = False + index_changes = list() # TODO (future) it would be nice to add support for deleting an index for name in global_index_map: - idx = dict(_generate_index(global_index_map[name])) + + idx = dict(_generate_index(global_index_map[name], include_throughput=include_throughput)) if name not in current_global_index_map: index_changes.append(dict(Create=idx)) else: @@ -718,7 +728,7 @@ def _global_index_changes(current_table): _current = current_global_index_map[name] _new = global_index_map[name] - if module.params.get('billing_mode') == "PROVISIONED": + if include_throughput: change = dict(_throughput_changes(_current, _new)) if change: update = dict( @@ -745,8 +755,11 @@ def _update_table(current_table): if throughput_changes: changes['ProvisionedThroughput'] = throughput_changes - if current_table.get('billing_mode') != module.params.get('billing_mode'): - changes['BillingMode'] = module.params.get('billing_mode') + current_billing_mode = current_table.get('billing_mode') + new_billing_mode = module.params.get('billing_mode', current_billing_mode) + + if current_billing_mode != new_billing_mode: + changes['BillingMode'] = new_billing_mode global_index_changes = _global_index_changes(current_table) if global_index_changes: @@ -755,7 +768,7 @@ def _update_table(current_table): # main update and deal with the others on a slow retry to wait for # completion - if current_table.get('billing_mode') == module.params.get('billing_mode'): + if current_billing_mode == new_billing_mode: if len(global_index_changes) > 1: changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] additional_global_index_changes = global_index_changes[1:] @@ -871,7 +884,7 @@ def create_table(): attributes = _generate_attributes() key_schema = _generate_schema() local_indexes = _generate_local_indexes() - global_indexes = _generate_global_indexes() + global_indexes = _generate_global_indexes(billing_mode) params = dict( TableName=table_name, @@ -961,7 +974,7 @@ def main(): hash_key_type=dict(type='str', choices=KEY_TYPE_CHOICES), range_key_name=dict(type='str'), range_key_type=dict(type='str', choices=KEY_TYPE_CHOICES), - billing_mode=dict(default='PROVISIONED', type='str', choices=['PROVISIONED', 'PAY_PER_REQUEST']), + billing_mode=dict(type='str', choices=['PROVISIONED', 'PAY_PER_REQUEST']), read_capacity=dict(type='int'), write_capacity=dict(type='int'), indexes=dict(default=[], type='list', elements='dict', options=index_options), From 4c60973e97f615cbd0acae97716d17afabaac7e0 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Wed, 13 Oct 2021 10:33:07 +0100 Subject: [PATCH 22/30] fix creation when billing_mode is omitted --- plugins/modules/dynamodb_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index a9e19268db5..3f19afd4f89 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -868,7 +868,7 @@ def update_table(current_table): def create_table(): table_name = module.params.get('name') hash_key_name = module.params.get('hash_key_name') - billing_mode = module.params.get('billing_mode') + billing_mode = module.params.get('billing_mode', 'PROVISIONED') tags = ansible_dict_to_boto3_tag_list(module.params.get('tags') or {}) From 9e350c1aafba45bee5d925fc147310e7de3cbc6d Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Wed, 13 Oct 2021 17:39:07 +0100 Subject: [PATCH 23/30] handle the fact the param is always set but to None --- plugins/modules/dynamodb_table.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 3f19afd4f89..8696161c58e 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -868,7 +868,10 @@ def update_table(current_table): def create_table(): table_name = module.params.get('name') hash_key_name = module.params.get('hash_key_name') - billing_mode = module.params.get('billing_mode', 'PROVISIONED') + billing_mode = module.params.get('billing_mode') + + if billing_mode == None: + billing_mode = "PROVISIONED" tags = ansible_dict_to_boto3_tag_list(module.params.get('tags') or {}) From 7502fcf8d3ecfb3531f50f76cc4fbad9134d8363 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Wed, 13 Oct 2021 20:13:27 +0100 Subject: [PATCH 24/30] tests fix --- plugins/modules/dynamodb_table.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 8696161c58e..a8725ab7d32 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -708,9 +708,14 @@ def _global_index_changes(current_table): current_billing_mode = current_table.get('billing_mode') + if module.params.get('billing_mode') is None: + billing_mode = current_billing_mode + else: + billing_mode = module.params.get('billing_mode') + include_throughput = True - if module.params.get('billing_mode', current_billing_mode) == "PAY_PER_REQUEST": + if billing_mode == "PAY_PER_REQUEST": include_throughput = False index_changes = list() @@ -756,7 +761,10 @@ def _update_table(current_table): changes['ProvisionedThroughput'] = throughput_changes current_billing_mode = current_table.get('billing_mode') - new_billing_mode = module.params.get('billing_mode', current_billing_mode) + new_billing_mode = module.params.get('billing_mode') + + if new_billing_mode is None: + new_billing_mode = current_billing_mode if current_billing_mode != new_billing_mode: changes['BillingMode'] = new_billing_mode From 51b0fc5b6ad2978f03d07b9c07734b14b420f4f6 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Wed, 13 Oct 2021 22:20:22 +0100 Subject: [PATCH 25/30] lint fix --- plugins/modules/dynamodb_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/dynamodb_table.py b/plugins/modules/dynamodb_table.py index a8725ab7d32..98d6fa632f9 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -878,7 +878,7 @@ def create_table(): hash_key_name = module.params.get('hash_key_name') billing_mode = module.params.get('billing_mode') - if billing_mode == None: + if billing_mode is None: billing_mode = "PROVISIONED" tags = ansible_dict_to_boto3_tag_list(module.params.get('tags') or {}) From 0367fb50f47fb931502b36fcaa5626edfec1abd2 Mon Sep 17 00:00:00 2001 From: Mark Woolley Date: Sat, 16 Oct 2021 14:02:12 +0100 Subject: [PATCH 26/30] Add missed changelog fragment for dynamodb (#766) Add missed changelog fragment for dynamodb SUMMARY Add missed change_log fragment for #753 ISSUE TYPE Docs Pull Request COMPONENT NAME dynamodb_table Reviewed-by: Mark Chappell Reviewed-by: None --- changelogs/fragments/753-dynamodb-billing_mode.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/753-dynamodb-billing_mode.yml diff --git a/changelogs/fragments/753-dynamodb-billing_mode.yml b/changelogs/fragments/753-dynamodb-billing_mode.yml new file mode 100644 index 00000000000..b883f45c8d4 --- /dev/null +++ b/changelogs/fragments/753-dynamodb-billing_mode.yml @@ -0,0 +1,2 @@ +minor_changes: + - dynamodb_table - add support for setting the `billing_mode` option (https://github.com/ansible-collections/community.aws/pull/753). From 02836f37160cd8082fe2c30e880d74927e9885f8 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Sun, 17 Oct 2021 11:01:37 +0200 Subject: [PATCH 27/30] ec2_eip tagging support (#332) ec2_eip tagging support SUMMARY Add support for tagging EIPs on creation. Todo: EIP Tagging Tests Retry decorator Note: While it's now possible to pass tagging information into the association call this was only added Dec 2020 so won't work for most folks. ISSUE TYPE Feature Pull Request COMPONENT NAME ec2_eip ec2_eip_info ADDITIONAL INFORMATION fixes: #331 Reviewed-by: Rick Mendes Reviewed-by: None --- changelogs/fragments/332-ec2_eip-tagging.yml | 4 + plugins/modules/ec2_eip.py | 22 +- plugins/modules/ec2_eip_info.py | 15 +- .../targets/ec2_eip/defaults/main.yml | 1 + .../integration/targets/ec2_eip/meta/main.yml | 2 +- .../targets/ec2_eip/tasks/main.yml | 212 +++++++++++++++--- 6 files changed, 222 insertions(+), 34 deletions(-) create mode 100644 changelogs/fragments/332-ec2_eip-tagging.yml diff --git a/changelogs/fragments/332-ec2_eip-tagging.yml b/changelogs/fragments/332-ec2_eip-tagging.yml new file mode 100644 index 00000000000..c9621b405fe --- /dev/null +++ b/changelogs/fragments/332-ec2_eip-tagging.yml @@ -0,0 +1,4 @@ +minor_changes: +- ec2_eip - added support for tagging EIPs (https://github.com/ansible-collections/community.aws/pull/332). +- ec2_eip_info - added support for tagging EIPs (https://github.com/ansible-collections/community.aws/pull/332). +- ec2_eip_info - added automatic retries for common temporary API failures (https://github.com/ansible-collections/community.aws/pull/332). diff --git a/plugins/modules/ec2_eip.py b/plugins/modules/ec2_eip.py index 927d31551b7..e38e941661f 100644 --- a/plugins/modules/ec2_eip.py +++ b/plugins/modules/ec2_eip.py @@ -64,6 +64,16 @@ network interface or instance to be re-associated with the specified instance or interface. default: false type: bool + tags: + description: A dictionary of tags to apply to the EIP. + type: dict + version_added: 2.1.0 + purge_tags: + description: Whether the I(tags) argument should cause tags not in the + dictionary to be removed. + default: True + type: bool + version_added: 2.1.0 tag_name: description: - When I(reuse_existing_ip_allowed=true), supplement with this option to only reuse @@ -227,6 +237,7 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags def associate_ip_and_device(ec2, module, address, private_ip_address, device_id, allow_reassociation, check_mode, is_instance=True): @@ -247,7 +258,7 @@ def associate_ip_and_device(ec2, module, address, private_ip_address, device_id, params['AllocationId'] = address['AllocationId'] else: params['PublicIp'] = address['PublicIp'] - res = ec2.associate_address(**params) + res = ec2.associate_address(aws_retry=True, **params) except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: msg = "Couldn't associate Elastic IP address with instance '{0}'".format(device_id) module.fail_json_aws(e, msg=msg) @@ -535,6 +546,8 @@ def main(): allow_reassociation=dict(type='bool', default=False), wait_timeout=dict(type='int', removed_at_date='2022-06-01', removed_from_collection='community.aws'), private_ip_address=dict(), + tags=dict(required=False, type='dict'), + purge_tags=dict(required=False, type='bool', default=True), tag_name=dict(), tag_value=dict(), public_ipv4_pool=dict() @@ -563,6 +576,8 @@ def main(): tag_name = module.params.get('tag_name') tag_value = module.params.get('tag_value') public_ipv4_pool = module.params.get('public_ipv4_pool') + tags = module.params.get('tags') + purge_tags = module.params.get('purge_tags') if instance_id: is_instance = True @@ -575,6 +590,7 @@ def main(): module.fail_json(msg="If you are specifying an ENI, in_vpc must be true") is_instance = False + # Tags for *searching* for an EIP. tag_dict = generate_tag_dict(module, tag_name, tag_value) try: @@ -603,6 +619,10 @@ def main(): 'public_ip': address['PublicIp'], 'allocation_id': address['AllocationId'] } + + result['changed'] |= ensure_ec2_tags( + ec2, module, result['allocation_id'], + resource_type='elastic-ip', tags=tags, purge_tags=purge_tags) else: if device_id: disassociated = ensure_absent( diff --git a/plugins/modules/ec2_eip_info.py b/plugins/modules/ec2_eip_info.py index 553930db67a..e38735c087e 100644 --- a/plugins/modules/ec2_eip_info.py +++ b/plugins/modules/ec2_eip_info.py @@ -97,22 +97,25 @@ ''' -from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - ) try: from botocore.exceptions import (BotoCoreError, ClientError) except ImportError: pass # caught by imported AnsibleAWSModule +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict + def get_eips_details(module): - connection = module.client('ec2') + connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) filters = module.params.get("filters") try: response = connection.describe_addresses( + aws_retry=True, Filters=ansible_dict_to_boto3_filter_list(filters) ) except (BotoCoreError, ClientError) as e: diff --git a/tests/integration/targets/ec2_eip/defaults/main.yml b/tests/integration/targets/ec2_eip/defaults/main.yml index bbaeb04820c..03b1ba51de0 100644 --- a/tests/integration/targets/ec2_eip/defaults/main.yml +++ b/tests/integration/targets/ec2_eip/defaults/main.yml @@ -3,3 +3,4 @@ # run multiple copies of the test concurrently. vpc_cidr: '10.{{ 256 | random(seed=resource_prefix) }}.0.0/16' subnet_cidr: '10.{{ 256 | random(seed=resource_prefix) }}.42.0/24' +subnet_az: '{{ ec2_availability_zone_names[0] }}' diff --git a/tests/integration/targets/ec2_eip/meta/main.yml b/tests/integration/targets/ec2_eip/meta/main.yml index 1f64f1169a9..930e8622824 100644 --- a/tests/integration/targets/ec2_eip/meta/main.yml +++ b/tests/integration/targets/ec2_eip/meta/main.yml @@ -1,3 +1,3 @@ dependencies: - prepare_tests - - setup_ec2 + - setup_ec2_facts diff --git a/tests/integration/targets/ec2_eip/tasks/main.yml b/tests/integration/targets/ec2_eip/tasks/main.yml index 9f03f5b5647..48db1d1048a 100644 --- a/tests/integration/targets/ec2_eip/tasks/main.yml +++ b/tests/integration/targets/ec2_eip/tasks/main.yml @@ -17,9 +17,6 @@ - name: list available AZs aws_az_info: null register: region_azs - - name: pick an AZ for testing - set_fact: - subnet_az: '{{ region_azs.availability_zones[0].zone_name }}' - name: create a VPC ec2_vpc_net: name: '{{ resource_prefix }}-vpc' @@ -40,12 +37,6 @@ state: present vpc_id: '{{ vpc_result.vpc.id }}' register: vpc_igw - - name: "Find AMI to use" - ec2_ami_info: - owners: 'amazon' - filters: - name: 'amzn2-ami-hvm-2.0.20190612-x86_64-gp2' - register: ec2_amis - name: "create a security group" ec2_group: state: present @@ -61,10 +52,11 @@ - name: Create instance for attaching ec2_instance: name: '{{ resource_prefix }}-instance' - image_id: '{{ ec2_amis.images[0].image_id }}' + image_id: '{{ ec2_ami_id }}' security_group: '{{ security_group.group_id }}' vpc_subnet_id: '{{ vpc_subnet_create.subnet.id }}' - wait: no ## Don't delay the tests, we'll check again before we need it + wait: yes + state: running register: create_ec2_instance_result # ===================================================== @@ -91,6 +83,7 @@ tags: AnsibleEIPTest: Running AnsibleEIPTestPrefix: '{{ resource_prefix }}' + # ===================================================== - name: Get current state of EIPs ec2_eip_info: null @@ -101,10 +94,14 @@ - eip_info_start is defined - '"addresses" in eip_info_start' - ( eip_info_start.addresses | length ) == ( eip_info_start | community.general.json_query("addresses[].association_id") | length ) + - name: Allocate a new eip (no conditions) ec2_eip: state: present + tags: + AnsibleEIPTestPrefix: '{{ resource_prefix }}' register: eip + - ec2_eip_info: null register: eip_info - assert: @@ -114,6 +111,7 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - ec2_eip_info: filters: public-ip: '{{ eip.public_ip }}' @@ -124,6 +122,9 @@ - eip_info.addresses[0].allocation_id == eip.allocation_id - eip_info.addresses[0].domain == "vpc" - eip_info.addresses[0].public_ip == eip.public_ip + - '"AnsibleEIPTestPrefix" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['AnsibleEIPTestPrefix'] == resource_prefix + - ec2_eip_info: filters: allocation-id: '{{ eip.allocation_id }}' @@ -134,6 +135,7 @@ - eip_info.addresses[0].allocation_id == eip.allocation_id - eip_info.addresses[0].domain == "vpc" - eip_info.addresses[0].public_ip == eip.public_ip + - name: Release eip ec2_eip: state: absent @@ -146,6 +148,7 @@ - eip_release is defined - eip_release is changed - ( eip_info_start.addresses | length ) == ( eip_info.addresses | length ) + - name: Allocate a new eip - attempt reusing unallocated ones (none available) ec2_eip: state: present @@ -160,6 +163,7 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: Re-Allocate a new eip - attempt reusing unallocated ones (one available) ec2_eip: state: present @@ -174,6 +178,7 @@ - reallocate_eip.public_ip is defined and ( reallocate_eip.public_ip | ansible.netcommon.ipaddr ) - reallocate_eip.allocation_id is defined and reallocate_eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: Release eip ec2_eip: state: absent @@ -186,6 +191,7 @@ - ( eip_info_start.addresses | length ) == ( eip_info.addresses | length ) - eip_release is defined - eip_release is changed + - name: Allocate a new eip ec2_eip: state: present @@ -199,6 +205,7 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: Match an existing eip (changed == false) ec2_eip: state: present @@ -213,6 +220,7 @@ - reallocate_eip.public_ip is defined and ( reallocate_eip.public_ip | ansible.netcommon.ipaddr ) - reallocate_eip.allocation_id is defined and reallocate_eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: Release eip ec2_eip: state: absent @@ -225,6 +233,7 @@ - eip_release is defined - eip_release is changed - ( eip_info_start.addresses | length ) == ( eip_info.addresses | length ) + - name: Allocate a new eip (no tags) ec2_eip: state: present @@ -238,6 +247,7 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: attempt reusing an existing eip with a tag (No match available) ec2_eip: state: present @@ -253,12 +263,14 @@ - no_tagged_eip.public_ip is defined and ( no_tagged_eip.public_ip | ansible.netcommon.ipaddr ) - no_tagged_eip.allocation_id is defined and no_tagged_eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 2 == ( eip_info.addresses | length ) + - name: tag eip so we can try matching it - ec2_tag: + ec2_eip: state: present - resource: '{{ eip.allocation_id }}' + public_ip: '{{ eip.public_ip }}' tags: Team: Frontend + - name: attempt reusing an existing eip with a tag (Match available) ec2_eip: state: present @@ -274,6 +286,7 @@ - reallocate_eip.public_ip is defined and ( reallocate_eip.public_ip | ansible.netcommon.ipaddr ) - reallocate_eip.allocation_id is defined and reallocate_eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 2 == ( eip_info.addresses | length ) + - name: attempt reusing an existing eip with a tag and it's value (no match available) ec2_eip: state: present @@ -290,12 +303,14 @@ - backend_eip.public_ip is defined and ( backend_eip.public_ip | ansible.netcommon.ipaddr ) - backend_eip.allocation_id is defined and backend_eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 3 == ( eip_info.addresses | length ) + - name: tag eip so we can try matching it - ec2_tag: + ec2_eip: state: present - resource: '{{ eip.allocation_id }}' + public_ip: '{{ eip.public_ip }}' tags: Team: Backend + - name: attempt reusing an existing eip with a tag and it's value (match available) ec2_eip: state: present @@ -312,6 +327,7 @@ - reallocate_eip.public_ip is defined and reallocate_eip.public_ip != "" - reallocate_eip.allocation_id is defined and reallocate_eip.allocation_id != "" - ( eip_info_start.addresses | length ) + 3 == ( eip_info.addresses | length ) + - name: Release backend_eip ec2_eip: state: absent @@ -324,6 +340,7 @@ - eip_release is defined - eip_release is changed - ( eip_info_start.addresses | length ) + 2 == ( eip_info.addresses | length ) + - name: Release no_tagged_eip ec2_eip: state: absent @@ -336,6 +353,7 @@ - eip_release is defined - eip_release is changed - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: Release eip ec2_eip: state: absent @@ -348,6 +366,7 @@ - eip_release is defined - eip_release is changed - ( eip_info_start.addresses | length ) == ( eip_info.addresses | length ) + - name: allocate a new eip from a pool ec2_eip: state: present @@ -362,14 +381,17 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + - name: create ENI A ec2_eni: subnet_id: '{{ vpc_subnet_create.subnet.id }}' register: eni_create_a + - name: create ENI B ec2_eni: subnet_id: '{{ vpc_subnet_create.subnet.id }}' register: eni_create_b + - name: Attach EIP to ENI A ec2_eip: public_ip: '{{ eip.public_ip }}' @@ -393,6 +415,7 @@ - eip_info.addresses[0].network_interface_id == eni_create_a.interface.id - eip_info.addresses[0].private_ip_address is defined and ( eip_info.addresses[0].private_ip_address | ansible.netcommon.ipaddr ) - eip_info.addresses[0].network_interface_owner_id == caller_info.account + - name: Re-Attach EIP to ENI A (no change) ec2_eip: public_ip: '{{ eip.public_ip }}' @@ -415,6 +438,7 @@ - eip_info.addresses[0].association_id is defined and eip_info.addresses[0].association_id.startswith("eipassoc-") - eip_info.addresses[0].network_interface_id == eni_create_a.interface.id - eip_info.addresses[0].private_ip_address is defined and ( eip_info.addresses[0].private_ip_address | ansible.netcommon.ipaddr ) + - name: Attach EIP to ENI B (should fail, already associated) ec2_eip: public_ip: '{{ eip.public_ip }}' @@ -436,6 +460,7 @@ - eip_info.addresses[0].association_id is defined and eip_info.addresses[0].association_id.startswith("eipassoc-") - eip_info.addresses[0].network_interface_id == eni_create_a.interface.id - eip_info.addresses[0].private_ip_address is defined and ( eip_info.addresses[0].private_ip_address | ansible.netcommon.ipaddr ) + - name: Attach EIP to ENI B ec2_eip: public_ip: '{{ eip.public_ip }}' @@ -459,6 +484,7 @@ - eip_info.addresses[0].association_id is defined and eip_info.addresses[0].association_id.startswith("eipassoc-") - eip_info.addresses[0].network_interface_id == eni_create_b.interface.id - eip_info.addresses[0].private_ip_address is defined and ( eip_info.addresses[0].private_ip_address | ansible.netcommon.ipaddr ) + - name: Detach EIP from ENI B, without enabling release on disassociation ec2_eip: state: absent @@ -474,6 +500,7 @@ - associate_eip is defined - associate_eip is changed - eip_info.addresses | length == 1 + - name: Re-detach EIP from ENI B, without enabling release on disassociation ec2_eip: state: absent @@ -489,6 +516,7 @@ - associate_eip is defined - associate_eip is not changed - eip_info.addresses | length == 1 + - name: Attach EIP to ENI A ec2_eip: public_ip: '{{ eip.public_ip }}' @@ -505,6 +533,7 @@ - associate_eip.public_ip is defined and eip.public_ip == associate_eip.public_ip - associate_eip.allocation_id is defined and eip.allocation_id == associate_eip.allocation_id - eip_info.addresses[0].network_interface_id == eni_create_a.interface.id + - name: Detach EIP from ENI A, enabling release on disassociation ec2_eip: state: absent @@ -521,6 +550,7 @@ - associate_eip is defined - associate_eip is changed - eip_info.addresses | length == 0 + - name: Re-detach EIP from ENI A, enabling release on disassociation ec2_eip: state: absent @@ -537,28 +567,26 @@ - associate_eip is defined - associate_eip is not changed - eip_info.addresses | length == 0 + - ec2_eip_info: null register: eip_info - assert: that: - ( eip_info_start.addresses | length ) == ( eip_info.addresses | length ) + - name: Cleanup ENI B ec2_eni: state: absent eni_id: '{{ eni_create_b.interface.id }}' + - name: Cleanup ENI A ec2_eni: state: absent eni_id: '{{ eni_create_a.interface.id }}' - - name: Make sure the instance is ready - ec2_instance_info: - filters: - "tag:Name": '{{ resource_prefix }}-instance' - register: instance_info - until: instance_info.instances[0].state.name == 'running' + - name: Attach eip to an EC2 instance ec2_eip: - device_id: '{{ instance_info.instances[0].instance_id }}' + device_id: '{{ create_ec2_instance_result.instance_ids[0] }}' state: present release_on_disassociation: yes register: instance_eip @@ -570,11 +598,12 @@ that: - instance_eip is success - eip_info.addresses[0].allocation_id is defined - - eip_info.addresses[0].instance_id == '{{ instance_info.instances[0].instance_id }}' + - eip_info.addresses[0].instance_id == '{{ create_ec2_instance_result.instance_ids[0] }}' + - name: Attach eip to an EC2 instance with private Ip specified ec2_eip: - device_id: '{{ instance_info.instances[0].instance_id }}' - private_ip_address: '{{ instance_info.instances[0].private_ip_address }}' + device_id: '{{ create_ec2_instance_result.instance_ids[0] }}' + private_ip_address: '{{ create_ec2_instance_result.instances[0].private_ip_address }}' state: present release_on_disassociation: yes register: instance_eip @@ -586,12 +615,15 @@ that: - instance_eip is success - eip_info.addresses[0].allocation_id is defined - - eip_info.addresses[0].instance_id == '{{ instance_info.instances[0].instance_id }}' + - eip_info.addresses[0].instance_id == '{{ create_ec2_instance_result.instance_ids[0] }}' + # ===================================================== + - name: Cleanup instance ec2_instance: instance_ids: '{{ create_ec2_instance_result.instance_ids }}' state: absent + - name: Cleanup instance eip ec2_eip: state: absent @@ -600,26 +632,31 @@ retries: 5 delay: 5 until: eip_cleanup is successful + - name: Cleanup IGW ec2_vpc_igw: state: absent vpc_id: '{{ vpc_result.vpc.id }}' register: vpc_igw + - name: Cleanup security group ec2_group: state: absent name: '{{ resource_prefix }}-sg' + - name: Cleanup Subnet ec2_vpc_subnet: state: absent cidr: '{{ subnet_cidr }}' vpc_id: '{{ vpc_result.vpc.id }}' + - name: Release eip ec2_eip: state: absent public_ip: '{{ eip.public_ip }}' register: eip_release ignore_errors: true + - name: allocate a new eip ec2_eip: state: present @@ -633,6 +670,129 @@ - eip.public_ip is defined and ( eip.public_ip | ansible.netcommon.ipaddr ) - eip.allocation_id is defined and eip.allocation_id.startswith("eipalloc-") - ( eip_info_start.addresses | length ) + 1 == ( eip_info.addresses | length ) + + ############################################################################################# + + - name: Tag EIP + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + AnsibleEIPTestPrefix: '{{ resource_prefix }}' + another_tag: 'another Value {{ resource_prefix }}' + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is changed + - '"AnsibleEIPTestPrefix" in eip_info.addresses[0].tags' + - '"another_tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['AnsibleEIPTestPrefix'] == resource_prefix + - eip_info.addresses[0].tags['another_tag'] == 'another Value ' + resource_prefix + + - name: Tag EIP + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + AnsibleEIPTestPrefix: '{{ resource_prefix }}' + another_tag: 'another Value {{ resource_prefix }}' + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is not changed + - '"AnsibleEIPTestPrefix" in eip_info.addresses[0].tags' + - '"another_tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['AnsibleEIPTestPrefix'] == resource_prefix + - eip_info.addresses[0].tags['another_tag'] == 'another Value ' + resource_prefix + + - name: Add another Tag + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + "third tag": 'Third tag - {{ resource_prefix }}' + purge_tags: False + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is changed + - '"AnsibleEIPTestPrefix" in eip_info.addresses[0].tags' + - '"another_tag" in eip_info.addresses[0].tags' + - '"third tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['AnsibleEIPTestPrefix'] == resource_prefix + - eip_info.addresses[0].tags['another_tag'] == 'another Value ' + resource_prefix + - eip_info.addresses[0].tags['third tag'] == 'Third tag - ' + resource_prefix + + - name: Add another Tag + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + "third tag": 'Third tag - {{ resource_prefix }}' + purge_tags: False + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is not changed + - '"AnsibleEIPTestPrefix" in eip_info.addresses[0].tags' + - '"another_tag" in eip_info.addresses[0].tags' + - '"third tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['AnsibleEIPTestPrefix'] == resource_prefix + - eip_info.addresses[0].tags['another_tag'] == 'another Value ' + resource_prefix + - eip_info.addresses[0].tags['third tag'] == 'Third tag - ' + resource_prefix + + - name: Purge most tags + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + "third tag": 'Third tag - {{ resource_prefix }}' + purge_tags: True + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is changed + - '"AnsibleEIPTestPrefix" not in eip_info.addresses[0].tags' + - '"another_tag" not in eip_info.addresses[0].tags' + - '"third tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['third tag'] == 'Third tag - ' + resource_prefix + + - name: Purge most tags + ec2_eip: + state: present + public_ip: '{{ eip.public_ip }}' + tags: + "third tag": 'Third tag - {{ resource_prefix }}' + purge_tags: True + register: tag_eip + - ec2_eip_info: null + register: eip_info + - assert: + that: + - tag_eip is defined + - tag_eip is not changed + - '"AnsibleEIPTestPrefix" not in eip_info.addresses[0].tags' + - '"another_tag" not in eip_info.addresses[0].tags' + - '"third tag" in eip_info.addresses[0].tags' + - eip_info.addresses[0].tags['third tag'] == 'Third tag - ' + resource_prefix + + ############################################################################################# + - name: Release eip ec2_eip: state: absent From 22a63708068d0d603ebc228f2e6673ac878e82bd Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Mon, 18 Oct 2021 08:30:22 +0200 Subject: [PATCH 28/30] Bulk update AWSRetry.backoff to AWSRetry.jittered_backoff (#764) Bulk update AWSRetry.backoff to AWSRetry.jittered_backoff SUMMARY CloudRetry.backoff has been deprecated in favour of CloudRetry{exponential,jittered}_backoff bulk update AWSRetry.backoff usage. ISSUE TYPE Feature Pull Request COMPONENT NAME plugins/modules/aws_config_delivery_channel.py plugins/modules/aws_direct_connect_confirm_connection.py plugins/modules/aws_direct_connect_connection.py plugins/modules/aws_direct_connect_link_aggregation_group.py plugins/modules/aws_direct_connect_virtual_interface.py plugins/modules/aws_inspector_target.py plugins/modules/aws_kms.py plugins/modules/aws_kms_info.py plugins/modules/cloudformation_stack_set.py plugins/modules/dms_endpoint.py plugins/modules/dms_replication_subnet_group.py plugins/modules/ec2_asg.py plugins/modules/ec2_elb_info.py plugins/modules/ecs_service_info.py plugins/modules/iam_managed_policy.py plugins/modules/iam_saml_federation.py plugins/modules/rds.py ADDITIONAL INFORMATION Reviewed-by: None Reviewed-by: None --- changelogs/fragments/764-awsretry-backoff.yml | 18 +++++++++ .../modules/aws_config_delivery_channel.py | 2 +- .../aws_direct_connect_confirm_connection.py | 4 +- .../modules/aws_direct_connect_connection.py | 10 ++--- ...s_direct_connect_link_aggregation_group.py | 2 +- .../aws_direct_connect_virtual_interface.py | 2 +- plugins/modules/aws_inspector_target.py | 2 +- plugins/modules/aws_kms.py | 16 ++++---- plugins/modules/aws_kms_info.py | 16 ++++---- plugins/modules/cloudformation_stack_set.py | 2 +- plugins/modules/dms_endpoint.py | 12 +++--- .../modules/dms_replication_subnet_group.py | 10 ++--- plugins/modules/ec2_asg.py | 38 +++++++++---------- plugins/modules/ec2_elb_info.py | 6 +-- plugins/modules/ecs_service_info.py | 4 +- plugins/modules/iam_managed_policy.py | 2 +- plugins/modules/iam_saml_federation.py | 10 ++--- plugins/modules/rds.py | 4 +- 18 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 changelogs/fragments/764-awsretry-backoff.yml diff --git a/changelogs/fragments/764-awsretry-backoff.yml b/changelogs/fragments/764-awsretry-backoff.yml new file mode 100644 index 00000000000..af366e6b5ab --- /dev/null +++ b/changelogs/fragments/764-awsretry-backoff.yml @@ -0,0 +1,18 @@ +minor_changes: +- aws_config_delivery_channel - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_direct_connect_confirm_connection - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_direct_connect_connection - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_direct_connect_link_aggregation_group - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_direct_connect_virtual_interface - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_inspector_target - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_kms - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- aws_kms_info - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- cloudformation_stack_set - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- dms_endpoint - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- dms_replication_subnet_group - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- ec2_asg - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- ec2_elb_info - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- ecs_service_info - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- iam_managed_policy - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- iam_saml_federation - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). +- rds - replaced use of deprecated backoff decorator (https://github.com/ansible-collections/community.aws/pull/764). diff --git a/plugins/modules/aws_config_delivery_channel.py b/plugins/modules/aws_config_delivery_channel.py index e6e9d40e62c..fb3851a4ecc 100644 --- a/plugins/modules/aws_config_delivery_channel.py +++ b/plugins/modules/aws_config_delivery_channel.py @@ -79,7 +79,7 @@ # this waits for an IAM role to become fully available, at the cost of # taking a long time to fail when the IAM role/policy really is invalid -retry_unavailable_iam_on_put_delivery = AWSRetry.backoff( +retry_unavailable_iam_on_put_delivery = AWSRetry.jittered_backoff( catch_extra_error_codes=['InsufficientDeliveryPolicyException'], ) diff --git a/plugins/modules/aws_direct_connect_confirm_connection.py b/plugins/modules/aws_direct_connect_confirm_connection.py index 7ea8527db72..b583def09d9 100644 --- a/plugins/modules/aws_direct_connect_confirm_connection.py +++ b/plugins/modules/aws_direct_connect_confirm_connection.py @@ -69,10 +69,10 @@ from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import DirectConnectError from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry -retry_params = {"tries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} +retry_params = {"retries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} -@AWSRetry.backoff(**retry_params) +@AWSRetry.jittered_backoff(**retry_params) def describe_connections(client, params): return client.describe_connections(**params) diff --git a/plugins/modules/aws_direct_connect_connection.py b/plugins/modules/aws_direct_connect_connection.py index 98afd701f3d..3764b1c7802 100644 --- a/plugins/modules/aws_direct_connect_connection.py +++ b/plugins/modules/aws_direct_connect_connection.py @@ -167,7 +167,7 @@ from ansible_collections.amazon.aws.plugins.module_utils.direct_connect import disassociate_connection_and_lag from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry -retry_params = {"tries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} +retry_params = {"retries": 10, "delay": 5, "backoff": 1.2, "catch_extra_error_codes": ["DirectConnectClientException"]} def connection_status(client, connection_id): @@ -179,7 +179,7 @@ def connection_exists(client, connection_id=None, connection_name=None, verify=T if connection_id: params['connectionId'] = connection_id try: - response = AWSRetry.backoff(**retry_params)(client.describe_connections)(**params) + response = AWSRetry.jittered_backoff(**retry_params)(client.describe_connections)(**params) except (BotoCoreError, ClientError) as e: if connection_id: msg = "Failed to describe DirectConnect ID {0}".format(connection_id) @@ -227,7 +227,7 @@ def create_connection(client, location, bandwidth, name, lag_id): params['lagId'] = lag_id try: - connection = AWSRetry.backoff(**retry_params)(client.create_connection)(**params) + connection = AWSRetry.jittered_backoff(**retry_params)(client.create_connection)(**params) except (BotoCoreError, ClientError) as e: raise DirectConnectError(msg="Failed to create DirectConnect connection {0}".format(name), last_traceback=traceback.format_exc(), @@ -242,7 +242,7 @@ def changed_properties(current_status, location, bandwidth): return current_bandwidth != bandwidth or current_location != location -@AWSRetry.backoff(**retry_params) +@AWSRetry.jittered_backoff(**retry_params) def update_associations(client, latest_state, connection_id, lag_id): changed = False if 'lagId' in latest_state and lag_id != latest_state['lagId']: @@ -277,7 +277,7 @@ def ensure_present(client, connection_id, connection_name, location, bandwidth, return False, connection_id -@AWSRetry.backoff(**retry_params) +@AWSRetry.jittered_backoff(**retry_params) def ensure_absent(client, connection_id): changed = False if connection_id: diff --git a/plugins/modules/aws_direct_connect_link_aggregation_group.py b/plugins/modules/aws_direct_connect_link_aggregation_group.py index 7b287bd61f3..0567ba90288 100644 --- a/plugins/modules/aws_direct_connect_link_aggregation_group.py +++ b/plugins/modules/aws_direct_connect_link_aggregation_group.py @@ -265,7 +265,7 @@ def delete_lag(client, lag_id): exception=e) -@AWSRetry.backoff(tries=5, delay=2, backoff=2.0, catch_extra_error_codes=['DirectConnectClientException']) +@AWSRetry.jittered_backoff(retries=5, delay=2, backoff=2.0, catch_extra_error_codes=['DirectConnectClientException']) def _update_lag(client, lag_id, lag_name, min_links): params = {} if min_links: diff --git a/plugins/modules/aws_direct_connect_virtual_interface.py b/plugins/modules/aws_direct_connect_virtual_interface.py index d520f0ee84f..d2d199c5527 100644 --- a/plugins/modules/aws_direct_connect_virtual_interface.py +++ b/plugins/modules/aws_direct_connect_virtual_interface.py @@ -267,7 +267,7 @@ def try_except_ClientError(failure_msg): def wrapper(f): def run_func(*args, **kwargs): try: - result = AWSRetry.backoff(tries=8, delay=5, catch_extra_error_codes=['DirectConnectClientException'])(f)(*args, **kwargs) + result = AWSRetry.jittered_backoff(retries=8, delay=5, catch_extra_error_codes=['DirectConnectClientException'])(f)(*args, **kwargs) except (ClientError, BotoCoreError) as e: raise DirectConnectError(failure_msg, traceback.format_exc(), e) return result diff --git a/plugins/modules/aws_inspector_target.py b/plugins/modules/aws_inspector_target.py index ceb4abd63dd..a84e245d152 100644 --- a/plugins/modules/aws_inspector_target.py +++ b/plugins/modules/aws_inspector_target.py @@ -110,7 +110,7 @@ pass # caught by AnsibleAWSModule -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def main(): argument_spec = dict( name=dict(required=True), diff --git a/plugins/modules/aws_kms.py b/plugins/modules/aws_kms.py index 05a520ac94a..13bbd7f4619 100644 --- a/plugins/modules/aws_kms.py +++ b/plugins/modules/aws_kms.py @@ -434,19 +434,19 @@ from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_iam_roles_with_backoff(connection): paginator = connection.get_paginator('list_roles') return paginator.paginate().build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_keys_with_backoff(connection): paginator = connection.get_paginator('list_keys') return paginator.paginate().build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_aliases_with_backoff(connection): paginator = connection.get_paginator('list_aliases') return paginator.paginate().build_full_result() @@ -465,30 +465,30 @@ def get_kms_aliases_lookup(connection): return _aliases -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_tags_with_backoff(connection, key_id, **kwargs): return connection.list_resource_tags(KeyId=key_id, **kwargs) -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_grants_with_backoff(connection, key_id): params = dict(KeyId=key_id) paginator = connection.get_paginator('list_grants') return paginator.paginate(**params).build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_metadata_with_backoff(connection, key_id): return connection.describe_key(KeyId=key_id) -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def list_key_policies_with_backoff(connection, key_id): paginator = connection.get_paginator('list_key_policies') return paginator.paginate(KeyId=key_id).build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_key_policy_with_backoff(connection, key_id, policy_name): return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name) diff --git a/plugins/modules/aws_kms_info.py b/plugins/modules/aws_kms_info.py index 3e606481e15..a7620dad005 100644 --- a/plugins/modules/aws_kms_info.py +++ b/plugins/modules/aws_kms_info.py @@ -261,13 +261,13 @@ _aliases = dict() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_keys_with_backoff(connection): paginator = connection.get_paginator('list_keys') return paginator.paginate().build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_aliases_with_backoff(connection): paginator = connection.get_paginator('list_aliases') return paginator.paginate().build_full_result() @@ -286,12 +286,12 @@ def get_kms_aliases_lookup(connection): return _aliases -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_tags_with_backoff(connection, key_id, **kwargs): return connection.list_resource_tags(KeyId=key_id, **kwargs) -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_grants_with_backoff(connection, key_id, **kwargs): params = dict(KeyId=key_id) if kwargs.get('tokens'): @@ -300,23 +300,23 @@ def get_kms_grants_with_backoff(connection, key_id, **kwargs): return paginator.paginate(**params).build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_kms_metadata_with_backoff(connection, key_id): return connection.describe_key(KeyId=key_id) -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def list_key_policies_with_backoff(connection, key_id): paginator = connection.get_paginator('list_key_policies') return paginator.paginate(KeyId=key_id).build_full_result() -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_key_policy_with_backoff(connection, key_id, policy_name): return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name) -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def get_enable_key_rotation_with_backoff(connection, key_id): try: current_rotation_status = connection.get_key_rotation_status(KeyId=key_id) diff --git a/plugins/modules/cloudformation_stack_set.py b/plugins/modules/cloudformation_stack_set.py index b10addf7485..750dceb2bf7 100644 --- a/plugins/modules/cloudformation_stack_set.py +++ b/plugins/modules/cloudformation_stack_set.py @@ -361,7 +361,7 @@ def compare_stack_instances(cfn, stack_set_name, accounts, regions): return (desired_stack_instances - existing_stack_instances), existing_stack_instances, (existing_stack_instances - desired_stack_instances) -@AWSRetry.backoff(tries=3, delay=4) +@AWSRetry.jittered_backoff(retries=3, delay=4) def stack_set_facts(cfn, stack_set_name): try: ss = cfn.describe_stack_set(StackSetName=stack_set_name)['StackSet'] diff --git a/plugins/modules/dms_endpoint.py b/plugins/modules/dms_endpoint.py index f4ab520903a..6cc3bc3f896 100644 --- a/plugins/modules/dms_endpoint.py +++ b/plugins/modules/dms_endpoint.py @@ -175,10 +175,10 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry -backoff_params = dict(tries=5, delay=1, backoff=1.5) +backoff_params = dict(retries=5, delay=1, backoff=1.5) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_endpoints(connection, endpoint_identifier): """ checks if the endpoint exists """ try: @@ -189,7 +189,7 @@ def describe_endpoints(connection, endpoint_identifier): return {'Endpoints': []} -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def dms_delete_endpoint(client, **params): """deletes the DMS endpoint based on the EndpointArn""" if module.params.get('wait'): @@ -198,19 +198,19 @@ def dms_delete_endpoint(client, **params): return client.delete_endpoint(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def dms_create_endpoint(client, **params): """ creates the DMS endpoint""" return client.create_endpoint(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def dms_modify_endpoint(client, **params): """ updates the endpoint""" return client.modify_endpoint(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def get_endpoint_deleted_waiter(client): return client.get_waiter('endpoint_deleted') diff --git a/plugins/modules/dms_replication_subnet_group.py b/plugins/modules/dms_replication_subnet_group.py index 305b6b5a85d..917f27438ff 100644 --- a/plugins/modules/dms_replication_subnet_group.py +++ b/plugins/modules/dms_replication_subnet_group.py @@ -66,10 +66,10 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry -backoff_params = dict(tries=5, delay=1, backoff=1.5) +backoff_params = dict(retries=5, delay=1, backoff=1.5) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_subnet_group(connection, subnet_group): """checks if instance exists""" try: @@ -80,18 +80,18 @@ def describe_subnet_group(connection, subnet_group): return {'ReplicationSubnetGroups': []} -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def replication_subnet_group_create(connection, **params): """ creates the replication subnet group """ return connection.create_replication_subnet_group(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def replication_subnet_group_modify(connection, **modify_params): return connection.modify_replication_subnet_group(**modify_params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def replication_subnet_group_delete(module, connection): subnetid = module.params.get('identifier') delete_parameters = dict(ReplicationSubnetGroupIdentifier=subnetid) diff --git a/plugins/modules/ec2_asg.py b/plugins/modules/ec2_asg.py index 662c23873b1..46cdcbf15b8 100644 --- a/plugins/modules/ec2_asg.py +++ b/plugins/modules/ec2_asg.py @@ -639,21 +639,21 @@ INSTANCE_ATTRIBUTES = ('instance_id', 'health_status', 'lifecycle_state', 'launch_config_name') -backoff_params = dict(tries=10, delay=3, backoff=1.5) +backoff_params = dict(retries=10, delay=3, backoff=1.5) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_autoscaling_groups(connection, group_name): pg = connection.get_paginator('describe_auto_scaling_groups') return pg.paginate(AutoScalingGroupNames=[group_name]).build_full_result().get('AutoScalingGroups', []) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def deregister_lb_instances(connection, lb_name, instance_id): connection.deregister_instances_from_load_balancer(LoadBalancerName=lb_name, Instances=[dict(InstanceId=instance_id)]) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_instance_health(connection, lb_name, instances): params = dict(LoadBalancerName=lb_name) if instances: @@ -661,28 +661,28 @@ def describe_instance_health(connection, lb_name, instances): return connection.describe_instance_health(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_target_health(connection, target_group_arn, instances): return connection.describe_target_health(TargetGroupArn=target_group_arn, Targets=instances) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def suspend_asg_processes(connection, asg_name, processes): connection.suspend_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def resume_asg_processes(connection, asg_name, processes): connection.resume_processes(AutoScalingGroupName=asg_name, ScalingProcesses=processes) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_launch_configurations(connection, launch_config_name): pg = connection.get_paginator('describe_launch_configurations') return pg.paginate(LaunchConfigurationNames=[launch_config_name]).build_full_result() -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def describe_launch_templates(connection, launch_template): if launch_template['launch_template_id'] is not None: try: @@ -698,12 +698,12 @@ def describe_launch_templates(connection, launch_template): module.fail_json(msg="No launch template found matching: %s" % launch_template) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def create_asg(connection, **params): connection.create_auto_scaling_group(**params) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def put_notification_config(connection, asg_name, topic_arn, notification_types): connection.put_notification_configuration( AutoScalingGroupName=asg_name, @@ -712,7 +712,7 @@ def put_notification_config(connection, asg_name, topic_arn, notification_types) ) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def del_notification_config(connection, asg_name, topic_arn): connection.delete_notification_configuration( AutoScalingGroupName=asg_name, @@ -720,37 +720,37 @@ def del_notification_config(connection, asg_name, topic_arn): ) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def attach_load_balancers(connection, asg_name, load_balancers): connection.attach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def detach_load_balancers(connection, asg_name, load_balancers): connection.detach_load_balancers(AutoScalingGroupName=asg_name, LoadBalancerNames=load_balancers) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def attach_lb_target_groups(connection, asg_name, target_group_arns): connection.attach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def detach_lb_target_groups(connection, asg_name, target_group_arns): connection.detach_load_balancer_target_groups(AutoScalingGroupName=asg_name, TargetGroupARNs=target_group_arns) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def update_asg(connection, **params): connection.update_auto_scaling_group(**params) -@AWSRetry.backoff(catch_extra_error_codes=['ScalingActivityInProgress'], **backoff_params) +@AWSRetry.jittered_backoff(catch_extra_error_codes=['ScalingActivityInProgress'], **backoff_params) def delete_asg(connection, asg_name, force_delete): connection.delete_auto_scaling_group(AutoScalingGroupName=asg_name, ForceDelete=force_delete) -@AWSRetry.backoff(**backoff_params) +@AWSRetry.jittered_backoff(**backoff_params) def terminate_asg_instance(connection, instance_id, decrement_capacity): connection.terminate_instance_in_auto_scaling_group(InstanceId=instance_id, ShouldDecrementDesiredCapacity=decrement_capacity) diff --git a/plugins/modules/ec2_elb_info.py b/plugins/modules/ec2_elb_info.py index add102ab87a..8b207111b60 100644 --- a/plugins/modules/ec2_elb_info.py +++ b/plugins/modules/ec2_elb_info.py @@ -109,7 +109,7 @@ def _get_tags(self, elbname): elb_tags = self.connection.get_list('DescribeTags', params, [('member', Tag)]) return dict((tag.Key, tag.Value) for tag in elb_tags if hasattr(tag, 'Key')) - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) + @AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def _get_elb_connection(self): return connect_to_aws(boto.ec2.elb, self.region, **self.aws_connect_params) @@ -158,7 +158,7 @@ def _get_health_check(self, health_check): health_check_dict['ping_path'] = path return health_check_dict - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) + @AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def _get_elb_info(self, elb): elb_info = { 'name': elb.name, @@ -202,7 +202,7 @@ def _get_elb_info(self, elb): def list_elbs(self): elb_array, token = [], None - get_elb_with_backoff = AWSRetry.backoff(tries=5, delay=5, backoff=2.0)(self.connection.get_all_load_balancers) + get_elb_with_backoff = AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0)(self.connection.get_all_load_balancers) while True: all_elbs = get_elb_with_backoff(marker=token) token = all_elbs.next_marker diff --git a/plugins/modules/ecs_service_info.py b/plugins/modules/ecs_service_info.py index 9b47b02a714..79332e55702 100644 --- a/plugins/modules/ecs_service_info.py +++ b/plugins/modules/ecs_service_info.py @@ -148,7 +148,7 @@ def __init__(self, module): self.module = module self.ecs = module.client('ecs') - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) + @AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def list_services_with_backoff(self, **kwargs): paginator = self.ecs.get_paginator('list_services') try: @@ -156,7 +156,7 @@ def list_services_with_backoff(self, **kwargs): except is_boto3_error_code('ClusterNotFoundException') as e: self.module.fail_json_aws(e, "Could not find cluster to list services") - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) + @AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def describe_services_with_backoff(self, **kwargs): return self.ecs.describe_services(**kwargs) diff --git a/plugins/modules/iam_managed_policy.py b/plugins/modules/iam_managed_policy.py index a56e76d037f..d6cdd33525e 100644 --- a/plugins/modules/iam_managed_policy.py +++ b/plugins/modules/iam_managed_policy.py @@ -141,7 +141,7 @@ from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) +@AWSRetry.jittered_backoff(retries=5, delay=5, backoff=2.0) def list_policies_with_backoff(iam): paginator = iam.get_paginator('list_policies') return paginator.paginate(Scope='Local').build_full_result() diff --git a/plugins/modules/iam_saml_federation.py b/plugins/modules/iam_saml_federation.py index a78decfe625..4b41f443134 100644 --- a/plugins/modules/iam_saml_federation.py +++ b/plugins/modules/iam_saml_federation.py @@ -123,23 +123,23 @@ def __init__(self, module): self.module.fail_json_aws(e, msg="Unknown boto error") # use retry decorator for boto3 calls - @AWSRetry.backoff(tries=3, delay=5) + @AWSRetry.jittered_backoff(retries=3, delay=5) def _list_saml_providers(self): return self.conn.list_saml_providers() - @AWSRetry.backoff(tries=3, delay=5) + @AWSRetry.jittered_backoff(retries=3, delay=5) def _get_saml_provider(self, arn): return self.conn.get_saml_provider(SAMLProviderArn=arn) - @AWSRetry.backoff(tries=3, delay=5) + @AWSRetry.jittered_backoff(retries=3, delay=5) def _update_saml_provider(self, arn, metadata): return self.conn.update_saml_provider(SAMLProviderArn=arn, SAMLMetadataDocument=metadata) - @AWSRetry.backoff(tries=3, delay=5) + @AWSRetry.jittered_backoff(retries=3, delay=5) def _create_saml_provider(self, metadata, name): return self.conn.create_saml_provider(SAMLMetadataDocument=metadata, Name=name) - @AWSRetry.backoff(tries=3, delay=5) + @AWSRetry.jittered_backoff(retries=3, delay=5) def _delete_saml_provider(self, arn): return self.conn.delete_saml_provider(SAMLProviderArn=arn) diff --git a/plugins/modules/rds.py b/plugins/modules/rds.py index a59b183925b..bfbf0019f6b 100644 --- a/plugins/modules/rds.py +++ b/plugins/modules/rds.py @@ -943,13 +943,13 @@ def await_resource(conn, resource, status, module): if resource.name is None: module.fail_json(msg="There was a problem waiting for RDS snapshot %s" % resource.snapshot) # Back off if we're getting throttled, since we're just waiting anyway - resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_snapshot)(resource.name) + resource = AWSRetry.jittered_backoff(retries=5, delay=20, backoff=1.5)(conn.get_db_snapshot)(resource.name) else: # Temporary until all the rds2 commands have their responses parsed if resource.name is None: module.fail_json(msg="There was a problem waiting for RDS instance %s" % resource.instance) # Back off if we're getting throttled, since we're just waiting anyway - resource = AWSRetry.backoff(tries=5, delay=20, backoff=1.5)(conn.get_db_instance)(resource.name) + resource = AWSRetry.jittered_backoff(retries=5, delay=20, backoff=1.5)(conn.get_db_instance)(resource.name) if resource is None: break # Some RDS resources take much longer than others to be ready. Check From 324801611f014a0b8fd9ed4d0b3154eb3b56c41c Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Mon, 18 Oct 2021 11:24:09 +0200 Subject: [PATCH 29/30] route53_health_check - add tagging support (#765) route53_health_check - add tagging support SUMMARY add tagging support to route53_health_check ISSUE TYPE Feature Pull Request COMPONENT NAME route53_health_check ADDITIONAL INFORMATION Reviewed-by: Alina Buzachis Reviewed-by: None --- .../765-route53_health_check-tags.yml | 2 + plugins/module_utils/route53.py | 3 + plugins/modules/route53_health_check.py | 28 +- .../route53_health_check/tasks/main.yml | 504 +++++++++++++++++- 4 files changed, 520 insertions(+), 17 deletions(-) create mode 100644 changelogs/fragments/765-route53_health_check-tags.yml diff --git a/changelogs/fragments/765-route53_health_check-tags.yml b/changelogs/fragments/765-route53_health_check-tags.yml new file mode 100644 index 00000000000..5f233ceb3cb --- /dev/null +++ b/changelogs/fragments/765-route53_health_check-tags.yml @@ -0,0 +1,2 @@ +minor_changes: +- route53_health_check - add support for tagging health checks (https://github.com/ansible-collections/community.aws/pull/765). diff --git a/plugins/module_utils/route53.py b/plugins/module_utils/route53.py index 3d21d8b0802..3e2940a5311 100644 --- a/plugins/module_utils/route53.py +++ b/plugins/module_utils/route53.py @@ -16,6 +16,9 @@ def manage_tags(module, client, resource_type, resource_id, new_tags, purge_tags): + if new_tags is None: + return False + old_tags = get_tags(module, client, resource_type, resource_id) tags_to_set, tags_to_delete = compare_aws_tags(old_tags, new_tags, purge_tags=purge_tags) diff --git a/plugins/modules/route53_health_check.py b/plugins/modules/route53_health_check.py index af482132e56..382be93ab6d 100644 --- a/plugins/modules/route53_health_check.py +++ b/plugins/modules/route53_health_check.py @@ -86,6 +86,17 @@ - Will default to C(3) if not specified on creation. choices: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] type: int + tags: + description: + - A hash/dictionary of tags to set on the health check. + type: dict + version_added: 2.1.0 + purge_tags: + description: + - Delete any tags not specified in I(tags). + default: false + type: bool + version_added: 2.1.0 author: "zimbatm (@zimbatm)" extends_documentation_fragment: - amazon.aws.aws @@ -198,6 +209,11 @@ type: bool returned: When the health check exists. sample: false + tags: + description: A dictionary representing the tags on the health check. + type: dict + returned: When the health check exists. + sample: '{"my_key": "my_value"}' ''' import uuid @@ -212,6 +228,8 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.community.aws.plugins.module_utils.route53 import get_tags +from ansible_collections.community.aws.plugins.module_utils.route53 import manage_tags def _list_health_checks(**params): @@ -332,7 +350,8 @@ def create_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_ except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: module.fail_json_aws(e, msg='Failed to create health check.', health_check=health_check) - return True, 'create', result.get('HealthCheck').get('Id') + check_id = result.get('HealthCheck').get('Id') + return True, 'create', check_id def update_health_check(existing_check): @@ -396,6 +415,8 @@ def describe_health_check(id): health_check = result.get('HealthCheck', {}) health_check = camel_dict_to_snake_dict(health_check) + tags = get_tags(module, client, 'healthcheck', id) + health_check['tags'] = tags return health_check @@ -411,6 +432,8 @@ def main(): string_match=dict(), request_interval=dict(type='int', choices=[10, 30], default=30), failure_threshold=dict(type='int', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + tags=dict(type='dict'), + purge_tags=dict(type='bool', default=False), ) args_one_of = [ @@ -473,6 +496,9 @@ def main(): changed, action, check_id = create_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_in) else: changed, action = update_health_check(existing_check) + if check_id: + changed |= manage_tags(module, client, 'healthcheck', check_id, + module.params.get('tags'), module.params.get('purge_tags')) health_check = describe_health_check(id=check_id) health_check['action'] = action diff --git a/tests/integration/targets/route53_health_check/tasks/main.yml b/tests/integration/targets/route53_health_check/tasks/main.yml index 8164b65e5bf..426a0461703 100644 --- a/tests/integration/targets/route53_health_check/tasks/main.yml +++ b/tests/integration/targets/route53_health_check/tasks/main.yml @@ -65,6 +65,7 @@ - '"id" in _health_check' - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action == 'create' - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -123,6 +124,7 @@ - '"id" in _health_check' - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -180,6 +182,7 @@ - _health_check.id == tcp_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -236,6 +239,7 @@ - _health_check.id == tcp_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -264,14 +268,14 @@ port: '{{ port }}' type: '{{ type }}' disabled: true - register: update_threshold + register: update_disabled check_mode: true - name: 'Check result - Update TCP health check - set disabled - check_mode' assert: that: - - update_threshold is successful - - update_threshold is changed + - update_disabled is successful + - update_disabled is changed - name: 'Update TCP health check - set disabled' route53_health_check: @@ -280,18 +284,19 @@ port: '{{ port }}' type: '{{ type }}' disabled: true - register: update_threshold + register: update_disabled - name: 'Check result - Update TCP health check - set disabled' assert: that: - - update_threshold is successful - - update_threshold is changed - - '"health_check" in update_threshold' + - update_disabled is successful + - update_disabled is changed + - '"health_check" in update_disabled' - '"id" in _health_check' - _health_check.id == tcp_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -310,7 +315,7 @@ - _check_config.ip_address == ip_address - _check_config.port == port vars: - _health_check: '{{ update_threshold.health_check }}' + _health_check: '{{ update_disabled.health_check }}' _check_config: '{{ _health_check.health_check_config }}' - name: 'Update TCP health check - set disabled - idempotency - check_mode' @@ -320,14 +325,14 @@ port: '{{ port }}' type: '{{ type }}' disabled: true - register: update_threshold + register: update_disabled check_mode: true - name: 'Check result - Update TCP health check - set disabled - idempotency - check_mode' assert: that: - - update_threshold is successful - - update_threshold is not changed + - update_disabled is successful + - update_disabled is not changed - name: 'Update TCP health check - set disabled - idempotency' route53_health_check: @@ -336,18 +341,19 @@ port: '{{ port }}' type: '{{ type }}' disabled: true - register: update_threshold + register: update_disabled - name: 'Check result - Update TCP health check - set disabled - idempotency' assert: that: - - update_threshold is successful - - update_threshold is not changed - - '"health_check" in update_threshold' + - update_disabled is successful + - update_disabled is not changed + - '"health_check" in update_disabled' - '"id" in _health_check' - _health_check.id == tcp_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -366,7 +372,419 @@ - _check_config.ip_address == ip_address - _check_config.port == port vars: - _health_check: '{{ update_threshold.health_check }}' + _health_check: '{{ update_disabled.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - set tags - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false + register: update_tags + check_mode: true + + - name: 'Check result - Update TCP health check - set tags - check_mode' + assert: + that: + - update_tags is successful + - update_tags is changed + + - name: 'Update TCP health check - set tags' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false + register: update_tags + + - name: 'Check result - Update TCP health check - set tags' + assert: + that: + - update_tags is successful + - update_tags is changed + - '"health_check" in update_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ update_tags.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - set tags - idempotency - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false + register: update_tags + check_mode: true + + - name: 'Check result - Update TCP health check - set tags - idempotency - check_mode' + assert: + that: + - update_tags is successful + - update_tags is not changed + + - name: 'Update TCP health check - set tags - idempotency' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false + register: update_tags + + - name: 'Check result - Update TCP health check - set tags - idempotency' + assert: + that: + - update_tags is successful + - update_tags is not changed + - '"health_check" in update_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ update_tags.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - add tags - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: false + register: add_tags + check_mode: true + + - name: 'Check result - Update TCP health check - add tags - check_mode' + assert: + that: + - add_tags is successful + - add_tags is changed + + - name: 'Update TCP health check - add tags' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: false + register: add_tags + + - name: 'Check result - Update TCP health check - add tags' + assert: + that: + - add_tags is successful + - add_tags is changed + - '"health_check" in add_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' + - '"anotherTag" in _health_check.tags' + - _health_check.tags['anotherTag'] == 'anotherValue' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ add_tags.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - add tags - idempotency - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: false + register: add_tags + check_mode: true + + - name: 'Check result - Update TCP health check - add tags - idempotency - check_mode' + assert: + that: + - add_tags is successful + - add_tags is not changed + + - name: 'Update TCP health check - add tags - idempotency' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: false + register: add_tags + + - name: 'Check result - Update TCP health check - add tags - idempotency' + assert: + that: + - add_tags is successful + - add_tags is not changed + - '"health_check" in add_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' + - '"anotherTag" in _health_check.tags' + - _health_check.tags['anotherTag'] == 'anotherValue' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ add_tags.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - purge tags - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: true + register: purge_tags + check_mode: true + + - name: 'Check result - Update TCP health check - purge tags - check_mode' + assert: + that: + - purge_tags is successful + - purge_tags is changed + + - name: 'Update TCP health check - purge tags' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: true + register: purge_tags + + - name: 'Check result - Update TCP health check - purge tags' + assert: + that: + - purge_tags is successful + - purge_tags is changed + - '"health_check" in purge_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" not in _health_check.tags' + - '"snake_case" not in _health_check.tags' + - '"with space" not in _health_check.tags' + - '"anotherTag" in _health_check.tags' + - _health_check.tags['anotherTag'] == 'anotherValue' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ purge_tags.health_check }}' + _check_config: '{{ _health_check.health_check_config }}' + + - name: 'Update TCP health check - purge tags - idempotency - check_mode' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: true + register: purge_tags + check_mode: true + + - name: 'Check result - Update TCP health check - purge tags - idempotency - check_mode' + assert: + that: + - purge_tags is successful + - purge_tags is not changed + + - name: 'Update TCP health check - purge tags - idempotency' + route53_health_check: + state: present + ip_address: '{{ ip_address }}' + port: '{{ port }}' + type: '{{ type }}' + tags: + anotherTag: anotherValue + purge_tags: true + register: purge_tags + + - name: 'Check result - Update TCP health check - purge tags - idempotency' + assert: + that: + - purge_tags is successful + - purge_tags is not changed + - '"health_check" in purge_tags' + - '"id" in _health_check' + - _health_check.id == tcp_check_id + - '"action" in _health_check' + - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" not in _health_check.tags' + - '"snake_case" not in _health_check.tags' + - '"with space" not in _health_check.tags' + - '"anotherTag" in _health_check.tags' + - _health_check.tags['anotherTag'] == 'anotherValue' + - create_check.health_check.action is none + - '"health_check_config" in create_check.health_check' + - '"type" in _check_config' + - '"disabled" in _check_config' + - '"failure_threshold" in _check_config' + - '"request_interval" in _check_config' + - '"fully_qualified_domain_name" not in _check_config' + - '"ip_address" in _check_config' + - '"port" in _check_config' + - '"resource_path" not in _check_config' + - '"search_string" not in _check_config' + - _check_config.disabled == true + - _check_config.type == 'TCP' + - _check_config.request_interval == 30 + - _check_config.failure_threshold == failure_threshold_updated + - _check_config.ip_address == ip_address + - _check_config.port == port + vars: + _health_check: '{{ purge_tags.health_check }}' _check_config: '{{ _health_check.health_check_config }}' # Delete the check @@ -468,6 +886,7 @@ - _health_check.id != tcp_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -533,6 +952,7 @@ - _health_check.id == match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -595,6 +1015,7 @@ - _health_check.id == match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -658,6 +1079,7 @@ - _health_check.id == match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -721,6 +1143,7 @@ - _health_check.id == match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -784,6 +1207,7 @@ - _health_check.id == match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -888,6 +1312,11 @@ resource_path: '{{ resource_path }}' failure_threshold: '{{ failure_threshold }}' disabled: true + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false register: create_complex check_mode: true @@ -909,6 +1338,11 @@ resource_path: '{{ resource_path }}' failure_threshold: '{{ failure_threshold }}' disabled: true + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false register: create_complex - name: 'Check result - Create Complex health check' @@ -922,6 +1356,13 @@ - _health_check.id != match_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -961,6 +1402,11 @@ resource_path: '{{ resource_path }}' failure_threshold: '{{ failure_threshold }}' disabled: true + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false register: create_complex check_mode: true @@ -982,6 +1428,11 @@ resource_path: '{{ resource_path }}' failure_threshold: '{{ failure_threshold }}' disabled: true + tags: + CamelCase: CamelCaseValue + snake_case: snake_case_value + "with space": Some value + purge_tags: false register: create_complex - name: 'Check result - Create Complex health check - idempotency' @@ -994,6 +1445,13 @@ - _health_check.id == complex_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -1061,6 +1519,13 @@ - _health_check.id == complex_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' @@ -1128,6 +1593,13 @@ - _health_check.id == complex_check_id - '"action" in _health_check' - '"health_check_version" in _health_check' + - '"tags" in _health_check' + - '"CamelCase" in _health_check.tags' + - _health_check.tags['CamelCase'] == 'CamelCaseValue' + - '"snake_case" in _health_check.tags' + - _health_check.tags['snake_case'] == 'snake_case_value' + - '"with space" in _health_check.tags' + - _health_check.tags['with space'] == 'Some value' - create_check.health_check.action is none - '"health_check_config" in create_check.health_check' - '"type" in _check_config' From 81d9abdad709752a402ed5c181c83748a91e0795 Mon Sep 17 00:00:00 2001 From: Mark Woolley Date: Mon, 18 Oct 2021 11:14:32 +0100 Subject: [PATCH 30/30] Add waiter to the iam_role module (#767) Add waiter to the iam_role module SUMMARY This change adds the wait param used in other AWS modules, adding usage of a waiter for the iam_role creation / updates. Currently there is no waiting done to ensure the iam_role has actually created and is available before exiting. The tests have also been split up into separate files to make it a bit more manageable. Fixes: #710 ISSUE TYPE Feature Pull Request COMPONENT NAME iam_role ADDITIONAL INFORMATION Successful run completed of the iam_role integration test suite locally: ansible-test integration --docker centos8 -v iam_role --allow-unsupported PLAY RECAP ********************************************************************* testhost : ok=198 changed=46 unreachable=0 failed=0 skipped=0 rescued=0 ignored=7 AWS ACTIONS: ['iam:AddRoleToInstanceProfile', 'iam:AttachRolePolicy', 'iam:CreateInstanceProfile', 'iam:CreatePolicy', 'iam:CreateRole', 'iam:DeleteInstanceProfile', 'iam:DeletePolicy', 'iam:DeleteRole', 'iam:DeleteRolePermissionsBoundary', 'iam:DeleteRolePolicy', 'iam:DetachRolePolicy', 'iam:GetRole', 'iam:GetRolePolicy', 'iam:ListAttachedRolePolicies', 'iam:ListEntitiesForPolicy', 'iam:ListInstanceProfilesForRole', 'iam:ListPolicies', 'iam:ListPolicyVersions', 'iam:ListRolePolicies', 'iam:ListRoleTags', 'iam:ListRoles', 'iam:PutRolePermissionsBoundary', 'iam:PutRolePolicy', 'iam:RemoveRoleFromInstanceProfile', 'iam:TagRole', 'iam:UntagRole', 'iam:UpdateRole'] Run command: docker exec 56cb328c6d9af293d9e820e1f2a94fb8ca87e0769b2b9b6d46bad661f9edde65 tar czf /root/output.tgz --exclude .tmp -C /root/ansible/ansible_collections/community/aws/tests output Run command: docker exec -i 56cb328c6d9af293d9e820e1f2a94fb8ca87e0769b2b9b6d46bad661f9edde65 dd if=/root/output.tgz bs=65536 Run command: tar oxzf /tmp/ansible-result-k2lnga3v.tgz -C /mnt/c/Users/mark.woolley/Documents/GitHub/public/ansible_collections/community/aws/tests Run command: docker rm -f 56cb328c6d9af293d9e820e1f2a94fb8ca87e0769b2b9b6d46bad661f9edde65 Reviewed-by: Mark Chappell Reviewed-by: None --- .../767-iam_role-add-waiter-support.yml | 2 + plugins/modules/iam_role.py | 189 +- .../targets/iam_role/defaults/main.yml | 2 - .../iam_role/tasks/boundary_policy.yml | 82 + .../iam_role/tasks/complex_role_creation.yml | 110 ++ .../iam_role/tasks/creation_deletion.yml | 353 ++++ .../iam_role/tasks/description_update.yml | 124 ++ .../iam_role/tasks/inline_policy_update.yml | 63 + .../targets/iam_role/tasks/main.yml | 1559 +---------------- .../iam_role/tasks/max_session_update.yml | 61 + .../iam_role/tasks/parameter_checks.yml | 90 + .../targets/iam_role/tasks/policy_update.yml | 208 +++ .../targets/iam_role/tasks/role_removal.yml | 53 + .../targets/iam_role/tasks/tags_update.yml | 286 +++ 14 files changed, 1639 insertions(+), 1543 deletions(-) create mode 100644 changelogs/767-iam_role-add-waiter-support.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/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/767-iam_role-add-waiter-support.yml b/changelogs/767-iam_role-add-waiter-support.yml new file mode 100644 index 00000000000..d7e5c42153f --- /dev/null +++ b/changelogs/767-iam_role-add-waiter-support.yml @@ -0,0 +1,2 @@ +minor_changes: + - iam_role - Added `wait` option for IAM role creation / updates (https://github.com/ansible-collections/community.aws/pull/767). diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py index 948358b184e..7ca0d8c4fbb 100644 --- a/plugins/modules/iam_role.py +++ b/plugins/modules/iam_role.py @@ -87,6 +87,17 @@ - Remove tags not listed in I(tags) when tags is specified. default: true 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.aws - amazon.aws.ec2 @@ -215,16 +226,40 @@ def compare_assume_role_policy_doc(current_policy_doc, new_policy_doc): @AWSRetry.jittered_backoff() -def _list_policies(connection): - paginator = connection.get_paginator('list_policies') +def _list_policies(): + paginator = client.get_paginator('list_policies') return paginator.paginate().build_full_result()['Policies'] -def convert_friendly_names_to_arns(connection, module, policy_names): +def wait_iam_exists(): + 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(policy_names): if not any(not policy.startswith('arn:') for policy in policy_names): return policy_names allpolicies = {} - policies = _list_policies(connection) + policies = _list_policies() for policy in policies: allpolicies[policy['PolicyName']] = policy['Arn'] @@ -235,31 +270,31 @@ def convert_friendly_names_to_arns(connection, module, policy_names): module.fail_json_aws(e, msg="Couldn't find policy") -def attach_policies(connection, module, policies_to_attach, params): +def attach_policies(policies_to_attach, params): changed = False for policy_arn in policies_to_attach: try: if not module.check_mode: - connection.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn, aws_retry=True) + client.attach_role_policy(RoleName=params['RoleName'], PolicyArn=policy_arn, aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to attach policy {0} to role {1}".format(policy_arn, params['RoleName'])) changed = True return changed -def remove_policies(connection, module, policies_to_remove, params): +def remove_policies(policies_to_remove, params): changed = False for policy in policies_to_remove: try: if not module.check_mode: - connection.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy, aws_retry=True) + client.detach_role_policy(RoleName=params['RoleName'], PolicyArn=policy, aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to detach policy {0} from {1}".format(policy, params['RoleName'])) changed = True return changed -def generate_create_params(module): +def generate_create_params(): params = dict() params['Path'] = module.params.get('path') params['RoleName'] = module.params.get('name') @@ -276,7 +311,7 @@ def generate_create_params(module): return params -def create_basic_role(connection, module, params): +def create_basic_role(params): """ Perform the Role creation. Assumes tests for the role existing have already been performed. @@ -284,11 +319,11 @@ def create_basic_role(connection, module, params): try: if not module.check_mode: - role = connection.create_role(aws_retry=True, **params) + 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(connection, module, params['RoleName']) + role = get_role_with_backoff(params['RoleName']) else: role = {'MadeInCheckMode': True} role['AssumeRolePolicyDocument'] = json.loads(params['AssumeRolePolicyDocument']) @@ -298,7 +333,7 @@ def create_basic_role(connection, module, params): return role -def update_role_assumed_policy(connection, module, params, role): +def update_role_assumed_policy(params, role): # Check Assumed Policy document if compare_assume_role_policy_doc(role['AssumeRolePolicyDocument'], params['AssumeRolePolicyDocument']): return False @@ -307,7 +342,7 @@ def update_role_assumed_policy(connection, module, params, role): return True try: - connection.update_assume_role_policy( + client.update_assume_role_policy( RoleName=params['RoleName'], PolicyDocument=json.dumps(json.loads(params['AssumeRolePolicyDocument'])), aws_retry=True) @@ -316,7 +351,7 @@ def update_role_assumed_policy(connection, module, params, role): return True -def update_role_description(connection, module, params, role): +def update_role_description(params, role): # Check Description update if params.get('Description') is None: return False @@ -327,13 +362,13 @@ def update_role_description(connection, module, params, role): return True try: - connection.update_role(RoleName=params['RoleName'], Description=params['Description'], aws_retry=True) + client.update_role(RoleName=params['RoleName'], Description=params['Description'], aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to update description for role {0}".format(params['RoleName'])) return True -def update_role_max_session_duration(connection, module, params, role): +def update_role_max_session_duration(params, role): # Check MaxSessionDuration update if params.get('MaxSessionDuration') is None: return False @@ -344,13 +379,13 @@ def update_role_max_session_duration(connection, module, params, role): return True try: - connection.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration'], aws_retry=True) + client.update_role(RoleName=params['RoleName'], MaxSessionDuration=params['MaxSessionDuration'], aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to update maximum session duration for role {0}".format(params['RoleName'])) return True -def update_role_permissions_boundary(connection, module, params, role): +def update_role_permissions_boundary(params, role): # Check PermissionsBoundary if params.get('PermissionsBoundary') is None: return False @@ -362,18 +397,18 @@ def update_role_permissions_boundary(connection, module, params, role): if params.get('PermissionsBoundary') == '': try: - connection.delete_role_permissions_boundary(RoleName=params['RoleName'], aws_retry=True) + client.delete_role_permissions_boundary(RoleName=params['RoleName'], aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to remove permission boundary for role {0}".format(params['RoleName'])) else: try: - connection.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary'], aws_retry=True) + client.put_role_permissions_boundary(RoleName=params['RoleName'], PermissionsBoundary=params['PermissionsBoundary'], aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to update permission boundary for role {0}".format(params['RoleName'])) return True -def update_managed_policies(connection, module, params, role, managed_policies, purge_policies): +def update_managed_policies(params, role, managed_policies, purge_policies): # Check Managed Policies if managed_policies is None: return False @@ -384,7 +419,7 @@ def update_managed_policies(connection, module, params, role, managed_policies, return True # Get list of current attached managed policies - current_attached_policies = get_attached_policy_list(connection, module, params['RoleName']) + current_attached_policies = get_attached_policy_list(params['RoleName']) current_attached_policies_arn_list = [policy['PolicyArn'] for policy in current_attached_policies] if len(managed_policies) == 1 and managed_policies[0] is None: @@ -396,16 +431,16 @@ def update_managed_policies(connection, module, params, role, managed_policies, changed = False if purge_policies: - changed |= remove_policies(connection, module, policies_to_remove, params) + changed |= remove_policies(policies_to_remove, params) - changed |= attach_policies(connection, module, policies_to_attach, params) + changed |= attach_policies(policies_to_attach, params) return changed -def create_or_update_role(connection, module): +def create_or_update_role(): - params = generate_create_params(module) + params = generate_create_params() role_name = params['RoleName'] create_instance_profile = module.params.get('create_instance_profile') purge_policies = module.params.get('purge_policies') @@ -414,48 +449,59 @@ def create_or_update_role(connection, module): 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(connection, module, managed_policies) + managed_policies = convert_friendly_names_to_arns(managed_policies) changed = False # Get role - role = get_role(connection, module, role_name) + role = get_role(role_name) # If role is None, create it if role is None: - role = create_basic_role(connection, module, params) + role = create_basic_role(params) + + if not module.check_mode and module.params.get('wait'): + wait_iam_exists() + changed = True else: - changed |= update_role_tags(connection, module, params, role) - changed |= update_role_assumed_policy(connection, module, params, role) - changed |= update_role_description(connection, module, params, role) - changed |= update_role_max_session_duration(connection, module, params, role) - changed |= update_role_permissions_boundary(connection, module, params, role) + changed |= update_role_tags(params, role) + changed |= update_role_assumed_policy(params, role) + changed |= update_role_description(params, role) + changed |= update_role_max_session_duration(params, role) + changed |= update_role_permissions_boundary(params, role) + + if not module.check_mode and module.params.get('wait'): + wait_iam_exists() if create_instance_profile: - changed |= create_instance_profiles(connection, module, params, role) + changed |= create_instance_profiles(params, role) - changed |= update_managed_policies(connection, module, params, role, managed_policies, purge_policies) + if not module.check_mode and module.params.get('wait'): + wait_iam_exists() + + changed |= update_managed_policies(params, role, managed_policies, purge_policies) + wait_iam_exists() # Get the role again if not role.get('MadeInCheckMode', False): - role = get_role(connection, module, params['RoleName']) - role['AttachedPolicies'] = get_attached_policy_list(connection, module, params['RoleName']) - role['tags'] = get_role_tags(connection, module) + role = get_role(params['RoleName']) + role['AttachedPolicies'] = get_attached_policy_list(params['RoleName']) + role['tags'] = get_role_tags() module.exit_json( changed=changed, iam_role=camel_dict_to_snake_dict(role, ignore_list=['tags']), **camel_dict_to_snake_dict(role, ignore_list=['tags'])) -def create_instance_profiles(connection, module, params, role): +def create_instance_profiles(params, role): if role.get('MadeInCheckMode', False): return False # Fetch existing Profiles try: - instance_profiles = connection.list_instance_profiles_for_role(RoleName=params['RoleName'], aws_retry=True)['InstanceProfiles'] + instance_profiles = client.list_instance_profiles_for_role(RoleName=params['RoleName'], aws_retry=True)['InstanceProfiles'] except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(params['RoleName'])) @@ -468,7 +514,7 @@ def create_instance_profiles(connection, module, params, role): # Make sure an instance profile is created try: - connection.create_instance_profile(InstanceProfileName=params['RoleName'], Path=params['Path'], aws_retry=True) + client.create_instance_profile(InstanceProfileName=params['RoleName'], Path=params['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... @@ -478,19 +524,19 @@ def create_instance_profiles(connection, module, params, role): # And attach the role to the profile try: - connection.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName'], aws_retry=True) + client.add_role_to_instance_profile(InstanceProfileName=params['RoleName'], RoleName=params['RoleName'], aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to attach role {0} to instance profile {0}".format(params['RoleName'])) return True -def remove_instance_profiles(connection, module, role_params, role): +def remove_instance_profiles(role_params, role): role_name = module.params.get('name') delete_profiles = module.params.get("delete_instance_profile") try: - instance_profiles = connection.list_instance_profiles_for_role(aws_retry=True, **role_params)['InstanceProfiles'] + instance_profiles = client.list_instance_profiles_for_role(aws_retry=True, **role_params)['InstanceProfiles'] except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to list instance profiles for role {0}".format(role_name)) @@ -499,21 +545,21 @@ def remove_instance_profiles(connection, module, role_params, role): profile_name = profile['InstanceProfileName'] try: if not module.check_mode: - connection.remove_role_from_instance_profile(aws_retry=True, InstanceProfileName=profile_name, **role_params) + client.remove_role_from_instance_profile(aws_retry=True, InstanceProfileName=profile_name, **role_params) if profile_name == role_name: if delete_profiles: try: - connection.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) + client.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to remove instance profile {0}".format(profile_name)) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to remove role {0} from instance profile {1}".format(role_name, profile_name)) -def destroy_role(connection, module): +def destroy_role(): role_name = module.params.get('name') - role = get_role(connection, module, role_name) + role = get_role(role_name) role_params = dict() role_params['RoleName'] = role_name boundary_params = dict(role_params) @@ -526,51 +572,51 @@ def destroy_role(connection, module): # - attached instance profiles # - attached managed policies # - permissions boundary - remove_instance_profiles(connection, module, role_params, role) - update_managed_policies(connection, module, role_params, role, [], True) - update_role_permissions_boundary(connection, module, boundary_params, role) + remove_instance_profiles(role_params, role) + update_managed_policies(role_params, role, [], True) + update_role_permissions_boundary(boundary_params, role) try: if not module.check_mode: - connection.delete_role(aws_retry=True, **role_params) + client.delete_role(aws_retry=True, **role_params) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to delete role") module.exit_json(changed=True) -def get_role_with_backoff(connection, module, name): +def get_role_with_backoff(name): try: - return AWSRetry.jittered_backoff(catch_extra_error_codes=['NoSuchEntity'])(connection.get_role)(RoleName=name)['Role'] + 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="Unable to get role {0}".format(name)) -def get_role(connection, module, name): +def get_role(name): try: - return connection.get_role(RoleName=name, aws_retry=True)['Role'] + 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="Unable to get role {0}".format(name)) -def get_attached_policy_list(connection, module, name): +def get_attached_policy_list(name): try: - return connection.list_attached_role_policies(RoleName=name, aws_retry=True)['AttachedPolicies'] + 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="Unable to list attached policies for role {0}".format(name)) -def get_role_tags(connection, module): +def get_role_tags(): role_name = module.params.get('name') try: - return boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) + 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="Unable to list tags for role {0}".format(role_name)) -def update_role_tags(connection, module, params, role): +def update_role_tags(params, role): new_tags = params.get('Tags') if new_tags is None: return False @@ -580,7 +626,7 @@ def update_role_tags(connection, module, params, role): purge_tags = module.params.get('purge_tags') try: - existing_tags = boto3_tag_list_to_ansible_dict(connection.list_role_tags(RoleName=role_name, aws_retry=True)['Tags']) + 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 = {} @@ -589,9 +635,9 @@ def update_role_tags(connection, module, params, role): if not module.check_mode: try: if tags_to_remove: - connection.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) + client.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) if tags_to_add: - connection.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True) + 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='Unable to set tags for role %s' % role_name) @@ -601,6 +647,9 @@ def update_role_tags(connection, module, params, role): def main(): + global module + global client + argument_spec = dict( name=dict(type='str', required=True), path=dict(type='str', default="/"), @@ -615,6 +664,8 @@ def main(): purge_policies=dict(type='bool', aliases=['purge_policy', 'purge_managed_policies']), tags=dict(type='dict'), 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'])], @@ -638,14 +689,14 @@ def main(): if not path.endswith('/') or not path.startswith('/'): module.fail_json(msg="path must begin and end with /") - connection = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) + client = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) state = module.params.get("state") if state == 'present': - create_or_update_role(connection, module) + create_or_update_role() else: - destroy_role(connection, module) + destroy_role() if __name__ == '__main__': diff --git a/tests/integration/targets/iam_role/defaults/main.yml b/tests/integration/targets/iam_role/defaults/main.yml index 46db605072e..d496c421636 100644 --- a/tests/integration/targets/iam_role/defaults/main.yml +++ b/tests/integration/targets/iam_role/defaults/main.yml @@ -4,5 +4,3 @@ test_path: '/{{ resource_prefix }}/' safe_managed_policy: 'AWSDenyAll' custom_policy_name: '{{ resource_prefix }}-denyall' boundary_policy: 'arn:aws:iam::aws:policy/AWSDenyAll' -paranoid_pauses: no -standard_pauses: no 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..9f4684d150d --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/boundary_policy.yml @@ -0,0 +1,82 @@ +--- +- 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)" + 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..93a7b7d8342 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml @@ -0,0 +1,110 @@ +--- +- 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)" + 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..88fc79f977c --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/creation_deletion.yml @@ -0,0 +1,353 @@ +--- +- 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 + check_mode: yes + register: iam_role + +- 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 )' + # 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 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]' + - '"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 == '/' + - '"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)" + 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" + 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)" + 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 \ No newline at end of file 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..d4ee520147f --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/description_update.yml @@ -0,0 +1,124 @@ +--- +- 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)" + 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)" + 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..7fb5eef3e37 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml @@ -0,0 +1,63 @@ +--- +- 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" + +# XXX iam_role fails to remove inline policies before deleting the role +- name: "Detach inline policy a" + iam_policy: + state: absent + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-a" + +- name: "Detach inline policy b" + iam_policy: + state: absent + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-b" diff --git a/tests/integration/targets/iam_role/tasks/main.yml b/tests/integration/targets/iam_role/tasks/main.yml index 34c17af3369..b4132c60c2a 100644 --- a/tests/integration/targets/iam_role/tasks/main.yml +++ b/tests/integration/targets/iam_role/tasks/main.yml @@ -22,1500 +22,115 @@ # Possible Bugs: # - Fails to delete role if inline policies not removed first -- name: 'Setup AWS connection info' +- name: "Setup AWS connection info" module_defaults: group/aws: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token | default(omit) }}' - region: '{{ aws_region }}' + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_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: - # =================================================================== - # 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' + # =================================================================== + # 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 - - 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' - - # =================================================================== - # 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 - - name: Try running some rapid fire create/delete tests - # We've previously seen issues with iam_role returning before creation's - # actually complete, if we think the issue's gone, let's try creating and - # deleting things in quick succession - when: not (standard_pauses | bool) - 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 - check_mode: yes - register: iam_role - - assert: - that: - - iam_role is changed - # Pause this first time, just in case we actually created something... - - name: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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 )' - # 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: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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]' - - '"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 == '/' - - '"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: Short pause for role removal to finish - pause: - seconds: 10 - when: paranoid_pauses | bool - - - 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: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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: Short pause for role removal to finish - pause: - seconds: 10 - when: paranoid_pauses | bool - - - 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' - 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: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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 - - # =================================================================== - # Max Session Duration Manipulation - - - 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 - - iam_role.iam_role.role_name == test_role - - - 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 - - # =================================================================== - # Description Manipulation - - - 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)' - 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)' - 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 - - - # =================================================================== - # Tag Manipulation - - - 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" + - create_managed_policy is succeeded - - 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)' - 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)' - 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)' - 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" - - - # =================================================================== - # Policy Manipulation - - - 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)' - 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)' - 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 + # =================================================================== + # Rapid Role Creation and deletion + - include_tasks: creation_deletion.yml - - 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" - - # =================================================================== - # Inline Policy (test _info behaviour) - - # XXX Not sure if it's a bug in Ansible or a "quirk" of AWS, but these two - # policies need to have at least different Sids or the second doesn't show - # up... - - - 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" - - # XXX iam_role fails to remove inline policies before deleting the role - - name: 'Detach inline policy a' - iam_policy: - state: absent - iam_type: 'role' - iam_name: '{{ test_role }}' - policy_name: 'inline-policy-a' - - - name: 'Detach inline policy b' - iam_policy: - state: absent - iam_type: 'role' - iam_name: '{{ test_role }}' - policy_name: 'inline-policy-b' - - # =================================================================== - # Role Removal - - 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: 'Short pause for role removal to finish' - pause: - seconds: 10 - when: paranoid_pauses | bool + # =================================================================== + # Max Session Duration Manipulation + - include_tasks: max_session_update.yml - - 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 + # =================================================================== + # Description Manipulation + - include_tasks: description_update.yml - - 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: 'Short pause for role removal to finish' - pause: - seconds: 10 - when: paranoid_pauses | bool + # =================================================================== + # Tag Manipulation + - include_tasks: tags_update.yml - - 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 + # =================================================================== + # Policy Manipulation + - include_tasks: policy_update.yml - - 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 - - name: 'Short pause for role removal to finish' - pause: - seconds: 10 - when: paranoid_pauses | bool + # =================================================================== + # Inline Policy (test _info behavior) + - include_tasks: inline_policy_update.yml - # =================================================================== - # Boundary Policy (requires create_instance_profile: no) - - 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 + # =================================================================== + # Role Removal + - include_tasks: role_removal.yml - - 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 + # =================================================================== + # Boundary Policy (requires create_instance_profile: no) + - include_tasks: boundary_policy.yml - - 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)' - 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 - - name: Short pause for role removal to finish - pause: - seconds: 10 - when: paranoid_pauses | bool - - # =================================================================== - # Complex role Creation - - 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: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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: Short pause for role creation to finish - pause: - seconds: 10 - when: standard_pauses | bool - - - 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" + # =================================================================== + # Complex role Creation + - include_tasks: complex_role_creation.yml always: - # =================================================================== - # Cleanup - - # XXX iam_role fails to remove inline policies before deleting the role - - name: 'Detach inline policy a' - iam_policy: - state: absent - iam_type: 'role' - iam_name: '{{ test_role }}' - policy_name: 'inline-policy-a' - ignore_errors: true + # =================================================================== + # Cleanup - - name: 'Detach inline policy b' - iam_policy: - state: absent - iam_type: 'role' - iam_name: '{{ test_role }}' - policy_name: 'inline-policy-b' - ignore_errors: true - - - name: 'Remove IAM Role' - iam_role: - state: absent - name: '{{ test_role }}' - delete_instance_profile: yes - ignore_errors: true + # XXX iam_role fails to remove inline policies before deleting the role + - name: "Detach inline policy a" + iam_policy: + state: absent + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-a" + 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: "Detach inline policy b" + iam_policy: + state: absent + iam_type: "role" + iam_name: "{{ test_role }}" + policy_name: "inline-policy-b" + ignore_errors: true - - name: 'iam_role_info after Role deletion' - iam_role_info: - name: '{{ test_role }}' - ignore_errors: true + - name: "Remove IAM Role" + iam_role: + state: absent + name: "{{ test_role }}" + delete_instance_profile: yes + ignore_errors: true - - name: 'Remove test managed policy' - iam_managed_policy: - state: absent - policy_name: '{{ custom_policy_name }}' + - 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..cbe64df684e --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/max_session_update.yml @@ -0,0 +1,61 @@ +--- +- 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 + - iam_role.iam_role.role_name == test_role + +- 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..a34f2a0ad30 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/policy_update.yml @@ -0,0 +1,208 @@ +--- +- 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)" + 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)" + 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)" + 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..1b8d10710a9 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/role_removal.yml @@ -0,0 +1,53 @@ +--- +- 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)" + 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..617fb9a1331 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/tags_update.yml @@ -0,0 +1,286 @@ +--- +- 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)" + 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)" + 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)" + 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)" + 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"