Skip to content

Commit 6445757

Browse files
mdellwegdralley
authored andcommitted
Create a parallel v4 API
closes #6462
1 parent d94e90f commit 6445757

File tree

12 files changed

+144
-47
lines changed

12 files changed

+144
-47
lines changed

CHANGES/6462.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

pulpcore/app/management/commands/analyze-publication.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ def handle(self, *args, **options):
4848
published_artifacts = publication.published_artifact.select_related(
4949
"content_artifact__artifact"
5050
).order_by("relative_path")
51-
artifact_href_prefix = reverse(get_view_name_for_model(Artifact, "list"))
51+
artifact_href_prefix = reverse(
52+
get_view_name_for_model(Artifact, "list")
53+
) # todo: reverse() + namespacing issues, print PRN instead?
5254

5355
if options["tabular"]:
5456
table = PrettyTable()

pulpcore/app/models/repository.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ def get_content_href(self, request=None):
14221422
ctype_model = ctypes[self.content_type]
14231423
ctype_view = get_view_name_for_model(ctype_model, "list")
14241424
try:
1425-
ctype_url = reverse(ctype_view, request=request)
1425+
ctype_url = reverse(ctype_view, request=request) # TODO: reverse() + namespacing issues
14261426
except django.urls.exceptions.NoReverseMatch:
14271427
# We've hit a content type for which there is no viewset.
14281428
# There's nothing we can do here, except to skip it.

pulpcore/app/response.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ def __init__(self, task, request):
2323
request (rest_framework.request.Request): Request used to generate the pulp_href urls
2424
"""
2525
kwargs = {"pk": task.pk}
26-
resp = {"task": reverse("tasks-detail", kwargs=kwargs, request=request)}
26+
resp = {
27+
"task": reverse("tasks-detail", kwargs=kwargs, request=request)
28+
} # reverse() + namespacing issues
2729
super().__init__(data=resp, status=202)
2830

2931

@@ -47,5 +49,7 @@ def __init__(self, task_group, request):
4749
request (rest_framework.request.Request): Request used to generate the pulp_href urls
4850
"""
4951
kwargs = {"pk": task_group.pk}
50-
resp = {"task_group": reverse("task-groups-detail", kwargs=kwargs, request=request)}
52+
resp = {
53+
"task_group": reverse("task-groups-detail", kwargs=kwargs, request=request)
54+
} # reverse() + namespacing issues
5155
super().__init__(data=resp, status=202)

pulpcore/app/serializers/base.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class HrefPrnFieldMixin:
6868

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

7474
def to_internal_value(self, data):
@@ -456,6 +456,30 @@ class Meta:
456456
read_only=True,
457457
)
458458

459+
# def __init__(self, *args, **kwargs):
460+
# super().__init__(*args, **kwargs)
461+
462+
# # The context kwarg is passed by the ViewSet
463+
# context = kwargs.get("context", {})
464+
# request = context.get("request")
465+
466+
# # If we are not in a context with a request, or if the namespace is v4,
467+
# # remove the 'pulp_href' field.
468+
# if request and request.resolver_match.namespace == "v4":
469+
# self.fields.pop("pulp_href", None)
470+
471+
def to_representation(self, instance):
472+
"""Overridden to drop the pulp_href field from responses"""
473+
representation = super().to_representation(instance)
474+
475+
if request := self.context.get("request"):
476+
if request.version == "v4":
477+
# TODO: this feels hacky, but apparently this code is being used on serializers
478+
# w/o pulp_href
479+
if "pulp_href" in representation:
480+
representation.pop("pulp_href")
481+
return representation
482+
459483
def _validate_relative_path(self, path):
460484
"""
461485
Validate a relative path (eg from a url) to ensure it forms a valid url and does not begin

pulpcore/app/serializers/fields.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,9 @@ def to_representation(self, value):
229229
if content_artifact.artifact_id:
230230
kwargs["pk"] = content_artifact.artifact_id
231231
request = self.context.get("request")
232-
url = reverse("artifacts-detail", kwargs=kwargs, request=request)
232+
url = reverse(
233+
"artifacts-detail", kwargs=kwargs, request=request
234+
) # TODO: reverse() + namespacing issues
233235
else:
234236
url = None
235237
ret[content_artifact.relative_path] = url

pulpcore/app/serializers/task.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ def get_created_by(self, obj) -> t.Optional[OpenApiTypes.URI]:
100100
if user_id := task_user_map.get(str(obj.pk)):
101101
kwargs = {"pk": user_id}
102102
request = self.context.get("request")
103-
return reverse("users-detail", kwargs=kwargs, request=request)
103+
return reverse(
104+
"users-detail", kwargs=kwargs, request=request
105+
) # TODO: reverse() + namespacing issues
104106
return None
105107

106108
class Meta:

pulpcore/app/settings.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@
9999
API_ROOT = "/pulp/"
100100
API_ROOT_REWRITE_HEADER = None
101101

102+
# Enable Pulp v4 API namespace
103+
ENABLE_V4_API = False
104+
102105
# Application definition
103106

104107
INSTALLED_APPS = [
@@ -192,7 +195,9 @@
192195
"rest_framework.authentication.SessionAuthentication",
193196
),
194197
"UPLOADED_FILES_USE_URL": False,
195-
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
198+
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
199+
"DEFAULT_VERSION": "v3",
200+
"ALLOWED_VERSIONS": ["v3", "v4"],
196201
"DEFAULT_SCHEMA_CLASS": "pulpcore.openapi.PulpAutoSchema",
197202
}
198203

@@ -639,3 +644,8 @@ def otel_middleware_hook(settings):
639644
settings.set("V3_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v3/")
640645
settings.set("V3_API_ROOT_NO_FRONT_SLASH", settings.V3_API_ROOT.lstrip("/"))
641646
settings.set("V3_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V3_DOMAIN_API_ROOT.lstrip("/"))
647+
648+
settings.set("V4_API_ROOT", api_root + "api/v4/") # Not user configurable
649+
settings.set("V4_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v4/")
650+
settings.set("V4_API_ROOT_NO_FRONT_SLASH", settings.V4_API_ROOT.lstrip("/"))
651+
settings.set("V4_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V4_DOMAIN_API_ROOT.lstrip("/"))

pulpcore/app/urls.py

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@
3232
API_ROOT = settings.V3_DOMAIN_API_ROOT_NO_FRONT_SLASH
3333
else:
3434
API_ROOT = settings.V3_API_ROOT_NO_FRONT_SLASH
35+
3536
if settings.API_ROOT_REWRITE_HEADER:
3637
V3_API_ROOT = settings.V3_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
38+
V4_API_ROOT = settings.V4_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
3739
else:
3840
V3_API_ROOT = settings.V3_API_ROOT
41+
V4_API_ROOT = settings.V4_API_ROOT
3942

4043

4144
class ViewSetNode:
@@ -153,70 +156,83 @@ class PulpDefaultRouter(routers.DefaultRouter):
153156
vs_tree.add_decendent(ViewSetNode(viewset))
154157

155158
special_views = [
156-
path("login/", LoginViewSet.as_view()),
157-
path("repair/", RepairView.as_view()),
159+
path("login/", LoginViewSet.as_view(), name="login"),
160+
path("repair/", RepairView.as_view(), name="repair"),
158161
path(
159162
"orphans/cleanup/",
160163
OrphansCleanupViewset.as_view(actions={"post": "cleanup"}),
164+
name="orphan-cleanup",
161165
),
162-
path("orphans/", OrphansView.as_view()),
166+
path("orphans/", OrphansView.as_view(), name="orphans"),
163167
path(
164168
"repository_versions/",
165169
ListRepositoryVersionViewSet.as_view(actions={"get": "list"}),
170+
name="repository-versions",
166171
),
167172
path(
168173
"repositories/reclaim_space/",
169174
ReclaimSpaceViewSet.as_view(actions={"post": "reclaim"}),
175+
name="reclaim",
170176
),
171177
path(
172178
"importers/core/pulp/import-check/",
173179
PulpImporterImportCheckView.as_view(),
180+
name="pulp-importer-import-check",
174181
),
175182
]
176183

177-
docs_and_status = [
178-
path("livez/", LivezView.as_view()),
179-
path("status/", StatusView.as_view()),
180-
path(
181-
"docs/api.json",
182-
SpectacularJSONAPIView.as_view(authentication_classes=[], permission_classes=[]),
183-
name="schema",
184-
),
185-
path(
186-
"docs/api.yaml",
187-
SpectacularYAMLAPIView.as_view(authentication_classes=[], permission_classes=[]),
188-
name="schema-yaml",
189-
),
190-
path(
191-
"docs/",
192-
SpectacularRedocView.as_view(
193-
authentication_classes=[],
194-
permission_classes=[],
195-
url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1",
184+
185+
def _docs_and_status(_api_root):
186+
paths = [
187+
path(
188+
"docs/api.json",
189+
SpectacularJSONAPIView.as_view(authentication_classes=[], permission_classes=[]),
190+
name="schema",
196191
),
197-
name="schema-redoc",
198-
),
199-
path(
200-
"swagger/",
201-
SpectacularSwaggerView.as_view(
202-
authentication_classes=[],
203-
permission_classes=[],
204-
url=f"{V3_API_ROOT}docs/api.json?include_html=1&pk_path=1",
192+
path(
193+
"docs/api.yaml",
194+
SpectacularYAMLAPIView.as_view(authentication_classes=[], permission_classes=[]),
195+
name="schema-yaml",
205196
),
206-
name="schema-swagger",
207-
),
208-
]
197+
path(
198+
"docs/",
199+
SpectacularRedocView.as_view(
200+
authentication_classes=[],
201+
permission_classes=[],
202+
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
203+
),
204+
name="schema-redoc",
205+
),
206+
path(
207+
"swagger/",
208+
SpectacularSwaggerView.as_view(
209+
authentication_classes=[],
210+
permission_classes=[],
211+
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
212+
),
213+
name="schema-swagger",
214+
),
215+
path("livez/", LivezView.as_view(), name="livez"),
216+
path("status/", StatusView.as_view(), name="status"),
217+
]
218+
219+
return paths
220+
221+
222+
v3_docs_and_status = _docs_and_status(V3_API_ROOT)
223+
v4_docs_and_status = _docs_and_status(V4_API_ROOT)
209224

210225
urlpatterns = [
211-
path(API_ROOT, include(special_views)),
212226
path("auth/", include("rest_framework.urls")),
213-
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(docs_and_status)),
227+
path(API_ROOT, include(special_views)),
228+
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(v3_docs_and_status)),
214229
]
215230

231+
216232
if settings.DOMAIN_ENABLED:
217233
# Ensure Docs and Status endpoints are available within domains, but are not shown in API schema
218234
docs_and_status_no_schema = []
219-
for p in docs_and_status:
235+
for p in v3_docs_and_status:
220236

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

246+
247+
if settings.ENABLE_V4_API:
248+
urlpatterns.extend(
249+
[
250+
path(V4_API_ROOT, include((special_views, "core"), namespace="v4")),
251+
path(
252+
settings.V4_API_ROOT_NO_FRONT_SLASH,
253+
include((v4_docs_and_status, "core"), namespace="v4"),
254+
),
255+
]
256+
)
257+
258+
259+
if settings.DOMAIN_ENABLED:
260+
# Ensure Docs and Status endpoints are available within domains, but are not shown in API schema
261+
docs_and_status_no_schema = []
262+
for p in v4_docs_and_status:
263+
264+
@extend_schema(exclude=True)
265+
class NoSchema(p.callback.cls):
266+
pass
267+
268+
view = NoSchema.as_view(**p.callback.initkwargs)
269+
name = p.name + "-domains" if p.name else None
270+
docs_and_status_no_schema.append(path(str(p.pattern), view, name=name))
271+
urlpatterns.insert(-1, path(API_ROOT, include(docs_and_status_no_schema)))
272+
273+
230274
if "social_django" in settings.INSTALLED_APPS:
231275
urlpatterns.append(
232276
path("", include("social_django.urls", namespace=settings.SOCIAL_AUTH_URL_NAMESPACE))
@@ -239,6 +283,12 @@ class NoSchema(p.callback.cls):
239283
for router in all_routers:
240284
urlpatterns.append(path(API_ROOT, include(router.urls)))
241285

286+
if settings.ENABLE_V4_API:
287+
for router in all_routers:
288+
urlpatterns.append(
289+
path(V4_API_ROOT.lstrip("/"), include((router.urls, "core"), namespace="v4"))
290+
)
291+
242292
# If plugins define a urls.py, include them into the root namespace.
243293
for plugin_pattern in plugin_patterns:
244294
urlpatterns.append(path("", include(plugin_pattern)))

pulpcore/app/util.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def get_url(model, domain=None, request=None):
8080
else:
8181
view_action = "list"
8282

83-
return reverse(get_view_name_for_model(model, view_action), kwargs=kwargs, request=request)
83+
return reverse(
84+
get_view_name_for_model(model, view_action), kwargs=kwargs, request=request
85+
) # TODO: reverse() + namespacing issues
8486

8587

8688
def get_prn(instance=None, uri=None):

0 commit comments

Comments
 (0)