From 7b516590372e0a32498864a3445033d79474cbab Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Wed, 2 Oct 2024 12:24:32 +0200 Subject: [PATCH] Prepare modules ec2_vpc_egress_igw for promotion (#2152) SUMMARY Refactor module ec2_vpc_egress_igw to use shared code from amazon.aws.plugins.module_utils.ec2 ec2_vpc_egress_igw - add support for tagging ISSUE TYPE Feature Pull Request New Module Pull Request COMPONENT NAME ec2_vpc_egress_igw Reviewed-by: Alina Buzachis Reviewed-by: Bikouo Aubin Reviewed-by: GomathiselviS --- ...23-refactor-ec2_vpc_egress_igw-modules.yml | 4 + plugins/modules/ec2_vpc_egress_igw.py | 191 +++++++++++------- .../targets/ec2_vpc_egress_igw/meta/main.yml | 1 - .../targets/ec2_vpc_egress_igw/tasks/main.yml | 159 +++++++++++---- 4 files changed, 238 insertions(+), 117 deletions(-) create mode 100644 changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml delete mode 100644 tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml diff --git a/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml b/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml new file mode 100644 index 00000000000..c67e90cc8d4 --- /dev/null +++ b/changelogs/fragments/20240923-refactor-ec2_vpc_egress_igw-modules.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - ec2_vpc_egress_igw - Refactor module to use shared code from ``amazon.aws.plugins.module_utils.ec2`` util (https://github.com/ansible-collections/community.aws/pull/2152). + - ec2_vpc_egress_igw - Add the possibility to update/add tags on Egress only internet gateway (https://github.com/ansible-collections/community.aws/pull/2152). \ No newline at end of file diff --git a/plugins/modules/ec2_vpc_egress_igw.py b/plugins/modules/ec2_vpc_egress_igw.py index 1bd65f501c8..8a1a520b7aa 100644 --- a/plugins/modules/ec2_vpc_egress_igw.py +++ b/plugins/modules/ec2_vpc_egress_igw.py @@ -25,10 +25,13 @@ default: present choices: [ 'present', 'absent' ] type: str +notes: + - Support for O(tags) and O(purge_tags) was added in release 9.0.0. extends_documentation_fragment: - amazon.aws.common.modules - amazon.aws.region.modules - amazon.aws.boto3 + - amazon.aws.tags.modules """ EXAMPLES = r""" @@ -36,10 +39,15 @@ # Ensure that the VPC has an Internet Gateway. # The Internet Gateway ID is can be accessed via {{eigw.gateway_id}} for use in setting up NATs etc. -- community.aws.ec2_vpc_egress_igw: +- name: Create Egress internet only gateway + community.aws.ec2_vpc_egress_igw: vpc_id: vpc-abcdefgh state: present - register: eigw + +- name: Delete Egress internet only gateway + community.aws.ec2_vpc_egress_igw: + vpc_id: vpc-abcdefgh + state: absent """ RETURN = r""" @@ -53,22 +61,30 @@ returned: always type: str sample: vpc-012345678 +tags: + description: Any tags assigned to the internet gateway. + returned: always + type: dict """ -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule +from typing import Any +from typing import Dict +from typing import Optional +from typing import Union from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code -from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AnsibleEC2Error +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import create_egress_only_internet_gateway +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import delete_egress_only_internet_gateway +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import describe_egress_only_internet_gateways +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ensure_ec2_tags +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule -def delete_eigw(module, connection, eigw_id): +def delete_eigw(module: AnsibleAWSModule, connection, eigw_id: str) -> Dict[str, Union[str, bool]]: """ Delete EIGW. @@ -76,27 +92,23 @@ def delete_eigw(module, connection, eigw_id): connection : boto3 client connection object eigw_id : ID of the EIGW to delete """ - changed = False - try: - response = connection.delete_egress_only_internet_gateway( - aws_retry=True, DryRun=module.check_mode, EgressOnlyInternetGatewayId=eigw_id + vpc_id = module.params.get("vpc_id") + + if module.check_mode: + return dict( + changed=True, msg=f"Would have deleted Egress internet only Gateway id '{eigw_id}' if not in check mode." ) - except is_boto3_error_code("DryRunOperation"): - changed = True - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Could not delete Egress-Only Internet Gateway {eigw_id} from VPC {module.vpc_id}") - if not module.check_mode: - changed = response.get("ReturnCode", False) + try: + changed = delete_egress_only_internet_gateway(connection, egress_only_internet_gateway_id=eigw_id) + except AnsibleEC2Error as e: + module.fail_json_aws(e) - return changed + return dict(changed=changed) -def create_eigw(module, connection, vpc_id): +def create_eigw(module: AnsibleAWSModule, connection, vpc_id: str) -> Dict[str, Union[str, bool]]: """ Create EIGW. @@ -104,43 +116,35 @@ def create_eigw(module, connection, vpc_id): connection : boto3 client connection object vpc_id : ID of the VPC we are operating on """ + + if module.check_mode: + return dict(changed=True, msg="Would have created Egress internet only Gateway if not in check mode.") + gateway_id = None changed = False try: - response = connection.create_egress_only_internet_gateway( - aws_retry=True, DryRun=module.check_mode, VpcId=vpc_id - ) - except is_boto3_error_code("DryRunOperation"): - # When boto3 method is run with DryRun=True it returns an error on success - # We need to catch the error and return something valid + response = create_egress_only_internet_gateway(connection, vpc_id=vpc_id, tags=module.params.get("tags")) changed = True - except is_boto3_error_code("InvalidVpcID.NotFound") as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"invalid vpc ID '{vpc_id}' provided") - except ( - botocore.exceptions.ClientError, - botocore.exceptions.BotoCoreError, - ) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg=f"Could not create Egress-Only Internet Gateway for vpc ID {vpc_id}") - - if not module.check_mode: - gateway = response.get("EgressOnlyInternetGateway", {}) - state = gateway.get("Attachments", [{}])[0].get("State") - gateway_id = gateway.get("EgressOnlyInternetGatewayId") - - if gateway_id and state in ("attached", "attaching"): - changed = True - else: - # EIGW gave back a bad attachment state or an invalid response so we error out - module.fail_json( - msg=f"Unable to create and attach Egress Only Internet Gateway to VPCId: {vpc_id}. Bad or no state in response", - **camel_dict_to_snake_dict(response), - ) + except AnsibleEC2Error as e: + module.fail_json_aws(e) + + gateway = response.get("EgressOnlyInternetGateway", {}) + state = gateway.get("Attachments", [{}])[0].get("State") + gateway_id = gateway.get("EgressOnlyInternetGatewayId") + tags = boto3_tag_list_to_ansible_dict(gateway.get("Tags", [])) + + if not gateway_id or state not in ("attached", "attaching"): + # EIGW gave back a bad attachment state or an invalid response so we error out + module.fail_json( + msg=f"Unable to create and attach Egress Only Internet Gateway to VPCId: {vpc_id}. Bad or no state in response", + **camel_dict_to_snake_dict(response), + ) - return changed, gateway_id + return dict(changed=changed, gateway_id=gateway_id, tags=tags) -def describe_eigws(module, connection, vpc_id): +def find_egress_only_igw(module: AnsibleAWSModule, connection, vpc_id: str) -> Optional[Dict[str, Any]]: """ Describe EIGWs. @@ -148,43 +152,80 @@ def describe_eigws(module, connection, vpc_id): connection : boto3 client connection object vpc_id : ID of the VPC we are operating on """ - gateway_id = None + result = None try: - response = connection.describe_egress_only_internet_gateways(aws_retry=True) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Could not get list of existing Egress-Only Internet Gateways") + for eigw in describe_egress_only_internet_gateways(connection): + for attachment in eigw.get("Attachments", []): + if attachment.get("VpcId") == vpc_id and attachment.get("State") in ("attached", "attaching"): + return { + "gateway_id": eigw.get("EgressOnlyInternetGatewayId"), + "tags": boto3_tag_list_to_ansible_dict(eigw.get("Tags", [])), + } + except AnsibleEC2Error as e: + module.fail_json_aws(e) - for eigw in response.get("EgressOnlyInternetGateways", []): - for attachment in eigw.get("Attachments", []): - if attachment.get("VpcId") == vpc_id and attachment.get("State") in ("attached", "attaching"): - gateway_id = eigw.get("EgressOnlyInternetGatewayId") + return result - return gateway_id + +def ensure_present(connection, module: AnsibleAWSModule, existing: Optional[Dict[str, Any]]) -> None: + vpc_id = module.params.get("vpc_id") + result = dict(vpc_id=vpc_id, changed=False) + + if not existing: + result.update(create_eigw(module, connection, vpc_id)) + else: + egress_only_igw_id = existing.get("gateway_id") + changed = False + result = existing + tags = module.params.get("tags") + purge_tags = module.params.get("purge_tags") + if tags is not None: + changed = ensure_ec2_tags( + connection, + module, + egress_only_igw_id, + resource_type="egress-only-internet-gateway", + tags=tags, + purge_tags=purge_tags, + ) + result.update(dict(changed=changed, vpc_id=vpc_id)) + + module.exit_json(**result) + + +def ensure_absent(connection, module: AnsibleAWSModule, existing: Optional[Dict[str, Any]]) -> None: + vpc_id = module.params.get("vpc_id") + if not existing: + module.exit_json(changed=False, msg=f"No Egress only internet gateway attached to the VPC id '{vpc_id}'") + + egress_only_igw_id = existing.get("gateway_id") + result = dict(gateway_id=egress_only_igw_id, vpc_id=vpc_id, changed=False) + result.update(delete_eigw(module, connection, egress_only_igw_id)) + module.exit_json(**result) def main(): - argument_spec = dict(vpc_id=dict(required=True), state=dict(default="present", choices=["present", "absent"])) + argument_spec = dict( + vpc_id=dict(required=True), + state=dict(default="present", choices=["present", "absent"]), + tags=dict(type="dict", aliases=["resource_tags"]), + purge_tags=dict(type="bool", default=True), + ) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - retry_decorator = AWSRetry.jittered_backoff(retries=10) - connection = module.client("ec2", retry_decorator=retry_decorator) + connection = module.client("ec2") vpc_id = module.params.get("vpc_id") state = module.params.get("state") - eigw_id = describe_eigws(module, connection, vpc_id) - - result = dict(gateway_id=eigw_id, vpc_id=vpc_id) - changed = False - - if state == "present" and not eigw_id: - changed, result["gateway_id"] = create_eigw(module, connection, vpc_id) - elif state == "absent" and eigw_id: - changed = delete_eigw(module, connection, eigw_id) + existing_egress_only_igw = find_egress_only_igw(module, connection, vpc_id) - module.exit_json(changed=changed, **result) + if state == "present": + ensure_present(connection, module, existing_egress_only_igw) + else: + ensure_absent(connection, module, existing_egress_only_igw) if __name__ == "__main__": diff --git a/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml b/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml deleted file mode 100644 index 32cf5dda7ed..00000000000 --- a/tests/integration/targets/ec2_vpc_egress_igw/meta/main.yml +++ /dev/null @@ -1 +0,0 @@ -dependencies: [] diff --git a/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml b/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml index 75fff0e4e22..56bb89decaf 100644 --- a/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_egress_igw/tasks/main.yml @@ -1,7 +1,5 @@ --- -- name: 'ec2_vpc_egress_igw integration tests' - collections: - - amazon.aws +- name: Run ec2_vpc_egress_igw integration tests module_defaults: group/aws: access_key: '{{ aws_access_key }}' @@ -11,35 +9,36 @@ block: # ============================================================ - - name: test failure with no parameters - ec2_vpc_egress_igw: + - name: Test failure with no parameters + community.aws.ec2_vpc_egress_igw: register: result ignore_errors: true - - name: assert failure with no parameters - assert: + - name: Assert failure with no parameters + ansible.builtin.assert: that: - - 'result.failed' + - result is failed - 'result.msg == "missing required arguments: vpc_id"' # ============================================================ - - name: test failure with non-existent VPC ID - ec2_vpc_egress_igw: + - name: Test failure with non-existent VPC ID + community.aws.ec2_vpc_egress_igw: state: present vpc_id: vpc-02394e50abc1807e8 register: result ignore_errors: true - - name: assert failure with non-existent VPC ID - assert: + - name: Assert failure with non-existent VPC ID + ansible.builtin.assert: that: - - 'result.failed' - - 'result.error.code == "InvalidVpcID.NotFound"' - - '"invalid vpc ID" in result.msg' + - result is failed + - e_msg in result.exception + vars: + e_msg: "The vpc ID 'vpc-02394e50abc1807e8' does not exist" # ============================================================ - - name: create a VPC - ec2_vpc_net: + - name: Create a VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: present cidr_block: "10.232.232.128/26" @@ -49,55 +48,133 @@ register: vpc_result # ============================================================ - - name: create egress-only internet gateway (expected changed=true) - ec2_vpc_egress_igw: + - name: Create egress-only internet gateway using check_mode=true + community.aws.ec2_vpc_egress_igw: + state: present + vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_create_check_mode + check_mode: true + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_create_check_mode is changed + + # # ============================================================ + - name: Create egress-only internet gateway (expected changed=true) + community.aws.ec2_vpc_egress_igw: state: present vpc_id: "{{ vpc_result.vpc.id }}" register: vpc_eigw_create - - name: assert creation happened (expected changed=true) - assert: + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: that: - - 'vpc_eigw_create' - - 'vpc_eigw_create.gateway_id.startswith("eigw-")' - - 'vpc_eigw_create.vpc_id == vpc_result.vpc.id' + - vpc_eigw_create is changed - # ============================================================ - - name: attempt to recreate egress-only internet gateway on VPC (expected changed=false) - ec2_vpc_egress_igw: + # # ============================================================ + - name: Create egress-only internet gateway once again (idempotency) + community.aws.ec2_vpc_egress_igw: state: present vpc_id: "{{ vpc_result.vpc.id }}" - register: vpc_eigw_recreate + register: vpc_eigw_create_idempotency - - name: assert recreation did nothing (expected changed=false) + - name: Assert module returned changed and the Egress IGW was not created assert: that: - - 'vpc_eigw_recreate.changed == False' - - 'vpc_eigw_recreate.gateway_id == vpc_eigw_create.gateway_id' - - 'vpc_eigw_recreate.vpc_id == vpc_eigw_create.vpc_id' + - vpc_eigw_create_idempotency is not changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_create.gateway_id - # ============================================================ - - name: test state=absent (expected changed=true) + # # ============================================================ + - name: Delete egress-only internet gateway (check_mode) ec2_vpc_egress_igw: state: absent vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_delete_check_mode + check_mode: true + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete_check_mode is changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_delete_check_mode.gateway_id + + # # ============================================================ + - name: Delete egress-only internet gateway once again (idempotency) + community.aws.ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" register: vpc_eigw_delete - - name: assert state=absent (expected changed=true) - assert: + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete is changed + - vpc_eigw_create_idempotency.gateway_id == vpc_eigw_delete.gateway_id + + # # ============================================================ + - name: Delete egress-only internet gateway + ec2_vpc_egress_igw: + state: absent + vpc_id: "{{ vpc_result.vpc.id }}" + register: vpc_eigw_delete_idempotency + + - name: Assert module returned changed and the Egress IGW was not created + ansible.builtin.assert: + that: + - vpc_eigw_delete_idempotency is not changed + + ## ============================================================ + ## Tagging + - name: Create Egress only internet gateway with tags + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + VpcId: "{{ vpc_result.vpc.id }}" + register: create_with_tags + + - name: Assert that the Egress IGW was created with tags + ansible.builtin.assert: + that: + - create_with_tags is changed + + - name: Trying to update tags (no change) + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + ResourcePrefix: "{{ resource_prefix }}" + VpcId: "{{ vpc_result.vpc.id }}" + register: update_tags + + - name: Assert that the Egress IGW was not updated + ansible.builtin.assert: + that: + - update_tags is not changed + + - name: Add tag to existing tags + community.aws.ec2_vpc_egress_igw: + vpc_id: "{{ vpc_result.vpc.id }}" + tags: + Phase: integration + purge_tags: false + register: add_tag + + - name: Assert that the Egress IGW was created with tags + ansible.builtin.assert: that: - - 'vpc_eigw_delete.changed' + - add_tag is changed always: # ============================================================ - - name: tidy up EIGW - ec2_vpc_egress_igw: + - name: Tidy up EIGW + community.aws.ec2_vpc_egress_igw: state: absent vpc_id: "{{ vpc_result.vpc.id }}" ignore_errors: true - - name: tidy up VPC - ec2_vpc_net: + - name: Tidy up VPC + amazon.aws.ec2_vpc_net: name: "{{ resource_prefix }}-vpc" state: absent cidr_block: "10.232.232.128/26"