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
This commit is contained in:
ali 2020-01-02 15:44:05 +00:00 committed by Renat Akhmerov
parent 165a3b3455
commit e1e75d61eb
9 changed files with 186 additions and 35 deletions

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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): class ActionExecutionManager(base.ResourceManager):
resource_class = ActionExecution resource_class = ActionExecution
def create(self, name, input=None, **params): def create(self, name, input=None, namespace='', **params):
self._ensure_not_empty(name=name) self._ensure_not_empty(name=name)
data = {'name': name} data = {'name': name}
@ -35,6 +36,9 @@ class ActionExecutionManager(base.ResourceManager):
if params: if params:
data['params'] = jsonutils.dumps(params) data['params'] = jsonutils.dumps(params)
if namespace:
data['workflow_namespace'] = namespace
return self._create( return self._create(
'/action_executions', '/action_executions',
data, data,

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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): class ActionManager(base.ResourceManager):
resource_class = Action resource_class = Action
def create(self, definition, scope='private'): def create(self, definition, scope='private', namespace=''):
self._ensure_not_empty(definition=definition) self._ensure_not_empty(definition=definition)
# If the specified definition is actually a file, read in the # 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) definition = utils.get_contents_if_file(definition)
return self._create( return self._create(
'/actions?scope=%s' % scope, '/actions?scope=%s&namespace=%s' % (scope, namespace),
definition, definition,
response_key='actions', response_key='actions',
dump_json=False, dump_json=False,
@ -39,17 +40,15 @@ class ActionManager(base.ResourceManager):
is_iter_resp=True 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) self._ensure_not_empty(definition=definition)
params = '?scope=%s&namespace=%s' % (scope, namespace)
url_pre = ('/actions/%s' % id) if id else '/actions' url = ('/actions/%s' % id if id else '/actions') + params
# If the specified definition is actually a file, read in the # If the specified definition is actually a file, read in the
# definition file # definition file
definition = utils.get_contents_if_file(definition) definition = utils.get_contents_if_file(definition)
return self._update( return self._update(
'%s?scope=%s' % (url_pre, scope), url,
definition, definition,
response_key='actions', response_key='actions',
dump_json=False, dump_json=False,
@ -59,7 +58,6 @@ class ActionManager(base.ResourceManager):
def list(self, marker='', limit=None, sort_keys='', sort_dirs='', def list(self, marker='', limit=None, sort_keys='', sort_dirs='',
fields='', **filters): fields='', **filters):
query_string = self._build_query_params( query_string = self._build_query_params(
marker=marker, marker=marker,
limit=limit, limit=limit,
@ -74,15 +72,15 @@ class ActionManager(base.ResourceManager):
response_key='actions', response_key='actions',
) )
def get(self, identifier): def get(self, identifier, namespace=''):
self._ensure_not_empty(identifier=identifier) 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._ensure_not_empty(identifier=identifier)
self._delete('/actions/%s' % identifier) self._delete('/actions/%s/%s' % (identifier, namespace))
def validate(self, definition): def validate(self, definition):
self._ensure_not_empty(definition=definition) self._ensure_not_empty(definition=definition)

View File

@ -1,5 +1,6 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2016 - Brocade Communications Systems, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -120,6 +121,12 @@ class Create(command.ShowOne):
dest='target', dest='target',
help='Action will be executed on <target> executor.' help='Action will be executed on <target> executor.'
) )
parser.add_argument(
'--namespace',
nargs='?',
default='',
help="Namespace of the action(s).",
)
return parser return parser
@ -145,6 +152,7 @@ class Create(command.ShowOne):
action_ex = mistral_client.action_executions.create( action_ex = mistral_client.action_executions.create(
parsed_args.name, parsed_args.name,
action_input, action_input,
namespace=parsed_args.namespace,
**params **params
) )

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2020 Nokia Software.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -26,12 +27,13 @@ class ActionFormatter(base.MistralFormatter):
COLUMNS = [ COLUMNS = [
('id', 'ID'), ('id', 'ID'),
('name', 'Name'), ('name', 'Name'),
('namespace', 'Namespace'),
('is_system', 'Is system'), ('is_system', 'Is system'),
('input', 'Input'), ('input', 'Input'),
('description', 'Description'), ('description', 'Description'),
('tags', 'Tags'), ('tags', 'Tags'),
('created_at', 'Created at'), ('created_at', 'Created at'),
('updated_at', 'Updated at') ('updated_at', 'Updated at'),
] ]
@staticmethod @staticmethod
@ -45,6 +47,7 @@ class ActionFormatter(base.MistralFormatter):
data = ( data = (
action.id, action.id,
action.name, action.name,
action.namespace,
action.is_system, action.is_system,
input_, input_,
desc, desc,
@ -93,12 +96,20 @@ class Get(command.ShowOne):
parser = super(Get, self).get_parser(prog_name) parser = super(Get, self).get_parser(prog_name)
parser.add_argument('action', help='Action (name or ID)') parser.add_argument('action', help='Action (name or ID)')
parser.add_argument(
'--namespace',
nargs='?',
default='',
help="Namespace to create the action within.",
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine 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) return ActionFormatter.format(action)
@ -119,6 +130,12 @@ class Create(base.MistralLister):
action='store_true', action='store_true',
help='With this flag action will be marked as "public".' 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 return parser
@ -136,6 +153,7 @@ class Create(base.MistralLister):
return mistral_client.actions.create( return mistral_client.actions.create(
parsed_args.definition.read(), parsed_args.definition.read(),
namespace=parsed_args.namespace,
scope=scope scope=scope
) )
@ -151,6 +169,12 @@ class Delete(command.Command):
nargs='+', nargs='+',
help='Name or ID of action(s).' help='Name or ID of action(s).'
) )
parser.add_argument(
'--namespace',
nargs='?',
default='',
help="Namespace of the action(s).",
)
return parser return parser
@ -158,7 +182,9 @@ class Delete(command.Command):
mistral_client = self.app.client_manager.workflow_engine mistral_client = self.app.client_manager.workflow_engine
utils.do_action_on_many( 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, parsed_args.action,
"Request to delete action %s has been accepted.", "Request to delete action %s has been accepted.",
"Unable to delete the specified action(s)." "Unable to delete the specified action(s)."
@ -182,6 +208,12 @@ class Update(base.MistralLister):
action='store_true', action='store_true',
help='With this flag action will be marked as "public".' help='With this flag action will be marked as "public".'
) )
parser.add_argument(
'--namespace',
nargs='?',
default='',
help="Namespace of the action.",
)
return parser return parser
@ -207,18 +239,27 @@ class GetDefinition(command.Command):
parser = super(GetDefinition, self).get_parser(prog_name) parser = super(GetDefinition, self).get_parser(prog_name)
parser.add_argument('name', help='Action name') parser.add_argument('name', help='Action name')
parser.add_argument(
'--namespace',
nargs='?',
default='',
help="Namespace of the action.",
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine 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") self.app.stdout.write(definition or "\n")
class Validate(command.ShowOne): class Validate(command.ShowOne):
"""Validate action.""" """Validate action."""
@staticmethod @staticmethod
def _format(result=None): def _format(result=None):
columns = ('Valid', 'Error') columns = ('Valid', 'Error')

View File

@ -1,4 +1,5 @@
# Copyright (c) 2014 Mirantis, Inc. # Copyright (c) 2014 Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # 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 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) params = '{0}'.format(act_def)
if scope == 'public': if scope == 'public':
params += ' --public' params += ' --public'
if namespace:
params += " --namespace " + namespace
acts = self.mistral_cli( acts = self.mistral_cli(
admin, admin,
'action-create', 'action-create',

View File

@ -1,4 +1,5 @@
# Copyright (c) 2014 Mirantis, Inc. # Copyright (c) 2014 Mirantis, Inc.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # 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) 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): def test_action_get_with_id(self):
created = self.action_create(self.act_def) created = self.action_create(self.act_def)

View File

@ -1,4 +1,5 @@
# Copyright 2015 Huawei Technologies Co., Ltd. # Copyright 2015 Huawei Technologies Co., Ltd.
# Copyright 2020 Nokia Software.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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('text/plain', last_request.headers['content-type'])
self.assertEqual(ACTION_DEF, last_request.text) 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): def test_create_with_file(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, self.requests_mock.post(self.TEST_URL + URL_TEMPLATE,
json={'actions': [ACTION]}, json={'actions': [ACTION]},
@ -106,7 +121,7 @@ class TestActionsV2(base.BaseClientV2Test):
last_request = self.requests_mock.last_request 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('text/plain', last_request.headers['content-type'])
self.assertEqual(ACTION_DEF, last_request.text) self.assertEqual(ACTION_DEF, last_request.text)
@ -121,7 +136,22 @@ class TestActionsV2(base.BaseClientV2Test):
last_request = self.requests_mock.last_request 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('text/plain', last_request.headers['content-type'])
self.assertEqual(ACTION_DEF, last_request.text) self.assertEqual(ACTION_DEF, last_request.text)
@ -145,7 +175,7 @@ class TestActionsV2(base.BaseClientV2Test):
self.assertEqual(ACTION_DEF, actions[0].definition) self.assertEqual(ACTION_DEF, actions[0].definition)
last_request = self.requests_mock.last_request 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('text/plain', last_request.headers['content-type'])
self.assertEqual(ACTION_DEF, last_request.text) self.assertEqual(ACTION_DEF, last_request.text)
@ -197,7 +227,7 @@ class TestActionsV2(base.BaseClientV2Test):
self.assertNotIn('limit', last_request.qs) self.assertNotIn('limit', last_request.qs)
def test_get(self): 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) json=ACTION)
action = self.actions.get('action') action = self.actions.get('action')
@ -208,14 +238,35 @@ class TestActionsV2(base.BaseClientV2Test):
action.to_dict() 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): 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) m = self.requests_mock.delete(url, status_code=204)
self.actions.delete('action') self.actions.delete('action')
self.assertEqual(1, m.call_count) 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): def test_validate(self):
self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE,
json={'valid': True}) json={'valid': True})

View File

@ -1,4 +1,5 @@
# Copyright 2014 Mirantis, Inc. # Copyright 2014 Mirantis, Inc.
# Copyright 2020 Nokia Software.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -31,7 +32,8 @@ ACTION_DICT = {
'description': 'My cool action', 'description': 'My cool action',
'tags': ['test'], 'tags': ['test'],
'created_at': '1', 'created_at': '1',
'updated_at': '1' 'updated_at': '1',
'namespace': 'test_namespace'
} }
ACTION_DEF = """ ACTION_DEF = """
@ -58,7 +60,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
result = self.call(action_cmd.Create, app_args=['1.txt']) result = self.call(action_cmd.Create, app_args=['1.txt'])
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, "param1", [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1')], 'My cool action', 'test', '1', '1')],
result[1] result[1]
) )
@ -73,7 +75,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
) )
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, "param1", [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1')], 'My cool action', 'test', '1', '1')],
result[1] result[1]
) )
@ -99,8 +101,9 @@ class TestCLIActionsV2(base.BaseCommandTest):
result = self.call(action_cmd.Create, app_args=['1.txt']) result = self.call(action_cmd.Create, app_args=['1.txt'])
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, cmd_base.cut(long_input), [('1234-4567-7894-7895', 'a', 'test_namespace',
'My cool action', 'test', '1', '1')], True, cmd_base.cut(long_input), 'My cool action',
'test', '1', '1')],
result[1] result[1]
) )
@ -111,7 +114,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
result = self.call(action_cmd.Update, app_args=['my_action.yaml']) result = self.call(action_cmd.Update, app_args=['my_action.yaml'])
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, "param1", [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1')], 'My cool action', 'test', '1', '1')],
result[1] result[1]
) )
@ -126,7 +129,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
) )
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, "param1", [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1')], 'My cool action', 'test', '1', '1')],
result[1] result[1]
) )
@ -142,7 +145,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
result = self.call(action_cmd.List) result = self.call(action_cmd.List)
self.assertEqual( self.assertEqual(
[('1234-4567-7894-7895', 'a', True, "param1", [('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1')], 'My cool action', 'test', '1', '1')],
result[1] result[1]
) )
@ -153,7 +156,7 @@ class TestCLIActionsV2(base.BaseCommandTest):
result = self.call(action_cmd.Get, app_args=['name']) result = self.call(action_cmd.Get, app_args=['name'])
self.assertEqual( self.assertEqual(
('1234-4567-7894-7895', 'a', True, "param1", ('1234-4567-7894-7895', 'a', 'test_namespace', True, "param1",
'My cool action', 'test', '1', '1'), 'My cool action', 'test', '1', '1'),
result[1] result[1]
) )
@ -161,14 +164,39 @@ class TestCLIActionsV2(base.BaseCommandTest):
def test_delete(self): def test_delete(self):
self.call(action_cmd.Delete, app_args=['name']) 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): def test_delete_with_multi_names(self):
self.call(action_cmd.Delete, app_args=['name1', 'name2']) self.call(action_cmd.Delete, app_args=['name1', 'name2'])
self.assertEqual(2, self.client.actions.delete.call_count) self.assertEqual(2, self.client.actions.delete.call_count)
self.assertEqual( self.assertEqual(
[mock.call('name1'), mock.call('name2')], [mock.call('name1', namespace=''),
mock.call('name2', namespace='')],
self.client.actions.delete.call_args_list self.client.actions.delete.call_args_list
) )

View File

@ -0,0 +1,5 @@
---
features:
- |
Add namespace parameter to action commands. Namespace parameter allows
to create multiple actions with same name under different namespaces.