From 056b035c98e286ac060695034dfee4b07000add1 Mon Sep 17 00:00:00 2001 From: Prasad Katti Date: Tue, 10 Dec 2019 11:08:49 -0800 Subject: [PATCH] add module aws_step_functions_state_machine_execution (#64431) * add module aws_step_functions_state_machine_execution * AWS step functions tests - Use module defaults * Return all attributes from aws api calls as ansible task output * aws_sfn - make start and stop execution idempotent and fix check mode * aws sfn - use build_full_result method of the paginator * aws sfn - remove changes made to help with local debugging --- .../testing_policies/compute-policy.json | 4 + lib/ansible/config/module_defaults.yml | 4 + ..._step_functions_state_machine_execution.py | 197 ++++++++++++++++++ .../aws_step_functions_state_machine/aliases | 1 + .../defaults/main.yml | 1 + .../files/alternative_state_machine.json | 5 + .../tasks/main.yml | 153 ++++++++++++-- 7 files changed, 344 insertions(+), 21 deletions(-) create mode 100644 lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py diff --git a/hacking/aws_config/testing_policies/compute-policy.json b/hacking/aws_config/testing_policies/compute-policy.json index 09c9e0307a8..e2652d9adab 100644 --- a/hacking/aws_config/testing_policies/compute-policy.json +++ b/hacking/aws_config/testing_policies/compute-policy.json @@ -235,9 +235,13 @@ "Action": [ "states:CreateStateMachine", "states:DeleteStateMachine", + "states:DescribeExecution", "states:DescribeStateMachine", + "states:ListExecutions", "states:ListStateMachines", "states:ListTagsForResource", + "states:StartExecution", + "states:StopExecution", "states:TagResource", "states:UntagResource", "states:UpdateStateMachine" diff --git a/lib/ansible/config/module_defaults.yml b/lib/ansible/config/module_defaults.yml index c00841701d8..9f79e300832 100644 --- a/lib/ansible/config/module_defaults.yml +++ b/lib/ansible/config/module_defaults.yml @@ -101,6 +101,10 @@ groupings: - aws aws_ssm_parameter_store: - aws + aws_step_functions_state_machine: + - aws + aws_step_functions_state_machine_execution: + - aws aws_waf_condition: - aws aws_waf_facts: diff --git a/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py b/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py new file mode 100644 index 00000000000..a6e0d7182dd --- /dev/null +++ b/lib/ansible/modules/cloud/amazon/aws_step_functions_state_machine_execution.py @@ -0,0 +1,197 @@ +#!/usr/bin/python +# Copyright (c) 2019, Prasad Katti (@prasadkatti) +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: aws_step_functions_state_machine_execution + +short_description: Start or stop execution of an AWS Step Functions state machine. + +version_added: "2.10" + +description: + - Start or stop execution of a state machine in AWS Step Functions. + +options: + action: + description: Desired action (start or stop) for a state machine execution. + default: start + choices: [ start, stop ] + type: str + name: + description: Name of the execution. + type: str + execution_input: + description: The JSON input data for the execution. + type: json + default: {} + state_machine_arn: + description: The ARN of the state machine that will be executed. + type: str + execution_arn: + description: The ARN of the execution you wish to stop. + type: str + cause: + description: A detailed explanation of the cause for stopping the execution. + type: str + default: '' + error: + description: The error code of the failure to pass in when stopping the execution. + type: str + default: '' + +extends_documentation_fragment: + - aws + - ec2 + +author: + - Prasad Katti (@prasadkatti) +''' + +EXAMPLES = ''' +- name: Start an execution of a state machine + aws_step_functions_state_machine_execution: + name: an_execution_name + execution_input: '{ "IsHelloWorldExample": true }' + state_machine_arn: "arn:aws:states:us-west-2:682285639423:stateMachine:HelloWorldStateMachine" + +- name: Stop an execution of a state machine + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "arn:aws:states:us-west-2:682285639423:execution:HelloWorldStateMachineCopy:a1e8e2b5-5dfe-d40e-d9e3-6201061047c8" + cause: "cause of task failure" + error: "error code of the failure" +''' + +RETURN = ''' +execution_arn: + description: ARN of the AWS Step Functions state machine execution. + type: str + returned: if action == start and changed == True + sample: "arn:aws:states:us-west-2:682285639423:execution:HelloWorldStateMachineCopy:a1e8e2b5-5dfe-d40e-d9e3-6201061047c8" +start_date: + description: The date the execution is started. + type: str + returned: if action == start and changed == True + sample: "2019-11-02T22:39:49.071000-07:00" +stop_date: + description: The date the execution is stopped. + type: str + returned: if action == stop + sample: "2019-11-02T22:39:49.071000-07:00" +''' + + +from ansible.module_utils.aws.core import AnsibleAWSModule +from ansible.module_utils.ec2 import camel_dict_to_snake_dict + +try: + from botocore.exceptions import ClientError, BotoCoreError +except ImportError: + pass # caught by AnsibleAWSModule + + +def start_execution(module, sfn_client): + ''' + start_execution uses execution name to determine if a previous execution already exists. + If an execution by the provided name exists, call client.start_execution will not be called. + ''' + + state_machine_arn = module.params.get('state_machine_arn') + name = module.params.get('name') + execution_input = module.params.get('execution_input') + + try: + # list_executions is eventually consistent + page_iterators = sfn_client.get_paginator('list_executions').paginate(stateMachineArn=state_machine_arn) + + for execution in page_iterators.build_full_result()['executions']: + if name == execution['name']: + check_mode(module, msg='State machine execution already exists.', changed=False) + module.exit_json(changed=False) + + check_mode(module, msg='State machine execution would be started.', changed=True) + res_execution = sfn_client.start_execution( + stateMachineArn=state_machine_arn, + name=name, + input=execution_input + ) + except (ClientError, BotoCoreError) as e: + if e.response['Error']['Code'] == 'ExecutionAlreadyExists': + # this will never be executed anymore + module.exit_json(changed=False) + module.fail_json_aws(e, msg="Failed to start execution.") + + module.exit_json(changed=True, **camel_dict_to_snake_dict(res_execution)) + + +def stop_execution(module, sfn_client): + + cause = module.params.get('cause') + error = module.params.get('error') + execution_arn = module.params.get('execution_arn') + + try: + # describe_execution is eventually consistent + execution_status = sfn_client.describe_execution(executionArn=execution_arn)['status'] + if execution_status != 'RUNNING': + check_mode(module, msg='State machine execution is not running.', changed=False) + module.exit_json(changed=False) + + check_mode(module, msg='State machine execution would be stopped.', changed=True) + res = sfn_client.stop_execution( + executionArn=execution_arn, + cause=cause, + error=error + ) + except (ClientError, BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to stop execution.") + + module.exit_json(changed=True, **camel_dict_to_snake_dict(res)) + + +def check_mode(module, msg='', changed=False): + if module.check_mode: + module.exit_json(changed=changed, output=msg) + + +def main(): + module_args = dict( + action=dict(choices=['start', 'stop'], default='start'), + name=dict(type='str'), + execution_input=dict(type='json', default={}), + state_machine_arn=dict(type='str'), + cause=dict(type='str', default=''), + error=dict(type='str', default=''), + execution_arn=dict(type='str') + ) + module = AnsibleAWSModule( + argument_spec=module_args, + required_if=[('action', 'start', ['name', 'state_machine_arn']), + ('action', 'stop', ['execution_arn']), + ], + supports_check_mode=True + ) + + sfn_client = module.client('stepfunctions') + + action = module.params.get('action') + if action == "start": + start_execution(module, sfn_client) + else: + stop_execution(module, sfn_client) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/aws_step_functions_state_machine/aliases b/test/integration/targets/aws_step_functions_state_machine/aliases index 6e3860bee23..65b315eb47e 100644 --- a/test/integration/targets/aws_step_functions_state_machine/aliases +++ b/test/integration/targets/aws_step_functions_state_machine/aliases @@ -1,2 +1,3 @@ cloud/aws shippable/aws/group2 +aws_step_functions_state_machine_execution diff --git a/test/integration/targets/aws_step_functions_state_machine/defaults/main.yml b/test/integration/targets/aws_step_functions_state_machine/defaults/main.yml index 15d14b3cf6b..273a0c783b8 100644 --- a/test/integration/targets/aws_step_functions_state_machine/defaults/main.yml +++ b/test/integration/targets/aws_step_functions_state_machine/defaults/main.yml @@ -1,3 +1,4 @@ # the random_num is generated in a set_fact task at the start of the testsuite state_machine_name: "{{ resource_prefix }}_step_functions_state_machine_ansible_test_{{ random_num }}" step_functions_role_name: "ansible-test-sts-{{ resource_prefix }}-step_functions-role" +execution_name: "{{ resource_prefix }}_sfn_execution" diff --git a/test/integration/targets/aws_step_functions_state_machine/files/alternative_state_machine.json b/test/integration/targets/aws_step_functions_state_machine/files/alternative_state_machine.json index 1c1275a6561..7b51bebb1a6 100644 --- a/test/integration/targets/aws_step_functions_state_machine/files/alternative_state_machine.json +++ b/test/integration/targets/aws_step_functions_state_machine/files/alternative_state_machine.json @@ -4,6 +4,11 @@ "HelloWorld": { "Type": "Pass", "Result": "Some other result", + "Next": "Wait" + }, + "Wait": { + "Type": "Wait", + "Seconds": 30, "End": true } } diff --git a/test/integration/targets/aws_step_functions_state_machine/tasks/main.yml b/test/integration/targets/aws_step_functions_state_machine/tasks/main.yml index fb5b67624e6..0a28ca3624c 100644 --- a/test/integration/targets/aws_step_functions_state_machine/tasks/main.yml +++ b/test/integration/targets/aws_step_functions_state_machine/tasks/main.yml @@ -1,26 +1,22 @@ --- - name: Integration test for AWS Step Function state machine module + module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" block: # ==== Setup ================================================== - - 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: Create IAM service role needed for Step Functions iam_role: name: "{{ step_functions_role_name }}" description: Role with permissions for AWS Step Functions actions. assume_role_policy_document: "{{ lookup('file', 'state_machines_iam_trust_policy.json') }}" state: present - <<: *aws_connection_info register: step_functions_role - name: Pause a few seconds to ensure IAM role is available to next task @@ -41,7 +37,6 @@ tags: project: helloWorld state: present - <<: *aws_connection_info register: creation_check check_mode: yes @@ -58,7 +53,6 @@ tags: project: helloWorld state: present - <<: *aws_connection_info register: creation_output - assert: @@ -77,7 +71,6 @@ tags: project: helloWorld state: present - <<: *aws_connection_info register: result check_mode: yes @@ -94,7 +87,6 @@ tags: project: helloWorld state: present - <<: *aws_connection_info register: result - assert: @@ -109,7 +101,6 @@ tags: differentTag: different_tag state: present - <<: *aws_connection_info register: update_check check_mode: yes @@ -126,7 +117,6 @@ tags: differentTag: different_tag state: present - <<: *aws_connection_info register: update_output - assert: @@ -134,11 +124,136 @@ - update_output.changed == True - update_output.state_machine_arn == creation_output.state_machine_arn + - name: Start execution of state machine -- check_mode + aws_step_functions_state_machine_execution: + name: "{{ execution_name }}" + execution_input: "{}" + state_machine_arn: "{{ creation_output.state_machine_arn }}" + register: start_execution_output + check_mode: yes + + - assert: + that: + - start_execution_output.changed == True + - "start_execution_output.output == 'State machine execution would be started.'" + + - name: Start execution of state machine + aws_step_functions_state_machine_execution: + name: "{{ execution_name }}" + execution_input: "{}" + state_machine_arn: "{{ creation_output.state_machine_arn }}" + register: start_execution_output + + - assert: + that: + - start_execution_output.changed + - "'execution_arn' in start_execution_output" + - "'start_date' in start_execution_output" + + - name: Start execution of state machine (check for idempotency) (check mode) + aws_step_functions_state_machine_execution: + name: "{{ execution_name }}" + execution_input: "{}" + state_machine_arn: "{{ creation_output.state_machine_arn }}" + register: start_execution_output_idem_check + check_mode: yes + + - assert: + that: + - not start_execution_output_idem_check.changed + - "start_execution_output_idem_check.output == 'State machine execution already exists.'" + + - name: Start execution of state machine (check for idempotency) + aws_step_functions_state_machine_execution: + name: "{{ execution_name }}" + execution_input: "{}" + state_machine_arn: "{{ creation_output.state_machine_arn }}" + register: start_execution_output_idem + + - assert: + that: + - not start_execution_output_idem.changed + + - name: Stop execution of state machine -- check_mode + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "{{ start_execution_output.execution_arn }}" + cause: "cause of the failure" + error: "error code of the failure" + register: stop_execution_output + check_mode: yes + + - assert: + that: + - stop_execution_output.changed + - "stop_execution_output.output == 'State machine execution would be stopped.'" + + - name: Stop execution of state machine + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "{{ start_execution_output.execution_arn }}" + cause: "cause of the failure" + error: "error code of the failure" + register: stop_execution_output + + - assert: + that: + - stop_execution_output.changed + - "'stop_date' in stop_execution_output" + + - name: Stop execution of state machine (check for idempotency) + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "{{ start_execution_output.execution_arn }}" + cause: "cause of the failure" + error: "error code of the failure" + register: stop_execution_output + + - assert: + that: + - not stop_execution_output.changed + + - name: Try stopping a non-running execution -- check_mode + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "{{ start_execution_output.execution_arn }}" + cause: "cause of the failure" + error: "error code of the failure" + register: stop_execution_output + check_mode: yes + + - assert: + that: + - not stop_execution_output.changed + - "stop_execution_output.output == 'State machine execution is not running.'" + + - name: Try stopping a non-running execution + aws_step_functions_state_machine_execution: + action: stop + execution_arn: "{{ start_execution_output.execution_arn }}" + cause: "cause of the failure" + error: "error code of the failure" + register: stop_execution_output + check_mode: yes + + - assert: + that: + - not stop_execution_output.changed + + - name: Start execution of state machine with the same execution name + aws_step_functions_state_machine_execution: + name: "{{ execution_name }}" + state_machine_arn: "{{ creation_output.state_machine_arn }}" + register: start_execution_output_again + + - assert: + that: + - not start_execution_output_again.changed + - name: Remove state machine -- check_mode aws_step_functions_state_machine: name: "{{ state_machine_name }}" state: absent - <<: *aws_connection_info register: deletion_check check_mode: yes @@ -151,7 +266,6 @@ aws_step_functions_state_machine: name: "{{ state_machine_name }}" state: absent - <<: *aws_connection_info register: deletion_output - assert: @@ -163,7 +277,6 @@ aws_step_functions_state_machine: name: "non_existing_state_machine" state: absent - <<: *aws_connection_info register: result - assert: @@ -178,12 +291,10 @@ aws_step_functions_state_machine: name: "{{ state_machine_name }}" state: absent - <<: *aws_connection_info ignore_errors: true - name: Cleanup - delete IAM role needed for Step Functions test iam_role: name: "{{ step_functions_role_name }}" state: absent - <<: *aws_connection_info ignore_errors: true