Merge "Add webhook api"

This commit is contained in:
Zuul
2020-01-10 03:30:02 +00:00
committed by Gerrit Code Review
9 changed files with 169 additions and 3 deletions

View File

@@ -38,6 +38,7 @@ app = {
'enable_acl': True, 'enable_acl': True,
'acl_public_routes': [ 'acl_public_routes': [
'/', '/',
'/v1/webhooks/.*',
], ],
} }

View File

@@ -42,6 +42,7 @@ from watcher.api.controllers.v1 import service
from watcher.api.controllers.v1 import strategy from watcher.api.controllers.v1 import strategy
from watcher.api.controllers.v1 import utils from watcher.api.controllers.v1 import utils
from watcher.api.controllers.v1 import versions from watcher.api.controllers.v1 import versions
from watcher.api.controllers.v1 import webhooks
def min_version(): def min_version():
@@ -131,6 +132,9 @@ class V1(APIBase):
services = [link.Link] services = [link.Link]
"""Links to the services resource""" """Links to the services resource"""
webhooks = [link.Link]
"""Links to the webhooks resource"""
links = [link.Link] links = [link.Link]
"""Links that point to a specific URL for this version and documentation""" """Links that point to a specific URL for this version and documentation"""
@@ -202,6 +206,14 @@ class V1(APIBase):
'services', '', 'services', '',
bookmark=True) bookmark=True)
] ]
if utils.allow_webhook_api():
v1.webhooks = [link.Link.make_link(
'self', base_url, 'webhooks', ''),
link.Link.make_link('bookmark',
base_url,
'webhooks', '',
bookmark=True)
]
return v1 return v1
@@ -217,6 +229,7 @@ class Controller(rest.RestController):
services = service.ServicesController() services = service.ServicesController()
strategies = strategy.StrategiesController() strategies = strategy.StrategiesController()
data_model = data_model.DataModelController() data_model = data_model.DataModelController()
webhooks = webhooks.WebhookController()
@wsme_pecan.wsexpose(V1) @wsme_pecan.wsexpose(V1)
def get(self): def get(self):

View File

@@ -185,3 +185,12 @@ def allow_list_datamodel():
""" """
return pecan.request.version.minor >= ( return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_3_DATAMODEL.value) versions.VERSIONS.MINOR_3_DATAMODEL.value)
def allow_webhook_api():
"""Check if we should support webhook API.
Version 1.4 of the API added support to trigger webhook.
"""
return pecan.request.version.minor >= (
versions.VERSIONS.MINOR_4_WEBHOOK_API.value)

View File

@@ -22,7 +22,8 @@ class VERSIONS(enum.Enum):
MINOR_1_START_END_TIMING = 1 # v1.1: Add start/end timei for audit MINOR_1_START_END_TIMING = 1 # v1.1: Add start/end timei for audit
MINOR_2_FORCE = 2 # v1.2: Add force field to audit MINOR_2_FORCE = 2 # v1.2: Add force field to audit
MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API MINOR_3_DATAMODEL = 3 # v1.3: Add list datamodel API
MINOR_MAX_VERSION = 3 MINOR_4_WEBHOOK_API = 4 # v1.4: Add webhook trigger API
MINOR_MAX_VERSION = 4
# This is the version 1 API # This is the version 1 API
BASE_VERSION = 1 BASE_VERSION = 1

View File

@@ -0,0 +1,62 @@
# 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.
"""
Webhook endpoint for Watcher v1 REST API.
"""
from oslo_log import log
import pecan
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from watcher.api.controllers.v1 import types
from watcher.api.controllers.v1 import utils
from watcher.common import exception
from watcher.decision_engine import rpcapi
from watcher import objects
LOG = log.getLogger(__name__)
class WebhookController(rest.RestController):
"""REST controller for webhooks resource."""
def __init__(self):
super(WebhookController, self).__init__()
self.dc_client = rpcapi.DecisionEngineAPI()
@wsme_pecan.wsexpose(None, wtypes.text, body=types.jsontype,
status_code=202)
def post(self, audit_ident, body):
"""Trigger the given audit.
:param audit_ident: UUID or name of an audit.
"""
LOG.debug("Webhook trigger Audit: %s.", audit_ident)
context = pecan.request.context
audit = utils.get_resource('Audit', audit_ident)
if audit is None:
raise exception.AuditNotFound(audit=audit_ident)
if audit.audit_type != objects.audit.AuditType.EVENT.value:
raise exception.AuditTypeNotAllowed(audit_type=audit.audit_type)
allowed_state = (
objects.audit.State.PENDING,
objects.audit.State.SUCCEEDED,
)
if audit.state not in allowed_state:
raise exception.AuditStateNotAllowed(state=audit.state)
# trigger decision-engine to run the audit
self.dc_client.trigger_audit(context, audit.uuid)

View File

@@ -243,6 +243,14 @@ class AuditTypeNotFound(Invalid):
msg_fmt = _("Audit type %(audit_type)s could not be found") msg_fmt = _("Audit type %(audit_type)s could not be found")
class AuditTypeNotAllowed(Invalid):
msg_fmt = _("Audit type %(audit_type)s is disallowed.")
class AuditStateNotAllowed(Invalid):
msg_fmt = _("Audit state %(state)s is disallowed.")
class AuditParameterNotAllowed(Invalid): class AuditParameterNotAllowed(Invalid):
msg_fmt = _("Audit parameter %(parameter)s are not allowed") msg_fmt = _("Audit parameter %(parameter)s are not allowed")

View File

@@ -75,6 +75,7 @@ class State(object):
class AuditType(enum.Enum): class AuditType(enum.Enum):
ONESHOT = 'ONESHOT' ONESHOT = 'ONESHOT'
CONTINUOUS = 'CONTINUOUS' CONTINUOUS = 'CONTINUOUS'
EVENT = 'EVENT'
@base.WatcherObjectRegistry.register @base.WatcherObjectRegistry.register

View File

@@ -29,7 +29,7 @@ class TestV1Root(base.FunctionalTest):
def test_get_v1_root_all(self): def test_get_v1_root_all(self):
data = self.get_json( data = self.get_json(
'/', headers={'OpenStack-API-Version': 'infra-optim 1.3'}) '/', headers={'OpenStack-API-Version': 'infra-optim 1.4'})
self.assertEqual('v1', data['id']) self.assertEqual('v1', data['id'])
# Check fields are not empty # Check fields are not empty
for f in data.keys(): for f in data.keys():
@@ -39,7 +39,7 @@ class TestV1Root(base.FunctionalTest):
actual_resources = tuple(set(data.keys()) - set(not_resources)) actual_resources = tuple(set(data.keys()) - set(not_resources))
expected_resources = ('audit_templates', 'audits', 'actions', expected_resources = ('audit_templates', 'audits', 'actions',
'action_plans', 'data_model', 'scoring_engines', 'action_plans', 'data_model', 'scoring_engines',
'services') 'services', 'webhooks')
self.assertEqual(sorted(expected_resources), sorted(actual_resources)) self.assertEqual(sorted(expected_resources), sorted(actual_resources))
self.assertIn({'type': 'application/vnd.openstack.watcher.v1+json', self.assertIn({'type': 'application/vnd.openstack.watcher.v1+json',

View File

@@ -0,0 +1,71 @@
# 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.
import mock
from watcher.decision_engine import rpcapi as deapi
from watcher import objects
from watcher.tests.api import base as api_base
from watcher.tests.objects import utils as obj_utils
class TestPost(api_base.FunctionalTest):
def setUp(self):
super(TestPost, self).setUp()
obj_utils.create_test_goal(self.context)
obj_utils.create_test_strategy(self.context)
obj_utils.create_test_audit_template(self.context)
@mock.patch.object(deapi.DecisionEngineAPI, 'trigger_audit')
def test_trigger_audit(self, mock_trigger_audit):
audit = obj_utils.create_test_audit(
self.context,
audit_type=objects.audit.AuditType.EVENT.value)
response = self.post_json(
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'})
self.assertEqual(202, response.status_int)
mock_trigger_audit.assert_called_once_with(
mock.ANY, audit['uuid'])
def test_trigger_audit_with_no_audit(self):
response = self.post_json(
'/webhooks/no-audit', {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(404, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_trigger_audit_with_not_allowed_audittype(self):
audit = obj_utils.create_test_audit(self.context)
response = self.post_json(
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])
def test_trigger_audit_with_not_allowed_audit_state(self):
audit = obj_utils.create_test_audit(
self.context,
audit_type=objects.audit.AuditType.EVENT.value,
state=objects.audit.State.FAILED)
response = self.post_json(
'/webhooks/%s' % audit['uuid'], {},
headers={'OpenStack-API-Version': 'infra-optim 1.4'},
expect_errors=True)
self.assertEqual(400, response.status_int)
self.assertEqual('application/json', response.content_type)
self.assertTrue(response.json['error_message'])