Refactor get_task_spec using mechanism of polymorphic DSL entities

* Changing the way of getting task spec using polymorphic mechanism,
which is the motivation of this patch.

* Moving the type injection in TaskSpec to WorkflowSpec, the 'type'
param is already there when TaskSpec initializing, and we could also
continue to use that for polymorphic mechanism of TaskSpec, and the
subclasses iteration mechanism can also be removed as well.

* Improving the schema validation for TaskSpec, replace the code logic
of prohibition of co-existance between action and workflow in task spec
with jsonschema restriction.

Change-Id: Ia88ce390298d54afc1ee72ebfb67de9056d9f444
This commit is contained in:
Lingxian Kong 2015-09-09 22:55:06 +08:00
parent ffe6524573
commit 7a12e30d07
8 changed files with 44 additions and 93 deletions

View File

@ -356,8 +356,8 @@ class ExecutionTestsV2(base.TestCase):
self.client.delete_obj('workflows', wf) self.client.delete_obj('workflows', wf)
self.client.workflows = [] self.client.workflows = []
for wf in self.client.executions: for ex in self.client.executions:
self.client.delete_obj('executions', wf) self.client.delete_obj('executions', ex)
self.client.executions = [] self.client.executions = []
super(ExecutionTestsV2, self).tearDown() super(ExecutionTestsV2, self).tearDown()

View File

@ -146,8 +146,7 @@ class TestWorkbooksController(base.FunctionalTest):
) )
self.assertEqual(resp.status_int, 400) self.assertEqual(resp.status_int, 400)
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.body)
"can't be specified both", resp.body)
@mock.patch.object(workbooks, "create_workbook_v2", MOCK_WORKBOOK) @mock.patch.object(workbooks, "create_workbook_v2", MOCK_WORKBOOK)
def test_post(self): def test_post(self):
@ -180,8 +179,7 @@ class TestWorkbooksController(base.FunctionalTest):
) )
self.assertEqual(resp.status_int, 400) self.assertEqual(resp.status_int, 400)
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.body)
"can't be specified both", resp.body)
@mock.patch.object(db_api, "delete_workbook", MOCK_DELETE) @mock.patch.object(db_api, "delete_workbook", MOCK_DELETE)
def test_delete(self): def test_delete(self):
@ -232,8 +230,7 @@ class TestWorkbooksController(base.FunctionalTest):
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertFalse(resp.json['valid']) self.assertFalse(resp.json['valid'])
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.json['error'])
"can't be specified both", resp.json['error'])
def test_validate_dsl_parse_exception(self): def test_validate_dsl_parse_exception(self):
resp = self.app.post( resp = self.app.post(

View File

@ -221,8 +221,7 @@ class TestWorkflowsController(base.FunctionalTest):
) )
self.assertEqual(resp.status_int, 400) self.assertEqual(resp.status_int, 400)
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.body)
"can't be specified both", resp.body)
@mock.patch.object(db_api, "create_workflow_definition") @mock.patch.object(db_api, "create_workflow_definition")
def test_post(self, mock_mtd): def test_post(self, mock_mtd):
@ -264,8 +263,7 @@ class TestWorkflowsController(base.FunctionalTest):
) )
self.assertEqual(resp.status_int, 400) self.assertEqual(resp.status_int, 400)
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.body)
"can't be specified both", resp.body)
@mock.patch.object(db_api, "delete_workflow_definition", MOCK_DELETE) @mock.patch.object(db_api, "delete_workflow_definition", MOCK_DELETE)
def test_delete(self): def test_delete(self):
@ -416,8 +414,7 @@ class TestWorkflowsController(base.FunctionalTest):
self.assertEqual(resp.status_int, 200) self.assertEqual(resp.status_int, 200)
self.assertFalse(resp.json['valid']) self.assertFalse(resp.json['valid'])
self.assertIn("Task properties 'action' and 'workflow' " self.assertIn("Invalid DSL", resp.json['error'])
"can't be specified both", resp.json['error'])
def test_validate_dsl_parse_exception(self): def test_validate_dsl_parse_exception(self):
resp = self.app.post( resp = self.app.post(

View File

@ -15,33 +15,14 @@
from oslo_log import log as logging from oslo_log import log as logging
from mistral import exceptions
from mistral.tests import base
from mistral.tests.unit.workbook.v2 import base as v2_base from mistral.tests.unit.workbook.v2 import base as v2_base
from mistral import utils from mistral import utils
from mistral.workbook.v2 import tasks
from mistral.workbook.v2 import workflows from mistral.workbook.v2 import workflows
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class TaskSpecListTest(base.BaseTest):
def test_get_class(self):
spec_list_cls = tasks.TaskSpecList.get_class('direct')
self.assertIs(spec_list_cls, tasks.DirectWfTaskSpecList)
def test_get_class_notfound(self):
exc = self.assertRaises(
exceptions.NotFoundException,
tasks.TaskSpecList.get_class,
"invalid"
)
self.assertIn("Can not find task list specification", str(exc))
class TaskSpecValidation(v2_base.WorkflowSpecValidationTestCase): class TaskSpecValidation(v2_base.WorkflowSpecValidationTestCase):
def test_type_injection(self): def test_type_injection(self):
tests = [ tests = [

View File

@ -20,7 +20,6 @@ import yaml
from mistral import exceptions as exc from mistral import exceptions as exc
from mistral.tests.unit.workbook.v2 import base from mistral.tests.unit.workbook.v2 import base
from mistral import utils from mistral import utils
from mistral.workbook.v2 import tasks
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -56,8 +55,6 @@ class WorkflowSpecValidation(base.WorkflowSpecValidationTestCase):
self.assertEqual(1, len(wfs_spec.get_workflows())) self.assertEqual(1, len(wfs_spec.get_workflows()))
self.assertEqual('test', wfs_spec.get_workflows()[0].get_name()) self.assertEqual('test', wfs_spec.get_workflows()[0].get_name())
self.assertEqual('direct', wfs_spec.get_workflows()[0].get_type()) self.assertEqual('direct', wfs_spec.get_workflows()[0].get_type())
self.assertIsInstance(wfs_spec.get_workflows()[0].get_tasks(),
tasks.DirectWfTaskSpecList)
def test_direct_workflow_invalid_task(self): def test_direct_workflow_invalid_task(self):
overlay = { overlay = {
@ -102,8 +99,6 @@ class WorkflowSpecValidation(base.WorkflowSpecValidationTestCase):
self.assertEqual(1, len(wfs_spec.get_workflows())) self.assertEqual(1, len(wfs_spec.get_workflows()))
self.assertEqual('test', wfs_spec.get_workflows()[0].get_name()) self.assertEqual('test', wfs_spec.get_workflows()[0].get_name())
self.assertEqual('reverse', wfs_spec.get_workflows()[0].get_type()) self.assertEqual('reverse', wfs_spec.get_workflows()[0].get_type())
self.assertIsInstance(wfs_spec.get_workflows()[0].get_tasks(),
tasks.ReverseWfTaskSpecList)
def test_reverse_workflow_invalid_task(self): def test_reverse_workflow_invalid_task(self):
overlay = {'test': {'type': 'reverse', 'tasks': {}}} overlay = {'test': {'type': 'reverse', 'tasks': {}}}

View File

@ -115,13 +115,6 @@ def get_workflow_list_spec_from_yaml(text):
def get_task_spec(spec_dict): def get_task_spec(spec_dict):
if _get_spec_version(spec_dict) == V2_0: if _get_spec_version(spec_dict) == V2_0:
workflow_type = spec_dict.get('type') return base.instantiate_spec(tasks_v2.TaskSpec, spec_dict)
if workflow_type == 'direct':
return tasks_v2.DirectWorkflowTaskSpec(spec_dict)
elif workflow_type == 'reverse':
return tasks_v2.ReverseWorkflowTaskSpec(spec_dict)
else:
raise Exception('Unsupported workflow type "%s".' % workflow_type)
return None return None

View File

@ -33,7 +33,7 @@ WITH_ITEMS_PTRN = re.compile(
class TaskSpec(base.BaseSpec): class TaskSpec(base.BaseSpec):
# See http://json-schema.org # See http://json-schema.org
_type = None _polymorphic_key = ('type', 'direct')
_schema = { _schema = {
"type": "object", "type": "object",
@ -58,7 +58,27 @@ class TaskSpec(base.BaseSpec):
"target": types.NONEMPTY_STRING, "target": types.NONEMPTY_STRING,
"keep-result": types.YAQL_OR_BOOLEAN "keep-result": types.YAQL_OR_BOOLEAN
}, },
"additionalProperties": False "additionalProperties": False,
"anyOf": [
{
"not": {
"type": "object",
"required": ["action", "workflow"]
},
},
{
"oneOf": [
{
"type": "object",
"required": ["action"]
},
{
"type": "object",
"required": ["workflow"]
}
]
}
]
} }
def __init__(self, data): def __init__(self, data):
@ -83,22 +103,16 @@ class TaskSpec(base.BaseSpec):
self._target = data.get('target') self._target = data.get('target')
self._keep_result = data.get('keep-result', True) self._keep_result = data.get('keep-result', True)
self._inject_type()
self._process_action_and_workflow() self._process_action_and_workflow()
def validate_schema(self): def validate_schema(self):
super(TaskSpec, self).validate_schema() super(TaskSpec, self).validate_schema()
self._transform_with_items()
action = self._data.get('action') action = self._data.get('action')
workflow = self._data.get('workflow') workflow = self._data.get('workflow')
if action and workflow:
msg = ("Task properties 'action' and 'workflow' can't be"
" specified both: %s" % self._data)
raise exc.InvalidModelException(msg)
self._transform_with_items()
# Validate YAQL expressions. # Validate YAQL expressions.
if action or workflow: if action or workflow:
inline_params = self._parse_cmd_and_input(action or workflow)[1] inline_params = self._parse_cmd_and_input(action or workflow)[1]
@ -146,10 +160,6 @@ class TaskSpec(base.BaseSpec):
return with_items return with_items
def _inject_type(self):
if self._type:
self._data['type'] = self._type
def _process_action_and_workflow(self): def _process_action_and_workflow(self):
params = {} params = {}
@ -198,7 +208,7 @@ class TaskSpec(base.BaseSpec):
class DirectWorkflowTaskSpec(TaskSpec): class DirectWorkflowTaskSpec(TaskSpec):
_type = 'direct' _polymorphic_value = 'direct'
_on_clause_type = { _on_clause_type = {
"oneOf": [ "oneOf": [
@ -210,7 +220,7 @@ class DirectWorkflowTaskSpec(TaskSpec):
_direct_workflow_schema = { _direct_workflow_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"type": {"enum": [_type]}, "type": {"enum": [_polymorphic_value]},
"join": { "join": {
"oneOf": [ "oneOf": [
{"enum": ["all", "one"]}, {"enum": ["all", "one"]},
@ -270,12 +280,12 @@ class DirectWorkflowTaskSpec(TaskSpec):
class ReverseWorkflowTaskSpec(TaskSpec): class ReverseWorkflowTaskSpec(TaskSpec):
_type = 'reverse' _polymorphic_value = 'reverse'
_reverse_workflow_schema = { _reverse_workflow_schema = {
"type": "object", "type": "object",
"properties": { "properties": {
"type": {"enum": [_type]}, "type": {"enum": [_polymorphic_value]},
"requires": { "requires": {
"oneOf": [types.NONEMPTY_STRING, types.UNIQUE_STRING_LIST] "oneOf": [types.NONEMPTY_STRING, types.UNIQUE_STRING_LIST]
} }
@ -299,28 +309,3 @@ class ReverseWorkflowTaskSpec(TaskSpec):
class TaskSpecList(base.BaseSpecList): class TaskSpecList(base.BaseSpecList):
item_class = TaskSpec item_class = TaskSpec
@staticmethod
def get_class(wf_type):
"""Gets a task specification list class by given workflow type.
:param wf_type: Workflow type
:returns: Task specification list class
"""
for spec_list_cls in utils.iter_subclasses(TaskSpecList):
if wf_type == spec_list_cls.__type__:
return spec_list_cls
msg = ("Can not find task list specification with workflow type:"
" %s" % wf_type)
raise exc.NotFoundException(msg)
class DirectWfTaskSpecList(TaskSpecList):
__type__ = 'direct'
item_class = DirectWorkflowTaskSpec
class ReverseWfTaskSpecList(TaskSpecList):
__type__ = 'reverse'
item_class = ReverseWorkflowTaskSpec

View File

@ -57,10 +57,13 @@ class WorkflowSpec(base.BaseSpec):
'task-defaults', 'task-defaults',
task_defaults.TaskDefaultsSpec task_defaults.TaskDefaultsSpec
) )
self._tasks = self._spec_property(
'tasks', # Inject 'type' here, so instantiate_spec function can recognize the
tasks.TaskSpecList.get_class(self._type) # specific subclass of TaskSpec.
) for task in self._data.get('tasks').itervalues():
task['type'] = self._type
self._tasks = self._spec_property('tasks', tasks.TaskSpecList)
def validate_schema(self): def validate_schema(self):
super(WorkflowSpec, self).validate_schema() super(WorkflowSpec, self).validate_schema()