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
This commit is contained in:
Prasad Katti 2019-12-10 11:08:49 -08:00 committed by ansibot
parent 95aef88a45
commit 056b035c98
7 changed files with 344 additions and 21 deletions

View file

@ -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"

View file

@ -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:

View file

@ -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()

View file

@ -1,2 +1,3 @@
cloud/aws
shippable/aws/group2
aws_step_functions_state_machine_execution

View file

@ -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"

View file

@ -4,6 +4,11 @@
"HelloWorld": {
"Type": "Pass",
"Result": "Some other result",
"Next": "Wait"
},
"Wait": {
"Type": "Wait",
"Seconds": 30,
"End": true
}
}

View file

@ -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