diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 7a839fb96..fd5ad3f1e 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6054,20 +6054,20 @@ - [ ] delete_activity - [X] delete_state_machine - [ ] describe_activity -- [ ] describe_execution +- [X] describe_execution - [X] describe_state_machine -- [ ] describe_state_machine_for_execution +- [x] describe_state_machine_for_execution - [ ] get_activity_task - [ ] get_execution_history - [ ] list_activities -- [ ] list_executions +- [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 diff --git a/moto/stepfunctions/exceptions.py b/moto/stepfunctions/exceptions.py index a7c0897a5..133d0cc83 100644 --- a/moto/stepfunctions/exceptions.py +++ b/moto/stepfunctions/exceptions.py @@ -20,6 +20,11 @@ class AccessDeniedException(AWSError): STATUS = 400 +class ExecutionDoesNotExist(AWSError): + CODE = 'ExecutionDoesNotExist' + STATUS = 400 + + class InvalidArn(AWSError): CODE = 'InvalidArn' STATUS = 400 diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py index 8571fbe9b..fd272624f 100644 --- a/moto/stepfunctions/models.py +++ b/moto/stepfunctions/models.py @@ -4,7 +4,8 @@ import re from datetime import datetime from moto.core import BaseBackend from moto.core.utils import iso_8601_datetime_without_milliseconds -from .exceptions import AccessDeniedException, InvalidArn, InvalidName, StateMachineDoesNotExist +from uuid import uuid4 +from .exceptions import AccessDeniedException, ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist class StateMachine(): @@ -17,6 +18,22 @@ class StateMachine(): 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 @@ -44,9 +61,11 @@ class StepFunctionBackend(BaseBackend): u'\u009A', u'\u009B', u'\u009C', u'\u009D', u'\u009E', u'\u009F'] accepted_role_arn_format = re.compile('arn:aws:iam:(?P[0-9]{12}):role/.+') accepted_mchn_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):stateMachine:.+') + accepted_exec_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):execution:.+') def __init__(self, region_name): self.state_machines = [] + self.executions = [] self.region_name = region_name self._account_id = None @@ -77,6 +96,29 @@ class StepFunctionBackend(BaseBackend): 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__ = {} @@ -101,6 +143,12 @@ class StepFunctionBackend(BaseBackend): invalid_msg="Invalid Role Arn: '" + machine_arn + "'", access_denied_msg='User moto is not authorized to access this resource') + 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 + "'", + access_denied_msg='User moto is not authorized to access this resource') + def _validate_arn(self, arn, regex, invalid_msg, access_denied_msg): match = regex.match(arn) if not arn or not match: diff --git a/moto/stepfunctions/responses.py b/moto/stepfunctions/responses.py index d729a5a38..0a170aa57 100644 --- a/moto/stepfunctions/responses.py +++ b/moto/stepfunctions/responses.py @@ -45,8 +45,12 @@ class StepFunctionResponse(BaseResponse): @amzn_request_id def describe_state_machine(self): arn = self._get_param('stateMachineArn') + return self._describe_state_machine(arn) + + @amzn_request_id + def _describe_state_machine(self, state_machine_arn): try: - state_machine = self.stepfunction_backend.describe_state_machine(arn) + state_machine = self.stepfunction_backend.describe_state_machine(state_machine_arn) response = { 'creationDate': state_machine.creation_date, 'stateMachineArn': state_machine.arn, @@ -78,3 +82,57 @@ class StepFunctionResponse(BaseResponse): tags = [] response = {'tags': tags} return 200, {}, json.dumps(response) + + @amzn_request_id + def start_execution(self): + arn = self._get_param('stateMachineArn') + execution = self.stepfunction_backend.start_execution(arn) + response = {'executionArn': execution.execution_arn, + 'startDate': execution.start_date} + return 200, {}, json.dumps(response) + + @amzn_request_id + def list_executions(self): + arn = self._get_param('stateMachineArn') + state_machine = self.stepfunction_backend.describe_state_machine(arn) + executions = self.stepfunction_backend.list_executions(arn) + executions = [{'executionArn': execution.execution_arn, + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': state_machine.arn, + 'status': execution.status} for execution in executions] + return 200, {}, json.dumps({'executions': executions}) + + @amzn_request_id + def describe_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + response = { + 'executionArn': arn, + 'input': '{}', + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': execution.state_machine_arn, + 'status': execution.status, + 'stopDate': execution.stop_date + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def describe_state_machine_for_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + return self._describe_state_machine(execution.state_machine_arn) + except AWSError as err: + return err.response() + + @amzn_request_id + def stop_execution(self): + arn = self._get_param('executionArn') + execution = self.stepfunction_backend.stop_execution(arn) + response = {'stopDate': execution.stop_date} + return 200, {}, json.dumps(response) diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py index 0b9df50a9..bf5c92570 100644 --- a/tests/test_stepfunctions/test_stepfunctions.py +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -157,7 +157,7 @@ def test_state_machine_creation_is_idempotent_by_name(): @mock_stepfunctions @mock_sts -def test_state_machine_creation_can_be_described_by_name(): +def test_state_machine_creation_can_be_described(): client = boto3.client('stepfunctions', region_name=region) # sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) @@ -274,3 +274,139 @@ def test_state_machine_list_tags_for_nonexisting_machine(): response = client.list_tags_for_resource(resourceArn=non_existing_state_machine) tags = response['tags'] tags.should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_start_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + # + execution['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + expected_exec_name = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:name:[a-zA-Z0-9-]+' + execution['executionArn'].should.match(expected_exec_name) + execution['startDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + execution_arn = execution['executionArn'] + execution_name = execution_arn[execution_arn.rindex(':')+1:] + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(1) + executions['executions'][0]['executionArn'].should.equal(execution_arn) + executions['executions'][0]['name'].should.equal(execution_name) + executions['executions'][0]['startDate'].should.equal(execution['startDate']) + executions['executions'][0]['stateMachineArn'].should.equal(sm['stateMachineArn']) + executions['executions'][0]['status'].should.equal('RUNNING') + executions['executions'][0].shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions_when_none_exist(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['executionArn'].should.equal(execution['executionArn']) + description['input'].should.equal("{}") + description['name'].shouldnt.be.empty + description['startDate'].should.equal(execution['startDate']) + description['stateMachineArn'].should.equal(sm['stateMachineArn']) + description['status'].should.equal('RUNNING') + description.shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + client.describe_execution(executionArn=unknown_execution) + exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') + exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_be_described_by_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + desc = client.describe_state_machine_for_execution(executionArn=execution['executionArn']) + desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + desc['definition'].should.equal(str(simple_definition)) + desc['name'].should.equal('name') + desc['roleArn'].should.equal(default_stepfunction_role) + desc['stateMachineArn'].should.equal(sm['stateMachineArn']) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + str(DEFAULT_ACCOUNT_ID) + ':execution:unknown' + client.describe_state_machine_for_execution(executionArn=unknown_execution) + exc.exception.response['Error']['Code'].should.equal('ExecutionDoesNotExist') + exc.exception.response['Error']['Message'].should.equal("Execution Does Not Exist: '" + unknown_execution + "'") + exc.exception.response['ResponseMetadata']['HTTPStatusCode'].should.equal(400) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_stop_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + start = client.start_execution(stateMachineArn=sm['stateMachineArn']) + stop = client.stop_execution(executionArn=start['executionArn']) + print(stop) + # + stop['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + stop['stopDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution_after_stoppage(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=default_stepfunction_role) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + client.stop_execution(executionArn=execution['executionArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['status'].should.equal('SUCCEEDED') + description['stopDate'].should.be.a(datetime)