From 7fa09390b003c12113a146935a365b08fde2cee2 Mon Sep 17 00:00:00 2001 From: Stephen Granger Date: Mon, 29 Jan 2018 15:38:32 -0800 Subject: [PATCH] Elasticbeanstalk app (#31079) * New AWS elasticbeanstalk app module * Integration tests for elasticbeanstalk_app --- .../cloud/amazon/aws_elasticbeanstalk_app.py | 220 ++++++++++++++++++ .../targets/aws_elasticbeanstalk_app/aliases | 2 + .../defaults/main.yml | 5 + .../aws_elasticbeanstalk_app/meta/main.yml | 3 + .../aws_elasticbeanstalk_app/tasks/main.yml | 156 +++++++++++++ 5 files changed, 386 insertions(+) create mode 100644 lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py create mode 100644 test/integration/targets/aws_elasticbeanstalk_app/aliases create mode 100644 test/integration/targets/aws_elasticbeanstalk_app/defaults/main.yml create mode 100644 test/integration/targets/aws_elasticbeanstalk_app/meta/main.yml create mode 100644 test/integration/targets/aws_elasticbeanstalk_app/tasks/main.yml diff --git a/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py b/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py new file mode 100644 index 00000000000..ad581369a0f --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_elasticbeanstalk_app.py @@ -0,0 +1,220 @@ +#!/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_elasticbeanstalk_app + +short_description: create, update, and delete an elastic beanstalk application + +version_added: "2.5" + +description: + - "creates, updates, deletes beanstalk applications if app_name is provided" + +options: + app_name: + description: + - name of the beanstalk application you wish to manage + aliases: [ 'name' ] + description: + description: + - the description of the application + state: + description: + - whether to ensure the application is present or absent + default: present + choices: ['absent','present'] + terminate_by_force: + description: + - when set to true, running environments will be terminated before deleting the application + default: false +author: + - Harpreet Singh (@hsingh) + - Stephen Granger (@viper233) +extends_documentation_fragment: + - aws + - ec2 +''' + +EXAMPLES = ''' +# Create or update an application +- aws_elasticbeanstalk_app: + app_name: Sample_App + description: "Hello World App" + state: present + +# Delete application +- aws_elasticbeanstalk_app: + app_name: Sample_App + state: absent + +''' + +RETURN = ''' +app: + description: beanstalk application + returned: always + type: dict + sample: { + "ApplicationName": "app-name", + "ConfigurationTemplates": [], + "DateCreated": "2016-12-28T14:50:03.185000+00:00", + "DateUpdated": "2016-12-28T14:50:03.185000+00:00", + "Description": "description", + "Versions": [ + "1.0.0", + "1.0.1" + ] + } +output: + description: message indicating what change will occur + returned: in check mode + type: string + sample: App is up-to-date +''' + +try: + from botocore.exceptions import BotoCoreError, ClientError +except ImportError: + pass # handled by AnsibleAWSModule + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info + + +def describe_app(ebs, app_name, module): + apps = list_apps(ebs, app_name, module) + + return None if len(apps) != 1 else apps[0] + + +def list_apps(ebs, app_name, module): + try: + if app_name is not None: + apps = ebs.describe_applications(ApplicationNames=[app_name]) + else: + apps = ebs.describe_applications() + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Could not describe application") + + return apps.get("Applications", []) + + +def check_app(ebs, app, module): + app_name = module.params['app_name'] + description = module.params['description'] + state = module.params['state'] + terminate_by_force = module.params['terminate_by_force'] + + result = {} + + if state == 'present' and app is None: + result = dict(changed=True, output="App would be created") + elif state == 'present' and app.get("Description", None) != description: + result = dict(changed=True, output="App would be updated", app=app) + elif state == 'present' and app.get("Description", None) == description: + result = dict(changed=False, output="App is up-to-date", app=app) + elif state == 'absent' and app is None: + result = dict(changed=False, output="App does not exist", app={}) + elif state == 'absent' and app is not None: + result = dict(changed=True, output="App will be deleted", app=app) + elif state == 'absent' and app is not None and terminate_by_force is True: + result = dict(changed=True, output="Running environments terminated before the App will be deleted", app=app) + + module.exit_json(**result) + + +def filter_empty(**kwargs): + retval = {} + for k, v in kwargs.items(): + if v: + retval[k] = v + return retval + + +def main(): + argument_spec = ec2_argument_spec() + + argument_spec.update( + dict( + app_name=dict(aliases=['name'], type='str', required=False), + description=dict(), + state=dict(choices=['present', 'absent'], default='present'), + terminate_by_force=dict(type='bool', default=False, required=False) + ) + ) + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + + app_name = module.params['app_name'] + description = module.params['description'] + state = module.params['state'] + terminate_by_force = module.params['terminate_by_force'] + + if app_name is None: + module.fail_json(msg='Module parameter "app_name" is required') + + result = {} + region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) + ebs = boto3_conn(module, conn_type='client', resource='elasticbeanstalk', + region=region, endpoint=ec2_url, **aws_connect_params) + + app = describe_app(ebs, app_name, module) + + if module.check_mode: + check_app(ebs, app, module) + module.fail_json(msg='ASSERTION FAILURE: check_app() should not return control.') + + if state == 'present': + if app is None: + try: + create_app = ebs.create_application(**filter_empty(ApplicationName=app_name, + Description=description)) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Could not create application") + + app = describe_app(ebs, app_name, module) + + result = dict(changed=True, app=app) + else: + if app.get("Description", None) != description: + try: + if not description: + ebs.update_application(ApplicationName=app_name) + else: + ebs.update_application(ApplicationName=app_name, Description=description) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Could not update application") + + app = describe_app(ebs, app_name, module) + + result = dict(changed=True, app=app) + else: + result = dict(changed=False, app=app) + + else: + if app is None: + result = dict(changed=False, output='Application not found', app={}) + else: + try: + if terminate_by_force: + # Running environments will be terminated before deleting the application + ebs.delete_application(ApplicationName=app_name, TerminateEnvByForce=terminate_by_force) + else: + ebs.delete_application(ApplicationName=app_name) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg="Cannot terminate app") + + result = dict(changed=True, app=app) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/aws_elasticbeanstalk_app/aliases b/test/integration/targets/aws_elasticbeanstalk_app/aliases new file mode 100644 index 00000000000..ebdf4aa5720 --- /dev/null +++ b/test/integration/targets/aws_elasticbeanstalk_app/aliases @@ -0,0 +1,2 @@ +cloud/aws +posix/ci/cloud/group1/aws diff --git a/test/integration/targets/aws_elasticbeanstalk_app/defaults/main.yml b/test/integration/targets/aws_elasticbeanstalk_app/defaults/main.yml new file mode 100644 index 00000000000..3f38e1a85af --- /dev/null +++ b/test/integration/targets/aws_elasticbeanstalk_app/defaults/main.yml @@ -0,0 +1,5 @@ +--- +# defaults file for aws_elasticbeanstalk_app +app_name: '{{ resource_prefix }}_eb_ansible_test' +description: 'eb_ansible_test app description' +alternate_description: 'eb_ansible_test app alternate_description' diff --git a/test/integration/targets/aws_elasticbeanstalk_app/meta/main.yml b/test/integration/targets/aws_elasticbeanstalk_app/meta/main.yml new file mode 100644 index 00000000000..1f64f1169a9 --- /dev/null +++ b/test/integration/targets/aws_elasticbeanstalk_app/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 diff --git a/test/integration/targets/aws_elasticbeanstalk_app/tasks/main.yml b/test/integration/targets/aws_elasticbeanstalk_app/tasks/main.yml new file mode 100644 index 00000000000..8fb35730e69 --- /dev/null +++ b/test/integration/targets/aws_elasticbeanstalk_app/tasks/main.yml @@ -0,0 +1,156 @@ +--- +# tasks file for aws_elasticbeanstalk_app +- block: + + - name: set connection information for all tasks + 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: test with no parameters + aws_elasticbeanstalk_app: + register: result + ignore_errors: true + + - name: assert failure when called with no parameters + assert: + that: + - 'result.failed' + + # ============================================================ + - name: test create app + aws_elasticbeanstalk_app: + app_name: "{{ app_name }}" + description: "{{ description }}" + state: present + <<: *aws_connection_info + register: result + + - name: assert changed is True + assert: + that: + - result.changed == True + + # ============================================================ + - name: test create when app already exists + aws_elasticbeanstalk_app: + app_name: "{{ app_name }}" + description: "{{ description }}" + state: present + <<: *aws_connection_info + register: result + + - name: assert changed is False since the app already exists + assert: + that: + - result.changed == False + + # ============================================================ + - name: make an update to an existing app + aws_elasticbeanstalk_app: + app_name: "{{ app_name }}" + description: "{{ alternate_description }}" + state: present + <<: *aws_connection_info + register: result + + - name: assert changed is True + assert: + that: + - result.changed == True + +# # ============================================================ +# - name: fail deleting an app that has environments that exist +# aws_elasticbeanstalk_app: +# app_name: "non_app" +# state: absent +# <<: *aws_connection_info +# register: result +# ignore_error: true +# +# - name: assert deleteing app with running environments fail +# assert: +# that: +# - result.changed == False + +# # ============================================================ +# - name: deleting an app that has environments that exist with terminate_by_force True +# aws_elasticbeanstalk_app: +# app_name: "non_app" +# state: absent +# terminate_by_force: True +# <<: *aws_connection_info +# register: result +# +# - name: assert deleteing app with running environments with terminate_by_force True +# assert: +# that: +# - result.changed == True +# + # ============================================================ +# - name: retrieve a list of apps +# aws_elasticbeanstalk_app_facts: +# <<: *aws_connection_info +# register: result + +# - name: assert changed is True +# assert: +# that: +# - result is success + +# # ============================================================ +# - name: deleting an app that has environments that exist with terminate_by_force True +# aws_elasticbeanstalk_app: +# app_name: "non_app" +# state: absent +# terminate_by_force: True +# <<: *aws_connection_info +# register: result +# +# - name: assert deleteing app with running environments with terminate_by_force True +# assert: +# that: +# - result.changed == True +# + # ============================================================ + - name: delete non existent app + aws_elasticbeanstalk_app: + app_name: "non_app" + state: absent + <<: *aws_connection_info + register: result + ignore_error: true + + - name: assert deleteing non existant app fails + assert: + that: + - result.changed == False + - 'result.output.startswith("Application not found")' + + # ============================================================ + - name: delete existing app + aws_elasticbeanstalk_app: + app_name: "{{ app_name }}" + state: absent + <<: *aws_connection_info + register: result + + - name: assert changed is True + assert: + that: + - result.changed == True + + # ============================================================ + + always: + + - name: delete existing app + aws_elasticbeanstalk_app: + app_name: "{{ app_name }}" + state: absent + <<: *aws_connection_info