Repository: pulp/pulpcore

Issue:
Provide a management command to generate the api.json files
This command should allow to generate all relevant flavors for api bindings generation.

Parameters the user must be able to specify:
* components
* pk_path
* bindings


Discussion:

Resolving patch:
diff --git a/pulpcore/app/checks.py b/pulpcore/app/checks.py
--- a/pulpcore/app/checks.py
+++ b/pulpcore/app/checks.py
@@ -1,7 +1,22 @@
 from pathlib import Path
 
 from django.conf import settings
-from django.core.checks import Warning as CheckWarning, register
+from django.core.checks import Error as CheckError, Warning as CheckWarning, register
+
+
+@register(deploy=True)
+def content_origin_check(app_configs, **kwargs):
+    messages = []
+    if not getattr(settings, "CONTENT_ORIGIN", None):
+        messages.append(
+            CheckError(
+                "CONTENT_ORIGIN is a required setting but it was not configured. This may be "
+                "caused by invalid read permissions of the settings file. Note that "
+                "CONTENT_ORIGIN is set by the installation automatically.",
+                id="pulpcore.E001",
+            )
+        )
+    return messages
 
 
 @register(deploy=True)
diff --git a/pulpcore/app/management/commands/openapi.py b/pulpcore/app/management/commands/openapi.py
new file mode 100644
--- /dev/null
+++ b/pulpcore/app/management/commands/openapi.py
@@ -0,0 +1,99 @@
+from textwrap import dedent
+
+from django.core.management.base import BaseCommand, CommandError
+from django.http import HttpRequest
+from django.utils import translation
+from django.utils.module_loading import import_string
+
+from rest_framework.request import Request
+
+from drf_spectacular.renderers import OpenApiJsonRenderer, OpenApiYamlRenderer
+from drf_spectacular.settings import patched_settings
+from drf_spectacular.validation import validate_schema
+
+from pulpcore.openapi import PulpSchemaGenerator
+
+
+class SchemaValidationError(CommandError):
+    pass
+
+
+class Command(BaseCommand):
+    help = dedent(
+        """
+        Generate OpenAPI3 schema for the Pulp API.
+
+        The type of schema generated can be modified by providing some options.
+          --component <str> comma separated list of app_labels
+          --bindings flag, to produce operation ids used for bindings generation
+          --pk-path flag, whether paths are presented with PK or href variable
+    """
+    )
+
+    requires_system_checks = []
+
+    def add_arguments(self, parser):
+        parser.add_argument("--component", dest="component", default=None, type=str)
+        parser.add_argument("--bindings", dest="bindings", action="store_true")
+        parser.add_argument("--pk-path", dest="pk_path", action="store_true")
+        parser.add_argument(
+            "--format",
+            dest="format",
+            choices=["openapi", "openapi-json"],
+            default="openapi-json",
+            type=str,
+        )
+        parser.add_argument("--urlconf", dest="urlconf", default=None, type=str)
+        parser.add_argument("--file", dest="file", default=None, type=str)
+        parser.add_argument("--validate", dest="validate", default=False, action="store_true")
+        parser.add_argument("--lang", dest="lang", default=None, type=str)
+        parser.add_argument("--custom-settings", dest="custom_settings", default=None, type=str)
+
+    def handle(self, *args, **options):
+        generator = PulpSchemaGenerator(
+            urlconf=options["urlconf"],
+        )
+
+        if options["custom_settings"]:
+            custom_settings = import_string(options["custom_settings"])
+        else:
+            custom_settings = None
+
+        with patched_settings(custom_settings):
+            request = Request(HttpRequest())
+            request.META["SERVER_NAME"] = "localhost"
+            request.META["SERVER_PORT"] = "24817"
+            if options["component"]:
+                request.query_params["component"] = options["component"]
+            if options["bindings"]:
+                request.query_params["bindings"] = 1
+            if options["pk_path"]:
+                request.query_params["pk_path"] = 1
+
+            if options["lang"]:
+                with translation.override(options["lang"]):
+                    schema = generator.get_schema(request=request, public=True)
+            else:
+                schema = generator.get_schema(request=request, public=True)
+
+        if options["validate"]:
+            try:
+                validate_schema(schema)
+            except Exception as e:
+                raise SchemaValidationError(e)
+
+        renderer = self.get_renderer(options["format"])
+        output = renderer.render(schema, renderer_context={})
+
+        if options["file"]:
+            with open(options["file"], "wb") as f:
+                f.write(output)
+        else:
+            self.stdout.write(output.decode())
+
+    def get_renderer(self, format):
+        renderer_cls = {
+            "openapi": OpenApiYamlRenderer,
+            "openapi-json": OpenApiJsonRenderer,
+        }[format]
+        return renderer_cls()
diff --git a/pulpcore/app/settings.py b/pulpcore/app/settings.py
--- a/pulpcore/app/settings.py
+++ b/pulpcore/app/settings.py
@@ -315,17 +315,6 @@
 from dynaconf import DjangoDynaconf, Validator  # noqa
 
 # Validators
-content_origin_validator = Validator(
-    "CONTENT_ORIGIN",
-    must_exist=True,
-    messages={
-        "must_exist_true": (
-            "CONTENT_ORIGIN is a required setting but it was not configured. This may be caused "
-            "by invalid read permissions of the settings file. Note that CONTENT_ORIGIN is set by "
-            "the installer automatically."
-        )
-    },
-)
 storage_validator = (
     Validator("REDIRECT_TO_OBJECT_STORAGE", eq=False)
     | Validator("DEFAULT_FILE_STORAGE", eq="pulpcore.app.models.storage.FileSystem")
@@ -390,7 +379,6 @@
     validators=[
         api_root_validator,
         cache_validator,
-        content_origin_validator,
         sha256_validator,
         storage_validator,
         unknown_algs_validator,
@@ -402,9 +390,8 @@
 
 
 if not (
-    Path(sys.argv[0]).name == "pytest"
-    or Path(sys.argv[0]).name == "sphinx-build"
-    or (len(sys.argv) >= 2 and sys.argv[1] == "collectstatic")
+    Path(sys.argv[0]).name in ["pytest", "sphinx-build"]
+    or (len(sys.argv) >= 2 and sys.argv[1] in ["collectstatic", "openapi"])
 ):
     try:
         with open(DB_ENCRYPTION_KEY, "rb") as key_file:
@@ -421,7 +408,12 @@
     ALLOWED_CONTENT_CHECKSUMS
 )
 
-_SKIPPED_COMMANDS_FOR_CONTENT_CHECKS = ["handle-artifact-checksums", "migrate", "collectstatic"]
+_SKIPPED_COMMANDS_FOR_CONTENT_CHECKS = [
+    "handle-artifact-checksums",
+    "migrate",
+    "collectstatic",
+    "openapi",
+]
 
 if not (len(sys.argv) >= 2 and sys.argv[1] in _SKIPPED_COMMANDS_FOR_CONTENT_CHECKS):
     try:


Test patch:
diff --git a/pulpcore/tests/unit/test_settings.py b/pulpcore/tests/unit/test_settings.py
--- a/pulpcore/tests/unit/test_settings.py
+++ b/pulpcore/tests/unit/test_settings.py
@@ -2,14 +2,6 @@
 from dynaconf.validator import ValidationError
 
 
-def test_content_origin(settings):
-    """Test validation error is raised when CONTENT_ORIGIN is missing."""
-    # force needs to be True in order to remove CONTENT_ORIGIN since keep makes it a default
-    settings.unset("CONTENT_ORIGIN", force=True)
-    with pytest.raises(ValidationError):
-        settings.validators.validate()
-
-
 def test_cache_enabled(settings):
     """Test that when CACHE_ENABLED is set REDIS_URL or REDIS_HOST & REDIS_PORT."""
     settings.set("CACHE_ENABLED", True)