Merge "Make webhook API compatible with Aodh"

This commit is contained in:
Zuul 2018-05-01 09:44:28 +00:00 committed by Gerrit Code Review
commit a7fea71b9e
10 changed files with 173 additions and 8 deletions

View File

@ -276,7 +276,8 @@ webhook_params:
type: object
in: query
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:
type: string

View File

@ -2,7 +2,11 @@
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
======================

View File

@ -105,3 +105,9 @@ it can be used by both users and developers.
trigger the immediate deletion of the nodes identified for deferred
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.

View File

@ -24,7 +24,7 @@ class VersionController(object):
# This includes any semantic changes which may not affect the input or
# output formats or even originate in the API code layer.
_MIN_API_VERSION = "1.0"
_MAX_API_VERSION = "1.9"
_MAX_API_VERSION = "1.10"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -24,6 +24,7 @@ class WebhookController(wsgi.Controller):
REQUEST_SCOPE = 'webhooks'
@wsgi.Controller.api_version("1.0", "1.9")
@util.policy_enforce
def trigger(self, req, webhook_id, body=None):
if body is None:
@ -39,3 +40,15 @@ class WebhookController(wsgi.Controller):
location = {'location': '/actions/%s' % res['action']}
res.update(location)
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

View File

@ -2493,7 +2493,11 @@ class EngineService(service.Service):
:return: A dictionary contains the ID of the action fired.
"""
identity = req.identity
params = req.body.params
if hasattr(req.body, 'params'):
# API version < 1.10
params = req.body.params
else:
params = req.body
LOG.info("Triggering webhook (%s)", identity)
receiver = receiver_obj.Receiver.find(ctx, identity)

View File

@ -14,6 +14,15 @@ from senlin.objects import base
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
class WebhookTriggerRequest(base.SenlinObject):

View File

@ -48,7 +48,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
}
req = self._post('/webhooks/test_webhook_id/trigger',
jsonutils.dumps(body))
jsonutils.dumps(body), version='1.10')
mock_call.return_value = engine_response
obj = mock.Mock()
mock_parse.return_value = obj
@ -58,7 +58,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
self.assertEqual(action_id, resp['action'])
self.assertEqual('/actions/test_action_id', resp['location'])
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.patch.object(util, 'parse_request')
@ -72,7 +72,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
engine_response = {'action': 'FAKE_ACTION'}
req = self._post('/webhooks/test_webhook_id/trigger',
jsonutils.dumps(body))
jsonutils.dumps(body), version='1.10')
mock_call.return_value = engine_response
obj = mock.Mock()
mock_parse.return_value = obj
@ -82,7 +82,7 @@ class WebhookControllerTest(shared.ControllerTest, base.SenlinTestCase):
self.assertEqual('FAKE_ACTION', resp['action'])
self.assertEqual('/actions/FAKE_ACTION', resp['location'])
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.patch.object(util, 'parse_request')

View File

@ -33,6 +33,105 @@ class WebhookTest(base.SenlinTestCase):
self.ctx = utils.dummy_context(project='webhook_test_project')
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(action_mod.Action, 'create')
@mock.patch.object(co.Cluster, 'find')

View File

@ -60,3 +60,32 @@ class TestWebhookTrigger(test_base.SenlinTestCase):
self.assertEqual('1.0', res['senlin_object.version'])
self.assertEqual('senlin', res['senlin_object.namespace'])
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'])