diff --git a/awx/api/swagger.py b/awx/api/swagger.py index d5ed23407a..f56c121e05 100644 --- a/awx/api/swagger.py +++ b/awx/api/swagger.py @@ -1,16 +1,10 @@ -import json import warnings -from coreapi.document import Object, Link - -from rest_framework import exceptions 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.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): @@ -55,43 +49,15 @@ class AutoSchema(DRFAuthSchema): return description -class SwaggerSchemaView(APIView): - _ignore_model_permissions = True - exclude_from_schema = True - permission_classes = [AllowAny] - renderer_classes = [CoreJSONRenderer, renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer] - - def get(self, request): - generator = SuperUserSchemaGenerator(title='Ansible Automation Platform controller API', patterns=None, urlconf=None) - schema = generator.get_schema(request=request) - # python core-api doesn't support the deprecation yet, so track it - # ourselves and return it in a response header - _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)}) +schema_view = get_schema_view( + openapi.Info( + title="Snippets API", + default_version='v1', + description="Test description", + terms_of_service="https://www.google.com/policies/terms/", + contact=openapi.Contact(email="contact@snippets.local"), + license=openapi.License(name="BSD License"), + ), + public=True, + permission_classes=[AllowAny], +) diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 94198f3766..5ecc30079d 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -167,10 +167,13 @@ urlpatterns = [ ] if MODE == 'development': # Only include these if we are in the development environment - from awx.api.swagger import SwaggerSchemaView - - urlpatterns += [re_path(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view')] + from awx.api.swagger import schema_view from awx.api.urls.debug import urls as debug_urls urlpatterns += [re_path(r'^debug/', include(debug_urls))] + urlpatterns += [ + re_path(r'^swagger(?P\.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'), + ] diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 4f65b01a15..b076df41c7 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -20,6 +20,7 @@ from rest_framework import status import requests +from awx import MODE from awx.api.generics import APIView from awx.conf.registry import settings_registry from awx.main.analytics import all_collectors @@ -54,6 +55,8 @@ class ApiRootView(APIView): data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO data['login_redirect_override'] = settings.LOGIN_REDIRECT_OVERRIDE + if MODE == 'development': + data['swagger'] = drf_reverse('api:schema-swagger-ui') return Response(data) diff --git a/awx/main/tests/docs/test_swagger_generation.py b/awx/main/tests/docs/test_swagger_generation.py index 658d8ad2d4..e7dd38e018 100644 --- a/awx/main/tests/docs/test_swagger_generation.py +++ b/awx/main/tests/docs/test_swagger_generation.py @@ -7,7 +7,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils.functional import Promise from django.utils.encoding import force_str -from openapi_codec.encode import generate_swagger_object +from drf_yasg.codecs import OpenAPICodecJson import pytest from awx.api.versioning import drf_reverse @@ -43,12 +43,12 @@ class TestSwaggerGeneration: @pytest.fixture(autouse=True, scope='function') def _prepare(self, get, admin): 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) - data = generate_swagger_object(response.data) + codec = OpenAPICodecJson([]) + data = codec.generate_swagger_object(response.data) if response.has_header('X-Deprecated-Paths'): data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths']) - data.update(response.accepted_renderer.get_customizations() or {}) data['host'] = None data['schemes'] = ['https'] @@ -60,12 +60,21 @@ class TestSwaggerGeneration: # change {version} in paths to the actual default API version (e.g., v2) revised_paths[path.replace('{version}', settings.REST_FRAMEWORK['DEFAULT_VERSION'])] = 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: node[method]['deprecated'] = True if 'description' in node[method]: # Pop off the first line and use that as the summary 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) # remove the required `version` parameter @@ -90,13 +99,13 @@ class TestSwaggerGeneration: # 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 paths = JSON['paths'] - assert 250 < len(paths) < 350 - assert list(paths['/api/'].keys()) == ['get'] - assert list(paths['/api/v2/'].keys()) == ['get'] - assert list(sorted(paths['/api/v2/credentials/'].keys())) == ['get', 'post'] - assert list(sorted(paths['/api/v2/credentials/{id}/'].keys())) == ['delete', 'get', 'patch', 'put'] - assert list(paths['/api/v2/settings/'].keys()) == ['get'] - assert list(paths['/api/v2/settings/{category_slug}/'].keys()) == ['get', 'put', 'patch', 'delete'] + assert 250 < len(paths) < 375 + assert set(list(paths['/api/'].keys())) == set(['get', 'parameters']) + assert set(list(paths['/api/v2/'].keys())) == set(['get', 'parameters']) + assert set(list(sorted(paths['/api/v2/credentials/'].keys()))) == set(['get', 'post', 'parameters']) + assert set(list(sorted(paths['/api/v2/credentials/{id}/'].keys()))) == set(['delete', 'get', 'patch', 'put', 'parameters']) + assert set(list(paths['/api/v2/settings/'].keys())) == set(['get', 'parameters']) + assert set(list(paths['/api/v2/settings/{category_slug}/'].keys())) == set(['get', 'put', 'patch', 'delete', 'parameters']) @pytest.mark.parametrize( 'path', diff --git a/awx/settings/development.py b/awx/settings/development.py index b5a2ba0a95..b402687d13 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -51,7 +51,7 @@ INSIGHTS_TRACKING_STATE = False # 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 diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 4db1327d40..2b658b065e 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,6 +1,6 @@ build django-debug-toolbar==3.2.4 -django-rest-swagger +drf-yasg # 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 unittest2