Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/6462.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a /pulp/api/v4/ namespace in parallel with the existing /pulp/api/v3/ namespace. This is disabled by default (`settings.ENABLE_V4_API`) and should be used only for development & experimentation.
4 changes: 3 additions & 1 deletion pulpcore/app/management/commands/analyze-publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def handle(self, *args, **options):
published_artifacts = publication.published_artifact.select_related(
"content_artifact__artifact"
).order_by("relative_path")
artifact_href_prefix = reverse(get_view_name_for_model(Artifact, "list"))
artifact_href_prefix = reverse(
get_view_name_for_model(Artifact, "list")
) # todo: reverse() + namespacing issues, print PRN instead?

if options["tabular"]:
table = PrettyTable()
Expand Down
2 changes: 1 addition & 1 deletion pulpcore/app/models/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -1422,7 +1422,7 @@ def get_content_href(self, request=None):
ctype_model = ctypes[self.content_type]
ctype_view = get_view_name_for_model(ctype_model, "list")
try:
ctype_url = reverse(ctype_view, request=request)
ctype_url = reverse(ctype_view, request=request) # TODO: reverse() + namespacing issues
except django.urls.exceptions.NoReverseMatch:
# We've hit a content type for which there is no viewset.
# There's nothing we can do here, except to skip it.
Expand Down
8 changes: 6 additions & 2 deletions pulpcore/app/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def __init__(self, task, request):
request (rest_framework.request.Request): Request used to generate the pulp_href urls
"""
kwargs = {"pk": task.pk}
resp = {"task": reverse("tasks-detail", kwargs=kwargs, request=request)}
resp = {
"task": reverse("tasks-detail", kwargs=kwargs, request=request)
} # reverse() + namespacing issues
super().__init__(data=resp, status=202)


Expand All @@ -47,5 +49,7 @@ def __init__(self, task_group, request):
request (rest_framework.request.Request): Request used to generate the pulp_href urls
"""
kwargs = {"pk": task_group.pk}
resp = {"task_group": reverse("task-groups-detail", kwargs=kwargs, request=request)}
resp = {
"task_group": reverse("task-groups-detail", kwargs=kwargs, request=request)
} # reverse() + namespacing issues
super().__init__(data=resp, status=202)
26 changes: 25 additions & 1 deletion pulpcore/app/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class HrefPrnFieldMixin:

def get_url(self, obj, view_name, request, *args, **kwargs):
# Use the Pulp reverse method to display relative hrefs.
self.reverse = _reverse(obj)
self.reverse = _reverse(obj) # TODO: reverse() + namespacing issues
return super().get_url(obj, view_name, request, *args, **kwargs)

def to_internal_value(self, data):
Expand Down Expand Up @@ -456,6 +456,30 @@ class Meta:
read_only=True,
)

# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)

# # The context kwarg is passed by the ViewSet
# context = kwargs.get("context", {})
# request = context.get("request")

# # If we are not in a context with a request, or if the namespace is v4,
# # remove the 'pulp_href' field.
# if request and request.resolver_match.namespace == "v4":
# self.fields.pop("pulp_href", None)

def to_representation(self, instance):
"""Overridden to drop the pulp_href field from responses"""
representation = super().to_representation(instance)

if request := self.context.get("request"):
if request.version == "v4":
# TODO: this feels hacky, but apparently this code is being used on serializers
# w/o pulp_href
if "pulp_href" in representation:
representation.pop("pulp_href")
Comment on lines +479 to +480
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pop(..., None) would work too, and I would call it resilient, not hacky. ;)

return representation

def _validate_relative_path(self, path):
"""
Validate a relative path (eg from a url) to ensure it forms a valid url and does not begin
Expand Down
4 changes: 3 additions & 1 deletion pulpcore/app/serializers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ def to_representation(self, value):
if content_artifact.artifact_id:
kwargs["pk"] = content_artifact.artifact_id
request = self.context.get("request")
url = reverse("artifacts-detail", kwargs=kwargs, request=request)
url = reverse(
"artifacts-detail", kwargs=kwargs, request=request
) # TODO: reverse() + namespacing issues
else:
url = None
ret[content_artifact.relative_path] = url
Expand Down
4 changes: 3 additions & 1 deletion pulpcore/app/serializers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def get_created_by(self, obj) -> t.Optional[OpenApiTypes.URI]:
if user_id := task_user_map.get(str(obj.pk)):
kwargs = {"pk": user_id}
request = self.context.get("request")
return reverse("users-detail", kwargs=kwargs, request=request)
return reverse(
"users-detail", kwargs=kwargs, request=request
) # TODO: reverse() + namespacing issues
return None

class Meta:
Expand Down
18 changes: 17 additions & 1 deletion pulpcore/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@
API_ROOT = "/pulp/"
API_ROOT_REWRITE_HEADER = None

# Enable Pulp v4 API namespace
ENABLE_V4_API = True

# Application definition

INSTALLED_APPS = [
Expand Down Expand Up @@ -192,7 +195,9 @@
"rest_framework.authentication.SessionAuthentication",
),
"UPLOADED_FILES_USE_URL": False,
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
"DEFAULT_VERSIONING_CLASS": "pulpcore.middleware.NamespaceVersioning",
"DEFAULT_VERSION": "v3",
"ALLOWED_VERSIONS": ["v3", "v4"],
"DEFAULT_SCHEMA_CLASS": "pulpcore.openapi.PulpAutoSchema",
}

Expand Down Expand Up @@ -635,7 +640,18 @@ def otel_middleware_hook(settings):
api_root = "/<path:api_root>/"
else:
api_root = settings.API_ROOT

settings.set("V3_API_ROOT", api_root + "api/v3/") # Not user configurable
settings.set("V3_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v3/")
settings.set("V3_API_ROOT_NO_FRONT_SLASH", settings.V3_API_ROOT.lstrip("/"))
settings.set("V3_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V3_DOMAIN_API_ROOT.lstrip("/"))

settings.set("V4_API_ROOT", api_root + "api/v4/") # Not user configurable
settings.set("V4_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v4/")
settings.set("V4_API_ROOT_NO_FRONT_SLASH", settings.V4_API_ROOT.lstrip("/"))
settings.set("V4_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V4_DOMAIN_API_ROOT.lstrip("/"))

if settings.API_ROOT_REWRITE_HEADER:
V3_API_ROOT = settings.V3_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
else:
V3_API_ROOT = settings.V3_API_ROOT
122 changes: 86 additions & 36 deletions pulpcore/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
API_ROOT = settings.V3_DOMAIN_API_ROOT_NO_FRONT_SLASH
else:
API_ROOT = settings.V3_API_ROOT_NO_FRONT_SLASH

if settings.API_ROOT_REWRITE_HEADER:
V3_API_ROOT = settings.V3_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
V4_API_ROOT = settings.V4_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
else:
V3_API_ROOT = settings.V3_API_ROOT
V4_API_ROOT = settings.V4_API_ROOT


class ViewSetNode:
Expand Down Expand Up @@ -153,70 +156,83 @@ class PulpDefaultRouter(routers.DefaultRouter):
vs_tree.add_decendent(ViewSetNode(viewset))

special_views = [
path("login/", LoginViewSet.as_view()),
path("repair/", RepairView.as_view()),
path("login/", LoginViewSet.as_view(), name="login"),
path("repair/", RepairView.as_view(), name="repair"),
path(
"orphans/cleanup/",
OrphansCleanupViewset.as_view(actions={"post": "cleanup"}),
name="orphan-cleanup",
),
path("orphans/", OrphansView.as_view()),
path("orphans/", OrphansView.as_view(), name="orphans"),
path(
"repository_versions/",
ListRepositoryVersionViewSet.as_view(actions={"get": "list"}),
name="repository-versions",
),
path(
"repositories/reclaim_space/",
ReclaimSpaceViewSet.as_view(actions={"post": "reclaim"}),
name="reclaim",
),
path(
"importers/core/pulp/import-check/",
PulpImporterImportCheckView.as_view(),
name="pulp-importer-import-check",
),
]

docs_and_status = [
path("livez/", LivezView.as_view()),
path("status/", StatusView.as_view()),
path(
"docs/api.json",
SpectacularJSONAPIView.as_view(authentication_classes=[], permission_classes=[]),
name="schema",
),
path(
"docs/api.yaml",
SpectacularYAMLAPIView.as_view(authentication_classes=[], permission_classes=[]),
name="schema-yaml",
),
path(
"docs/",
SpectacularRedocView.as_view(
authentication_classes=[],
permission_classes=[],
url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1",

def _docs_and_status(_api_root):
paths = [
path(
"docs/api.json",
SpectacularJSONAPIView.as_view(authentication_classes=[], permission_classes=[]),
name="schema",
),
name="schema-redoc",
),
path(
"swagger/",
SpectacularSwaggerView.as_view(
authentication_classes=[],
permission_classes=[],
url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1",
path(
"docs/api.yaml",
SpectacularYAMLAPIView.as_view(authentication_classes=[], permission_classes=[]),
name="schema-yaml",
),
name="schema-swagger",
),
]
path(
"docs/",
SpectacularRedocView.as_view(
authentication_classes=[],
permission_classes=[],
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
),
name="schema-redoc",
),
path(
"swagger/",
SpectacularSwaggerView.as_view(
authentication_classes=[],
permission_classes=[],
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
),
name="schema-swagger",
),
path("livez/", LivezView.as_view(), name="livez"),
path("status/", StatusView.as_view(), name="status"),
]

return paths


v3_docs_and_status = _docs_and_status(V3_API_ROOT)
v4_docs_and_status = _docs_and_status(V4_API_ROOT)

urlpatterns = [
path(API_ROOT, include(special_views)),
path("auth/", include("rest_framework.urls")),
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(docs_and_status)),
path(API_ROOT, include(special_views)),
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(v3_docs_and_status)),
]


if settings.DOMAIN_ENABLED:
# Ensure Docs and Status endpoints are available within domains, but are not shown in API schema
docs_and_status_no_schema = []
for p in docs_and_status:
for p in v3_docs_and_status:

@extend_schema(exclude=True)
class NoSchema(p.callback.cls):
Expand All @@ -227,6 +243,34 @@ class NoSchema(p.callback.cls):
docs_and_status_no_schema.append(path(str(p.pattern), view, name=name))
urlpatterns.insert(-1, path(API_ROOT, include(docs_and_status_no_schema)))


if settings.ENABLE_V4_API:
urlpatterns.extend(
[
path(V4_API_ROOT, include((special_views, "core"), namespace="v4")),
path(
settings.V4_API_ROOT_NO_FRONT_SLASH,
include((v4_docs_and_status, "core"), namespace="v4"),
),
]
)


if settings.DOMAIN_ENABLED:
# Ensure Docs and Status endpoints are available within domains, but are not shown in API schema
docs_and_status_no_schema = []
for p in v4_docs_and_status:

@extend_schema(exclude=True)
class NoSchema(p.callback.cls):
pass

view = NoSchema.as_view(**p.callback.initkwargs)
name = p.name + "-domains" if p.name else None
docs_and_status_no_schema.append(path(str(p.pattern), view, name=name))
urlpatterns.insert(-1, path(API_ROOT, include(docs_and_status_no_schema)))


if "social_django" in settings.INSTALLED_APPS:
urlpatterns.append(
path("", include("social_django.urls", namespace=settings.SOCIAL_AUTH_URL_NAMESPACE))
Expand All @@ -239,6 +283,12 @@ class NoSchema(p.callback.cls):
for router in all_routers:
urlpatterns.append(path(API_ROOT, include(router.urls)))

if settings.ENABLE_V4_API:
for router in all_routers:
urlpatterns.append(
path(V4_API_ROOT.lstrip("/"), include((router.urls, "core"), namespace="v4"))
)

# If plugins define a urls.py, include them into the root namespace.
for plugin_pattern in plugin_patterns:
urlpatterns.append(path("", include(plugin_pattern)))
4 changes: 3 additions & 1 deletion pulpcore/app/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def get_url(model, domain=None, request=None):
else:
view_action = "list"

return reverse(get_view_name_for_model(model, view_action), kwargs=kwargs, request=request)
return reverse(
get_view_name_for_model(model, view_action), kwargs=kwargs, request=request
) # TODO: reverse() + namespacing issues


def get_prn(instance=None, uri=None):
Expand Down
2 changes: 1 addition & 1 deletion pulpcore/app/viewsets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def get_resource(uri, model=None):
found_kwargs["pk"] = pk
else:
try:
match = resolve(urlparse(uri).path)
match = resolve(urlparse(uri).path) # TODO: resolve() + namespacing issues
except Resolver404:
raise DRFValidationError(detail=_("URI not valid: {u}").format(u=uri))
else:
Expand Down
Loading
Loading