Merge "Make webhook API compatible with Aodh"
This commit is contained in:
commit
a7fea71b9e
|
@ -276,7 +276,8 @@ webhook_params:
|
||||||
type: object
|
type: object
|
||||||
in: query
|
in: query
|
||||||
description: |
|
description: |
|
||||||
The query string that forms the inputs to use for the targeted action.
|
The query string that forms the inputs to use for the targeted action
|
||||||
|
for API microversion less than 1.10.
|
||||||
|
|
||||||
webhook_version:
|
webhook_version:
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
Webhooks (webhooks)
|
Webhooks (webhooks)
|
||||||
===================
|
===================
|
||||||
|
|
||||||
Triggers an action represented by a webhook.
|
Triggers an action represented by a webhook. For API microversion less than
|
||||||
|
1.10, optional params in the query are sent as inputs to be used by the
|
||||||
|
targeted action. For API microversion equal or greater than 1.10, any
|
||||||
|
key-value pairs in the request body are sent as inputs to be used by the
|
||||||
|
targeted action.
|
||||||
|
|
||||||
Trigger webhook action
|
Trigger webhook action
|
||||||
======================
|
======================
|
||||||
|
|
|
@ -105,3 +105,9 @@ it can be used by both users and developers.
|
||||||
trigger the immediate deletion of the nodes identified for deferred
|
trigger the immediate deletion of the nodes identified for deferred
|
||||||
deletion during scale-in operation.
|
deletion during scale-in operation.
|
||||||
|
|
||||||
|
1.10
|
||||||
|
---
|
||||||
|
- Modified the ``webhook_trigger`` API. Inputs for the targeted action
|
||||||
|
are now sent directly in the query body rather than in the params
|
||||||
|
field.
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class VersionController(object):
|
||||||
# This includes any semantic changes which may not affect the input or
|
# This includes any semantic changes which may not affect the input or
|
||||||
# output formats or even originate in the API code layer.
|
# output formats or even originate in the API code layer.
|
||||||
_MIN_API_VERSION = "1.0"
|
_MIN_API_VERSION = "1.0"
|
||||||
_MAX_API_VERSION = "1.9"
|
_MAX_API_VERSION = "1.10"
|
||||||
|
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ class WebhookController(wsgi.Controller):
|
||||||
|
|
||||||
REQUEST_SCOPE = 'webhooks'
|
REQUEST_SCOPE = 'webhooks'
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("1.0", "1.9")
|
||||||
@util.policy_enforce
|
@util.policy_enforce
|
||||||
def trigger(self, req, webhook_id, body=None):
|
def trigger(self, req, webhook_id, body=None):
|
||||||
if body is None:
|
if body is None:
|
||||||
|
@ -39,3 +40,15 @@ class WebhookController(wsgi.Controller):
|
||||||
location = {'location': '/actions/%s' % res['action']}
|
location = {'location': '/actions/%s' % res['action']}
|
||||||
res.update(location)
|
res.update(location)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version("1.10") # noqa
|
||||||
|
@util.policy_enforce
|
||||||
|
def trigger(self, req, webhook_id, body=None):
|
||||||
|
obj = util.parse_request(
|
||||||
|
'WebhookTriggerRequestParamsInBody', req, {'identity': webhook_id,
|
||||||
|
'body': body})
|
||||||
|
|
||||||
|
res = self.rpc_client.call(req.context, 'webhook_trigger', obj)
|
||||||
|
location = {'location': '/actions/%s' % res['action']}
|
||||||
|
res.update(location)
|
||||||
|
return res
|
||||||
|
|
|
@ -2493,7 +2493,11 @@ class EngineService(service.Service):
|
||||||
:return: A dictionary contains the ID of the action fired.
|
:return: A dictionary contains the ID of the action fired.
|
||||||
"""
|
"""
|
||||||
identity = req.identity
|
identity = req.identity
|
||||||
|
if hasattr(req.body, 'params'):
|
||||||
|
# API version < 1.10
|
||||||
params = req.body.params
|
params = req.body.params
|
||||||
|
else:
|
||||||
|
params = req.body
|
||||||
|
|
||||||
LOG.info("Triggering webhook (%s)", identity)
|
LOG.info("Triggering webhook (%s)", identity)
|
||||||
receiver = receiver_obj.Receiver.find(ctx, identity)
|
receiver = receiver_obj.Receiver.find(ctx, identity)
|
||||||
|
|
|
@ -14,6 +14,15 @@ from senlin.objects import base
|
||||||
from senlin.objects import fields
|
from senlin.objects import fields
|
||||||
|
|
||||||
|
|
||||||
|
@base.SenlinObjectRegistry.register
|
||||||
|
class WebhookTriggerRequestParamsInBody(base.SenlinObject):
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'identity': fields.StringField(),
|
||||||
|
'body': fields.JsonField(nullable=True, default={})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@base.SenlinObjectRegistry.register
|
@base.SenlinObjectRegistry.register
|
||||||
class WebhookTriggerRequest(base.SenlinObject):
|
class WebhookTriggerRequest(base.SenlinObject):
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
req = self._post('/webhooks/test_webhook_id/trigger',
|
req = self._post('/webhooks/test_webhook_id/trigger',
|
||||||
jsonutils.dumps(body))
|
jsonutils.dumps(body), version='1.10')
|
||||||
mock_call.return_value = engine_response
|
mock_call.return_value = engine_response
|
||||||
obj = mock.Mock()
|
obj = mock.Mock()
|
||||||
mock_parse.return_value = obj
|
mock_parse.return_value = obj
|
||||||
|
@ -58,7 +58,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
|
||||||
self.assertEqual(action_id, resp['action'])
|
self.assertEqual(action_id, resp['action'])
|
||||||
self.assertEqual('/actions/test_action_id', resp['location'])
|
self.assertEqual('/actions/test_action_id', resp['location'])
|
||||||
mock_parse.assert_called_once_with(
|
mock_parse.assert_called_once_with(
|
||||||
'WebhookTriggerRequest', req, mock.ANY)
|
'WebhookTriggerRequestParamsInBody', req, mock.ANY)
|
||||||
mock_call.assert_called_once_with(req.context, 'webhook_trigger', obj)
|
mock_call.assert_called_once_with(req.context, 'webhook_trigger', obj)
|
||||||
|
|
||||||
@mock.patch.object(util, 'parse_request')
|
@mock.patch.object(util, 'parse_request')
|
||||||
|
@ -72,7 +72,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
|
||||||
engine_response = {'action': 'FAKE_ACTION'}
|
engine_response = {'action': 'FAKE_ACTION'}
|
||||||
|
|
||||||
req = self._post('/webhooks/test_webhook_id/trigger',
|
req = self._post('/webhooks/test_webhook_id/trigger',
|
||||||
jsonutils.dumps(body))
|
jsonutils.dumps(body), version='1.10')
|
||||||
mock_call.return_value = engine_response
|
mock_call.return_value = engine_response
|
||||||
obj = mock.Mock()
|
obj = mock.Mock()
|
||||||
mock_parse.return_value = obj
|
mock_parse.return_value = obj
|
||||||
|
@ -82,7 +82,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
|
||||||
self.assertEqual('FAKE_ACTION', resp['action'])
|
self.assertEqual('FAKE_ACTION', resp['action'])
|
||||||
self.assertEqual('/actions/FAKE_ACTION', resp['location'])
|
self.assertEqual('/actions/FAKE_ACTION', resp['location'])
|
||||||
mock_parse.assert_called_once_with(
|
mock_parse.assert_called_once_with(
|
||||||
'WebhookTriggerRequest', req, mock.ANY)
|
'WebhookTriggerRequestParamsInBody', req, mock.ANY)
|
||||||
mock_call.assert_called_once_with(req.context, 'webhook_trigger', obj)
|
mock_call.assert_called_once_with(req.context, 'webhook_trigger', obj)
|
||||||
|
|
||||||
@mock.patch.object(util, 'parse_request')
|
@mock.patch.object(util, 'parse_request')
|
||||||
|
|
|
@ -33,6 +33,105 @@ class WebhookTest(base.SenlinTestCase):
|
||||||
self.ctx = utils.dummy_context(project='webhook_test_project')
|
self.ctx = utils.dummy_context(project='webhook_test_project')
|
||||||
self.eng = service.EngineService('host-a', 'topic-a')
|
self.eng = service.EngineService('host-a', 'topic-a')
|
||||||
|
|
||||||
|
@mock.patch.object(dispatcher, 'start_action')
|
||||||
|
@mock.patch.object(action_mod.Action, 'create')
|
||||||
|
@mock.patch.object(co.Cluster, 'find')
|
||||||
|
@mock.patch.object(ro.Receiver, 'find')
|
||||||
|
def test_webhook_trigger_params_in_body_with_params(
|
||||||
|
self, mock_get, mock_find, mock_action, notify):
|
||||||
|
mock_find.return_value = mock.Mock(id='FAKE_CLUSTER')
|
||||||
|
mock_get.return_value = mock.Mock(id='01234567-abcd-efef',
|
||||||
|
cluster_id='FAKE_CLUSTER',
|
||||||
|
action='DANCE',
|
||||||
|
params={'foo': 'bar'})
|
||||||
|
mock_action.return_value = 'ACTION_ID'
|
||||||
|
|
||||||
|
body = {'kee': 'vee'}
|
||||||
|
req = vorw.WebhookTriggerRequestParamsInBody(identity='FAKE_RECEIVER',
|
||||||
|
body=body)
|
||||||
|
res = self.eng.webhook_trigger(self.ctx, req.obj_to_primitive())
|
||||||
|
|
||||||
|
self.assertEqual({'action': 'ACTION_ID'}, res)
|
||||||
|
|
||||||
|
mock_get.assert_called_once_with(self.ctx, 'FAKE_RECEIVER')
|
||||||
|
mock_find.assert_called_once_with(self.ctx, 'FAKE_CLUSTER')
|
||||||
|
mock_action.assert_called_once_with(
|
||||||
|
self.ctx, 'FAKE_CLUSTER', 'DANCE',
|
||||||
|
name='webhook_01234567',
|
||||||
|
cause=consts.CAUSE_RPC,
|
||||||
|
status=action_mod.Action.READY,
|
||||||
|
inputs={'kee': 'vee', 'foo': 'bar'},
|
||||||
|
)
|
||||||
|
notify.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(dispatcher, 'start_action')
|
||||||
|
@mock.patch.object(action_mod.Action, 'create')
|
||||||
|
@mock.patch.object(co.Cluster, 'find')
|
||||||
|
@mock.patch.object(ro.Receiver, 'find')
|
||||||
|
def test_webhook_trigger_params_in_body_no_params(
|
||||||
|
self, mock_get, mock_find, mock_action, notify):
|
||||||
|
mock_find.return_value = mock.Mock(id='FAKE_CLUSTER')
|
||||||
|
mock_get.return_value = mock.Mock(id='01234567-abcd-efef',
|
||||||
|
cluster_id='FAKE_CLUSTER',
|
||||||
|
action='DANCE',
|
||||||
|
params={'foo': 'bar'})
|
||||||
|
mock_action.return_value = 'ACTION_ID'
|
||||||
|
|
||||||
|
body = {}
|
||||||
|
req = vorw.WebhookTriggerRequestParamsInBody(identity='FAKE_RECEIVER',
|
||||||
|
body=body)
|
||||||
|
res = self.eng.webhook_trigger(self.ctx, req.obj_to_primitive())
|
||||||
|
|
||||||
|
self.assertEqual({'action': 'ACTION_ID'}, res)
|
||||||
|
|
||||||
|
mock_get.assert_called_once_with(self.ctx, 'FAKE_RECEIVER')
|
||||||
|
mock_find.assert_called_once_with(self.ctx, 'FAKE_CLUSTER')
|
||||||
|
mock_action.assert_called_once_with(
|
||||||
|
self.ctx, 'FAKE_CLUSTER', 'DANCE',
|
||||||
|
name='webhook_01234567',
|
||||||
|
cause=consts.CAUSE_RPC,
|
||||||
|
status=action_mod.Action.READY,
|
||||||
|
inputs={'foo': 'bar'},
|
||||||
|
)
|
||||||
|
notify.assert_called_once_with()
|
||||||
|
|
||||||
|
@mock.patch.object(ro.Receiver, 'find')
|
||||||
|
def test_webhook_trigger_params_in_body_receiver_not_found(
|
||||||
|
self, mock_find):
|
||||||
|
mock_find.side_effect = exception.ResourceNotFound(type='receiver',
|
||||||
|
id='RRR')
|
||||||
|
body = None
|
||||||
|
req = vorw.WebhookTriggerRequestParamsInBody(identity='RRR', body=body)
|
||||||
|
ex = self.assertRaises(rpc.ExpectedException,
|
||||||
|
self.eng.webhook_trigger, self.ctx,
|
||||||
|
req.obj_to_primitive())
|
||||||
|
|
||||||
|
self.assertEqual(exception.ResourceNotFound, ex.exc_info[0])
|
||||||
|
self.assertEqual("The receiver 'RRR' could not be found.",
|
||||||
|
six.text_type(ex.exc_info[1]))
|
||||||
|
mock_find.assert_called_once_with(self.ctx, 'RRR')
|
||||||
|
|
||||||
|
@mock.patch.object(ro.Receiver, 'find')
|
||||||
|
@mock.patch.object(co.Cluster, 'find')
|
||||||
|
def test_webhook_trigger_params_in_body_cluster_not_found(
|
||||||
|
self, mock_cluster, mock_find):
|
||||||
|
receiver = mock.Mock()
|
||||||
|
receiver.cluster_id = 'BOGUS'
|
||||||
|
mock_find.return_value = receiver
|
||||||
|
mock_cluster.side_effect = exception.ResourceNotFound(type='cluster',
|
||||||
|
id='BOGUS')
|
||||||
|
body = None
|
||||||
|
req = vorw.WebhookTriggerRequestParamsInBody(identity='RRR', body=body)
|
||||||
|
ex = self.assertRaises(rpc.ExpectedException,
|
||||||
|
self.eng.webhook_trigger, self.ctx,
|
||||||
|
req.obj_to_primitive())
|
||||||
|
|
||||||
|
self.assertEqual(exception.BadRequest, ex.exc_info[0])
|
||||||
|
self.assertEqual("The referenced cluster 'BOGUS' could not be found.",
|
||||||
|
six.text_type(ex.exc_info[1]))
|
||||||
|
mock_find.assert_called_once_with(self.ctx, 'RRR')
|
||||||
|
mock_cluster.assert_called_once_with(self.ctx, 'BOGUS')
|
||||||
|
|
||||||
@mock.patch.object(dispatcher, 'start_action')
|
@mock.patch.object(dispatcher, 'start_action')
|
||||||
@mock.patch.object(action_mod.Action, 'create')
|
@mock.patch.object(action_mod.Action, 'create')
|
||||||
@mock.patch.object(co.Cluster, 'find')
|
@mock.patch.object(co.Cluster, 'find')
|
||||||
|
|
|
@ -60,3 +60,32 @@ class TestWebhookTrigger(test_base.SenlinTestCase):
|
||||||
self.assertEqual('1.0', res['senlin_object.version'])
|
self.assertEqual('1.0', res['senlin_object.version'])
|
||||||
self.assertEqual('senlin', res['senlin_object.namespace'])
|
self.assertEqual('senlin', res['senlin_object.namespace'])
|
||||||
self.assertEqual(u'fake', res['senlin_object.data']['identity'])
|
self.assertEqual(u'fake', res['senlin_object.data']['identity'])
|
||||||
|
|
||||||
|
def test_webhook_trigger_params_in_body_none_param(self):
|
||||||
|
body = None
|
||||||
|
sot = webhooks.WebhookTriggerRequestParamsInBody(
|
||||||
|
identity='fake', params=body)
|
||||||
|
self.assertEqual('fake', sot.identity)
|
||||||
|
self.assertIsNone(sot.params)
|
||||||
|
|
||||||
|
def test_webhook_trigger_params_in_body(self):
|
||||||
|
body = {'foo': 'boo'}
|
||||||
|
sot = webhooks.WebhookTriggerRequestParamsInBody(
|
||||||
|
identity='fake', params=body)
|
||||||
|
self.assertEqual('fake', sot.identity)
|
||||||
|
self.assertIsInstance(sot.params, dict)
|
||||||
|
|
||||||
|
def test_webhook_trigger_params_in_body_to_primitive(self):
|
||||||
|
body = {'foo': 'boo'}
|
||||||
|
sot = webhooks.WebhookTriggerRequestParamsInBody(
|
||||||
|
identity='fake', params=body)
|
||||||
|
self.assertEqual('fake', sot.identity)
|
||||||
|
self.assertIsInstance(sot.params, dict)
|
||||||
|
|
||||||
|
res = sot.obj_to_primitive()
|
||||||
|
|
||||||
|
self.assertIn('identity', res['senlin_object.changes'])
|
||||||
|
self.assertIn('WebhookTriggerRequest', res['senlin_object.name'])
|
||||||
|
self.assertEqual('1.0', res['senlin_object.version'])
|
||||||
|
self.assertEqual('senlin', res['senlin_object.namespace'])
|
||||||
|
self.assertEqual(u'fake', res['senlin_object.data']['identity'])
|
||||||
|
|
Loading…
Reference in New Issue