mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 23:46:05 -03:30
Fix /api/swagger endpoint (available only in development mode) (#13197)
Co-authored-by: John Westcott IV <john.westcott.iv@redhat.com>
This commit is contained in:
@@ -1,16 +1,10 @@
|
|||||||
import json
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from coreapi.document import Object, Link
|
|
||||||
|
|
||||||
from rest_framework import exceptions
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.renderers import CoreJSONRenderer
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework.schemas import SchemaGenerator, AutoSchema as DRFAuthSchema
|
from rest_framework.schemas import SchemaGenerator, AutoSchema as DRFAuthSchema
|
||||||
from rest_framework.views import APIView
|
|
||||||
|
|
||||||
from rest_framework_swagger import renderers
|
from drf_yasg.views import get_schema_view
|
||||||
|
from drf_yasg import openapi
|
||||||
|
|
||||||
|
|
||||||
class SuperUserSchemaGenerator(SchemaGenerator):
|
class SuperUserSchemaGenerator(SchemaGenerator):
|
||||||
@@ -55,43 +49,15 @@ class AutoSchema(DRFAuthSchema):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
class SwaggerSchemaView(APIView):
|
schema_view = get_schema_view(
|
||||||
_ignore_model_permissions = True
|
openapi.Info(
|
||||||
exclude_from_schema = True
|
title="Snippets API",
|
||||||
permission_classes = [AllowAny]
|
default_version='v1',
|
||||||
renderer_classes = [CoreJSONRenderer, renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer]
|
description="Test description",
|
||||||
|
terms_of_service="https://www.google.com/policies/terms/",
|
||||||
def get(self, request):
|
contact=openapi.Contact(email="contact@snippets.local"),
|
||||||
generator = SuperUserSchemaGenerator(title='Ansible Automation Platform controller API', patterns=None, urlconf=None)
|
license=openapi.License(name="BSD License"),
|
||||||
schema = generator.get_schema(request=request)
|
),
|
||||||
# python core-api doesn't support the deprecation yet, so track it
|
public=True,
|
||||||
# ourselves and return it in a response header
|
permission_classes=[AllowAny],
|
||||||
_deprecated = []
|
)
|
||||||
|
|
||||||
# By default, DRF OpenAPI serialization places all endpoints in
|
|
||||||
# a single node based on their root path (/api). Instead, we want to
|
|
||||||
# group them by topic/tag so that they're categorized in the rendered
|
|
||||||
# output
|
|
||||||
document = schema._data.pop('api')
|
|
||||||
for path, node in document.items():
|
|
||||||
if isinstance(node, Object):
|
|
||||||
for action in node.values():
|
|
||||||
topic = getattr(action, 'topic', None)
|
|
||||||
if topic:
|
|
||||||
schema._data.setdefault(topic, Object())
|
|
||||||
schema._data[topic]._data[path] = node
|
|
||||||
|
|
||||||
if isinstance(action, Object):
|
|
||||||
for link in action.links.values():
|
|
||||||
if link.deprecated:
|
|
||||||
_deprecated.append(link.url)
|
|
||||||
elif isinstance(node, Link):
|
|
||||||
topic = getattr(node, 'topic', None)
|
|
||||||
if topic:
|
|
||||||
schema._data.setdefault(topic, Object())
|
|
||||||
schema._data[topic]._data[path] = node
|
|
||||||
|
|
||||||
if not schema:
|
|
||||||
raise exceptions.ValidationError('The schema generator did not return a schema Document')
|
|
||||||
|
|
||||||
return Response(schema, headers={'X-Deprecated-Paths': json.dumps(_deprecated)})
|
|
||||||
|
|||||||
@@ -167,10 +167,13 @@ urlpatterns = [
|
|||||||
]
|
]
|
||||||
if MODE == 'development':
|
if MODE == 'development':
|
||||||
# Only include these if we are in the development environment
|
# Only include these if we are in the development environment
|
||||||
from awx.api.swagger import SwaggerSchemaView
|
from awx.api.swagger import schema_view
|
||||||
|
|
||||||
urlpatterns += [re_path(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view')]
|
|
||||||
|
|
||||||
from awx.api.urls.debug import urls as debug_urls
|
from awx.api.urls.debug import urls as debug_urls
|
||||||
|
|
||||||
urlpatterns += [re_path(r'^debug/', include(debug_urls))]
|
urlpatterns += [re_path(r'^debug/', include(debug_urls))]
|
||||||
|
urlpatterns += [
|
||||||
|
re_path(r'^swagger(?P<format>\.json|\.yaml)/$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||||
|
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||||
|
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||||
|
]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from rest_framework import status
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from awx import MODE
|
||||||
from awx.api.generics import APIView
|
from awx.api.generics import APIView
|
||||||
from awx.conf.registry import settings_registry
|
from awx.conf.registry import settings_registry
|
||||||
from awx.main.analytics import all_collectors
|
from awx.main.analytics import all_collectors
|
||||||
@@ -54,6 +55,8 @@ class ApiRootView(APIView):
|
|||||||
data['custom_logo'] = settings.CUSTOM_LOGO
|
data['custom_logo'] = settings.CUSTOM_LOGO
|
||||||
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO
|
||||||
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
|
data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE
|
||||||
|
if MODE == 'development':
|
||||||
|
data['swagger'] = drf_reverse('api:schema-swagger-ui')
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from openapi_codec.encode import generate_swagger_object
|
from drf_yasg.codecs import OpenAPICodecJson
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from awx.api.versioning import drf_reverse
|
from awx.api.versioning import drf_reverse
|
||||||
@@ -43,12 +43,12 @@ class TestSwaggerGeneration:
|
|||||||
@pytest.fixture(autouse=True, scope='function')
|
@pytest.fixture(autouse=True, scope='function')
|
||||||
def _prepare(self, get, admin):
|
def _prepare(self, get, admin):
|
||||||
if not self.__class__.JSON:
|
if not self.__class__.JSON:
|
||||||
url = drf_reverse('api:swagger_view') + '?format=openapi'
|
url = drf_reverse('api:schema-swagger-ui') + '?format=openapi'
|
||||||
response = get(url, user=admin)
|
response = get(url, user=admin)
|
||||||
data = generate_swagger_object(response.data)
|
codec = OpenAPICodecJson([])
|
||||||
|
data = codec.generate_swagger_object(response.data)
|
||||||
if response.has_header('X-Deprecated-Paths'):
|
if response.has_header('X-Deprecated-Paths'):
|
||||||
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
|
data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths'])
|
||||||
data.update(response.accepted_renderer.get_customizations() or {})
|
|
||||||
|
|
||||||
data['host'] = None
|
data['host'] = None
|
||||||
data['schemes'] = ['https']
|
data['schemes'] = ['https']
|
||||||
@@ -60,12 +60,21 @@ class TestSwaggerGeneration:
|
|||||||
# change {version} in paths to the actual default API version (e.g., v2)
|
# change {version} in paths to the actual default API version (e.g., v2)
|
||||||
revised_paths[path.replace('{version}', settings.REST_FRAMEWORK['DEFAULT_VERSION'])] = node
|
revised_paths[path.replace('{version}', settings.REST_FRAMEWORK['DEFAULT_VERSION'])] = node
|
||||||
for method in node:
|
for method in node:
|
||||||
|
# Ignore any parameters methods, these cause issues because it can come as an array instead of a dict
|
||||||
|
# Which causes issues in the last for loop in here
|
||||||
|
if method == 'parameters':
|
||||||
|
continue
|
||||||
|
|
||||||
if path in deprecated_paths:
|
if path in deprecated_paths:
|
||||||
node[method]['deprecated'] = True
|
node[method]['deprecated'] = True
|
||||||
if 'description' in node[method]:
|
if 'description' in node[method]:
|
||||||
# Pop off the first line and use that as the summary
|
# Pop off the first line and use that as the summary
|
||||||
lines = node[method]['description'].splitlines()
|
lines = node[method]['description'].splitlines()
|
||||||
node[method]['summary'] = lines.pop(0).strip('#:')
|
# If there was a description then set the summary as the description, otherwise make something up
|
||||||
|
if lines:
|
||||||
|
node[method]['summary'] = lines.pop(0).strip('#:')
|
||||||
|
else:
|
||||||
|
node[method]['summary'] = f'No Description for {method} on {path}'
|
||||||
node[method]['description'] = '\n'.join(lines)
|
node[method]['description'] = '\n'.join(lines)
|
||||||
|
|
||||||
# remove the required `version` parameter
|
# remove the required `version` parameter
|
||||||
@@ -90,13 +99,13 @@ class TestSwaggerGeneration:
|
|||||||
# The number of API endpoints changes over time, but let's just check
|
# The number of API endpoints changes over time, but let's just check
|
||||||
# for a reasonable number here; if this test starts failing, raise/lower the bounds
|
# for a reasonable number here; if this test starts failing, raise/lower the bounds
|
||||||
paths = JSON['paths']
|
paths = JSON['paths']
|
||||||
assert 250 < len(paths) < 350
|
assert 250 < len(paths) < 375
|
||||||
assert list(paths['/api/'].keys()) == ['get']
|
assert set(list(paths['/api/'].keys())) == set(['get', 'parameters'])
|
||||||
assert list(paths['/api/v2/'].keys()) == ['get']
|
assert set(list(paths['/api/v2/'].keys())) == set(['get', 'parameters'])
|
||||||
assert list(sorted(paths['/api/v2/credentials/'].keys())) == ['get', 'post']
|
assert set(list(sorted(paths['/api/v2/credentials/'].keys()))) == set(['get', 'post', 'parameters'])
|
||||||
assert list(sorted(paths['/api/v2/credentials/{id}/'].keys())) == ['delete', 'get', 'patch', 'put']
|
assert set(list(sorted(paths['/api/v2/credentials/{id}/'].keys()))) == set(['delete', 'get', 'patch', 'put', 'parameters'])
|
||||||
assert list(paths['/api/v2/settings/'].keys()) == ['get']
|
assert set(list(paths['/api/v2/settings/'].keys())) == set(['get', 'parameters'])
|
||||||
assert list(paths['/api/v2/settings/{category_slug}/'].keys()) == ['get', 'put', 'patch', 'delete']
|
assert set(list(paths['/api/v2/settings/{category_slug}/'].keys())) == set(['get', 'put', 'patch', 'delete', 'parameters'])
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'path',
|
'path',
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ INSIGHTS_TRACKING_STATE = False
|
|||||||
|
|
||||||
# debug toolbar and swagger assume that requirements/requirements_dev.txt are installed
|
# debug toolbar and swagger assume that requirements/requirements_dev.txt are installed
|
||||||
|
|
||||||
INSTALLED_APPS += ['rest_framework_swagger', 'debug_toolbar'] # NOQA
|
INSTALLED_APPS += ['drf_yasg', 'debug_toolbar'] # NOQA
|
||||||
|
|
||||||
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE # NOQA
|
MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE # NOQA
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
build
|
build
|
||||||
django-debug-toolbar==3.2.4
|
django-debug-toolbar==3.2.4
|
||||||
django-rest-swagger
|
drf-yasg
|
||||||
# pprofile - re-add once https://github.com/vpelletier/pprofile/issues/41 is addressed
|
# pprofile - re-add once https://github.com/vpelletier/pprofile/issues/41 is addressed
|
||||||
ipython>=7.31.1 # https://github.com/ansible/awx/security/dependabot/30
|
ipython>=7.31.1 # https://github.com/ansible/awx/security/dependabot/30
|
||||||
unittest2
|
unittest2
|
||||||
|
|||||||
Reference in New Issue
Block a user