diff --git a/meta/runtime.yml b/meta/runtime.yml index 15743d53e91..72cdde706d3 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -119,6 +119,8 @@ action_groups: - eks_cluster - eks_fargate_profile - eks_nodegroup + - eks_pod_identity_association + - eks_pod_identity_association_info - elasticache - elasticache_info - elasticache_parameter_group diff --git a/plugins/modules/eks_pod_identity_association.py b/plugins/modules/eks_pod_identity_association.py new file mode 100644 index 00000000000..f1b09fd42f9 --- /dev/null +++ b/plugins/modules/eks_pod_identity_association.py @@ -0,0 +1,339 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: eks_pod_identity_association +version_added: 10.1.0 +short_description: Manage an EKS pod identity association +description: + - Manage an AWS EKS pod identity association. See + U(https://docs.aws.amazon.com/eks/latest/userguide/pod-id-association.html) + U(https://docs.aws.amazon.com/eks/latest/userguide/pod-id-assign-target-role.html) for details. +author: + - "Ali AlKhalidi (@doteast)" +options: + cluster_name: + description: Name of EKS Cluster. + required: true + type: str + role_arn: + description: ARN of IAM role to associate with the service account. + type: str + required: true + namespace: + description: + - EKS Kubernetes namespace inside the cluster to create the association in. + - Can be used only during creation. + type: str + required: true + service_account: + description: + - EKS Kubernetes service account inside the cluster to associate the IAM credentials with. + - Can be used only during creation. + type: str + required: true + tags: + description: + - A dictionary of resource tags. + - Can be used only during creation. + type: dict + aliases: ['resource_tags'] + state: + description: + - Create or destroy the association. + required: false + default: present + choices: [ 'present', 'absent' ] + type: str +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.tags + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create pod identity association + community.aws.eks_pod_identity_association: + cluster_name: myeks + role_arn: arn:aws:iam:us-east-1:1231231123:role/abcd + namespace: test-ns + service_account: test-sa + tags: + foo: bar + state: present + +- name: Delete pod identity association + community.aws.eks_pod_identity_association: + cluster_name: myeks + role_arn: arn:aws:iam:us-east-1:1231231123:role/abcd + namespace: test-ns + service_account: test-sa + tags: + foo: bar + state: absent +""" + +RETURN = r""" +cluster_name: + description: The name of the cluster that the association is in. + returned: when state is present + type: str + sample: test_cluster +role_arn: + description: ARN of the IAM role to associate with the service account. + returned: when state is present + type: str + sample: arn:aws:iam:us-east-1:1231231123:role/abcd +namespace: + description: The name of the Kubernetes namespace inside the cluster to create the association in. + returned: when state present + type: str + sample: test-ns +service_account: + description: The name of the Kubernetes service account inside the cluster to associate the IAM credentials with. + returned: when state present + type: str + sample: test-sa +association_arn: + description: ARN of the association. + returned: when state present + type: str + sample: arn:aws:els:us-east-1:1231231123:association/abcd +association_id: + description: The ID of the association. + returned: when state present + type: str + sample: a-a1b2c3d4e5f6g7h8i +tags: + description: Metadata that assists with categorization and organization. + returned: when state present + type: dict + sample: + foo: bar +created_at: + description: Association creation date and time. + returned: when state is present + type: str + sample: '2022-01-18T20:00:00.111000+00:00' +modified_at: + description: Association modified date and time. + returned: when state is present + type: str + sample: '2022-01-18T20:00:00.111000+00:00' +""" + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule + +PARAMS_MAP = { + "cluster_name": "clusterName", + "role_arn": "roleArn", + "namespace": "namespace", + "service_account": "serviceAccount", + "tags": "tags", +} + +DEFAULTS = {} + +CREATE_ONLY_PARAMS = [ + "namespace", + "service_account", + "tags", +] + + +def _set_kwarg(kwargs, key, value): + mapped_key = PARAMS_MAP[key] + key_list = [mapped_key] + data = kwargs + data[key_list[0]] = value + + +def _fill_kwargs(module, apply_defaults=True, ignore_create_params=False): + kwargs = {} + if apply_defaults: + for p_name, p_value in DEFAULTS.items(): + _set_kwarg(kwargs, p_name, p_value) + for p_name in module.params: + if ignore_create_params and p_name in CREATE_ONLY_PARAMS: + # ignore CREATE_ONLY_PARAMS on update + continue + if p_name in PARAMS_MAP and module.params[p_name] is not None: + _set_kwarg(kwargs, p_name, module.params[p_name]) + else: + # ignore + pass + return kwargs + + +def _needs_change(current, desired): + needs_change = False + for key in desired: + current_value = current[key] + desired_value = desired[key] + if current_value != desired_value: + needs_change = True + break + # + return needs_change + + +def get_association_id(client, module): + cluster_name = module.params["cluster_name"] + namespace = module.params["namespace"] + service_account = module.params["service_account"] + association_id = None + try: + response = client.list_pod_identity_associations( + clusterName=cluster_name, namespace=namespace, serviceAccount=service_account + ) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't list pod identity associations.") + + if len(response["associations"]) > 1: + module.warn("found multiple associations matcing fields") + if len(response["associations"]) >= 1: + association_id = response["associations"][0]["associationId"] + return association_id + + +def get_association_info(client, module, association_id, cluster_name): + try: + return client.describe_pod_identity_association(clusterName=cluster_name, associationId=association_id) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get pod identity association details.") + + +def update_assocition(client, module, association_id): + kwargs = _fill_kwargs(module, apply_defaults=False, ignore_create_params=True) + cluster_name = module.params["cluster_name"] + kwargs["associationId"] = association_id + # get current state for comparison: + api_result = get_association_info(client, module, association_id, cluster_name) + result = camel_dict_to_snake_dict(api_result["association"], ignore_list=["Tags"]) + changed = False + if _needs_change(api_result["association"], kwargs): + changed = True + if not module.check_mode: + try: + api_result = client.update_pod_identity_association(**kwargs) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't update pod identity association.") + result = camel_dict_to_snake_dict(api_result["association"], ignore_list=["Tags"]) + # + + return {"association": result, "changed": changed} + + +def delete_association(client, module, association_id, cluster_name): + try: + response = client.delete_pod_identity_association(clusterName=cluster_name, associationId=association_id) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't delete pod identity association.") + + return response + + +def create_assocition(client, module): + kwargs = _fill_kwargs(module) + + changed = True + result = {"association_arn": "fakeArn", "association_id": "fakeId"} + if not module.check_mode: + try: + result = client.create_pod_identity_association(**kwargs) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except ( + botocore.exceptions.BotoCoreError, + botocore.exceptions.ClientError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Couldn't create association") + + return {"association": camel_dict_to_snake_dict(result, ignore_list=["Tags"]), "changed": changed} + + +def ensure_present(client, module): + association_id = get_association_id(client, module) + if association_id: + return update_assocition(client, module, association_id) + + return create_assocition(client, module) + + +def ensure_absent(client, module): + cluster_name = module.params["cluster_name"] + changed = False + result = {"cluster_name": cluster_name, "association_id": "fakeId"} + association_id = get_association_id(client, module) + result["association_id"] = association_id if association_id else "fakeId" + + if not association_id: + # silently ignore delete of unknown association + return {"association": camel_dict_to_snake_dict(result, ignore_list=["Tags"]), "changed": changed} + + if not module.check_mode: + try: + result = delete_association(client, module, association_id, cluster_name) + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e) + + return {"association": result, "changed": True} + + +def main(): + argument_spec = dict( + cluster_name=dict(type="str", required=True), + role_arn=dict(type="str", required=True), + target_role_arn=dict(type="str"), + namespace=dict(type="str", required=True), + service_account=dict(type="str", required=True), + tags=dict(type="dict"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + try: + client = module.client("eks") + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Couldn't connect to AWS.") + + if module.params.get("state") == "present": + try: + result = ensure_present(client, module) + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e) + else: + try: + result = ensure_absent(client, module) + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/eks_pod_identity_association_info.py b/plugins/modules/eks_pod_identity_association_info.py new file mode 100644 index 00000000000..78dc928726b --- /dev/null +++ b/plugins/modules/eks_pod_identity_association_info.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: eks_pod_identity_association_info +version_added: 10.1.0 +short_description: Retrieve EKS pod identity association details +description: + - Get details about a pod identity association. +author: + - Ali AlKhalidi (@doteast) +options: + cluster_name: + description: Name of EKS Cluster. + required: true + type: str + association_id: + description: + - Get details for pod identity assosciation with specified Id. + type: str + namespace: + description: + - Get details for pod identity assosciations with specified namespace. + - Ignore when association Id is provided. + - Must be coupled with service_account. + type: str + service_account: + description: + - Get details for pod identity assosciation with specified service account. + - Ignore when association Id is provided. + - Must be coupled with namespace. + type: str +extends_documentation_fragment: + - amazon.aws.boto3 + - amazon.aws.common.modules +""" + + +EXAMPLES = r""" +- name: get current pod identity association settings by id + community.aws.eks_pod_identity_association_info: + cluster_name: myeks + association_id: a-a1b2c3d4e5f6g7h8i + register: association_info + +- name: get current pod identity association settings by service account and namespace + community.aws.eks_pod_identity_association_info: + cluster_name: myeks + namespace: test-ns + service_account: test-sa + register: association_info +""" + +RETURN = r""" +broker: + description: API response of describe_pod_identity_association() converted to snake yaml. + type: dict + returned: success +""" + +try: + import botocore +except ImportError: + # handled by AnsibleAWSModule + pass + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +def get_association_id(client, module): + cluster_name = module.params["cluster_name"] + namespace = module.params["namespace"] + service_account = module.params["service_account"] + association_id = None + try: + response = client.list_pod_identity_associations( + clusterName=cluster_name, namespace=namespace, serviceAccount=service_account + ) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't list pod identity associations.") + + if len(response["associations"]) > 1: + module.warn("found multiple associations matcing fields") + if len(response["associations"]) >= 1: + association_id = response["associations"][0]["associationId"] + return association_id + + +def get_association_info(client, module, association_id, cluster_name): + try: + return client.describe_pod_identity_association(clusterName=cluster_name, associationId=association_id) + except botocore.exceptions.EndpointConnectionError: # pylint: disable=duplicate-except + module.fail_json(msg=f"Region {client.meta.region_name} is not supported by EKS") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't get pod identity association details.") + + +def main(): + argument_spec = dict( + cluster_name=dict(type="str", required=True), + association_id=dict(type="str"), + namespace=dict(type="str"), + service_account=dict(type="str"), + ) + required_one_of = ( + ( + "association_id", + "namespace", + ), + ) + required_together = ( + ( + "namespace", + "service_account", + ), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + mutually_exclusive=required_one_of, + required_together=required_together, + supports_check_mode=True, + ) + cluster_name = module.params["cluster_name"] + association_id = module.params["association_id"] + + client = module.client("eks") + changed = False + try: + if not association_id: + association_id = get_association_id(client, module) + if not association_id: + if module.check_mode: + module.exit_json( + association={ + "association_id": "fakeId", + "clusert_name": cluster_name if cluster_name else "fakeName", + }, + changed=changed, + ) + result = get_association_info(client, module, association_id, cluster_name) + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e) + # + module.exit_json(association=camel_dict_to_snake_dict(result["association"], ignore_list=["Tags"]), changed=changed) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/requirements.txt b/tests/integration/requirements.txt index aa71c96813e..9c40f069634 100644 --- a/tests/integration/requirements.txt +++ b/tests/integration/requirements.txt @@ -2,6 +2,9 @@ boto3 botocore +# Needed for EKS related tests +kubernetes + # netaddr is needed for ansible.utils.ipv6 netaddr virtualenv diff --git a/tests/integration/requirements.yml b/tests/integration/requirements.yml index d3e5b30321f..3b1fa1b4050 100644 --- a/tests/integration/requirements.yml +++ b/tests/integration/requirements.yml @@ -6,3 +6,4 @@ collections: - ansible.windows - community.crypto - community.general + - kubernetes.core diff --git a/tests/integration/targets/eks_pod_identity_association/aliases b/tests/integration/targets/eks_pod_identity_association/aliases new file mode 100644 index 00000000000..4c06f2662f0 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/aliases @@ -0,0 +1,2 @@ +cloud/aws +time=60m diff --git a/tests/integration/targets/eks_pod_identity_association/defaults/main.yml b/tests/integration/targets/eks_pod_identity_association/defaults/main.yml new file mode 100644 index 00000000000..ef47754f45d --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/defaults/main.yml @@ -0,0 +1,40 @@ +eks_cluster_name: "{{ resource_prefix }}" +eks_nodegroup_name_a: "ng-template-{{ resource_prefix }}-a" +s3_bucket_name: "pia-{{ resource_prefix }}" +k8s_namespace: "pia-{{ resource_prefix }}-ns" +k8s_service_account: "pia-{{ resource_prefix }}-sa" + +eks_subnets: + - zone: a + cidr: 10.0.1.0/24 + type: private + - zone: b + cidr: 10.0.2.0/24 + type: public + +eks_nodegroup_ami_type: "AL2_x86_64" + +eks_security_groups: + - name: "{{ eks_cluster_name }}-control-plane-sg" + description: "EKS Control Plane Security Group" + rules: + - group_name: "{{ eks_cluster_name }}-workers-sg" + group_desc: "EKS Worker Security Group" + ports: 443 + proto: tcp + rules_egress: + - group_name: "{{ eks_cluster_name }}-workers-sg" + group_desc: "EKS Worker Security Group" + from_port: 1025 + to_port: 65535 + proto: tcp + - name: "{{ eks_cluster_name }}-workers-sg" + description: "EKS Worker Security Group" + rules: + - group_name: "{{ eks_cluster_name }}-workers-sg" + proto: tcp + from_port: 1 + to_port: 65535 + - group_name: "{{ eks_cluster_name }}-control-plane-sg" + ports: 10250 + proto: tcp diff --git a/tests/integration/targets/eks_pod_identity_association/files/eks-nodegroup-trust-policy.json b/tests/integration/targets/eks_pod_identity_association/files/eks-nodegroup-trust-policy.json new file mode 100644 index 00000000000..fa43376a6e4 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/files/eks-nodegroup-trust-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/tests/integration/targets/eks_pod_identity_association/files/eks-pod-identity-association-trust-policy.json b/tests/integration/targets/eks_pod_identity_association/files/eks-pod-identity-association-trust-policy.json new file mode 100644 index 00000000000..60dd799259e --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/files/eks-pod-identity-association-trust-policy.json @@ -0,0 +1,16 @@ +{ + "Version":"2012-10-17", + "Statement": [ + { + "Sid": "AllowEksAuthToAssumeRoleForPodIdentity", + "Effect": "Allow", + "Principal": { + "Service": "pods.eks.amazonaws.com" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + } + ] +} \ No newline at end of file diff --git a/tests/integration/targets/eks_pod_identity_association/files/eks-trust-policy.json b/tests/integration/targets/eks_pod_identity_association/files/eks-trust-policy.json new file mode 100644 index 00000000000..85cfb59dd28 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/files/eks-trust-policy.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/tests/integration/targets/eks_pod_identity_association/tasks/cleanup.yml b/tests/integration/targets/eks_pod_identity_association/tasks/cleanup.yml new file mode 100644 index 00000000000..52bef9ed099 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/tasks/cleanup.yml @@ -0,0 +1,115 @@ +- name: delete policy + amazon.aws.iam_policy: + iam_type: role + iam_name: "{{ pod_identity_association_role.iam_role.role_name }}" + policy_name: 'ansible-test-{{ tiny_prefix }}-eks-pod-identity-association' + state: absent + policy_json: "{{ lookup('template', 'eks-pod-identity-association-policy.json.j2') }}" + ignore_errors: true + +- name: delete IAM role for Pod Identity + amazon.aws.iam_role: + name: 'ansible-test-{{ tiny_prefix }}-eks-pod-identity-association' + assume_role_policy_document: '{{ lookup(''file'',''eks-pod-identity-association-trust-policy.json'') }}' + state: absent + create_instance_profile: no + managed_policies: [] + ignore_errors: true + +- name: delete policy for replacement role + amazon.aws.iam_policy: + iam_type: role + iam_name: "{{ pod_identity_association_role_replacement.iam_role.role_name }}" + policy_name: 'ansible-test-{{ tiny_prefix }}-epia-replacement' + state: absent + policy_json: "{{ lookup('template', 'eks-pod-identity-association-policy.json.j2') }}" + ignore_errors: true + +- name: delete replacement IAM role for Pod Identity + amazon.aws.iam_role: + name: 'ansible-test-{{ tiny_prefix }}-epia-replacement' + assume_role_policy_document: '{{ lookup(''file'',''eks-pod-identity-association-trust-policy.json'') }}' + state: absent + create_instance_profile: no + managed_policies: [] + ignore_errors: true + +- name: remove EKS cluster + community.aws.eks_cluster: + name: '{{ eks_cluster_name }}' + state: absent + wait: true + register: eks_delete + ignore_errors: true +- name: create list of all additional EKS security groups + set_fact: + additional_eks_sg: + - name: '{{ eks_cluster_name }}-workers-sg' + +- name: set all security group rule lists to empty to remove circular dependency + amazon.aws.ec2_security_group: + name: '{{ item.name }}' + description: '{{ item.description }}' + state: present + rules: [] + rules_egress: [] + purge_rules: true + purge_rules_egress: true + vpc_id: '{{ setup_vpc.vpc.id }}' + with_items: '{{ eks_security_groups }}' + ignore_errors: true + +- name: remove security groups + amazon.aws.ec2_security_group: + name: '{{ item.name }}' + state: absent + vpc_id: '{{ setup_vpc.vpc.id }}' + with_items: '{{ eks_security_groups|reverse|list + additional_eks_sg }}' + ignore_errors: true + +- name: Delete securitygroup for node access + amazon.aws.ec2_security_group: + name: 'ansible-test-eks_nodegroup' + description: "SSH access" + vpc_id: '{{ setup_vpc.vpc.id }}' + rules: [] + state: absent + ignore_errors: true + +- name: Delete Keypair for Access to Nodegroup nodes + amazon.aws.ec2_key: + name: "ansible-test-eks_nodegroup" + state: absent + ignore_errors: true + +- name: remove Route Tables + amazon.aws.ec2_vpc_route_table: + state: absent + vpc_id: '{{ setup_vpc.vpc.id }}' + route_table_id: '{{ item }}' + lookup: id + with_items: + - '{{ public_route_table.route_table.route_table_id }}' + ignore_errors: true + +- name: remove setup subnet + amazon.aws.ec2_vpc_subnet: + az: '{{ aws_region }}{{ item.zone }}' + vpc_id: '{{ setup_vpc.vpc.id }}' + cidr: '{{ item.cidr}}' + state: absent + with_items: '{{ eks_subnets }}' + ignore_errors: true + +- name: remove Internet Gateway + amazon.aws.ec2_vpc_igw: + state: absent + vpc_id: '{{ setup_vpc.vpc.id}}' + ignore_errors: true + +- name: remove setup VPC + amazon.aws.ec2_vpc_net: + cidr_block: 10.0.0.0/16 + state: absent + name: '{{ resource_prefix }}_aws_eks' + ignore_errors: true diff --git a/tests/integration/targets/eks_pod_identity_association/tasks/dependecies.yml b/tests/integration/targets/eks_pod_identity_association/tasks/dependecies.yml new file mode 100644 index 00000000000..7e9ec096a66 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/tasks/dependecies.yml @@ -0,0 +1,193 @@ +# Create a EKS Cluster and NodeGroup to test pod identity association +# This space was a copy by eks_nodegroup integration test +- name: ensure IAM instance role exists + amazon.aws.iam_role: + name: ansible-test-{{ tiny_prefix }}-eks_nodegroup-cluster + assume_role_policy_document: '{{ lookup(''file'',''eks-trust-policy.json'') }}' + state: present + create_instance_profile: 'no' + managed_policies: + - AmazonEKSServicePolicy + - AmazonEKSClusterPolicy + register: iam_role + +- name: create a VPC to work in + amazon.aws.ec2_vpc_net: + cidr_block: 10.0.0.0/16 + state: present + name: '{{ resource_prefix }}_aws_eks' + resource_tags: + Name: '{{ resource_prefix }}_aws_eks' + register: setup_vpc + +- name: create subnets + amazon.aws.ec2_vpc_subnet: + az: '{{ aws_region }}{{ item.zone }}' + tags: '{ "Name": "{{ resource_prefix }}_aws_eks-subnet-{{ item.type }}-{{ item.zone }}" }' + vpc_id: '{{ setup_vpc.vpc.id }}' + cidr: '{{ item.cidr }}' + map_public: 'yes' + state: present + register: setup_subnets + with_items: + - '{{ eks_subnets }}' + +- name: create Internet Gateway + amazon.aws.ec2_vpc_igw: + vpc_id: '{{ setup_vpc.vpc.id }}' + state: present + tags: + Name: '{{ resource_prefix }}_IGW' + register: setup_igw + +- name: Set up public subnet route table + community.aws.ec2_vpc_route_table: + vpc_id: '{{ setup_vpc.vpc.id }}' + tags: + Name: "EKS-ng-{{ tiny_prefix }}" + subnets: '{{ setup_subnets.results | map(attribute=''subnet.id'') }}' + routes: + - dest: 0.0.0.0/0 + gateway_id: '{{ setup_igw.gateway_id }}' + register: public_route_table + +- name: create security groups to use for EKS + amazon.aws.ec2_security_group: + name: '{{ item.name }}' + description: '{{ item.description }}' + state: present + rules: '{{ item.rules }}' + rules_egress: '{{ item.rules_egress|default(omit) }}' + vpc_id: '{{ setup_vpc.vpc.id }}' + with_items: '{{ eks_security_groups }}' + register: setup_security_groups + +- name: create EKS cluster + community.aws.eks_cluster: + name: '{{ eks_cluster_name }}' + security_groups: '{{ eks_security_groups | map(attribute=''name'') }}' + subnets: '{{ setup_subnets.results | map(attribute=''subnet.id'') }}' + role_arn: '{{ iam_role.iam_role.arn }}' + version: 1.32 + wait: true + register: eks_create + +- name: check that EKS cluster was created + assert: + that: + - eks_create.name == eks_cluster_name + +# Dependecies to eks nodegroup +- name: create IAM instance role + amazon.aws.iam_role: + name: 'ansible-test-{{ tiny_prefix }}-eks_nodegroup-ng' + assume_role_policy_document: '{{ lookup(''file'',''eks-nodegroup-trust-policy.json'') }}' + state: present + create_instance_profile: no + managed_policies: + - AmazonEKSWorkerNodePolicy + - AmazonEC2ContainerRegistryReadOnly + - AmazonEKS_CNI_Policy + register: iam_role_eks_nodegroup + +- name: Pause a few seconds to ensure IAM role is available to next task + pause: + seconds: 10 + +- name: Create securitygroup for node access + amazon.aws.ec2_security_group: + name: 'ansible-test-eks_nodegroup' + description: "SSH access" + vpc_id: '{{ setup_vpc.vpc.id }}' + rules: + - proto: tcp + ports: + - 22 + cidr_ip: 0.0.0.0/0 + register: securitygroup_eks_nodegroup + +- name: Create Keypair for Access to Nodegroup nodes + amazon.aws.ec2_key: + name: "ansible-test-eks_nodegroup" + register: ec2_key_eks_nodegroup + +- name: create nodegroup + community.aws.eks_nodegroup: + name: '{{ eks_nodegroup_name_a }}' + state: present + cluster_name: '{{ eks_cluster_name }}' + node_role: '{{ iam_role_eks_nodegroup.iam_role.arn }}' + subnets: '{{ setup_subnets.results | map(attribute=''subnet.id'') }}' + scaling_config: + min_size: 1 + max_size: 3 + desired_size: 2 + disk_size: 30 + instance_types: ['t3.small'] + ami_type: '{{ eks_nodegroup_ami_type }}' + update_config: + max_unavailable_percentage: 50 + labels: + 'env': 'test' + taints: + - key: 'env' + value: 'test' + effect: 'PREFER_NO_SCHEDULE' + capacity_type: 'SPOT' + tags: + 'foo': 'bar' + remote_access: + ec2_ssh_key: "{{ ec2_key_eks_nodegroup.key.name }}" + source_sg: + - "{{ securitygroup_eks_nodegroup.group_id }}" + wait: False + register: eks_nodegroup_result + +- name: check that eks_nodegroup is created + assert: + that: + - eks_nodegroup_result.changed + +- name: create IAM role for Pod Identity + amazon.aws.iam_role: + name: 'ansible-test-{{ tiny_prefix }}-eks-pod-identity-association' + assume_role_policy_document: '{{ lookup(''file'',''eks-pod-identity-association-trust-policy.json'') }}' + state: present + create_instance_profile: no + managed_policies: [] + register: pod_identity_association_role + +- name: Create policy from file + amazon.aws.iam_policy: + iam_type: role + iam_name: "{{ pod_identity_association_role.iam_role.role_name }}" + policy_name: 'ansible-test-{{ tiny_prefix }}-eks-pod-identity-association' + state: present + policy_json: "{{ lookup('template', 'eks-pod-identity-association-policy.json.j2') }}" + register: pod_identity_association_policy + +- name: create replacement IAM role for Pod Identity + amazon.aws.iam_role: + name: 'ansible-test-{{ tiny_prefix }}-epia-replacement' + assume_role_policy_document: '{{ lookup(''file'',''eks-pod-identity-association-trust-policy.json'') }}' + state: present + create_instance_profile: no + managed_policies: [] + register: pod_identity_association_role_replacement + +- name: Create policy from file for replacement role + amazon.aws.iam_policy: + iam_type: role + iam_name: "{{ pod_identity_association_role_replacement.iam_role.role_name }}" + policy_name: 'ansible-test-{{ tiny_prefix }}-epia-replacement' + state: present + policy_json: "{{ lookup('template', 'eks-pod-identity-association-policy.json.j2') }}" + register: pod_identity_association_policy_replacement + +- name: Create Kubeconfig cluster file + command: > + aws eks update-kubeconfig --region {{ aws_region }} --name {{ eks_cluster_name }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" \ No newline at end of file diff --git a/tests/integration/targets/eks_pod_identity_association/tasks/full_test.yml b/tests/integration/targets/eks_pod_identity_association/tasks/full_test.yml new file mode 100644 index 00000000000..c09a7902564 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/tasks/full_test.yml @@ -0,0 +1,328 @@ +#### +# Create +- name: Create pod identity association (check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association is created (check mode) + assert: + that: + - pod_identity_result.changed + + +- name: Create pod identity association + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + +- name: check that eks_pod_identity_association is created + assert: + that: + - pod_identity_result.changed + +- name: Create pod identity association (idempotency)(check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association is not changed (idempotency)(check mode) + assert: + that: + - not pod_identity_result.changed + + +- name: Create pod identity association (idempotency) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + +- name: check that eks_pod_identity_association is not changed (idempotency) + assert: + that: + - not pod_identity_result.changed + +## +# info +- name: Get pod identity association info for non existing cluster + community.aws.eks_pod_identity_association_info: + cluster_name: 'NoSuchCluster' + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + register: pod_identity_info_result + ignore_errors: true + +- name: check that eks_pod_identity_association failed + assert: + that: + - not pod_identity_info_result.changed + - pod_identity_info_result is failed + +- name: Get pod identity association info for non existing namespace + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + namespace: "no-such-namespace" + service_account: "{{ k8s_service_account }}" + register: pod_identity_info_result + ignore_errors: true + +- name: check that eks_pod_identity_association failed + assert: + that: + - not pod_identity_info_result.changed + - pod_identity_info_result is failed + +- name: Get pod identity association info for non existing service account + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + namespace: "{{ k8s_namespace }}" + service_account: "no-such-sa" + register: pod_identity_info_result + ignore_errors: true + +- name: check that eks_pod_identity_association failed + assert: + that: + - not pod_identity_info_result.changed + - pod_identity_info_result is failed + +- name: Get pod identity association info for non existing association id + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + association_id: "a-a1b2c3d4e5f6g7h8i" + register: pod_identity_info_result + ignore_errors: true + +- name: check that eks_pod_identity_association failed + assert: + that: + - not pod_identity_info_result.changed + - pod_identity_info_result is failed + +- name: Get pod identity association info with conflicting parameters + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + association_id: "{{ pod_identity_result.association.association_id }}" + register: pod_identity_info_result + ignore_errors: true + +- name: Get pod identity association info with missing parameters + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + namespace: "{{ k8s_namespace }}" + register: pod_identity_info_result + ignore_errors: true + +- name: Get pod identity association info + community.aws.eks_pod_identity_association_info: + cluster_name: '{{ eks_cluster_name }}' + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + register: pod_identity_result_i1 + +- name: check that eks_pod_identity_association_info succeeds and returns same results + assert: + that: + - not pod_identity_result_i1.changed + - pod_identity_result_i1.association.association_id == pod_identity_result.association.association_id + +### +# Update ignored + +- name: Test - attempt to update association with ignored parameter (tags) (check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'bar': 'foo' + state: present + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association did nothing (check mode) + assert: + that: + - not pod_identity_result.changed + +- name: Test - attempt to update association with ignored parameter (tags) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'bar': 'foo' + state: present + register: pod_identity_result + +- name: check that eks_pod_identity_association did nothing + assert: + that: + - not pod_identity_result.changed + +### +# Update +- name: Test - attempt to update association with parameter (role_arn) (check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association is updated (check mode) + assert: + that: + - pod_identity_result.changed + +- name: Test - attempt to update association with parameter (role_arn) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + +- name: check that eks_pod_identity_association is updated + assert: + that: + - pod_identity_result.changed + +- name: Test - attempt to update association with parameter (role_arn) (idempotency)(check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association did nothing (check mode) + assert: + that: + - not pod_identity_result.changed + +- name: Test - attempt to update association with parameter (role_arn) (idempotency) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: present + register: pod_identity_result + +- name: check that eks_pod_identity_association did nothing + assert: + that: + - not pod_identity_result.changed + +### +# Delete + +- name: Delete association (check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: absent + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association is deleted (check mode) + assert: + that: + - pod_identity_result.changed + +- name: Delete association + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: absent + register: pod_identity_result + +- name: check that eks_pod_identity_association is deleted + assert: + that: + - pod_identity_result.changed + +- name: Delete association (idempotency)(check mode) + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: absent + register: pod_identity_result + check_mode: True + +- name: check that eks_pod_identity_association is not changed (check mode) + assert: + that: + - not pod_identity_result.changed + +- name: Delete association + community.aws.eks_pod_identity_association: + cluster_name: '{{ eks_cluster_name }}' + role_arn: "{{ pod_identity_association_role_replacement.iam_role.arn }}" + namespace: "{{ k8s_namespace }}" + service_account: "{{ k8s_service_account }}" + tags: + 'foo': 'bar' + state: absent + register: pod_identity_result + +- name: check that eks_pod_identity_association is not changed + assert: + that: + - not pod_identity_result.changed diff --git a/tests/integration/targets/eks_pod_identity_association/tasks/k8s_depedencies.yml b/tests/integration/targets/eks_pod_identity_association/tasks/k8s_depedencies.yml new file mode 100644 index 00000000000..ba3fa5f56b6 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/tasks/k8s_depedencies.yml @@ -0,0 +1,24 @@ +- name: Create a k8s namespace + kubernetes.core.k8s: + name: "{{ k8s_namespace }}" + api_version: v1 + kind: Namespace + state: present + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" + +- name: Create a ServiceAccount object from an inline definition + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "{{ k8s_service_account }}" + namespace: "{{ k8s_namespace }}" + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default(omit) }}" \ No newline at end of file diff --git a/tests/integration/targets/eks_pod_identity_association/tasks/main.yml b/tests/integration/targets/eks_pod_identity_association/tasks/main.yml new file mode 100644 index 00000000000..078fda697df --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: 'eks_pod_identity_association and eks_pod_identity_association_info integration tests' + collections: + - amazon.aws + - community.aws + - kubernetes.core + module_defaults: + group/aws: + access_key: '{{ aws_access_key }}' + secret_key: '{{ aws_secret_key }}' + session_token: '{{ security_token | default(omit) }}' + region: "{{ aws_region }}" + block: + - include_tasks: dependecies.yml + - include_tasks: k8s_depedencies.yml + - include_tasks: full_test.yml + always: + - include_tasks: cleanup.yml diff --git a/tests/integration/targets/eks_pod_identity_association/templates/eks-pod-identity-association-policy.json.j2 b/tests/integration/targets/eks_pod_identity_association/templates/eks-pod-identity-association-policy.json.j2 new file mode 100644 index 00000000000..0c9dabfff98 --- /dev/null +++ b/tests/integration/targets/eks_pod_identity_association/templates/eks-pod-identity-association-policy.json.j2 @@ -0,0 +1,10 @@ +{ + "Version":"2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::{{ s3_bucket_name }}" + } + ] +} \ No newline at end of file