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
name = wtypes.text
namespace = wtypes.text
definition = wtypes.text
"workbook definition in Mistral v2 DSL"
@ -62,7 +63,8 @@ class Workbook(resource.Resource, ScopedResource):
scope='private',
project_id='a7eb669e9819420ea4bd1453e672c0a7',
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):

View File

@ -43,11 +43,12 @@ class WorkbooksController(rest.RestController, hooks.HookController):
spec_parser.get_workbook_spec_from_yaml)
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(resources.Workbook, wtypes.text)
def get(self, name):
@wsme_pecan.wsexpose(resources.Workbook, wtypes.text, wtypes.text)
def get(self, name, namespace=''):
"""Return the named workbook.
:param name: Name of workbook to retrieve
:param namespace: Namespace of workbook to retrieve
"""
acl.enforce('workbooks:get', context.ctx())
@ -55,13 +56,15 @@ class WorkbooksController(rest.RestController, hooks.HookController):
# Use retries to prevent possible failures.
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)
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain")
def put(self):
def put(self, namespace=''):
"""Update a workbook."""
acl.enforce('workbooks:update', context.ctx())
@ -73,15 +76,23 @@ class WorkbooksController(rest.RestController, hooks.HookController):
LOG.debug("Update workbook [definition=%s]", definition)
wb_db = rest_utils.rest_retry_on_db_error(
workbooks.update_workbook_v2
)(definition, scope=scope)
workbooks.update_workbook_v2)(
definition,
namespace=namespace,
scope=scope
)
return resources.Workbook.from_db_model(wb_db).to_json()
@rest_utils.wrap_pecan_controller_exception
@pecan.expose(content_type="text/plain")
def post(self):
"""Create a new workbook."""
def post(self, namespace=''):
"""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())
definition = pecan.request.text
@ -92,16 +103,19 @@ class WorkbooksController(rest.RestController, hooks.HookController):
LOG.debug("Create workbook [definition=%s]", definition)
wb_db = rest_utils.rest_retry_on_db_error(
workbooks.create_workbook_v2
)(definition, scope=scope)
workbooks.create_workbook_v2)(
definition,
namespace=namespace,
scope=scope
)
pecan.response.status = 201
return resources.Workbook.from_db_model(wb_db).to_json()
@rest_utils.wrap_wsme_controller_exception
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, name):
@wsme_pecan.wsexpose(None, wtypes.text, wtypes.text, status_code=204)
def delete(self, name, namespace=''):
"""Delete the named workbook.
:param name: Name of workbook to delete
@ -110,17 +124,21 @@ class WorkbooksController(rest.RestController, hooks.HookController):
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
@wsme_pecan.wsexpose(resources.Workbooks, types.uuid, int,
types.uniquelist, types.list, types.uniquelist,
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',
sort_dirs='asc', fields='', created_at=None,
definition=None, name=None, scope=None, tags=None,
updated_at=None):
updated_at=None, namespace=None):
"""Return a list of workbooks.
:param marker: Optional. Pagination marker for large data sets.
@ -154,7 +172,8 @@ class WorkbooksController(rest.RestController, hooks.HookController):
name=name,
scope=scope,
tags=tags,
updated_at=updated_at
updated_at=updated_at,
namespace=namespace
)
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.
def get_workbook(name, fields=()):
return IMPL.get_workbook(name, fields=fields)
def get_workbook(name, namespace, 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."""
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,
@ -104,8 +104,8 @@ def create_or_update_workbook(name, values):
return IMPL.create_or_update_workbook(name, values)
def delete_workbook(name):
IMPL.delete_workbook(name)
def delete_workbook(name, namespace=None):
IMPL.delete_workbook(name, namespace)
def delete_workbooks(**kwargs):
@ -147,8 +147,8 @@ def create_workflow_definition(values):
return IMPL.create_workflow_definition(values)
def update_workflow_definition(identifier, values, namespace):
return IMPL.update_workflow_definition(identifier, values, namespace)
def update_workflow_definition(identifier, values):
return IMPL.update_workflow_definition(identifier, 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()
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.
@b.session_aware()
def get_workbook(name, fields=(), session=None):
wb = _get_db_object_by_name(models.Workbook, name, columns=fields)
def get_workbook(name, namespace=None, fields=(), session=None):
wb = _get_db_object_by_name_and_namespace(
models.Workbook,
name,
namespace,
columns=fields
)
if not wb:
raise exc.DBEntityNotFoundError(
"Workbook not found [workbook_name=%s]" % name
"Workbook not found [name=%s, namespace=%s]" % (name, namespace)
)
return wb
@b.session_aware()
def load_workbook(name, fields=(), session=None):
return _get_db_object_by_name(models.Workbook, name, columns=fields)
def load_workbook(name, namespace=None, fields=(), session=None):
return _get_db_object_by_name_and_namespace(
models.Workbook,
name,
namespace,
columns=fields
)
@b.session_aware()
@ -353,8 +385,9 @@ def create_workbook(values, session=None):
wb.save(session=session)
except db_exc.DBDuplicateEntry:
raise exc.DBDuplicateEntryError(
"Duplicate entry for WorkbookDefinition ['name', 'project_id']: "
"{}, {}".format(wb.name, wb.project_id)
"Duplicate entry for WorkbookDefinition "
"['name', 'namespace', 'project_id']: {}, {}, {}".format(
wb.name, wb.namespace, wb.project_id)
)
return wb
@ -362,7 +395,8 @@ def create_workbook(values, session=None):
@b.session_aware()
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())
@ -378,13 +412,20 @@ def create_or_update_workbook(name, values, session=None):
@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(
models.Workbook.name == name).delete()
sa.and_(
models.Workbook.name == name,
models.Workbook.namespace == namespace
)
).delete()
if count == 0:
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()
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)
m_dbutils.check_db_obj_access(wf_def)
@ -528,10 +570,13 @@ def update_workflow_definition(identifier, values, namespace='', session=None):
@b.session_aware()
def create_or_update_workflow_definition(name, values, session=None):
if not _get_db_object_by_name(models.WorkflowDefinition, name):
return create_workflow_definition(values)
else:
namespace = values.get('namespace')
if _get_db_object_by_name_and_namespace_or_id(
models.WorkflowDefinition,
name,
namespace=namespace):
return update_workflow_definition(name, values)
return create_workflow_definition(values)
@b.session_aware()

View File

@ -113,9 +113,14 @@ class Workbook(Definition):
"""Contains info about workbook (including definition in Mistral DSL)."""
__tablename__ = 'workbooks_v2'
namespace = sa.Column(sa.String(255), nullable=True)
__table_args__ = (
sa.UniqueConstraint('name', 'project_id'),
sa.UniqueConstraint(
'name',
'namespace',
'project_id'
),
sa.Index('%s_project_id' % __tablename__, 'project_id'),
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
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_values = _get_workbook_values(
wb_spec,
definition,
scope
scope,
namespace
)
with db_api_v2.transaction():
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
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)
values = _get_workbook_values(wb_spec, definition, scope)
values = _get_workbook_values(wb_spec, definition, scope, namespace)
with db_api_v2.transaction():
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
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_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
@ -86,7 +90,7 @@ def _create_or_update_actions(wb_db, actions_spec):
return db_actions
def _create_or_update_workflows(wb_db, workflows_spec):
def _create_or_update_workflows(wb_db, workflows_spec, namespace):
db_wfs = []
if workflows_spec:
@ -99,7 +103,7 @@ def _create_or_update_workflows(wb_db, workflows_spec):
'spec': wf_spec.to_dict(),
'scope': wb_db.scope,
'project_id': wb_db.project_id,
'namespace': '',
'namespace': namespace,
'tags': wf_spec.get_tags(),
'is_system': False
}
@ -111,13 +115,14 @@ def _create_or_update_workflows(wb_db, workflows_spec):
return db_wfs
def _get_workbook_values(wb_spec, definition, scope):
def _get_workbook_values(wb_spec, definition, scope, namespace=None):
values = {
'name': wb_spec.get_name(),
'tags': wb_spec.get_tags(),
'definition': definition,
'spec': wb_spec.to_dict(),
'scope': scope,
'namespace': namespace,
'is_system': False
}

View File

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

View File

@ -72,6 +72,17 @@ WORKBOOK = {
'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 = {
'id': '123e4567-e89b-12d3-a456-426655440000',
'name': 'step',
@ -95,6 +106,8 @@ ACTION_DB.update(ACTION)
WORKBOOK_DB = models.Workbook()
WORKBOOK_DB.update(WORKBOOK)
WB_DB_WITH_NAMESPACE = models.Workbook(**WB_WITH_NAMESPACE)
WF_DB = models.WorkflowDefinition()
WF_DB.update(WF)
@ -139,6 +152,7 @@ workflows:
"""
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_UPDATED_WORKBOOK = mock.MagicMock(return_value=UPDATED_WORKBOOK_DB)
MOCK_DELETE = mock.MagicMock(return_value=None)
@ -155,6 +169,13 @@ class TestWorkbooksController(base.APITest):
self.assertEqual(200, resp.status_int)
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')
def test_get_operational_error(self, mocked_get):
mocked_get.side_effect = [
@ -258,6 +279,19 @@ class TestWorkbooksController(base.APITest):
self.assertEqual(201, resp.status_int)
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)
def test_post_dup(self):
resp = self.app.post(

View File

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

View File

@ -170,10 +170,13 @@ ACTION_DEFINITION = """concat:
class WorkbookServiceTest(base.DbTestCase):
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.assertEqual('my_wb', wb_db.name)
self.assertEqual(namespace, wb_db.namespace)
self.assertEqual(WORKBOOK, wb_db.definition)
self.assertIsNotNone(wb_db.spec)
self.assertListEqual(['test'], wb_db.tags)
@ -205,6 +208,7 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('reverse', wf1_spec.get_type())
self.assertListEqual(['wf_test'], wf1_spec.get_tags())
self.assertListEqual(['wf_test'], wf1_db.tags)
self.assertEqual(namespace, wf1_db.namespace)
self.assertEqual(WORKBOOK_WF1_DEFINITION, wf1_db.definition)
# Workflow 2.
@ -213,20 +217,36 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf2', wf2_spec.get_name())
self.assertEqual('direct', wf2_spec.get_type())
self.assertEqual(namespace, wf2_db.namespace)
self.assertEqual(WORKBOOK_WF2_DEFINITION, wf2_db.definition)
def test_update_workbook(self):
# Create workbook.
def test_create_workbook_with_default_namespace(self):
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.assertEqual(2, len(db_api.get_workflow_definitions()))
# 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.assertEqual('my_wb', wb_db.name)
self.assertEqual(namespace, wb_db.namespace)
self.assertEqual(UPDATED_WORKBOOK, wb_db.definition)
self.assertListEqual(['test'], wb_db.tags)
@ -240,6 +260,7 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf1', wf1_spec.get_name())
self.assertEqual('direct', wf1_spec.get_type())
self.assertEqual(namespace, wf1_db.namespace)
self.assertEqual(UPDATED_WORKBOOK_WF1_DEFINITION, wf1_db.definition)
# Workflow 2.
@ -248,4 +269,26 @@ class WorkbookServiceTest(base.DbTestCase):
self.assertEqual('wf2', wf2_spec.get_name())
self.assertEqual('reverse', wf2_spec.get_type())
self.assertEqual(namespace, wf2_db.namespace)
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.