Skip to content

Commit 33067e6

Browse files
mdellwegdralley
authored andcommitted
Create a parallel v4 API
closes #6462
1 parent 016174e commit 33067e6

File tree

11 files changed

+126
-46
lines changed

11 files changed

+126
-46
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
@@ -1361,7 +1361,7 @@ def get_content_href(self, request=None):
13611361
ctype_model = ctypes[self.content_type]
13621362
ctype_view = get_view_name_for_model(ctype_model, "list")
13631363
try:
1364-
ctype_url = reverse(ctype_view, request=request)
1364+
ctype_url = reverse(ctype_view, request=request) # TODO: reverse() + namespacing issues
13651365
except django.urls.exceptions.NoReverseMatch:
13661366
# We've hit a content type for which there is no viewset.
13671367
# 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: 11 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,16 @@ class Meta:
456456
read_only=True,
457457
)
458458

459+
def to_representation(self, instance):
460+
"""Overridden to drop the pulp_href field from responses"""
461+
representation = super().to_representation(instance)
462+
if self.context.get("request").version == "v4":
463+
# TODO: this feels hacky, but apparently this code is being used on serializers
464+
# w/o pulp_href
465+
if "pulp_href" in representation:
466+
representation.pop("pulp_href")
467+
return representation
468+
459469
def _validate_relative_path(self, path):
460470
"""
461471
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
@@ -94,7 +94,9 @@ def get_created_by(self, obj):
9494
if user_id := task_user_map.get(str(obj.pk)):
9595
kwargs = {"pk": user_id}
9696
request = self.context.get("request")
97-
return reverse("users-detail", kwargs=kwargs, request=request)
97+
return reverse(
98+
"users-detail", kwargs=kwargs, request=request
99+
) # TODO: reverse() + namespacing issues
98100
return None
99101

100102
class Meta:

pulpcore/app/settings.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@
119119
API_ROOT = "/pulp/"
120120
API_ROOT_REWRITE_HEADER = None
121121

122+
# Enable Pulp v4 API namespace
123+
ENABLE_V4_API = False
124+
122125
# Application definition
123126

124127
INSTALLED_APPS = [
@@ -211,7 +214,9 @@
211214
"rest_framework.authentication.SessionAuthentication",
212215
),
213216
"UPLOADED_FILES_USE_URL": False,
214-
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning",
217+
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
218+
"DEFAULT_VERSION": "v3",
219+
"ALLOWED_VERSIONS": ["v3", "v4"],
215220
"DEFAULT_SCHEMA_CLASS": "pulpcore.openapi.PulpAutoSchema",
216221
}
217222

@@ -652,3 +657,8 @@ def otel_middleware_hook(settings):
652657
settings.set("V3_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v3/")
653658
settings.set("V3_API_ROOT_NO_FRONT_SLASH", settings.V3_API_ROOT.lstrip("/"))
654659
settings.set("V3_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V3_DOMAIN_API_ROOT.lstrip("/"))
660+
661+
settings.set("V4_API_ROOT", api_root + "api/v4/") # Not user configurable
662+
settings.set("V4_DOMAIN_API_ROOT", api_root + "<slug:pulp_domain>/api/v4/")
663+
settings.set("V4_API_ROOT_NO_FRONT_SLASH", settings.V4_API_ROOT.lstrip("/"))
664+
settings.set("V4_DOMAIN_API_ROOT_NO_FRONT_SLASH", settings.V4_DOMAIN_API_ROOT.lstrip("/"))

pulpcore/app/urls.py

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
API_ROOT = settings.V3_API_ROOT_NO_FRONT_SLASH
3535
if settings.API_ROOT_REWRITE_HEADER:
3636
V3_API_ROOT = settings.V3_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
37+
V4_API_ROOT = settings.V4_API_ROOT.replace("/<path:api_root>/", settings.API_ROOT)
3738
else:
3839
V3_API_ROOT = settings.V3_API_ROOT
40+
V4_API_ROOT = settings.V4_API_ROOT
3941

4042

4143
class ViewSetNode:
@@ -153,70 +155,83 @@ class PulpDefaultRouter(routers.DefaultRouter):
153155
vs_tree.add_decendent(ViewSetNode(viewset))
154156

155157
special_views = [
156-
path("login/", LoginViewSet.as_view()),
157-
path("repair/", RepairView.as_view()),
158+
path("login/", LoginViewSet.as_view(), name="login"),
159+
path("repair/", RepairView.as_view(), name="repair"),
158160
path(
159161
"orphans/cleanup/",
160162
OrphansCleanupViewset.as_view(actions={"post": "cleanup"}),
163+
name="orphan-cleanup",
161164
),
162-
path("orphans/", OrphansView.as_view()),
165+
path("orphans/", OrphansView.as_view(), name="orphans"),
163166
path(
164167
"repository_versions/",
165168
ListRepositoryVersionViewSet.as_view(actions={"get": "list"}),
169+
name="repository-versions",
166170
),
167171
path(
168172
"repositories/reclaim_space/",
169173
ReclaimSpaceViewSet.as_view(actions={"post": "reclaim"}),
174+
name="reclaim",
170175
),
171176
path(
172177
"importers/core/pulp/import-check/",
173178
PulpImporterImportCheckView.as_view(),
179+
name="pulp-importer-import-check",
174180
),
175181
]
176182

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",
183+
184+
def _docs_and_status(_api_root):
185+
paths = [
186+
path(
187+
"docs/api.json",
188+
SpectacularJSONAPIView.as_view(authentication_classes=[], permission_classes=[]),
189+
name="schema",
196190
),
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",
191+
path(
192+
"docs/api.yaml",
193+
SpectacularYAMLAPIView.as_view(authentication_classes=[], permission_classes=[]),
194+
name="schema-yaml",
205195
),
206-
name="schema-swagger",
207-
),
208-
]
196+
path(
197+
"docs/",
198+
SpectacularRedocView.as_view(
199+
authentication_classes=[],
200+
permission_classes=[],
201+
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
202+
),
203+
name="schema-redoc",
204+
),
205+
path(
206+
"swagger/",
207+
SpectacularSwaggerView.as_view(
208+
authentication_classes=[],
209+
permission_classes=[],
210+
url=f"{_api_root}docs/api.json?include_html=1&pk_path=1",
211+
),
212+
name="schema-swagger",
213+
),
214+
path("livez/", LivezView.as_view(), name="livez"),
215+
path("status/", StatusView.as_view(), name="status"),
216+
]
217+
218+
return paths
219+
220+
221+
v3_docs_and_status = _docs_and_status(V3_API_ROOT)
222+
v4_docs_and_status = _docs_and_status(V4_API_ROOT)
209223

210224
urlpatterns = [
211-
path(API_ROOT, include(special_views)),
212225
path("auth/", include("rest_framework.urls")),
213-
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(docs_and_status)),
226+
path(API_ROOT, include(special_views)),
227+
path(settings.V3_API_ROOT_NO_FRONT_SLASH, include(v3_docs_and_status)),
214228
]
215229

230+
216231
if settings.DOMAIN_ENABLED:
217232
# Ensure Docs and Status endpoints are available within domains, but are not shown in API schema
218233
docs_and_status_no_schema = []
219-
for p in docs_and_status:
234+
for p in v3_docs_and_status:
220235

221236
@extend_schema(exclude=True)
222237
class NoSchema(p.callback.cls):
@@ -227,6 +242,34 @@ class NoSchema(p.callback.cls):
227242
docs_and_status_no_schema.append(path(str(p.pattern), view, name=name))
228243
urlpatterns.insert(-1, path(API_ROOT, include(docs_and_status_no_schema)))
229244

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

285+
if settings.ENABLE_V4_API:
286+
for router in all_routers:
287+
urlpatterns.append(path(V4_API_ROOT, include((router.urls, "core"), namespace="v4")))
288+
242289
# If plugins define a urls.py, include them into the root namespace.
243290
for plugin_pattern in plugin_patterns:
244291
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)