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:
parent
0867becb8f
commit
834747b5d9
@ -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):
|
||||
|
@ -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, "
|
||||
|
@ -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']
|
||||
)
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
15
releasenotes/notes/namespace_for_workbooks.yaml
Normal file
15
releasenotes/notes/namespace_for_workbooks.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user