Add namespace support for workbooks

This patch brings namespace support to workbooks.
Namespace of the workbook is inherited by workflows.

Implements: blueprint mistral-namespace-for-actions-workbooks
Change-Id: I2c66b3961915f0f35a9c468eb6dd0c0c70995234
This commit is contained in:
hardikj 2018-07-13 14:52:09 +05:30
parent 0867becb8f
commit 834747b5d9
12 changed files with 352 additions and 80 deletions

View File

@ -41,6 +41,7 @@ class Workbook(resource.Resource, ScopedResource):
id = wtypes.text id = wtypes.text
name = wtypes.text name = wtypes.text
namespace = wtypes.text
definition = wtypes.text definition = wtypes.text
"workbook definition in Mistral v2 DSL" "workbook definition in Mistral v2 DSL"
@ -62,7 +63,8 @@ class Workbook(resource.Resource, ScopedResource):
scope='private', scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7', project_id='a7eb669e9819420ea4bd1453e672c0a7',
created_at='1970-01-01T00:00:00.000000', created_at='1970-01-01T00:00:00.000000',
updated_at='1970-01-01T00:00:00.000000') updated_at='1970-01-01T00:00:00.000000',
namespace='')
class Workbooks(resource.ResourceList): class Workbooks(resource.ResourceList):

View File

@ -43,11 +43,12 @@ class WorkbooksController(rest.RestController, hooks.HookController):
spec_parser.get_workbook_spec_from_yaml) spec_parser.get_workbook_spec_from_yaml)
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.Workbook, wtypes.text) @wsme_pecan.wsexpose(resources.Workbook, wtypes.text, wtypes.text)
def get(self, name): def get(self, name, namespace=''):
"""Return the named workbook. """Return the named workbook.
:param name: Name of workbook to retrieve :param name: Name of workbook to retrieve
:param namespace: Namespace of workbook to retrieve
""" """
acl.enforce('workbooks:get', context.ctx()) acl.enforce('workbooks:get', context.ctx())
@ -55,13 +56,15 @@ class WorkbooksController(rest.RestController, hooks.HookController):
# Use retries to prevent possible failures. # Use retries to prevent possible failures.
r = rest_utils.create_db_retry_object() r = rest_utils.create_db_retry_object()
db_model = r.call(db_api.get_workbook, name) db_model = r.call(db_api.get_workbook,
name,
namespace=namespace)
return resources.Workbook.from_db_model(db_model) return resources.Workbook.from_db_model(db_model)
@rest_utils.wrap_pecan_controller_exception @rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain") @pecan.expose(content_type="text/plain")
def put(self): def put(self, namespace=''):
"""Update a workbook.""" """Update a workbook."""
acl.enforce('workbooks:update', context.ctx()) acl.enforce('workbooks:update', context.ctx())
@ -73,15 +76,23 @@ class WorkbooksController(rest.RestController, hooks.HookController):
LOG.debug("Update workbook [definition=%s]", definition) LOG.debug("Update workbook [definition=%s]", definition)
wb_db = rest_utils.rest_retry_on_db_error( wb_db = rest_utils.rest_retry_on_db_error(
workbooks.update_workbook_v2 workbooks.update_workbook_v2)(
)(definition, scope=scope) definition,
namespace=namespace,
scope=scope
)
return resources.Workbook.from_db_model(wb_db).to_json() return resources.Workbook.from_db_model(wb_db).to_json()
@rest_utils.wrap_pecan_controller_exception @rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain") @pecan.expose(content_type="text/plain")
def post(self): def post(self, namespace=''):
"""Create a new workbook.""" """Create a new workbook.
:param namespace: Optional. The namespace to create the workbook
in. Workbooks with the same name can be added to a given
project if they are in two different namespaces.
"""
acl.enforce('workbooks:create', context.ctx()) acl.enforce('workbooks:create', context.ctx())
definition = pecan.request.text definition = pecan.request.text
@ -92,16 +103,19 @@ class WorkbooksController(rest.RestController, hooks.HookController):
LOG.debug("Create workbook [definition=%s]", definition) LOG.debug("Create workbook [definition=%s]", definition)
wb_db = rest_utils.rest_retry_on_db_error( wb_db = rest_utils.rest_retry_on_db_error(
workbooks.create_workbook_v2 workbooks.create_workbook_v2)(
)(definition, scope=scope) definition,
namespace=namespace,
scope=scope
)
pecan.response.status = 201 pecan.response.status = 201
return resources.Workbook.from_db_model(wb_db).to_json() return resources.Workbook.from_db_model(wb_db).to_json()
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, name): def delete(self, name, namespace=''):
"""Delete the named workbook. """Delete the named workbook.
:param name: Name of workbook to delete :param name: Name of workbook to delete
@ -110,17 +124,21 @@ class WorkbooksController(rest.RestController, hooks.HookController):
LOG.debug("Delete workbook [name=%s]", name) LOG.debug("Delete workbook [name=%s]", name)
rest_utils.rest_retry_on_db_error(db_api.delete_workbook)(name) rest_utils.rest_retry_on_db_error(db_api.delete_workbook)(
name,
namespace
)
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.Workbooks, types.uuid, int, @wsme_pecan.wsexpose(resources.Workbooks, types.uuid, int,
types.uniquelist, types.list, types.uniquelist, types.uniquelist, types.list, types.uniquelist,
wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text, wtypes.text,
resources.SCOPE_TYPES, wtypes.text, wtypes.text) resources.SCOPE_TYPES, wtypes.text,
wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_keys='created_at', def get_all(self, marker=None, limit=None, sort_keys='created_at',
sort_dirs='asc', fields='', created_at=None, sort_dirs='asc', fields='', created_at=None,
definition=None, name=None, scope=None, tags=None, definition=None, name=None, scope=None, tags=None,
updated_at=None): updated_at=None, namespace=None):
"""Return a list of workbooks. """Return a list of workbooks.
:param marker: Optional. Pagination marker for large data sets. :param marker: Optional. Pagination marker for large data sets.
@ -154,7 +172,8 @@ class WorkbooksController(rest.RestController, hooks.HookController):
name=name, name=name,
scope=scope, scope=scope,
tags=tags, tags=tags,
updated_at=updated_at updated_at=updated_at,
namespace=namespace
) )
LOG.debug("Fetch workbooks. marker=%s, limit=%s, sort_keys=%s, " LOG.debug("Fetch workbooks. marker=%s, limit=%s, sort_keys=%s, "

View File

@ -0,0 +1,54 @@
# Copyright 2018 OpenStack Foundation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""add namespace column to workbooks
Revision ID: 028
Revises: 027
Create Date: 2018-07-17 15:39:25.031935
"""
# revision identifiers, used by Alembic.
revision = '028'
down_revision = '027'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.engine import reflection
def upgrade():
op.add_column(
'workbooks_v2',
sa.Column('namespace', sa.String(length=255), nullable=True)
)
inspect = reflection.Inspector.from_engine(op.get_bind())
unique_constraints = [
unique_constraint['name'] for unique_constraint in
inspect.get_unique_constraints('workbooks_v2')
]
if 'name' in unique_constraints:
op.drop_index('name', table_name='workbooks_v2')
op.create_unique_constraint(
None,
'workbooks_v2',
['name', 'namespace', 'project_id']
)

View File

@ -71,13 +71,13 @@ def acquire_lock(model, id):
# Workbooks. # Workbooks.
def get_workbook(name, fields=()): def get_workbook(name, namespace, fields=()):
return IMPL.get_workbook(name, fields=fields) return IMPL.get_workbook(name, namespace=namespace, fields=fields)
def load_workbook(name, fields=()): def load_workbook(name, namespace, fields=()):
"""Unlike get_workbook this method is allowed to return None.""" """Unlike get_workbook this method is allowed to return None."""
return IMPL.load_workbook(name, fields=fields) return IMPL.load_workbook(name, namespace=namespace, fields=fields)
def get_workbooks(limit=None, marker=None, sort_keys=None, def get_workbooks(limit=None, marker=None, sort_keys=None,
@ -104,8 +104,8 @@ def create_or_update_workbook(name, values):
return IMPL.create_or_update_workbook(name, values) return IMPL.create_or_update_workbook(name, values)
def delete_workbook(name): def delete_workbook(name, namespace=None):
IMPL.delete_workbook(name) IMPL.delete_workbook(name, namespace)
def delete_workbooks(**kwargs): def delete_workbooks(**kwargs):
@ -147,8 +147,8 @@ def create_workflow_definition(values):
return IMPL.create_workflow_definition(values) return IMPL.create_workflow_definition(values)
def update_workflow_definition(identifier, values, namespace): def update_workflow_definition(identifier, values):
return IMPL.update_workflow_definition(identifier, values, namespace) return IMPL.update_workflow_definition(identifier, values)
def create_or_update_workflow_definition(name, values): def create_or_update_workflow_definition(name, values):

View File

@ -319,23 +319,55 @@ def _get_db_object_by_name_and_namespace_or_id(model, identifier,
return query.first() return query.first()
def _get_db_object_by_name_and_namespace(model, name,
namespace, insecure=False,
columns=()):
query = (
b.model_query(model, columns=columns)
if insecure
else _secure_query(model, *columns)
)
if namespace is None:
namespace = ''
query = query.filter(
sa.and_(
model.name == name,
model.namespace == namespace
)
)
return query.first()
# Workbook definitions. # Workbook definitions.
@b.session_aware() @b.session_aware()
def get_workbook(name, fields=(), session=None): def get_workbook(name, namespace=None, fields=(), session=None):
wb = _get_db_object_by_name(models.Workbook, name, columns=fields) wb = _get_db_object_by_name_and_namespace(
models.Workbook,
name,
namespace,
columns=fields
)
if not wb: if not wb:
raise exc.DBEntityNotFoundError( raise exc.DBEntityNotFoundError(
"Workbook not found [workbook_name=%s]" % name "Workbook not found [name=%s, namespace=%s]" % (name, namespace)
) )
return wb return wb
@b.session_aware() @b.session_aware()
def load_workbook(name, fields=(), session=None): def load_workbook(name, namespace=None, fields=(), session=None):
return _get_db_object_by_name(models.Workbook, name, columns=fields) return _get_db_object_by_name_and_namespace(
models.Workbook,
name,
namespace,
columns=fields
)
@b.session_aware() @b.session_aware()
@ -353,8 +385,9 @@ def create_workbook(values, session=None):
wb.save(session=session) wb.save(session=session)
except db_exc.DBDuplicateEntry: except db_exc.DBDuplicateEntry:
raise exc.DBDuplicateEntryError( raise exc.DBDuplicateEntryError(
"Duplicate entry for WorkbookDefinition ['name', 'project_id']: " "Duplicate entry for WorkbookDefinition "
"{}, {}".format(wb.name, wb.project_id) "['name', 'namespace', 'project_id']: {}, {}, {}".format(
wb.name, wb.namespace, wb.project_id)
) )
return wb return wb
@ -362,7 +395,8 @@ def create_workbook(values, session=None):
@b.session_aware() @b.session_aware()
def update_workbook(name, values, session=None): def update_workbook(name, values, session=None):
wb = get_workbook(name) namespace = values.get('namespace')
wb = get_workbook(name, namespace=namespace)
wb.update(values.copy()) wb.update(values.copy())
@ -378,13 +412,20 @@ def create_or_update_workbook(name, values, session=None):
@b.session_aware() @b.session_aware()
def delete_workbook(name, session=None): def delete_workbook(name, namespace=None, session=None):
namespace = namespace or ''
count = _secure_query(models.Workbook).filter( count = _secure_query(models.Workbook).filter(
models.Workbook.name == name).delete() sa.and_(
models.Workbook.name == name,
models.Workbook.namespace == namespace
)
).delete()
if count == 0: if count == 0:
raise exc.DBEntityNotFoundError( raise exc.DBEntityNotFoundError(
"Workbook not found [workbook_name=%s]" % name "Workbook not found [workbook_name=%s, namespace=%s]"
% (name, namespace)
) )
@ -490,7 +531,8 @@ def create_workflow_definition(values, session=None):
@b.session_aware() @b.session_aware()
def update_workflow_definition(identifier, values, namespace='', session=None): def update_workflow_definition(identifier, values, session=None):
namespace = values.get('namespace')
wf_def = get_workflow_definition(identifier, namespace=namespace) wf_def = get_workflow_definition(identifier, namespace=namespace)
m_dbutils.check_db_obj_access(wf_def) m_dbutils.check_db_obj_access(wf_def)
@ -528,10 +570,13 @@ def update_workflow_definition(identifier, values, namespace='', session=None):
@b.session_aware() @b.session_aware()
def create_or_update_workflow_definition(name, values, session=None): def create_or_update_workflow_definition(name, values, session=None):
if not _get_db_object_by_name(models.WorkflowDefinition, name): namespace = values.get('namespace')
return create_workflow_definition(values) if _get_db_object_by_name_and_namespace_or_id(
else: models.WorkflowDefinition,
name,
namespace=namespace):
return update_workflow_definition(name, values) return update_workflow_definition(name, values)
return create_workflow_definition(values)
@b.session_aware() @b.session_aware()

View File

@ -113,9 +113,14 @@ class Workbook(Definition):
"""Contains info about workbook (including definition in Mistral DSL).""" """Contains info about workbook (including definition in Mistral DSL)."""
__tablename__ = 'workbooks_v2' __tablename__ = 'workbooks_v2'
namespace = sa.Column(sa.String(255), nullable=True)
__table_args__ = ( __table_args__ = (
sa.UniqueConstraint('name', 'project_id'), sa.UniqueConstraint(
'name',
'namespace',
'project_id'
),
sa.Index('%s_project_id' % __tablename__, 'project_id'), sa.Index('%s_project_id' % __tablename__, 'project_id'),
sa.Index('%s_scope' % __tablename__, 'scope'), sa.Index('%s_scope' % __tablename__, 'scope'),
) )

View File

@ -17,39 +17,43 @@ from mistral.lang import parser as spec_parser
from mistral.services import actions from mistral.services import actions
def create_workbook_v2(definition, scope='private'): def create_workbook_v2(definition, namespace='', scope='private'):
wb_spec = spec_parser.get_workbook_spec_from_yaml(definition) wb_spec = spec_parser.get_workbook_spec_from_yaml(definition)
wb_values = _get_workbook_values( wb_values = _get_workbook_values(
wb_spec, wb_spec,
definition, definition,
scope scope,
namespace
) )
with db_api_v2.transaction(): with db_api_v2.transaction():
wb_db = db_api_v2.create_workbook(wb_values) wb_db = db_api_v2.create_workbook(wb_values)
_on_workbook_update(wb_db, wb_spec) _on_workbook_update(wb_db, wb_spec, namespace)
return wb_db return wb_db
def update_workbook_v2(definition, scope='private'): def update_workbook_v2(definition, namespace='', scope='private'):
wb_spec = spec_parser.get_workbook_spec_from_yaml(definition) wb_spec = spec_parser.get_workbook_spec_from_yaml(definition)
values = _get_workbook_values(wb_spec, definition, scope) values = _get_workbook_values(wb_spec, definition, scope, namespace)
with db_api_v2.transaction(): with db_api_v2.transaction():
wb_db = db_api_v2.update_workbook(values['name'], values) wb_db = db_api_v2.update_workbook(values['name'], values)
_, db_wfs = _on_workbook_update(wb_db, wb_spec) _, db_wfs = _on_workbook_update(wb_db, wb_spec, namespace)
return wb_db return wb_db
def _on_workbook_update(wb_db, wb_spec): def _on_workbook_update(wb_db, wb_spec, namespace):
# TODO(hardikj) Handle actions for namespace
db_actions = _create_or_update_actions(wb_db, wb_spec.get_actions()) db_actions = _create_or_update_actions(wb_db, wb_spec.get_actions())
db_wfs = _create_or_update_workflows(wb_db, wb_spec.get_workflows()) db_wfs = _create_or_update_workflows(wb_db,
wb_spec.get_workflows(),
namespace)
return db_actions, db_wfs return db_actions, db_wfs
@ -86,7 +90,7 @@ def _create_or_update_actions(wb_db, actions_spec):
return db_actions return db_actions
def _create_or_update_workflows(wb_db, workflows_spec): def _create_or_update_workflows(wb_db, workflows_spec, namespace):
db_wfs = [] db_wfs = []
if workflows_spec: if workflows_spec:
@ -99,7 +103,7 @@ def _create_or_update_workflows(wb_db, workflows_spec):
'spec': wf_spec.to_dict(), 'spec': wf_spec.to_dict(),
'scope': wb_db.scope, 'scope': wb_db.scope,
'project_id': wb_db.project_id, 'project_id': wb_db.project_id,
'namespace': '', 'namespace': namespace,
'tags': wf_spec.get_tags(), 'tags': wf_spec.get_tags(),
'is_system': False 'is_system': False
} }
@ -111,13 +115,14 @@ def _create_or_update_workflows(wb_db, workflows_spec):
return db_wfs return db_wfs
def _get_workbook_values(wb_spec, definition, scope): def _get_workbook_values(wb_spec, definition, scope, namespace=None):
values = { values = {
'name': wb_spec.get_name(), 'name': wb_spec.get_name(),
'tags': wb_spec.get_tags(), 'tags': wb_spec.get_tags(),
'definition': definition, 'definition': definition,
'spec': wb_spec.to_dict(), 'spec': wb_spec.to_dict(),
'scope': scope, 'scope': scope,
'namespace': namespace,
'is_system': False 'is_system': False
} }

View File

@ -165,6 +165,5 @@ def _update_workflow(wf_spec, definition, scope, identifier=None,
return db_api.update_workflow_definition( return db_api.update_workflow_definition(
identifier if identifier else values['name'], identifier if identifier else values['name'],
values, values
namespace=namespace
) )

View File

@ -72,6 +72,17 @@ WORKBOOK = {
'updated_at': '1970-01-01 00:00:00' 'updated_at': '1970-01-01 00:00:00'
} }
WB_WITH_NAMESPACE = {
'id': '123',
'name': 'test',
'namespace': 'xyz',
'definition': WORKBOOK_DEF,
'tags': ['deployment', 'demo'],
'scope': 'public',
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00'
}
ACTION = { ACTION = {
'id': '123e4567-e89b-12d3-a456-426655440000', 'id': '123e4567-e89b-12d3-a456-426655440000',
'name': 'step', 'name': 'step',
@ -95,6 +106,8 @@ ACTION_DB.update(ACTION)
WORKBOOK_DB = models.Workbook() WORKBOOK_DB = models.Workbook()
WORKBOOK_DB.update(WORKBOOK) WORKBOOK_DB.update(WORKBOOK)
WB_DB_WITH_NAMESPACE = models.Workbook(**WB_WITH_NAMESPACE)
WF_DB = models.WorkflowDefinition() WF_DB = models.WorkflowDefinition()
WF_DB.update(WF) WF_DB.update(WF)
@ -139,6 +152,7 @@ workflows:
""" """
MOCK_WORKBOOK = mock.MagicMock(return_value=WORKBOOK_DB) MOCK_WORKBOOK = mock.MagicMock(return_value=WORKBOOK_DB)
MOCK_WB_WITH_NAMESPACE = mock.MagicMock(return_value=WB_DB_WITH_NAMESPACE)
MOCK_WORKBOOKS = mock.MagicMock(return_value=[WORKBOOK_DB]) MOCK_WORKBOOKS = mock.MagicMock(return_value=[WORKBOOK_DB])
MOCK_UPDATED_WORKBOOK = mock.MagicMock(return_value=UPDATED_WORKBOOK_DB) MOCK_UPDATED_WORKBOOK = mock.MagicMock(return_value=UPDATED_WORKBOOK_DB)
MOCK_DELETE = mock.MagicMock(return_value=None) MOCK_DELETE = mock.MagicMock(return_value=None)
@ -155,6 +169,13 @@ class TestWorkbooksController(base.APITest):
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
self.assertDictEqual(WORKBOOK, resp.json) self.assertDictEqual(WORKBOOK, resp.json)
@mock.patch.object(db_api, "get_workbook", MOCK_WB_WITH_NAMESPACE)
def test_get_with_namespace(self):
resp = self.app.get('/v2/workbooks/123?namespace=xyz')
self.assertEqual(200, resp.status_int)
self.assertDictEqual(WB_WITH_NAMESPACE, resp.json)
@mock.patch.object(db_api, 'get_workbook') @mock.patch.object(db_api, 'get_workbook')
def test_get_operational_error(self, mocked_get): def test_get_operational_error(self, mocked_get):
mocked_get.side_effect = [ mocked_get.side_effect = [
@ -258,6 +279,19 @@ class TestWorkbooksController(base.APITest):
self.assertEqual(201, resp.status_int) self.assertEqual(201, resp.status_int)
self.assertEqual(WORKBOOK, resp.json) self.assertEqual(WORKBOOK, resp.json)
@mock.patch.object(workbooks, "create_workbook_v2", MOCK_WB_WITH_NAMESPACE)
def test_post_namespace(self):
namespace = 'xyz'
resp = self.app.post(
'/v2/workbooks?namespace=%s' % namespace,
WORKBOOK_DEF,
headers={'Content-Type': 'text/plain'}
)
self.assertEqual(201, resp.status_int)
self.assertEqual(WB_WITH_NAMESPACE, resp.json)
@mock.patch.object(workbooks, "create_workbook_v2", MOCK_DUPLICATE) @mock.patch.object(workbooks, "create_workbook_v2", MOCK_DUPLICATE)
def test_post_dup(self): def test_post_dup(self):
resp = self.app.post( resp = self.app.post(

View File

@ -37,6 +37,7 @@ ADM_CTX = test_base.get_context(default=False, admin=True)
WORKBOOKS = [ WORKBOOKS = [
{ {
'name': 'my_workbook1', 'name': 'my_workbook1',
'namespace': 'test',
'definition': 'empty', 'definition': 'empty',
'spec': {}, 'spec': {},
'tags': ['mc'], 'tags': ['mc'],
@ -48,6 +49,7 @@ WORKBOOKS = [
}, },
{ {
'name': 'my_workbook2', 'name': 'my_workbook2',
'namespace': 'test',
'description': 'my description', 'description': 'my description',
'definition': 'empty', 'definition': 'empty',
'spec': {}, 'spec': {},
@ -58,6 +60,19 @@ WORKBOOKS = [
'trust_id': '12345', 'trust_id': '12345',
'created_at': datetime.datetime(2016, 12, 1, 15, 1, 0) 'created_at': datetime.datetime(2016, 12, 1, 15, 1, 0)
}, },
{
'name': 'my_workbook3',
'namespace': '',
'description': 'my description',
'definition': 'empty',
'spec': {},
'tags': ['nonamespace'],
'scope': 'private',
'updated_at': None,
'project_id': '1233',
'trust_id': '12345',
'created_at': datetime.datetime(2018, 7, 1, 15, 1, 0)
}
] ]
@ -74,6 +89,19 @@ class WorkbookTest(SQLAlchemyTest):
def test_create_and_get_and_load_workbook(self): def test_create_and_get_and_load_workbook(self):
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created['name'], created['namespace'])
self.assertEqual(created, fetched)
fetched = db_api.load_workbook(created.name, created.namespace)
self.assertEqual(created, fetched)
self.assertIsNone(db_api.load_workbook("not-existing-wb"))
def test_create_and_get_and_load_workbook_with_default_namespace(self):
created = db_api.create_workbook(WORKBOOKS[2])
fetched = db_api.get_workbook(created['name']) fetched = db_api.get_workbook(created['name'])
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
@ -82,14 +110,13 @@ class WorkbookTest(SQLAlchemyTest):
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertIsNone(db_api.load_workbook("not-existing-wb"))
def test_get_workbook_with_fields(self): def test_get_workbook_with_fields(self):
with db_api.transaction(): with db_api.transaction():
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook( fetched = db_api.get_workbook(
created['name'], created['name'],
namespace=created['namespace'],
fields=(db_models.Workbook.scope,) fields=(db_models.Workbook.scope,)
) )
@ -104,8 +131,9 @@ class WorkbookTest(SQLAlchemyTest):
self.assertRaisesWithMessage( self.assertRaisesWithMessage(
exc.DBDuplicateEntryError, exc.DBDuplicateEntryError,
"Duplicate entry for WorkbookDefinition ['name', 'project_id']:" "Duplicate entry for WorkbookDefinition "
" my_workbook1, <default-project>", "['name', 'namespace', 'project_id']:"
" my_workbook1, test, <default-project>",
db_api.create_workbook, db_api.create_workbook,
WORKBOOKS[0] WORKBOOKS[0]
) )
@ -117,20 +145,27 @@ class WorkbookTest(SQLAlchemyTest):
updated = db_api.update_workbook( updated = db_api.update_workbook(
created.name, created.name,
{'definition': 'my new definition'} {
'definition': 'my new definition',
'namespace': 'test'
}
) )
self.assertEqual('my new definition', updated.definition) self.assertEqual('my new definition', updated.definition)
fetched = db_api.get_workbook(created['name']) fetched = db_api.get_workbook(
created['name'],
namespace=created['namespace']
)
self.assertEqual(updated, fetched) self.assertEqual(updated, fetched)
self.assertIsNotNone(fetched.updated_at) self.assertIsNotNone(fetched.updated_at)
def test_create_or_update_workbook(self): def test_create_or_update_workbook(self):
name = WORKBOOKS[0]['name'] name = WORKBOOKS[0]['name']
namespace = WORKBOOKS[0]['namespace']
self.assertIsNone(db_api.load_workbook(name)) self.assertIsNone(db_api.load_workbook(name, namespace=namespace))
created = db_api.create_or_update_workbook( created = db_api.create_or_update_workbook(
name, name,
@ -142,16 +177,19 @@ class WorkbookTest(SQLAlchemyTest):
updated = db_api.create_or_update_workbook( updated = db_api.create_or_update_workbook(
created.name, created.name,
{'definition': 'my new definition'} {
'definition': 'my new definition',
'namespace': 'test'
}
) )
self.assertEqual('my new definition', updated.definition) self.assertEqual('my new definition', updated.definition)
self.assertEqual( self.assertEqual(
'my new definition', 'my new definition',
db_api.load_workbook(updated.name).definition db_api.load_workbook(updated.name, updated.namespace).definition
) )
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, created.namespace)
self.assertEqual(updated, fetched) self.assertEqual(updated, fetched)
@ -331,16 +369,17 @@ class WorkbookTest(SQLAlchemyTest):
def test_delete_workbook(self): def test_delete_workbook(self):
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, created.namespace)
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
db_api.delete_workbook(created.name) db_api.delete_workbook(created.name, created.namespace)
self.assertRaises( self.assertRaises(
exc.DBEntityNotFoundError, exc.DBEntityNotFoundError,
db_api.get_workbook, db_api.get_workbook,
created.name created.name,
created.namespace
) )
def test_workbooks_in_two_projects(self): def test_workbooks_in_two_projects(self):
@ -2714,7 +2753,7 @@ class TXTest(SQLAlchemyTest):
try: try:
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, namespace='test')
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
@ -2736,7 +2775,7 @@ class TXTest(SQLAlchemyTest):
try: try:
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, namespace='test')
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
@ -2747,7 +2786,7 @@ class TXTest(SQLAlchemyTest):
self.assertFalse(self.is_db_session_open()) self.assertFalse(self.is_db_session_open())
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, namespace='test')
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertFalse(self.is_db_session_open()) self.assertFalse(self.is_db_session_open())
@ -2755,14 +2794,14 @@ class TXTest(SQLAlchemyTest):
def test_commit_transaction(self): def test_commit_transaction(self):
with db_api.transaction(): with db_api.transaction():
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, namespace='test')
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
self.assertFalse(self.is_db_session_open()) self.assertFalse(self.is_db_session_open())
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(created.name, namespace='test')
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertFalse(self.is_db_session_open()) self.assertFalse(self.is_db_session_open())
@ -2777,7 +2816,10 @@ class TXTest(SQLAlchemyTest):
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
created_wb = db_api.create_workbook(WORKBOOKS[0]) created_wb = db_api.create_workbook(WORKBOOKS[0])
fetched_wb = db_api.get_workbook(created_wb.name) fetched_wb = db_api.get_workbook(
created_wb.name,
namespace=created_wb.namespace
)
self.assertEqual(created_wb, fetched_wb) self.assertEqual(created_wb, fetched_wb)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
@ -2804,7 +2846,10 @@ class TXTest(SQLAlchemyTest):
try: try:
with db_api.transaction(): with db_api.transaction():
created = db_api.create_workbook(WORKBOOKS[0]) created = db_api.create_workbook(WORKBOOKS[0])
fetched = db_api.get_workbook(created.name) fetched = db_api.get_workbook(
created.name,
namespace=created.namespace
)
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
@ -2831,7 +2876,10 @@ class TXTest(SQLAlchemyTest):
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
created_wb = db_api.create_workbook(WORKBOOKS[0]) created_wb = db_api.create_workbook(WORKBOOKS[0])
fetched_wb = db_api.get_workbook(created_wb.name) fetched_wb = db_api.get_workbook(
created_wb.name,
namespace=created_wb.namespace
)
self.assertEqual(created_wb, fetched_wb) self.assertEqual(created_wb, fetched_wb)
self.assertTrue(self.is_db_session_open()) self.assertTrue(self.is_db_session_open())
@ -2847,7 +2895,10 @@ class TXTest(SQLAlchemyTest):
self.assertEqual(created, fetched) self.assertEqual(created, fetched)
fetched_wb = db_api.get_workbook(created_wb.name) fetched_wb = db_api.get_workbook(
created_wb.name,
namespace=created_wb.namespace
)
self.assertEqual(created_wb, fetched_wb) self.assertEqual(created_wb, fetched_wb)

View File

@ -170,10 +170,13 @@ ACTION_DEFINITION = """concat:
class WorkbookServiceTest(base.DbTestCase): class WorkbookServiceTest(base.DbTestCase):
def test_create_workbook(self): def test_create_workbook(self):
wb_db = wb_service.create_workbook_v2(WORKBOOK) namespace = 'test_workbook_service_0123_namespace'
wb_db = wb_service.create_workbook_v2(WORKBOOK, namespace=namespace)
self.assertIsNotNone(wb_db) self.assertIsNotNone(wb_db)
self.assertEqual('my_wb', wb_db.name) self.assertEqual('my_wb', wb_db.name)
self.assertEqual(namespace, wb_db.namespace)
self.assertEqual(WORKBOOK, wb_db.definition) self.assertEqual(WORKBOOK, wb_db.definition)
self.assertIsNotNone(wb_db.spec) self.assertIsNotNone(wb_db.spec)
self.assertListEqual(['test'], wb_db.tags) self.assertListEqual(['test'], wb_db.tags)
@ -205,6 +208,7 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('reverse', wf1_spec.get_type()) self.assertEqual('reverse', wf1_spec.get_type())
self.assertListEqual(['wf_test'], wf1_spec.get_tags()) self.assertListEqual(['wf_test'], wf1_spec.get_tags())
self.assertListEqual(['wf_test'], wf1_db.tags) self.assertListEqual(['wf_test'], wf1_db.tags)
self.assertEqual(namespace, wf1_db.namespace)
self.assertEqual(WORKBOOK_WF1_DEFINITION, wf1_db.definition) self.assertEqual(WORKBOOK_WF1_DEFINITION, wf1_db.definition)
# Workflow 2. # Workflow 2.
@ -213,20 +217,36 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf2', wf2_spec.get_name()) self.assertEqual('wf2', wf2_spec.get_name())
self.assertEqual('direct', wf2_spec.get_type()) self.assertEqual('direct', wf2_spec.get_type())
self.assertEqual(namespace, wf2_db.namespace)
self.assertEqual(WORKBOOK_WF2_DEFINITION, wf2_db.definition) self.assertEqual(WORKBOOK_WF2_DEFINITION, wf2_db.definition)
def test_update_workbook(self): def test_create_workbook_with_default_namespace(self):
# Create workbook.
wb_db = wb_service.create_workbook_v2(WORKBOOK) wb_db = wb_service.create_workbook_v2(WORKBOOK)
self.assertIsNotNone(wb_db)
self.assertEqual('my_wb', wb_db.name)
self.assertEqual('', wb_db.namespace)
db_api.delete_workbook('my_wb')
def test_update_workbook(self):
namespace = 'test_workbook_service_0123_namespace'
# Create workbook.
wb_db = wb_service.create_workbook_v2(WORKBOOK, namespace=namespace)
self.assertIsNotNone(wb_db) self.assertIsNotNone(wb_db)
self.assertEqual(2, len(db_api.get_workflow_definitions())) self.assertEqual(2, len(db_api.get_workflow_definitions()))
# Update workbook. # Update workbook.
wb_db = wb_service.update_workbook_v2(UPDATED_WORKBOOK) wb_db = wb_service.update_workbook_v2(
UPDATED_WORKBOOK,
namespace=namespace
)
self.assertIsNotNone(wb_db) self.assertIsNotNone(wb_db)
self.assertEqual('my_wb', wb_db.name) self.assertEqual('my_wb', wb_db.name)
self.assertEqual(namespace, wb_db.namespace)
self.assertEqual(UPDATED_WORKBOOK, wb_db.definition) self.assertEqual(UPDATED_WORKBOOK, wb_db.definition)
self.assertListEqual(['test'], wb_db.tags) self.assertListEqual(['test'], wb_db.tags)
@ -240,6 +260,7 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf1', wf1_spec.get_name()) self.assertEqual('wf1', wf1_spec.get_name())
self.assertEqual('direct', wf1_spec.get_type()) self.assertEqual('direct', wf1_spec.get_type())
self.assertEqual(namespace, wf1_db.namespace)
self.assertEqual(UPDATED_WORKBOOK_WF1_DEFINITION, wf1_db.definition) self.assertEqual(UPDATED_WORKBOOK_WF1_DEFINITION, wf1_db.definition)
# Workflow 2. # Workflow 2.
@ -248,4 +269,26 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf2', wf2_spec.get_name()) self.assertEqual('wf2', wf2_spec.get_name())
self.assertEqual('reverse', wf2_spec.get_type()) self.assertEqual('reverse', wf2_spec.get_type())
self.assertEqual(namespace, wf2_db.namespace)
self.assertEqual(UPDATED_WORKBOOK_WF2_DEFINITION, wf2_db.definition) self.assertEqual(UPDATED_WORKBOOK_WF2_DEFINITION, wf2_db.definition)
def test_delete_workbook(self):
namespace = 'pqr'
# Create workbook.
wb_service.create_workbook_v2(WORKBOOK, namespace=namespace)
db_wfs = db_api.get_workflow_definitions()
db_actions = db_api.get_action_definitions(name='my_wb.concat')
self.assertEqual(2, len(db_wfs))
self.assertEqual(1, len(db_actions))
db_api.delete_workbook('my_wb', namespace=namespace)
db_wfs = db_api.get_workflow_definitions()
db_actions = db_api.get_action_definitions(name='my_wb.concat')
# Deleting workbook shouldn't delete workflows and actions
self.assertEqual(2, len(db_wfs))
self.assertEqual(1, len(db_actions))

View File

@ -0,0 +1,15 @@
---
features:
- |
Add support for creating workbooks in a namespace. Creating workbooks
with same name is now possible inside the same project now. This feature
is backward compatible.
All existing workbooks are assumed to be in the default namespace,
represented by an empty string. Also, if a workbook is created without a
namespace specified, it is assumed to be in the default namespace.
When a workbook is created, its namespace is inherited by the
workflows contained within it. All operations on a particular workbook
require combination of name and namespace to uniquely identify a workbook
inside a project.