From e1e75d61ebc3947e10552327740f0d6ff31f0eed Mon Sep 17 00:00:00 2001 From: ali Date: Thu, 2 Jan 2020 15:44:05 +0000 Subject: [PATCH] Add namespace support for actions to client When creating an action-definition user can use the option --namespace to create the action in that specific namespace, if it was not provided the action will be created in the default namespace. default namespace is ''. *Added --namespace to action commands, *Added --namespace option to run-action Depends-On: I07862e30adf28404ec70a473571a9213e53d8a08 Implements: blueprint create-and-run-workflows-within-a-namespace Change-Id: I18dbd9faee06c3cd2209f7e579eeb2e1a24c88d9 --- mistralclient/api/v2/action_executions.py | 6 +- mistralclient/api/v2/actions.py | 24 ++++---- .../commands/v2/action_executions.py | 8 +++ mistralclient/commands/v2/actions.py | 49 +++++++++++++-- .../tests/functional/cli/v2/base_v2.py | 7 ++- .../tests/functional/cli/v2/test_cli_v2.py | 11 ++++ mistralclient/tests/unit/v2/test_actions.py | 61 +++++++++++++++++-- .../tests/unit/v2/test_cli_actions.py | 50 +++++++++++---- .../add_namespace_option_to_actions.yaml | 5 ++ 9 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 releasenotes/notes/add_namespace_option_to_actions.yaml diff --git a/mistralclient/api/v2/action_executions.py b/mistralclient/api/v2/action_executions.py index ef98261f..dcd034ce 100644 --- a/mistralclient/api/v2/action_executions.py +++ b/mistralclient/api/v2/action_executions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -24,7 +25,7 @@ class ActionExecution(base.Resource): class ActionExecutionManager(base.ResourceManager): resource_class = ActionExecution - def create(self, name, input=None, **params): + def create(self, name, input=None, namespace='', **params): self._ensure_not_empty(name=name) data = {'name': name} @@ -35,6 +36,9 @@ class ActionExecutionManager(base.ResourceManager): if params: data['params'] = jsonutils.dumps(params) + if namespace: + data['workflow_namespace'] = namespace + return self._create( '/action_executions', data, diff --git a/mistralclient/api/v2/actions.py b/mistralclient/api/v2/actions.py index 58a7bfc7..ef3c3e0a 100644 --- a/mistralclient/api/v2/actions.py +++ b/mistralclient/api/v2/actions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,7 +24,7 @@ class Action(base.Resource): class ActionManager(base.ResourceManager): resource_class = Action - def create(self, definition, scope='private'): + def create(self, definition, scope='private', namespace=''): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the @@ -31,7 +32,7 @@ class ActionManager(base.ResourceManager): definition = utils.get_contents_if_file(definition) return self._create( - '/actions?scope=%s' % scope, + '/actions?scope=%s&namespace=%s' % (scope, namespace), definition, response_key='actions', dump_json=False, @@ -39,17 +40,15 @@ class ActionManager(base.ResourceManager): is_iter_resp=True ) - def update(self, definition, scope='private', id=None): + def update(self, definition, scope='private', id=None, namespace=''): self._ensure_not_empty(definition=definition) - - url_pre = ('/actions/%s' % id) if id else '/actions' - + params = '?scope=%s&namespace=%s' % (scope, namespace) + url = ('/actions/%s' % id if id else '/actions') + params # If the specified definition is actually a file, read in the # definition file definition = utils.get_contents_if_file(definition) - return self._update( - '%s?scope=%s' % (url_pre, scope), + url, definition, response_key='actions', dump_json=False, @@ -59,7 +58,6 @@ class ActionManager(base.ResourceManager): def list(self, marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): - query_string = self._build_query_params( marker=marker, limit=limit, @@ -74,15 +72,15 @@ class ActionManager(base.ResourceManager): response_key='actions', ) - def get(self, identifier): + def get(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) - return self._get('/actions/%s' % identifier) + return self._get('/actions/%s/%s' % (identifier, namespace)) - def delete(self, identifier): + def delete(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) - self._delete('/actions/%s' % identifier) + self._delete('/actions/%s/%s' % (identifier, namespace)) def validate(self, definition): self._ensure_not_empty(definition=definition) diff --git a/mistralclient/commands/v2/action_executions.py b/mistralclient/commands/v2/action_executions.py index e4be423c..166329bd 100644 --- a/mistralclient/commands/v2/action_executions.py +++ b/mistralclient/commands/v2/action_executions.py @@ -1,5 +1,6 @@ # Copyright 2014 - Mirantis, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. +# Copyright 2020 Nokia Software. # # 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 @@ -120,6 +121,12 @@ class Create(command.ShowOne): dest='target', help='Action will be executed on executor.' ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the action(s).", + ) return parser @@ -145,6 +152,7 @@ class Create(command.ShowOne): action_ex = mistral_client.action_executions.create( parsed_args.name, action_input, + namespace=parsed_args.namespace, **params ) diff --git a/mistralclient/commands/v2/actions.py b/mistralclient/commands/v2/actions.py index d0583b64..288e8349 100644 --- a/mistralclient/commands/v2/actions.py +++ b/mistralclient/commands/v2/actions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2020 Nokia Software. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -26,12 +27,13 @@ class ActionFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), + ('namespace', 'Namespace'), ('is_system', 'Is system'), ('input', 'Input'), ('description', 'Description'), ('tags', 'Tags'), ('created_at', 'Created at'), - ('updated_at', 'Updated at') + ('updated_at', 'Updated at'), ] @staticmethod @@ -45,6 +47,7 @@ class ActionFormatter(base.MistralFormatter): data = ( action.id, action.name, + action.namespace, action.is_system, input_, desc, @@ -93,12 +96,20 @@ class Get(command.ShowOne): parser = super(Get, self).get_parser(prog_name) parser.add_argument('action', help='Action (name or ID)') + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to create the action within.", + ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine - action = mistral_client.actions.get(parsed_args.action) + action = mistral_client.actions.get( + parsed_args.action, + parsed_args.namespace) return ActionFormatter.format(action) @@ -119,6 +130,12 @@ class Create(base.MistralLister): action='store_true', help='With this flag action will be marked as "public".' ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace to create the action within.", + ) return parser @@ -136,6 +153,7 @@ class Create(base.MistralLister): return mistral_client.actions.create( parsed_args.definition.read(), + namespace=parsed_args.namespace, scope=scope ) @@ -151,6 +169,12 @@ class Delete(command.Command): nargs='+', help='Name or ID of action(s).' ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the action(s).", + ) return parser @@ -158,7 +182,9 @@ class Delete(command.Command): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( - lambda s: mistral_client.actions.delete(s), + lambda s: mistral_client.actions.delete( + s, + namespace=parsed_args.namespace), parsed_args.action, "Request to delete action %s has been accepted.", "Unable to delete the specified action(s)." @@ -182,6 +208,12 @@ class Update(base.MistralLister): action='store_true', help='With this flag action will be marked as "public".' ) + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the action.", + ) return parser @@ -207,18 +239,27 @@ class GetDefinition(command.Command): parser = super(GetDefinition, self).get_parser(prog_name) parser.add_argument('name', help='Action name') + parser.add_argument( + '--namespace', + nargs='?', + default='', + help="Namespace of the action.", + ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine - definition = mistral_client.actions.get(parsed_args.name).definition + definition = mistral_client.actions.get( + parsed_args.name, + namespace=parsed_args.namespace).definition self.app.stdout.write(definition or "\n") class Validate(command.ShowOne): """Validate action.""" + @staticmethod def _format(result=None): columns = ('Valid', 'Error') diff --git a/mistralclient/tests/functional/cli/v2/base_v2.py b/mistralclient/tests/functional/cli/v2/base_v2.py index 9eee93cf..47fe47f3 100644 --- a/mistralclient/tests/functional/cli/v2/base_v2.py +++ b/mistralclient/tests/functional/cli/v2/base_v2.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 Mirantis, Inc. +# Copyright 2020 Nokia Software. # # 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 @@ -192,12 +193,16 @@ class MistralClientTestBase(base.MistralCLIAuth, base.MistralCLIAltAuth): return member - def action_create(self, act_def, admin=True, scope='private'): + def action_create(self, act_def, admin=True, + scope='private', namespace=''): params = '{0}'.format(act_def) if scope == 'public': params += ' --public' + if namespace: + params += " --namespace " + namespace + acts = self.mistral_cli( admin, 'action-create', diff --git a/mistralclient/tests/functional/cli/v2/test_cli_v2.py b/mistralclient/tests/functional/cli/v2/test_cli_v2.py index 9fffc950..902c7f75 100644 --- a/mistralclient/tests/functional/cli/v2/test_cli_v2.py +++ b/mistralclient/tests/functional/cli/v2/test_cli_v2.py @@ -1,4 +1,5 @@ # Copyright (c) 2014 Mirantis, Inc. +# Copyright 2020 Nokia Software. # # 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 @@ -1381,6 +1382,16 @@ class ActionCLITests(base_v2.MistralClientTestBase): self.assertNotIn('404 Not Found', definition) + def test_action_get_definition_with_namespace(self): + self.action_create(self.act_def) + + definition = self.mistral_admin( + 'action-get-definition', + params='greeting --namespace test_namespace' + ) + + self.assertNotIn('404 Not Found', definition) + def test_action_get_with_id(self): created = self.action_create(self.act_def) diff --git a/mistralclient/tests/unit/v2/test_actions.py b/mistralclient/tests/unit/v2/test_actions.py index 6cba05f2..13ccf45a 100644 --- a/mistralclient/tests/unit/v2/test_actions.py +++ b/mistralclient/tests/unit/v2/test_actions.py @@ -1,4 +1,5 @@ # Copyright 2015 Huawei Technologies Co., Ltd. +# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -75,6 +76,20 @@ class TestActionsV2(base.BaseClientV2Test): self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) + def test_create_with_namespace(self): + self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, + json={'actions': [ACTION]}, + status_code=201) + + actions = self.actions.create(ACTION_DEF, namespace='test_namespace') + + self.assertIsNotNone(actions) + self.assertEqual(ACTION_DEF, actions[0].definition) + + last_request = self.requests_mock.last_request + self.assertEqual('text/plain', last_request.headers['content-type']) + self.assertEqual(ACTION_DEF, last_request.text) + def test_create_with_file(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}, @@ -106,7 +121,7 @@ class TestActionsV2(base.BaseClientV2Test): last_request = self.requests_mock.last_request - self.assertEqual('scope=private', last_request.query) + self.assertEqual('scope=private&namespace=', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) @@ -121,7 +136,22 @@ class TestActionsV2(base.BaseClientV2Test): last_request = self.requests_mock.last_request - self.assertEqual('scope=private', last_request.query) + self.assertEqual('scope=private&namespace=', last_request.query) + self.assertEqual('text/plain', last_request.headers['content-type']) + self.assertEqual(ACTION_DEF, last_request.text) + + def test_update_with_namespace(self): + self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, + json={'actions': [ACTION]}) + actions = self.actions.update(ACTION_DEF, namespace='test_namespace') + + self.assertIsNotNone(actions) + self.assertEqual(ACTION_DEF, actions[0].definition) + + last_request = self.requests_mock.last_request + + self.assertEqual('scope=private&namespace=test_namespace', + last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) @@ -145,7 +175,7 @@ class TestActionsV2(base.BaseClientV2Test): self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request - self.assertEqual('scope=private', last_request.query) + self.assertEqual('scope=private&namespace=', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) @@ -197,7 +227,7 @@ class TestActionsV2(base.BaseClientV2Test): self.assertNotIn('limit', last_request.qs) def test_get(self): - self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME % 'action', + self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME % 'action/', json=ACTION) action = self.actions.get('action') @@ -208,14 +238,35 @@ class TestActionsV2(base.BaseClientV2Test): action.to_dict() ) + def test_get_with_namespace(self): + self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME + % 'action/namespace', + json=ACTION) + + action = self.actions.get('action', 'namespace') + + self.assertIsNotNone(action) + self.assertEqual( + actions.Action(self.actions, ACTION).to_dict(), + action.to_dict() + ) + def test_delete(self): - url = self.TEST_URL + URL_TEMPLATE_NAME % 'action' + url = self.TEST_URL + URL_TEMPLATE_NAME % 'action/' m = self.requests_mock.delete(url, status_code=204) self.actions.delete('action') self.assertEqual(1, m.call_count) + def test_delete_with_namespace(self): + url = self.TEST_URL + URL_TEMPLATE_NAME % 'action/namespace' + m = self.requests_mock.delete(url, status_code=204) + + self.actions.delete('action', 'namespace') + + self.assertEqual(1, m.call_count) + def test_validate(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, json={'valid': True}) diff --git a/mistralclient/tests/unit/v2/test_cli_actions.py b/mistralclient/tests/unit/v2/test_cli_actions.py index b2e48720..d9b516e8 100644 --- a/mistralclient/tests/unit/v2/test_cli_actions.py +++ b/mistralclient/tests/unit/v2/test_cli_actions.py @@ -1,4 +1,5 @@ # Copyright 2014 Mirantis, Inc. +# Copyright 2020 Nokia Software. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -31,7 +32,8 @@ ACTION_DICT = { 'description': 'My cool action', 'tags': ['test'], 'created_at': '1', - 'updated_at': '1' + 'updated_at': '1', + 'namespace': 'test_namespace' } ACTION_DEF = """ @@ -58,7 +60,7 @@ class TestCLIActionsV2(base.BaseCommandTest): result = self.call(action_cmd.Create, app_args=['1.txt']) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, "param1", + [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @@ -73,7 +75,7 @@ class TestCLIActionsV2(base.BaseCommandTest): ) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, "param1", + [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @@ -99,8 +101,9 @@ class TestCLIActionsV2(base.BaseCommandTest): result = self.call(action_cmd.Create, app_args=['1.txt']) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, cmd_base.cut(long_input), - 'My cool action', 'test', '1', '1')], + [('1234-4567-7894-7895', 'a', 'test_namespace', + True, cmd_base.cut(long_input), 'My cool action', + 'test', '1', '1')], result[1] ) @@ -111,7 +114,7 @@ class TestCLIActionsV2(base.BaseCommandTest): result = self.call(action_cmd.Update, app_args=['my_action.yaml']) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, "param1", + [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @@ -126,7 +129,7 @@ class TestCLIActionsV2(base.BaseCommandTest): ) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, "param1", + [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @@ -142,7 +145,7 @@ class TestCLIActionsV2(base.BaseCommandTest): result = self.call(action_cmd.List) self.assertEqual( - [('1234-4567-7894-7895', 'a', True, "param1", + [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @@ -153,7 +156,7 @@ class TestCLIActionsV2(base.BaseCommandTest): result = self.call(action_cmd.Get, app_args=['name']) self.assertEqual( - ('1234-4567-7894-7895', 'a', True, "param1", + ('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1", 'My cool action', 'test', '1', '1'), result[1] ) @@ -161,14 +164,39 @@ class TestCLIActionsV2(base.BaseCommandTest): def test_delete(self): self.call(action_cmd.Delete, app_args=['name']) - self.client.actions.delete.assert_called_once_with('name') + self.client.actions.delete.assert_called_once_with('name', + namespace='') + + def test_delete_with_namespace(self): + self.call(action_cmd.Delete, app_args=['name', + '--namespace', + 'test_namespace'] + ) + + self.client.actions.delete.assert_called_once_with( + 'name', + namespace='test_namespace') + + def test_delete_with_multi_names_and_namespace(self): + self.call(action_cmd.Delete, app_args=['name1', + 'name2', + '--namespace', + 'test_namespace']) + + self.assertEqual(2, self.client.actions.delete.call_count) + self.assertEqual( + [mock.call('name1', namespace='test_namespace'), + mock.call('name2', namespace='test_namespace')], + self.client.actions.delete.call_args_list + ) def test_delete_with_multi_names(self): self.call(action_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.actions.delete.call_count) self.assertEqual( - [mock.call('name1'), mock.call('name2')], + [mock.call('name1', namespace=''), + mock.call('name2', namespace='')], self.client.actions.delete.call_args_list ) diff --git a/releasenotes/notes/add_namespace_option_to_actions.yaml b/releasenotes/notes/add_namespace_option_to_actions.yaml new file mode 100644 index 00000000..cef24314 --- /dev/null +++ b/releasenotes/notes/add_namespace_option_to_actions.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add namespace parameter to action commands. Namespace parameter allows + to create multiple actions with same name under different namespaces.