Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step Functions - basic method implementation #2409

Merged
merged 5 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions IMPLEMENTATION_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6050,24 +6050,24 @@
## stepfunctions
0% implemented
- [ ] create_activity
- [ ] create_state_machine
- [X] create_state_machine
- [ ] delete_activity
- [ ] delete_state_machine
- [X] delete_state_machine
- [ ] describe_activity
- [ ] describe_execution
- [ ] describe_state_machine
- [ ] describe_state_machine_for_execution
- [X] describe_execution
- [X] describe_state_machine
- [x] describe_state_machine_for_execution
- [ ] get_activity_task
- [ ] get_execution_history
- [ ] list_activities
- [ ] list_executions
- [ ] list_state_machines
- [ ] list_tags_for_resource
- [X] list_executions
- [X] list_state_machines
- [X] list_tags_for_resource
- [ ] send_task_failure
- [ ] send_task_heartbeat
- [ ] send_task_success
- [ ] start_execution
- [ ] stop_execution
- [X] start_execution
- [X] stop_execution
- [ ] tag_resource
- [ ] untag_resource
- [ ] update_state_machine
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ Currently implemented Services:
+---------------------------+-----------------------+------------------------------------+
| SES | @mock_ses | all endpoints done |
+---------------------------+-----------------------+------------------------------------+
| SFN | @mock_stepfunctions | basic endpoints done |
+---------------------------+-----------------------+------------------------------------+
| SNS | @mock_sns | all endpoints done |
+---------------------------+-----------------------+------------------------------------+
| SQS | @mock_sqs | core endpoints done |
Expand Down
1 change: 1 addition & 0 deletions moto/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .secretsmanager import mock_secretsmanager # flake8: noqa
from .sns import mock_sns, mock_sns_deprecated # flake8: noqa
from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa
from .stepfunctions import mock_stepfunctions # flake8: noqa
from .sts import mock_sts, mock_sts_deprecated # flake8: noqa
from .ssm import mock_ssm # flake8: noqa
from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa
Expand Down
2 changes: 2 additions & 0 deletions moto/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from moto.sns import sns_backends
from moto.sqs import sqs_backends
from moto.ssm import ssm_backends
from moto.stepfunctions import stepfunction_backends
from moto.sts import sts_backends
from moto.swf import swf_backends
from moto.xray import xray_backends
Expand Down Expand Up @@ -91,6 +92,7 @@
'sns': sns_backends,
'sqs': sqs_backends,
'ssm': ssm_backends,
'stepfunctions': stepfunction_backends,
'sts': sts_backends,
'swf': swf_backends,
'route53': route53_backends,
Expand Down
6 changes: 6 additions & 0 deletions moto/stepfunctions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from __future__ import unicode_literals
from .models import stepfunction_backends
from ..core.models import base_decorator

stepfunction_backend = stepfunction_backends['us-east-1']
mock_stepfunctions = base_decorator(stepfunction_backends)
35 changes: 35 additions & 0 deletions moto/stepfunctions/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import unicode_literals
import json


class AWSError(Exception):
TYPE = None
STATUS = 400

def __init__(self, message, type=None, status=None):
self.message = message
self.type = type if type is not None else self.TYPE
self.status = status if status is not None else self.STATUS

def response(self):
return json.dumps({'__type': self.type, 'message': self.message}), dict(status=self.status)


class ExecutionDoesNotExist(AWSError):
TYPE = 'ExecutionDoesNotExist'
STATUS = 400


class InvalidArn(AWSError):
TYPE = 'InvalidArn'
STATUS = 400


class InvalidName(AWSError):
TYPE = 'InvalidName'
STATUS = 400


class StateMachineDoesNotExist(AWSError):
TYPE = 'StateMachineDoesNotExist'
STATUS = 400
162 changes: 162 additions & 0 deletions moto/stepfunctions/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import boto
import re
from datetime import datetime
from moto.core import BaseBackend
from moto.core.utils import iso_8601_datetime_without_milliseconds
from moto.sts.models import ACCOUNT_ID
from uuid import uuid4
from .exceptions import ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist


class StateMachine():
def __init__(self, arn, name, definition, roleArn, tags=None):
self.creation_date = iso_8601_datetime_without_milliseconds(datetime.now())
self.arn = arn
self.name = name
self.definition = definition
self.roleArn = roleArn
self.tags = tags


class Execution():
def __init__(self, region_name, account_id, state_machine_name, execution_name, state_machine_arn):
execution_arn = 'arn:aws:states:{}:{}:execution:{}:{}'
execution_arn = execution_arn.format(region_name, account_id, state_machine_name, execution_name)
self.execution_arn = execution_arn
self.name = execution_name
self.start_date = iso_8601_datetime_without_milliseconds(datetime.now())
self.state_machine_arn = state_machine_arn
self.status = 'RUNNING'
self.stop_date = None

def stop(self):
self.status = 'SUCCEEDED'
self.stop_date = iso_8601_datetime_without_milliseconds(datetime.now())


class StepFunctionBackend(BaseBackend):

# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions.html#SFN.Client.create_state_machine
# A name must not contain:
# whitespace
# brackets < > { } [ ]
# wildcard characters ? *
# special characters " # % \ ^ | ~ ` $ & , ; : /
invalid_chars_for_name = [' ', '{', '}', '[', ']', '<', '>',
'?', '*',
'"', '#', '%', '\\', '^', '|', '~', '`', '$', '&', ',', ';', ':', '/']
# control characters (U+0000-001F , U+007F-009F )
invalid_unicodes_for_name = [u'\u0000', u'\u0001', u'\u0002', u'\u0003', u'\u0004',
u'\u0005', u'\u0006', u'\u0007', u'\u0008', u'\u0009',
u'\u000A', u'\u000B', u'\u000C', u'\u000D', u'\u000E', u'\u000F',
u'\u0010', u'\u0011', u'\u0012', u'\u0013', u'\u0014',
u'\u0015', u'\u0016', u'\u0017', u'\u0018', u'\u0019',
u'\u001A', u'\u001B', u'\u001C', u'\u001D', u'\u001E', u'\u001F',
u'\u007F',
u'\u0080', u'\u0081', u'\u0082', u'\u0083', u'\u0084', u'\u0085',
u'\u0086', u'\u0087', u'\u0088', u'\u0089',
u'\u008A', u'\u008B', u'\u008C', u'\u008D', u'\u008E', u'\u008F',
u'\u0090', u'\u0091', u'\u0092', u'\u0093', u'\u0094', u'\u0095',
u'\u0096', u'\u0097', u'\u0098', u'\u0099',
u'\u009A', u'\u009B', u'\u009C', u'\u009D', u'\u009E', u'\u009F']
accepted_role_arn_format = re.compile('arn:aws:iam:(?P<account_id>[0-9]{12}):role/.+')
accepted_mchn_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P<account_id>[0-9]{12}):stateMachine:.+')
accepted_exec_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P<account_id>[0-9]{12}):execution:.+')

def __init__(self, region_name):
self.state_machines = []
self.executions = []
self.region_name = region_name
self._account_id = None

def create_state_machine(self, name, definition, roleArn, tags=None):
self._validate_name(name)
self._validate_role_arn(roleArn)
arn = 'arn:aws:states:' + self.region_name + ':' + str(self._get_account_id()) + ':stateMachine:' + name
try:
return self.describe_state_machine(arn)
except StateMachineDoesNotExist:
state_machine = StateMachine(arn, name, definition, roleArn, tags)
self.state_machines.append(state_machine)
return state_machine

def list_state_machines(self):
return self.state_machines

def describe_state_machine(self, arn):
self._validate_machine_arn(arn)
sm = next((x for x in self.state_machines if x.arn == arn), None)
if not sm:
raise StateMachineDoesNotExist("State Machine Does Not Exist: '" + arn + "'")
return sm

def delete_state_machine(self, arn):
self._validate_machine_arn(arn)
sm = next((x for x in self.state_machines if x.arn == arn), None)
if sm:
self.state_machines.remove(sm)

def start_execution(self, state_machine_arn):
state_machine_name = self.describe_state_machine(state_machine_arn).name
execution = Execution(region_name=self.region_name,
account_id=self._get_account_id(),
state_machine_name=state_machine_name,
execution_name=str(uuid4()),
state_machine_arn=state_machine_arn)
self.executions.append(execution)
return execution

def stop_execution(self, execution_arn):
execution = next((x for x in self.executions if x.execution_arn == execution_arn), None)
if not execution:
raise ExecutionDoesNotExist("Execution Does Not Exist: '" + execution_arn + "'")
execution.stop()
return execution

def list_executions(self, state_machine_arn):
return [execution for execution in self.executions if execution.state_machine_arn == state_machine_arn]

def describe_execution(self, arn):
self._validate_execution_arn(arn)
exctn = next((x for x in self.executions if x.execution_arn == arn), None)
if not exctn:
raise ExecutionDoesNotExist("Execution Does Not Exist: '" + arn + "'")
return exctn

def reset(self):
region_name = self.region_name
self.__dict__ = {}
self.__init__(region_name)

def _validate_name(self, name):
if any(invalid_char in name for invalid_char in self.invalid_chars_for_name):
raise InvalidName("Invalid Name: '" + name + "'")

if any(name.find(char) >= 0 for char in self.invalid_unicodes_for_name):
raise InvalidName("Invalid Name: '" + name + "'")

def _validate_role_arn(self, role_arn):
self._validate_arn(arn=role_arn,
regex=self.accepted_role_arn_format,
invalid_msg="Invalid Role Arn: '" + role_arn + "'")

def _validate_machine_arn(self, machine_arn):
self._validate_arn(arn=machine_arn,
regex=self.accepted_mchn_arn_format,
invalid_msg="Invalid Role Arn: '" + machine_arn + "'")

def _validate_execution_arn(self, execution_arn):
self._validate_arn(arn=execution_arn,
regex=self.accepted_exec_arn_format,
invalid_msg="Execution Does Not Exist: '" + execution_arn + "'")

def _validate_arn(self, arn, regex, invalid_msg):
match = regex.match(arn)
if not arn or not match:
raise InvalidArn(invalid_msg)

def _get_account_id(self):
return ACCOUNT_ID


stepfunction_backends = {_region.name: StepFunctionBackend(_region.name) for _region in boto.awslambda.regions()}
Loading