diff --git a/mistral/api/controllers/v2/resources.py b/mistral/api/controllers/v2/resources.py index c1166b41a..171c7fbf6 100644 --- a/mistral/api/controllers/v2/resources.py +++ b/mistral/api/controllers/v2/resources.py @@ -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): diff --git a/mistral/api/controllers/v2/workbook.py b/mistral/api/controllers/v2/workbook.py index a3e30c64c..2310163ba 100644 --- a/mistral/api/controllers/v2/workbook.py +++ b/mistral/api/controllers/v2/workbook.py @@ -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, " diff --git a/mistral/db/sqlalchemy/migration/alembic_migrations/versions/028_add_namespace_column_to_workbooks.py b/mistral/db/sqlalchemy/migration/alembic_migrations/versions/028_add_namespace_column_to_workbooks.py new file mode 100644 index 000000000..0a77db455 --- /dev/null +++ b/mistral/db/sqlalchemy/migration/alembic_migrations/versions/028_add_namespace_column_to_workbooks.py @@ -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'] + ) diff --git a/mistral/db/v2/api.py b/mistral/db/v2/api.py index eb81c50ce..477914003 100644 --- a/mistral/db/v2/api.py +++ b/mistral/db/v2/api.py @@ -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): diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index 98a41e056..dddf7bb01 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -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() diff --git a/mistral/db/v2/sqlalchemy/models.py b/mistral/db/v2/sqlalchemy/models.py index 1f54eec1c..471e339e3 100644 --- a/mistral/db/v2/sqlalchemy/models.py +++ b/mistral/db/v2/sqlalchemy/models.py @@ -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'), ) diff --git a/mistral/services/workbooks.py b/mistral/services/workbooks.py index 81b068f43..f55333cda 100644 --- a/mistral/services/workbooks.py +++ b/mistral/services/workbooks.py @@ -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 } diff --git a/mistral/services/workflows.py b/mistral/services/workflows.py index 4e6330ca6..c9e9a1d8f 100644 --- a/mistral/services/workflows.py +++ b/mistral/services/workflows.py @@ -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 ) diff --git a/mistral/tests/unit/api/v2/test_workbooks.py b/mistral/tests/unit/api/v2/test_workbooks.py index c961fd456..9058019b7 100644 --- a/mistral/tests/unit/api/v2/test_workbooks.py +++ b/mistral/tests/unit/api/v2/test_workbooks.py @@ -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( diff --git a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py index 960d9431c..176bb10e8 100644 --- a/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py +++ b/mistral/tests/unit/db/v2/test_sqlalchemy_db_api.py @@ -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, ", + "Duplicate entry for WorkbookDefinition " + "['name', 'namespace', 'project_id']:" + " my_workbook1, test, ", 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) diff --git a/mistral/tests/unit/services/test_workbook_service.py b/mistral/tests/unit/services/test_workbook_service.py index f86821773..25abce86c 100644 --- a/mistral/tests/unit/services/test_workbook_service.py +++ b/mistral/tests/unit/services/test_workbook_service.py @@ -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)) diff --git a/releasenotes/notes/namespace_for_workbooks.yaml b/releasenotes/notes/namespace_for_workbooks.yaml new file mode 100644 index 000000000..e6cdfa4f3 --- /dev/null +++ b/releasenotes/notes/namespace_for_workbooks.yaml @@ -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.