Merge pull request #41395 from rhcs-dashboard/fix-50855-master

mgr/dashboard: API Version changes do not apply to pre-defined methods (list, create etc.)

Reviewed-by: Aashish Sharma <aasharma@redhat.com>
Reviewed-by: Alfonso Martínez <almartin@redhat.com>
Reviewed-by: Avan Thakkar <athakkar@redhat.com>
Reviewed-by: Ernesto Puerta <epuertat@redhat.com>
Reviewed-by: Nizamudeen A <nia@redhat.com>
This commit is contained in:
Ernesto Puerta 2021-06-01 16:28:48 +02:00 committed by GitHub
commit 1b312db505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 41 deletions

View File

@ -1466,6 +1466,25 @@ same applies to other request types:
| DELETE | Yes | delete | 204 |
+--------------+------------+----------------+-------------+
To use a custom endpoint for the above listed methods, you can
use ``@RESTController.MethodMap``
.. code-block:: python
import cherrypy
from ..tools import ApiController, RESTController
@RESTController.MethodMap(version='0.1')
def create(self):
return {"msg": "Hello"}
This decorator supports three parameters to customize the
endpoint:
* ``resource"``: resource id.
* ``status=200``: set the HTTP status response code
* ``version``: version
How to use a custom API endpoint in a RESTController?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1494,7 +1513,7 @@ used. To use a custom endpoint inside a restricted ``RESTController`` use
def some_post_endpoint(self, **data):
return {"msg": data}
Both decorators also support four parameters to customize the
Both decorators also support five parameters to customize the
endpoint:
* ``method="GET"``: the HTTP method allowed to access this endpoint.
@ -1503,6 +1522,7 @@ endpoint:
* ``status=200``: set the HTTP status response code
* ``query_params=[]``: list of method parameter names that correspond to URL
query parameters.
* ``version``: version
How to restrict access to a controller?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -811,14 +811,14 @@ class RESTController(BaseController):
}
_method_mapping = collections.OrderedDict([
('list', {'method': 'GET', 'resource': False, 'status': 200}),
('create', {'method': 'POST', 'resource': False, 'status': 201}),
('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200}),
('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204}),
('get', {'method': 'GET', 'resource': True, 'status': 200}),
('delete', {'method': 'DELETE', 'resource': True, 'status': 204}),
('set', {'method': 'PUT', 'resource': True, 'status': 200}),
('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200})
('list', {'method': 'GET', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}),
('create', {'method': 'POST', 'resource': False, 'status': 201, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
('bulk_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
('bulk_delete', {'method': 'DELETE', 'resource': False, 'status': 204, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
('get', {'method': 'GET', 'resource': True, 'status': 200, 'version': DEFAULT_VERSION}),
('delete', {'method': 'DELETE', 'resource': True, 'status': 204, 'version': DEFAULT_VERSION}), # noqa E501 #pylint: disable=line-too-long
('set', {'method': 'PUT', 'resource': True, 'status': 200, 'version': DEFAULT_VERSION}),
('singleton_set', {'method': 'PUT', 'resource': False, 'status': 200, 'version': DEFAULT_VERSION}) # noqa E501 #pylint: disable=line-too-long
])
@classmethod
@ -857,10 +857,10 @@ class RESTController(BaseController):
cls._update_endpoint_params_method_map(
func, res_id_params, endpoint_params)
elif hasattr(func, "_collection_method_"):
elif hasattr(func, "__collection_method__"):
cls._update_endpoint_params_collection_map(func, endpoint_params)
elif hasattr(func, "_resource_method_"):
elif hasattr(func, "__resource_method__"):
cls._update_endpoint_params_resource_method(
res_id_params, endpoint_params, func)
@ -899,27 +899,27 @@ class RESTController(BaseController):
else:
path_params = ["{{{}}}".format(p) for p in res_id_params]
endpoint_params['path'] += "/{}".format("/".join(path_params))
if func._resource_method_['path']:
endpoint_params['path'] += func._resource_method_['path']
if func.__resource_method__['path']:
endpoint_params['path'] += func.__resource_method__['path']
else:
endpoint_params['path'] += "/{}".format(func.__name__)
endpoint_params['status'] = func._resource_method_['status']
endpoint_params['method'] = func._resource_method_['method']
endpoint_params['version'] = func._resource_method_['version']
endpoint_params['query_params'] = func._resource_method_['query_params']
endpoint_params['status'] = func.__resource_method__['status']
endpoint_params['method'] = func.__resource_method__['method']
endpoint_params['version'] = func.__resource_method__['version']
endpoint_params['query_params'] = func.__resource_method__['query_params']
if not endpoint_params['sec_permissions']:
endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
@classmethod
def _update_endpoint_params_collection_map(cls, func, endpoint_params):
if func._collection_method_['path']:
endpoint_params['path'] = func._collection_method_['path']
if func.__collection_method__['path']:
endpoint_params['path'] = func.__collection_method__['path']
else:
endpoint_params['path'] = "/{}".format(func.__name__)
endpoint_params['status'] = func._collection_method_['status']
endpoint_params['method'] = func._collection_method_['method']
endpoint_params['query_params'] = func._collection_method_['query_params']
endpoint_params['version'] = func._collection_method_['version']
endpoint_params['status'] = func.__collection_method__['status']
endpoint_params['method'] = func.__collection_method__['method']
endpoint_params['query_params'] = func.__collection_method__['query_params']
endpoint_params['version'] = func.__collection_method__['version']
if not endpoint_params['sec_permissions']:
endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
@ -936,6 +936,8 @@ class RESTController(BaseController):
endpoint_params['status'] = meth['status']
endpoint_params['method'] = meth['method']
if hasattr(func, "__method_map_method__"):
endpoint_params['version'] = func.__method_map_method__['version']
if not endpoint_params['sec_permissions']:
endpoint_params['permission'] = cls._permission_map[endpoint_params['method']]
@ -958,7 +960,7 @@ class RESTController(BaseController):
status = 200
def _wrapper(func):
func._resource_method_ = {
func.__resource_method__ = {
'method': method,
'path': path,
'status': status,
@ -968,6 +970,21 @@ class RESTController(BaseController):
return func
return _wrapper
@staticmethod
def MethodMap(resource=False, status=None, version=DEFAULT_VERSION): # noqa: N802
if status is None:
status = 200
def _wrapper(func):
func.__method_map_method__ = {
'resource': resource,
'status': status,
'version': version
}
return func
return _wrapper
@staticmethod
def Collection(method=None, path=None, status=None, query_params=None, # noqa: N802
version=DEFAULT_VERSION):
@ -978,7 +995,7 @@ class RESTController(BaseController):
status = 200
def _wrapper(func):
func._collection_method_ = {
func.__collection_method__ = {
'method': method,
'path': path,
'status': status,

View File

@ -183,7 +183,7 @@ class Docs(BaseController):
return schema.as_dict()
@classmethod
def _gen_responses(cls, method, resp_object=None):
def _gen_responses(cls, method, resp_object=None, version=None):
resp: Dict[str, Dict[str, Union[str, Any]]] = {
'400': {
"description": "Operation exception. Please check the "
@ -201,26 +201,30 @@ class Docs(BaseController):
"response body for the stack trace."
}
}
if not version:
version = DEFAULT_VERSION
if method.lower() == 'get':
resp['200'] = {'description': "OK",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
'content': {'application/vnd.ceph.api.v{}+json'.format(version):
{'type': 'object'}}}
if method.lower() == 'post':
resp['201'] = {'description': "Resource created.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
'content': {'application/vnd.ceph.api.v{}+json'.format(version):
{'type': 'object'}}}
if method.lower() == 'put':
resp['200'] = {'description': "Resource updated.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
'content': {'application/vnd.ceph.api.v{}+json'.format(version):
{'type': 'object'}}}
if method.lower() == 'delete':
resp['204'] = {'description': "Resource deleted.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
'content': {'application/vnd.ceph.api.v{}+json'.format(version):
{'type': 'object'}}}
if method.lower() in ['post', 'put', 'delete']:
resp['202'] = {'description': "Operation is still executing."
" Please check the task queue.",
'content': {'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION):
'content': {'application/vnd.ceph.api.v{}+json'.format(version):
{'type': 'object'}}}
if resp_object:
@ -228,7 +232,7 @@ class Docs(BaseController):
if status_code in resp:
resp[status_code].update({
'content': {
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'application/vnd.ceph.api.v{}+json'.format(version): {
'schema': cls._gen_schema_for_content(response_body)}}})
return resp
@ -261,7 +265,7 @@ class Docs(BaseController):
return parameters
@classmethod
def _gen_paths(cls, all_endpoints):
def gen_paths(cls, all_endpoints):
# pylint: disable=R0912
method_order = ['get', 'post', 'put', 'delete']
paths = {}
@ -281,8 +285,19 @@ class Docs(BaseController):
func = endpoint.func
summary = ''
version = ''
resp = {}
p_info = []
if hasattr(func, '__method_map_method__'):
version = func.__method_map_method__['version']
elif hasattr(func, '__resource_method__'):
version = func.__resource_method__['version']
elif hasattr(func, '__collection_method__'):
version = func.__collection_method__['version']
if hasattr(func, 'doc_info'):
if func.doc_info['summary']:
summary = func.doc_info['summary']
@ -302,7 +317,7 @@ class Docs(BaseController):
'tags': [cls._get_tag(endpoint)],
'description': func.__doc__,
'parameters': params,
'responses': cls._gen_responses(method, resp)
'responses': cls._gen_responses(method, resp, version)
}
if summary:
methods[method.lower()]['summary'] = summary
@ -338,7 +353,7 @@ class Docs(BaseController):
host = cherrypy.request.base.split('://', 1)[1] if not offline else 'example.com'
logger.debug("Host: %s", host)
paths = cls._gen_paths(all_endpoints)
paths = cls.gen_paths(all_endpoints)
if not base_url:
base_url = "/"

View File

@ -2,7 +2,6 @@
import unittest
from .. import DEFAULT_VERSION
from ..api.doc import SchemaType
from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController
from ..controllers.docs import Docs
@ -31,10 +30,14 @@ class DecoratedController(RESTController):
},
)
@Endpoint(json_response=False)
@RESTController.Resource('PUT')
@RESTController.Resource('PUT', version='0.1')
def decorated_func(self, parameter):
pass
@RESTController.MethodMap(version='0.1')
def list(self):
pass
# To assure functionality of @EndpointDoc, @GroupDoc
class DocDecoratorsTest(ControllerTestCase):
@ -76,7 +79,7 @@ class DocsTest(ControllerTestCase):
self.assertEqual(Docs()._type_to_str(None), str(SchemaType.OBJECT))
def test_gen_paths(self):
outcome = Docs()._gen_paths(False)['/api/doctest//{doctest}/decorated_func']['put']
outcome = Docs().gen_paths(False)['/api/doctest//{doctest}/decorated_func']['put']
self.assertIn('tags', outcome)
self.assertIn('summary', outcome)
self.assertIn('parameters', outcome)
@ -84,7 +87,7 @@ class DocsTest(ControllerTestCase):
expected_response_content = {
'200': {
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'application/vnd.ceph.api.v0.1+json': {
'schema': {'type': 'array',
'items': {'type': 'object', 'properties': {
'my_prop': {
@ -92,7 +95,7 @@ class DocsTest(ControllerTestCase):
'description': '200 property desc.'}}},
'required': ['my_prop']}}},
'202': {
'application/vnd.ceph.api.v{}+json'.format(DEFAULT_VERSION): {
'application/vnd.ceph.api.v0.1+json': {
'schema': {'type': 'object',
'properties': {'my_prop': {
'type': 'string',
@ -105,8 +108,14 @@ class DocsTest(ControllerTestCase):
# Check that a schema of type 'object' is received in the response.
self.assertEqual(expected_response_content['202'], outcome['responses']['202']['content'])
def test_gen_method_paths(self):
outcome = Docs().gen_paths(False)['/api/doctest/']['get']
self.assertEqual({'application/vnd.ceph.api.v0.1+json': {'type': 'object'}},
outcome['responses']['200']['content'])
def test_gen_paths_all(self):
paths = Docs()._gen_paths(False)
paths = Docs().gen_paths(False)
for key in paths:
self.assertTrue(any(base in key.split('/')[1] for base in ['api', 'ui-api']))

View File

@ -10,6 +10,7 @@ from . import ControllerTestCase # pylint: disable=no-name-in-module
class VTest(RESTController):
RESOURCE_ID = "vid"
@RESTController.MethodMap(version="0.1")
def list(self):
return {'version': ""}
@ -30,6 +31,15 @@ class RESTVersioningTest(ControllerTestCase, unittest.TestCase):
def setup_server(cls):
cls.setup_controllers([VTest], "/test")
def test_list(self):
for (version, expected_status) in [
("0.1", 200),
("2.0", 415)
]:
with self.subTest(version=version):
self._get('/test/api/vtest', version=version)
self.assertStatus(expected_status)
def test_v1(self):
for (version, expected_status) in [
("1.0", 200),