Merge "Add 'interface' field to Workflow resource"

This commit is contained in:
Zuul 2020-01-17 12:27:16 +00:00 committed by Gerrit Code Review
commit 5e90c610f8
6 changed files with 172 additions and 30 deletions

View File

@ -29,6 +29,7 @@ SCOPE_TYPES = wtypes.Enum(str, 'private', 'public')
class ScopedResource(object):
"""Utilities for scoped resources"""
@classmethod
def validate_scope(cls, scope):
if scope not in SCOPE_TYPES.values:
@ -60,7 +61,7 @@ class Workbook(resource.Resource, ScopedResource):
return cls(id='123e4567-e89b-12d3-a456-426655440000',
name='book',
definition='HERE GOES'
'WORKBOOK DEFINITION IN MISTRAL DSL v2',
'WORKBOOK DEFINITION IN MISTRAL DSL v2',
tags=['large', 'expensive'],
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
@ -91,9 +92,10 @@ class Workflow(resource.Resource, ScopedResource):
name = wtypes.text
namespace = wtypes.text
input = wtypes.text
interface = types.jsontype
"input and output of the workflow"
definition = wtypes.text
"Workflow definition in Mistral v2 DSL"
"workflow text written in Mistral v2 language"
tags = [wtypes.text]
scope = SCOPE_TYPES
"'private' or 'public'"
@ -108,16 +110,23 @@ class Workflow(resource.Resource, ScopedResource):
name='flow',
input='param1, param2',
definition='HERE GOES'
'WORKFLOW DEFINITION IN MISTRAL DSL v2',
'WORKFLOW DEFINITION IN MISTRAL DSL v2',
tags=['large', 'expensive'],
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000',
namespace='')
namespace='',
interface={"input": ["param1", {"param2": 2}],
"output": []}
)
@classmethod
def _set_input(cls, obj, wf_spec):
def _set_attributes_from_spec(self, wf_spec):
# Sets input and interface fields for the Workflow resource.
self._set_input(wf_spec)
self._set_interface(wf_spec)
def _set_input(self, wf_spec):
input_list = []
if wf_spec:
@ -130,35 +139,45 @@ class Workflow(resource.Resource, ScopedResource):
else:
input_list.append(param)
setattr(obj, 'input', ", ".join(input_list) if input_list else '')
self.input = ", ".join(input_list) if input_list else ''
return obj
def _set_interface(self, wf_spec):
self.interface = {}
if wf_spec:
self.interface['input'] = wf_spec.get('input', [])
self.interface['output'] = [output for output
in wf_spec.get('output', {})]
@classmethod
def from_dict(cls, d):
obj = super(Workflow, cls).from_dict(d)
obj._set_attributes_from_spec(d.get('spec'))
return cls._set_input(obj, d.get('spec'))
return obj
@classmethod
def from_db_model(cls, db_model):
obj = super(Workflow, cls).from_db_model(db_model)
obj._set_attributes_from_spec(db_model.get('spec'))
return cls._set_input(obj, db_model.spec)
return obj
@classmethod
def from_tuples(cls, tuple_iterator):
obj = cls()
spec = None
for col_name, col_val in tuple_iterator:
if hasattr(obj, col_name):
# Convert all datetime values to strings.
setattr(obj, col_name, utils.datetime_to_str(col_val))
if col_name == 'spec':
spec = col_val
if spec:
obj = cls._set_input(obj, spec)
obj._set_attributes_from_spec(spec)
return obj

View File

@ -17,6 +17,7 @@ import datetime
from mistral.api.controllers.v2 import resources
from mistral.db.v2 import api as db_api
from mistral.services import workflows as wf_service
from mistral.tests.unit import base
from mistral_lib import utils
@ -39,6 +40,37 @@ WF_EXEC = {
'some_invalid_field': "foobar"
}
WF = """---
version: '2.0'
WF:
type: direct
input:
- no_default_value_input
- string_param: "string"
- json_param: {
string_param: "string"
}
output:
output1: 'output'
tasks:
task1:
action: std.noop
"""
WF_NO_PARAMS = """---
version: '2.0'
WF_NO_PARAMS:
type: direct
tasks:
task1:
action: std.noop
"""
class TestRestResource(base.DbTestCase):
def test_from_db_model(self):
@ -57,6 +89,38 @@ class TestRestResource(base.DbTestCase):
self.assertDictEqual(expected, wf_ex.to_dict())
def test_from_db_model_workflow_resource(self):
expected_interface = {
'output': ['output1'],
'input': [
'no_default_value_input',
{'string_param': 'string'},
{
'json_param': {'string_param': 'string'}
}
]
}
workflows_list = wf_service.create_workflows(WF)
self.assertEqual(1, len(workflows_list))
wf_resource = resources.Workflow.from_db_model(workflows_list[0])
self.assertDictEqual(expected_interface, wf_resource.interface)
def test_from_db_model_workflow_resource_no_params(self):
expected_interface = {
'input': [],
'output': []
}
workflows_list = wf_service.create_workflows(WF_NO_PARAMS)
self.assertEqual(1, len(workflows_list))
wf_resource = resources.Workflow.from_db_model(workflows_list[0])
self.assertDictEqual(expected_interface, wf_resource.interface)
def test_from_dict(self):
wf_ex = db_api.create_workflow_execution(WF_EXEC)

View File

@ -13,6 +13,7 @@
# limitations under the License.
import datetime
import json
import mock
import pecan
import pecan.testing
@ -70,7 +71,8 @@ WF = {
'definition': WF_DEFINITION,
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00',
'input': 'param1'
'input': 'param1',
'interface': {"input": ["param1"], "output": []}
}
@ -296,8 +298,11 @@ class TestKeyCloakOIDCAuthScenarios(base.DbTestCase):
with mock.patch("jwt.decode", return_value=token):
resp = self.app.get('/v2/workflows/123', headers=headers)
resp_json = resp.json
resp_json['interface'] = json.loads(resp_json['interface'])
self.assertEqual(200, resp.status_code)
self.assertDictEqual(WF, resp.json)
self.assertDictEqual(WF, resp_json)
@mock.patch("requests.get")
@mock.patch.object(db_api, 'get_workflow_definition', MOCK_WF)

View File

@ -16,6 +16,7 @@
import copy
import datetime
import json
import mock
import sqlalchemy as sa
import yaml
@ -59,7 +60,8 @@ WF = {
'definition': WF_DEFINITION,
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00',
'input': 'param1'
'input': 'param1',
'interface': {"output": [], "input": ["param1"]}
}
WF_DB_WITHIN_ABC_NAMESPACE = models.WorkflowDefinition(
@ -79,7 +81,8 @@ WF_WITH_NAMESPACE = {
'definition': WF_DEFINITION,
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00',
'input': 'param1'
'input': 'param1',
'interface': {'input': ['param1'], 'output': []}
}
WF_DEFINITION_WITH_INPUT = """
@ -110,7 +113,11 @@ WF_WITH_DEFAULT_INPUT = {
'definition': WF_DEFINITION_WITH_INPUT,
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00',
'input': 'param1, param2=2'
'input': 'param1, param2=2',
'interface': {
"input": ["param1", {"param2": 2}],
"output": []
}
}
WF_DB_PROJECT_ID = WF_DB.get_clone()
@ -250,8 +257,11 @@ class TestWorkflowsController(base.APITest):
def test_get(self):
resp = self.app.get('/v2/workflows/123')
resp_json = resp.json
resp_json['interface'] = json.loads(resp_json['interface'])
self.assertEqual(200, resp.status_int)
self.assertDictEqual(WF, resp.json)
self.assertDictEqual(WF, resp_json)
@mock.patch.object(db_api, 'get_workflow_definition')
def test_get_operational_error(self, mocked_get):
@ -263,8 +273,11 @@ class TestWorkflowsController(base.APITest):
resp = self.app.get('/v2/workflows/123')
resp_json = resp.json
resp_json['interface'] = json.loads(resp_json['interface'])
self.assertEqual(200, resp.status_int)
self.assertDictEqual(WF, resp.json)
self.assertDictEqual(WF, resp_json)
@mock.patch.object(db_api, "get_workflow_definition", MOCK_WF_WITH_INPUT)
def test_get_with_input(self):
@ -272,8 +285,11 @@ class TestWorkflowsController(base.APITest):
self.maxDiff = None
resp_json = resp.json
resp_json['interface'] = json.loads(resp_json['interface'])
self.assertEqual(200, resp.status_int)
self.assertDictEqual(WF_WITH_DEFAULT_INPUT, resp.json)
self.assertDictEqual(WF_WITH_DEFAULT_INPUT, resp_json)
@mock.patch.object(db_api, "get_workflow_definition", MOCK_NOT_FOUND)
def test_get_not_found(self):
@ -416,6 +432,9 @@ class TestWorkflowsController(base.APITest):
@mock.patch.object(db_api, "update_workflow_definition")
def test_put_multiple(self, mock_mtd):
spec_mock = mock_mtd.return_value.get.return_value
spec_mock.get.return_value = {}
self.app.put(
'/v2/workflows',
WFS_DEFINITION,
@ -499,6 +518,9 @@ class TestWorkflowsController(base.APITest):
@mock.patch.object(db_api, "create_workflow_definition")
def test_post_multiple(self, mock_mtd):
spec_mock = mock_mtd.return_value.get.return_value
spec_mock.get.return_value = {}
self.app.post(
'/v2/workflows',
WFS_DEFINITION,
@ -564,8 +586,11 @@ class TestWorkflowsController(base.APITest):
self.assertEqual(200, resp.status_int)
resp_json = resp.json['workflows'][0]
resp_json['interface'] = json.loads(resp_json['interface'])
self.assertEqual(1, len(resp.json['workflows']))
self.assertDictEqual(WF, resp.json['workflows'][0])
self.assertDictEqual(WF, resp_json)
@mock.patch.object(db_api, 'get_workflow_definitions')
def test_get_all_operational_error(self, mocked_get_all):
@ -577,10 +602,13 @@ class TestWorkflowsController(base.APITest):
resp = self.app.get('/v2/workflows')
self.assertEqual(200, resp.status_int)
resp_workflow_json = resp.json['workflows'][0]
resp_workflow_json['interface'] = \
json.loads(resp_workflow_json['interface'])
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['workflows']))
self.assertDictEqual(WF, resp.json['workflows'][0])
self.assertDictEqual(WF, resp_workflow_json)
@mock.patch.object(db_api, "get_workflow_definitions", MOCK_EMPTY)
def test_get_all_empty(self):
@ -619,8 +647,12 @@ class TestWorkflowsController(base.APITest):
self.assertIn('next', resp.json)
resp_workflow_json = resp.json['workflows'][0]
resp_workflow_json['interface'] = \
json.loads(resp_workflow_json['interface'])
self.assertEqual(1, len(resp.json['workflows']))
self.assertDictEqual(WF, resp.json['workflows'][0])
self.assertDictEqual(WF, resp_workflow_json)
param_dict = utils.get_dict_from_string(
resp.json['next'].split('?')[1],
@ -699,6 +731,17 @@ class TestWorkflowsController(base.APITest):
@mock.patch('mistral.db.v2.api.get_workflow_definitions')
def test_get_all_with_fields_input_filter(self, mock_get_db_wfs):
expected_dict = {
'id': '65df1f59-938f-4c17-bc2a-562524ef5e40',
'input': 'param1, param2=2',
'interface': {
"output": [],
"input": ["param1",
{"param2": 2}
]
}
}
def mock_get_defintions(fields=None, session=None, **kwargs):
if fields and 'input' in fields:
fields.remove('input')
@ -716,12 +759,11 @@ class TestWorkflowsController(base.APITest):
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['workflows']))
expected_dict = {
'id': '65df1f59-938f-4c17-bc2a-562524ef5e40',
'input': 'param1, param2=2'
}
resp_workflow_json = resp.json['workflows'][0]
resp_workflow_json['interface'] = \
json.loads(resp_workflow_json['interface'])
self.assertDictEqual(expected_dict, resp.json['workflows'][0])
self.assertDictEqual(expected_dict, resp_workflow_json)
def test_get_all_with_invalid_field(self):
resp = self.app.get(

View File

@ -580,7 +580,7 @@ class WorkflowDefinitionTest(SQLAlchemyTest):
self.assertEqual(1, len(fetched))
self.assertEqual(created, fetched[0])
def test_filter_workflow_definition_by_not_equal_valiue(self):
def test_filter_workflow_definition_by_not_equal_value(self):
created0 = db_api.create_workflow_definition(WF_DEFINITIONS[0])
created1 = db_api.create_workflow_definition(WF_DEFINITIONS[1])

View File

@ -82,6 +82,9 @@ class TestWorkflowPolicy(base.APITest):
@mock.patch.object(db_api, "create_workflow_definition")
def test_workflow_create_allowed(self, mock_obj):
spec_mock = mock_obj.return_value.get.return_value
spec_mock.get.return_value = {}
self.policy.change_policy_definition(
{"workflows:create": "role:FAKE or rule:admin_or_owner"}
)
@ -111,6 +114,9 @@ class TestWorkflowPolicy(base.APITest):
@mock.patch.object(db_api, "create_workflow_definition")
def test_workflow_create_public_allowed(self, mock_obj):
spec_mock = mock_obj.return_value.get.return_value
spec_mock.get.return_value = {}
self.policy.change_policy_definition({
"workflows:create": "role:FAKE or rule:admin_or_owner",
"workflows:publicize": "role:FAKE or rule:admin_or_owner"
@ -236,6 +242,9 @@ class TestWorkflowPolicy(base.APITest):
@mock.patch.object(db_api, "update_workflow_definition")
def test_workflow_update_allowed(self, mock_obj):
spec_mock = mock_obj.return_value.get.return_value
spec_mock.get.return_value = {}
self.policy.change_policy_definition(
{"workflows:update": "role:FAKE or rule:admin_or_owner"}
)
@ -265,6 +274,9 @@ class TestWorkflowPolicy(base.APITest):
@mock.patch.object(db_api, "update_workflow_definition")
def test_workflow_update_public_allowed(self, mock_obj):
spec_mock = mock_obj.return_value.get.return_value
spec_mock.get.return_value = {}
self.policy.change_policy_definition({
"workflows:update": "role:FAKE or rule:admin_or_owner",
"workflows:publicize": "role:FAKE or rule:admin_or_owner"