Skip to content

Enable API-versioning and allow for both v3 and v4 versions.#7802

Open
ggainey wants to merge 1 commit into
pulp:mainfrom
ggainey:v4_api
Open

Enable API-versioning and allow for both v3 and v4 versions.#7802
ggainey wants to merge 1 commit into
pulp:mainfrom
ggainey:v4_api

Conversation

@ggainey

@ggainey ggainey commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

This is a tech-preview PR, and will only "turn on" if the ENABLE_V4_API is set to True in settings.

It will allow urls for /v3/ and /v4/, and reject other versions.

For urlpatterns registered with a /<str:version>/ slug, views will be called with a version= kwarg.

NOTE: To play this game, plugins will need to insure that all views accept **kwargs first, and then update the patterns in their urls.py to include the version-slug.

See the /status/ view and serializer(s) for an example of how to respond based on the incoming request.

NOTE to implementers: existing tests must pass without changes whether the ENABLE flag is True or False. If that isn't the case - you're doing something wrong.

closes #6462

@ggainey
ggainey force-pushed the v4_api branch 2 times, most recently from 75e4f88 to fcdcf5b Compare June 16, 2026 19:29
@ggainey
ggainey force-pushed the v4_api branch 3 times, most recently from d38417a to 1d45f72 Compare June 16, 2026 20:05
Comment thread pulpcore/app/models/task.py Outdated

result = models.JSONField(default=None, null=True, encoder=DjangoJSONEncoder)

version = models.TextField(default=REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we call this api_version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"version" as an attribute is "magic" at the DRF level - it's what gets filled in by the content of the <str:version> slug in the urlpattern. I debated on exactly this - but making it "version" means it's filled directly by the incoming **kwargs to the view, naming it anything else means calling it out specifically. I could be convinced of either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was just abit afraid that we could be confusing here. And changing a database column name later is a pain.

(Pulp is in the domain for versioning on all levels. Pulp-version, pulp-api-version, repository-version, deb-version, rpm-version, ansible-collection-version, ...)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Made the change to api_version - it bent some of the "magic" connections, but I think clarity is more important.

Comment thread pulpcore/app/serializers/task.py Outdated

version = serializers.CharField(
help_text=_("The API-version that was invoked when creating the task."),
default=REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
default=REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"),

I don't think we need this, tasks will never be created via this serializer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Possibly so, will dig a bit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think we need this, tasks will never be created via this serializer.

You would think, but I remember there being some surprising ways and places in which the serializers were getting initialized.

Comment thread pulpcore/app/settings.py Outdated
Comment thread pulpcore/app/contexts.py Outdated
_current_user_func = ContextVar("current_user", default=lambda: None)
_current_domain = ContextVar("current_domain", default=None)
x_task_diagnostics_var = ContextVar("x_profile_task")
_current_pulp_version = ContextVar(

@ggainey ggainey Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Comment: Is this really how we want to "know" what pulp-version we are operating under? Author, please review!

(also - even if we keep this, name needs to be _current_pulp_api_version)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That I kind ow liked. It's non-invasive, yet omnipresent inside the task code.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ggainey Did you mean to rename this? I agree that it should be renamed and it looks like you intended to do so

@ggainey

ggainey commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

RE ENABLE_V4_API : I am going to turn it on in this PR, in order to make sure CI runs clean with it enabled. BEFORE WE MERGE, we will turn it OFF, and let ppl who want to experiment "opt-in" to it, for several releases.

@ggainey
ggainey force-pushed the v4_api branch 3 times, most recently from 949527d to 147579e Compare June 17, 2026 17:14
Comment thread pulp_file/app/viewsets.py
)
@action(detail=True, methods=["post"], serializer_class=FileRepositorySyncURLSerializer)
def sync(self, request, pk):
def sync(self, request, pk, **kwargs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Apart from version, what arguments can end up being passed here? And what is the likelihood that any would overlap with task arguments?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Especially provided this applies across basically all tasks, so there are a wide variety of inputs that could potentially overlap.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To address this, changed to be specific about version - and also responded to @mdellweg 's "can this be api_version" comment, because that will also reduce collision-chance.

Comment thread pulpcore/app/contexts.py Outdated
if not task.version:
vers_token = _current_pulp_version.set(REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"))
else:
vers_token = _current_pulp_version.set(task.version)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So, is this a correct description

"The task needs to know about the API version it was spawned from because the task might use the serializers internally, and the serializers need to know what API version they are operating from"

and

"if the task didn't need to potentially provide that data to Serializers, the context var (within tasks) and the field on the task model would be needless"

Comment thread pulpcore/app/urls.py
path("datarepair/7272/", DataRepair7272View.as_view()),
path(
"orphans/cleanup/",
re_path(r"^login/$", LoginViewSet.as_view()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What purpose does regex matching serve for these simple paths?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"Consistency", mostly. I can do one last experiment where I'm more selective about that.

@ggainey
ggainey force-pushed the v4_api branch 3 times, most recently from 7f2b196 to 5308d95 Compare July 17, 2026 00:53
@ggainey
ggainey requested review from dralley and mdellweg July 17, 2026 01:32
Comment thread pulpcore/app/models/task.py Outdated
from pulpcore.app.models.fields import EncryptedJSONField
from pulpcore.app.models.status import AppStatus
from pulpcore.app.role_util import get_users_with_perms
from pulpcore.app.settings import REST_FRAMEWORK

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs to be from django.conf import settings.

@ggainey
ggainey force-pushed the v4_api branch 3 times, most recently from 2920117 to 0fd5df3 Compare July 17, 2026 15:30
mdellweg
mdellweg previously approved these changes Jul 17, 2026
@ggainey

ggainey commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

RE ENABLE_V4_API : I am going to turn it on in this PR, in order to make sure CI runs clean with it enabled. BEFORE WE MERGE, we will turn it OFF, and let ppl who want to experiment "opt-in" to it, for several releases.

ENABLE_V4_API is now False by default in this PR (see the warning in the commit-message for why). The PR is now ready to merge.

mdellweg
mdellweg previously approved these changes Jul 20, 2026
Comment thread pulp_file/app/viewsets.py Outdated
"optimize": optimize,
}
if kwargs.get("version"):
task_kwargs["api_version"] = kwargs.get("version")

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would it be worth making this even more specific, pulp_api_version or similar? To fully minimize the risk of overlap.

One could potentially imagine passing an api_version field into a sync task for example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We could - but it feels like overkill? We own this code, and "api version" is WAY more likely to be "Pulp". The edge-case would be adding api-version for...someting else?

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps but I'm in favor of overkill if it's just a matter of naming and has no other downsides. It also just feels like maintaining consistency with e.g. pulp_domain

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I found an even-better reason to change to pulp_api_version: https://github.com/pulp/pulpcore/blob/main/pulpcore/app/serializers/domain.py#L295

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That is indeed a pretty good reason!

(well, it's not a direct conflict, but it's an example of potential overlap issues)

Comment thread pulpcore/app/models/task.py Outdated

result = models.JSONField(default=None, null=True, encoder=DjangoJSONEncoder)

api_version = models.TextField(default=settings.REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"))

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we not rely on DEFAULT_VERSION existing? I'd prefer that be reliable and we not need a local default fallback here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My thinking here (and in the case below) is that I'd rather Absolutely Positively Guarantee we get something here, than have code that wants to use api-version have to deal with it maybe being None.

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If it was somehow None, then that would be a misconfiguration, and I'd personally rather that failure be more obvious than have the behavior be dependent on a dozen local fallbacks which might accidentally drift apart over time. It introduces ways for inconsistency to creep in.


api_version = serializers.CharField(
help_text=_("The API-version that was invoked when creating the task."),
default=settings.REST_FRAMEWORK.get("DEFAULT_VERSION", "v3"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Likewise, having local fallbacks kinda sucks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also is there a good reason to serialize this? It's useful for debugging I suppose but otherwise I'm not sure it's relevant to the user at all. We don't serialize task arguments for example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hm - is there a good reason to not do so?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It just feels like an implementation detail, and I would default to not exposing those.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mmm, I guess I see this differently - knowing how something was created is (or at some point will be, as we modify based-on-version more) important for knowing what to expect back. So to me, this is useful information for a user looking at the task.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how to resolve "you don't think it's useful and I do" :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, what is a concrete potential use for that info? I'm fine with exposing it if there's a solid case for it being useful, I just don't want to expose implementation details otherwise. Especially if it could be added back later should one arise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can't predict the future. I can imagine a task's method being invoked from a worker, looking at the task-context, and deciding "oh, I don't do this The Old Way, I do this the vN way", which results in a different code path/data/whatever. I don't see "what API version was I created with" to be "an implementation detail" - it's specifically public information.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's already tracked in the task arguments though (specifically Task.enc_kwargs). Strictly speaking it doesn't even need to be a separate field on the Task model, much less the serializer.

IMO if we need to debug, we would just do so in the exact same way we would debug anything else. I don't see a need to special case this.

All sorts of other options can trigger different codepaths within the task - mirror already being mentioned, but also the likes of skip_types, optimize, x-task-diagnostics (which is probably the most similar case), etc. We don't expose those publicly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I will probably submit a followup PR with this, but won't block this PR as-is.

else:
data["api_version"] = request.version
data["supported_api_versions"] = settings.REST_FRAMEWORK["ALLOWED_VERSIONS"]
serializer = V4StatusSerializer(data, context=context)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need two different versions of StatusSerializer or can we just alter the behavior dynamically?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are multiple ways to deal with versioning - this is intended as "an example". Altering "inside of" the view/serializer/model is another one, absolutely.

Comment thread pulpcore/tests/functional/api/test_v4_status.py
Comment thread pulpcore/tasking/tasks.py Outdated
"deferred": deferred,
"profile_options": x_task_diagnostics_var.get(None),
"app_lock": app_lock,
"api_version": kwargs.get("version", default_vers) if kwargs else default_vers,

@dralley dralley Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code could check the context variable, thus making it automatically self-propagating should a task be spawned from another task.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I will submit a followup PR with this.

This is a tech-preview PR, and will only "turn on" if the
ENABLE_V4_API is set to True in settings.

It will allow urls for /v3/ and /v4/, and reject other versions.

For urlpatterns registered with a `/<str:version>/` slug, views
will be called with a `version=` kwarg.

NOTE: To play this game, plugins will need to insure that all
views accept `**kwargs` first, and then update the patterns in their
`urls.py` to include the version-slug.

See the `/status/` view and serializer(s) for an example of
how to respond based on the incoming request.

NOTE to implementers: **existing tests must pass without changes**
whether the ENABLE flag is True or False. If that isn't the case -
you're doing something wrong.

closes pulp#6462
@dralley

dralley commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@ggainey @mdellweg What do you think about these tweaks? ggainey#275

(they can be considered and taken separately if needed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API namespacing

3 participants