diff --git a/hacking/aws_config/testing_policies/compute-policy.json b/hacking/aws_config/testing_policies/compute-policy.json index be4c4d0d51..1e7171ce53 100644 --- a/hacking/aws_config/testing_policies/compute-policy.json +++ b/hacking/aws_config/testing_policies/compute-policy.json @@ -212,7 +212,8 @@ "Resource": [ "arn:aws:iam::{{aws_account}}:role/ansible_lambda_role", "arn:aws:iam::{{aws_account}}:role/ecsInstanceRole", - "arn:aws:iam::{{aws_account}}:role/ecsServiceRole" + "arn:aws:iam::{{aws_account}}:role/ecsServiceRole", + "arn:aws:iam::{{aws_account}}:role/aws_eks_cluster_role" ] }, { diff --git a/hacking/aws_config/testing_policies/ecs-policy.json b/hacking/aws_config/testing_policies/container-policy.json similarity index 87% rename from hacking/aws_config/testing_policies/ecs-policy.json rename to hacking/aws_config/testing_policies/container-policy.json index 19db32c8ae..225efc7dfa 100644 --- a/hacking/aws_config/testing_policies/ecs-policy.json +++ b/hacking/aws_config/testing_policies/container-policy.json @@ -56,6 +56,20 @@ "Resource": [ "*" ] + }, + { + "Effect": "Allow", + "Action": [ + "eks:CreateCluster", + "eks:DeleteCluster", + "eks:DescribeCluster", + "eks:ListClusters" + ], + "Resource": [ + "*" + ] } + + ] } diff --git a/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py b/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py new file mode 100644 index 0000000000..c7795484ef --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_eks_cluster.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: aws_eks_cluster +short_description: Manage Elastic Kubernetes Service Clusters +description: + - Manage Elastic Kubernetes Service Clusters +version_added: "2.7" + +author: Will Thames (@willthames) + +options: + name: + description: Name of EKS cluster + required: True + version: + description: Kubernetes version - defaults to latest + role_arn: + description: ARN of IAM role used by the EKS cluster + subnets: + description: list of subnet IDs for the Kubernetes cluster + security_groups: + description: list of security group names or IDs + state: + description: desired state of the EKS cluster + choices: + - absent + - present + default: present + + +requirements: [ 'botocore', 'boto3' ] +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create an EKS cluster + aws_eks_cluster: + name: my_cluster + version: v1.10.0 + role_arn: my_eks_role + subnets: + - subnet-aaaa1111 + security_groups: + - my_eks_sg + - sg-abcd1234 + register: caller_facts + +- name: Remove an EKS cluster + aws_eks_cluster: + name: my_cluster + state: absent +''' + +RETURN = ''' +arn: + description: ARN of the EKS cluster + returned: when state is present + type: string + sample: arn:aws:eks:us-west-2:111111111111:cluster/my-eks-cluster +certificate_authority: + description: Certificate Authority Data for cluster + returned: after creation + type: complex + contains: {} +created_at: + description: Cluster creation date and time + returned: when state is present + type: string + sample: '2018-06-06T11:56:56.242000+00:00' +name: + description: EKS cluster name + returned: when state is present + type: string + sample: my-eks-cluster +resources_vpc_config: + description: VPC configuration of the cluster + returned: when state is present + type: complex + contains: + security_group_ids: + description: List of security group IDs + returned: always + type: list + sample: + - sg-abcd1234 + - sg-aaaa1111 + subnet_ids: + description: List of subnet IDs + returned: always + type: list + sample: + - subnet-abcdef12 + - subnet-345678ab + - subnet-cdef1234 + vpc_id: + description: VPC id + returned: always + type: string + sample: vpc-a1b2c3d4 +role_arn: + description: ARN of the IAM role used by the cluster + returned: when state is present + type: string + sample: arn:aws:iam::111111111111:role/aws_eks_cluster_role +status: + description: status of the EKS cluster + returned: when state is present + type: string + sample: CREATING +version: + description: Kubernetes version of the cluster + returned: when state is present + type: string + sample: '1.10' +''' + + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names + +try: + import botocore.exceptions +except ImportError: + pass # caught by AnsibleAWSModule + + +def ensure_present(client, module): + name = module.params.get('name') + subnets = module.params['subnets'] + groups = module.params['security_groups'] + cluster = get_cluster(client, module) + try: + ec2 = module.client('ec2') + vpc_id = ec2.describe_subnets(SubnetIds=[subnets[0]])['Subnets'][0]['VpcId'] + groups = get_ec2_security_group_ids_from_names(groups, ec2, vpc_id) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Couldn't lookup security groups") + + if cluster: + if set(cluster['resourcesVpcConfig']['subnetIds']) != set(subnets): + module.fail_json(msg="Cannot modify subnets of existing cluster") + if set(cluster['resourcesVpcConfig']['securityGroupIds']) != set(groups): + module.fail_json(msg="Cannot modify security groups of existing cluster") + if module.params.get('version') and module.params.get('version') != cluster['version']: + module.fail_json(msg="Cannot modify version of existing cluster") + module.exit_json(changed=False, **camel_dict_to_snake_dict(cluster)) + + if module.check_mode: + module.exit_json(changed=True) + try: + params = dict(name=name, + roleArn=module.params['role_arn'], + resourcesVpcConfig=dict( + subnetIds=subnets, + securityGroupIds=groups), + clientRequestToken='ansible-create-%s' % name) + if module.params['version']: + params['version'] = module.params['version'] + cluster = client.create_cluster(**params)['cluster'] + except botocore.exceptions.EndpointConnectionError as e: + module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Couldn't create cluster %s" % name) + module.exit_json(changed=True, **camel_dict_to_snake_dict(cluster)) + + +def ensure_absent(client, module): + name = module.params.get('name') + existing = get_cluster(client, module) + if not existing: + module.exit_json(changed=False) + if not module.check_mode: + try: + client.delete_cluster(name=module.params['name']) + except botocore.exceptions.EndpointConnectionError as e: + module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Couldn't delete cluster %s" % name) + module.exit_json(changed=True) + + +def get_cluster(client, module): + name = module.params.get('name') + try: + return client.describe_cluster(name=name)['cluster'] + except client.exceptions.from_code('ResourceNotFoundException'): + return None + except botocore.exceptions.EndpointConnectionError as e: + module.fail_json(msg="Region %s is not supported by EKS" % client.meta.region_name) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json(e, msg="Couldn't get cluster %s" % name) + + +def main(): + argument_spec = dict( + name=dict(required=True), + version=dict(), + role_arn=dict(), + subnets=dict(type='list'), + security_groups=dict(type='list'), + state=dict(choices=['absent', 'present'], default='present'), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + required_if=[['state', 'present', ['role_arn', 'subnets', 'security_groups']]], + supports_check_mode=True, + ) + + if not module.botocore_at_least("1.10.32"): + module.fail_json(msg="aws_eks_cluster module requires botocore >= 1.10.32") + + client = module.client('eks') + + if module.params.get('state') == 'present': + ensure_present(client, module) + else: + ensure_absent(client, module) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/aws_eks/aliases b/test/integration/targets/aws_eks/aliases new file mode 100644 index 0000000000..811b1890f6 --- /dev/null +++ b/test/integration/targets/aws_eks/aliases @@ -0,0 +1,3 @@ +cloud/aws +unsupported +aws_eks_cluster diff --git a/test/integration/targets/aws_eks/playbooks/full_test.yml b/test/integration/targets/aws_eks/playbooks/full_test.yml new file mode 100644 index 0000000000..cd020be571 --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/full_test.yml @@ -0,0 +1,5 @@ +- hosts: localhost + connection: local + + roles: + - aws_eks diff --git a/test/integration/targets/aws_eks/playbooks/old_version.yml b/test/integration/targets/aws_eks/playbooks/old_version.yml new file mode 100644 index 0000000000..d14dabcf00 --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/old_version.yml @@ -0,0 +1,16 @@ +- hosts: localhost + connection: local + + tasks: + - name: try and use aws_eks_cluster module + aws_eks_cluster: + state: absent + name: my_cluster + ignore_errors: yes + register: aws_eks_cluster + + - name: ensure that aws_eks fails with friendly error message + assert: + that: + - '"msg" in aws_eks_cluster' + - aws_eks_cluster is failed diff --git a/test/integration/targets/aws_eks/playbooks/roles/aws_eks/defaults/main.yml b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/defaults/main.yml new file mode 100644 index 0000000000..33c4f4212f --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/defaults/main.yml @@ -0,0 +1,33 @@ +eks_cluster_name: "{{ resource_prefix }}" +eks_subnets: + - zone: a + cidr: 10.0.1.0/24 + - zone: b + cidr: 10.0.2.0/24 + - zone: c + cidr: 10.0.3.0/24 + +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 }}-worker-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/test/integration/targets/aws_eks/playbooks/roles/aws_eks/files/eks-trust-policy.json b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/files/eks-trust-policy.json new file mode 100644 index 0000000000..85cfb59dd2 --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/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/test/integration/targets/aws_eks/playbooks/roles/aws_eks/meta/main.yml b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/meta/main.yml new file mode 100644 index 0000000000..32cf5dda7e --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/test/integration/targets/aws_eks/playbooks/roles/aws_eks/tasks/main.yml b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/tasks/main.yml new file mode 100644 index 0000000000..1c86f76f6a --- /dev/null +++ b/test/integration/targets/aws_eks/playbooks/roles/aws_eks/tasks/main.yml @@ -0,0 +1,182 @@ +--- +# tasks file for aws_eks modules + +- block: + # FIXME: ap-south-1 only has two AZs, ap-south-1a and ap-south-1b + # That makes it my best guess as to it being among the last to support EKS + # If it does become supported, change this test to use an unsupported region + # or if all regions are supported, delete this test + - name: attempt to use eks in unsupported region + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + state: absent + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: ap-south-1 + register: aws_eks_unsupported_region + ignore_errors: yes + + - name: check that aws_eks_cluster did nothing + assert: + that: + - aws_eks_unsupported_region is failed + - '"msg" in aws_eks_unsupported_region' + + - name: set up aws connection info + set_fact: + aws_connection_info: &aws_connection_info + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token }}" + region: "{{ aws_region }}" + no_log: yes + + - name: delete an as yet non-existent EKS cluster + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + state: absent + <<: *aws_connection_info + register: aws_eks_delete_non_existent + + - name: check that aws_eks_cluster did nothing + assert: + that: + - aws_eks_delete_non_existent is not changed + + - name: ensure IAM instance role exists + iam_role: + name: aws_eks_cluster_role + assume_role_policy_document: "{{ lookup('file','eks-trust-policy.json') }}" + state: present + create_instance_profile: no + managed_policies: + - AmazonEKSServicePolicy + - AmazonEKSClusterPolicy + <<: *aws_connection_info + register: iam_role + + - name: create a VPC to work in + ec2_vpc_net: + cidr_block: 10.0.0.0/16 + state: present + name: '{{ resource_prefix }}_aws_eks' + resource_tags: + Name: '{{ resource_prefix }}_aws_eks' + <<: *aws_connection_info + register: setup_vpc + + - name: create subnets + ec2_vpc_subnet: + az: '{{ aws_region }}{{ item.zone }}' + tags: + Name: '{{ resource_prefix }}_aws_eks-subnet-{{ item.zone }}' + vpc_id: '{{ setup_vpc.vpc.id }}' + cidr: "{{ item.cidr }}" + state: present + <<: *aws_connection_info + register: setup_subnets + with_items: + - "{{ eks_subnets }}" + + - name: create security groups to use for EKS + ec2_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 }}' + <<: *aws_connection_info + with_items: "{{ eks_security_groups }}" + register: setup_security_groups + + - name: create EKS cluster + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + security_groups: "{{ eks_security_groups | json_query('[].name') }}" + subnets: "{{ setup_subnets.results | json_query('[].subnet.id') }}" + role_arn: "{{ iam_role.arn }}" + <<: *aws_connection_info + register: eks_create + + - name: check that EKS cluster was created + assert: + that: + - eks_create is changed + - eks_create.name == eks_cluster_name + + - name: create EKS cluster with same details but using SG ids + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + security_groups: "{{ setup_security_groups.results | json_query('[].group_id') }}" + subnets: "{{ setup_subnets.results | json_query('[].subnet.id') }}" + role_arn: "{{ iam_role.arn }}" + <<: *aws_connection_info + register: eks_create + + - name: check that EKS cluster did not change + assert: + that: + - eks_create is not changed + - eks_create.name == eks_cluster_name + + - name: remove EKS cluster + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + state: absent + <<: *aws_connection_info + register: eks_delete + + - name: check that EKS cluster was created + assert: + that: + - eks_delete is changed + + always: + - name: Announce teardown start + debug: + msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" + + - name: remove EKS cluster + aws_eks_cluster: + name: "{{ eks_cluster_name }}" + state: absent + <<: *aws_connection_info + register: eks_delete + ignore_errors: yes + + - debug: + msg: "{{ eks_security_groups|reverse|list }}" + + - name: create list of all additional EKS security groups + set_fact: + additional_eks_sg: + - name: "{{ eks_cluster_name }}-workers-sg" + + - name: remove security groups + ec2_group: + name: '{{ item.name }}' + state: absent + vpc_id: '{{ setup_vpc.vpc.id }}' + <<: *aws_connection_info + with_items: "{{ eks_security_groups|reverse|list + additional_eks_sg }}" + ignore_errors: yes + + - name: remove setup subnet + ec2_vpc_subnet: + az: '{{ aws_region }}{{ item.zone }}' + vpc_id: '{{ setup_vpc.vpc.id }}' + cidr: "{{ item.cidr}}" + state: absent + <<: *aws_connection_info + with_items: "{{ eks_subnets }}" + ignore_errors: yes + + - name: remove setup VPC + ec2_vpc_net: + cidr_block: 10.0.0.0/16 + state: absent + name: '{{ resource_prefix }}_aws_eks' + <<: *aws_connection_info + ignore_errors: yes diff --git a/test/integration/targets/aws_eks/runme.sh b/test/integration/targets/aws_eks/runme.sh new file mode 100755 index 0000000000..fc009446ac --- /dev/null +++ b/test/integration/targets/aws_eks/runme.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# We don't set -u here, due to pypa/virtualenv#150 +set -ex + +MYTMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir') + +trap 'rm -rf "${MYTMPDIR}"' EXIT + +# This is needed for the ubuntu1604py3 tests +# Ubuntu patches virtualenv to make the default python2 +# but for the python3 tests we need virtualenv to use python3 +PYTHON=${ANSIBLE_TEST_PYTHON_INTERPRETER:-python} + +# Test graceful failure for older versions of botocore +virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/botocore-1.7.40" +source "${MYTMPDIR}/botocore-1.7.40/bin/activate" +$PYTHON -m pip install 'botocore<1.10.0' boto3 +ansible-playbook -i ../../inventory -e @../../integration_config.yml -e @../../cloud-config-aws.yml -v playbooks/old_version.yml "$@" + +# Run full test suite +virtualenv --system-site-packages --python "${PYTHON}" "${MYTMPDIR}/botocore-recent" +source "${MYTMPDIR}/botocore-recent/bin/activate" +$PYTHON -m pip install 'botocore>=1.10.1' boto3 +ansible-playbook -i ../../inventory -e @../../integration_config.yml -e @../../cloud-config-aws.yml -v playbooks/full_test.yml "$@"