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/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/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/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). 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/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/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/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 10753f63584..13bbd7f4619 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: @@ -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/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/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/dynamodb_table.py b/plugins/modules/dynamodb_table.py index 86c08f90934..98d6fa632f9 100644 --- a/plugins/modules/dynamodb_table.py +++ b/plugins/modules/dynamodb_table.py @@ -49,6 +49,11 @@ - 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'] + type: str read_capacity: description: - Read throughput capacity (units) to provision. @@ -130,7 +135,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: @@ -165,6 +170,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 +380,7 @@ def compatability_results(current_table): if not current_table: return dict() - throughput = current_table.get('provisioned_throughput', {}) + billing_mode = current_table.get('billing_mode') primary_indexes = _decode_primary_index(current_table) @@ -394,14 +407,18 @@ def compatability_results(current_table): range_key_name=range_key_name, range_key_type=range_key_type, indexes=indexes, - read_capacity=throughput.get('read_capacity_units', None), + billing_mode=billing_mode, 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), ) + 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) + return compat_results @@ -435,6 +452,13 @@ def get_dynamodb_table(): table['size'] = table['table_size_bytes'] table['tags'] = tags + # 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'] = 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() @@ -568,9 +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 @@ -579,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) @@ -589,6 +619,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']: @@ -659,6 +690,7 @@ def _generate_index(index, include_throughput=True): KeySchema=key_schema, Projection=projection, ) + if include_throughput: idx['ProvisionedThroughput'] = throughput @@ -674,11 +706,24 @@ 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') + + 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 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: @@ -687,13 +732,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 include_throughput: + change = dict(_throughput_changes(_current, _new)) + if change: + update = dict( + IndexName=name, + ProvisionedThroughput=change, + ) + index_changes.append(dict(Update=update)) return index_changes @@ -713,15 +760,26 @@ def _update_table(current_table): if throughput_changes: changes['ProvisionedThroughput'] = throughput_changes + current_billing_mode = current_table.get('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 + 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 + # 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 len(global_index_changes) > 1: - changes['GlobalSecondaryIndexUpdates'] = [global_index_changes[0]] - additional_global_index_changes = global_index_changes[1:] + + 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:] local_index_changes = _local_index_changes(current_table) if local_index_changes: @@ -818,6 +876,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') + + if billing_mode is None: + billing_mode = "PROVISIONED" tags = ansible_dict_to_boto3_tag_list(module.params.get('tags') or {}) @@ -827,23 +889,27 @@ 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() - global_indexes = _generate_global_indexes() + global_indexes = _generate_global_indexes(billing_mode) params = dict( 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,13 +985,14 @@ 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(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), 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( 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_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/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/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/ec2_win_password.py b/plugins/modules/ec2_win_password.py index 3ed0afb79d4..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,10 +54,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. ''' EXAMPLES = ''' @@ -110,11 +105,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(): @@ -126,10 +125,19 @@ 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 +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 +152,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 +206,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.') 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/ecs_taskdefinition.py b/plugins/modules/ecs_taskdefinition.py index 86319b8bb3c..505a4207117 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 @@ -518,6 +517,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 +682,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): + 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,9 @@ 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', + options=dict(type=dict(type='str'), expression=dict(type='str'))), ) module = AnsibleAWSModule(argument_spec=argument_spec, @@ -801,8 +821,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 +993,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/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_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_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 f5699edf8b5..7ca0d8c4fbb 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'] @@ -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: @@ -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,13 +664,15 @@ 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'])], 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'): @@ -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/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 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' ] 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/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/integration/targets/dynamodb_table/defaults/main.yml b/tests/integration/targets/dynamodb_table/defaults/main.yml index 55c8441bfa6..96df5538146 100644 --- a/tests/integration/targets/dynamodb_table/defaults/main.yml +++ b/tests/integration/targets/dynamodb_table/defaults/main.yml @@ -1,11 +1,12 @@ --- -table_name: '{{ resource_prefix }}' +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 @@ -27,6 +28,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 @@ -36,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 @@ -52,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 diff --git a/tests/integration/targets/dynamodb_table/tasks/main.yml b/tests/integration/targets/dynamodb_table/tasks/main.yml index 0cda98bcdbd..a5f3f7a3882 100644 --- a/tests/integration/targets/dynamodb_table/tasks/main.yml +++ b/tests/integration/targets/dynamodb_table/tasks/main.yml @@ -13,6 +13,8 @@ region: '{{ aws_region }}' block: + - include: "test_pay_per_request.yml" + # ============================================== - name: Create table - check_mode @@ -906,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 new file mode 100644 index 00000000000..059561dea05 --- /dev/null +++ b/tests/integration/targets/dynamodb_table/tasks/test_pay_per_request.yml @@ -0,0 +1,114 @@ +--- +- name: Create table - pay-per-request - check_mode + dynamodb_table: + state: present + name: "{{ table_name_on_demand }}" + 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_on_demand }}" + 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 changed + - create_table.billing_mode == "PAY_PER_REQUEST" + +# ============================================== + +- name: Create complex table - check_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: 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_on_demand }}" + hash_key_name: "{{ table_index }}" + hash_key_type: "{{ table_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.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 }}" + 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_on_demand }}" + wait: false + register: delete_table 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 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 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" 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' 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