Skip to content

Commit 5bda07b

Browse files
gerrod3cursoragent
andcommitted
Add migrate endpoint for push repositories.
Provide an async API to convert legacy ContainerPushRepository instances into ContainerRepository while preserving content and distribution links. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1f0abc0 commit 5bda07b

6 files changed

Lines changed: 179 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a `migrate` endpoint to push repositories that converts a `ContainerPushRepository` into a `ContainerRepository`.

pulp_container/app/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,21 @@ class Meta:
290290
model = models.ContainerPushRepository
291291

292292

293+
class MigratePushRepositorySerializer(ValidateFieldsMixin, serializers.Serializer):
294+
"""
295+
Serializer for migrating a push repository to a container repository.
296+
"""
297+
298+
copy_versions = serializers.BooleanField(
299+
required=False,
300+
default=False,
301+
help_text=_(
302+
"If True, copy the full repository version history. "
303+
"If False, only the latest repository version content is copied."
304+
),
305+
)
306+
307+
293308
class ContainerRemoteSerializer(RemoteSerializer):
294309
"""
295310
A Serializer for ContainerRemote.

pulp_container/app/tasks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .download_image_data import aadd_and_remove, download_image_data # noqa
22
from .builder import build_image_from_containerfile, build_image # noqa
3+
from .migrate_push_repository import migrate_push_repository # noqa
34
from .recursive_add import recursive_add_content # noqa
45
from .recursive_remove import recursive_remove_content # noqa
56
from .sign import sign # noqa
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from django.db import transaction
2+
3+
from pulpcore.plugin.models import CreatedResource
4+
5+
from pulp_container.app.models import (
6+
ContainerDistribution,
7+
ContainerPushRepository,
8+
ContainerRepository,
9+
)
10+
from pulp_container.app.serializers import ContainerRepositorySerializer
11+
12+
13+
def migrate_push_repository(push_repository_pk, copy_versions=False):
14+
"""
15+
Convert a ContainerPushRepository into a ContainerRepository.
16+
17+
Creates a new container repository with the same name and metadata, copies
18+
content from the push repository, reassociates any distributions, and deletes
19+
the original push repository.
20+
21+
Args:
22+
push_repository_pk (str): The primary key for the push repository to migrate.
23+
copy_versions (bool): If True, copy the full repository version history. If False,
24+
only copy content from the latest repository version.
25+
"""
26+
push_repository = ContainerPushRepository.objects.get(pk=push_repository_pk)
27+
28+
original_name = push_repository.name
29+
temp_name = f"{original_name}__migrating__{push_repository.pk}"
30+
31+
with transaction.atomic():
32+
push_repository.name = temp_name
33+
push_repository.save(update_fields=["name"])
34+
35+
container_repository = ContainerRepository(
36+
name=original_name,
37+
pulp_domain=push_repository.pulp_domain,
38+
pulp_labels=push_repository.pulp_labels,
39+
description=push_repository.description,
40+
retain_repo_versions=push_repository.retain_repo_versions,
41+
retain_checkpoints=push_repository.retain_checkpoints,
42+
user_hidden=push_repository.user_hidden,
43+
manifest_signing_service=push_repository.manifest_signing_service,
44+
)
45+
container_repository.save()
46+
47+
container_repository.pending_blobs.set(push_repository.pending_blobs.all())
48+
container_repository.pending_manifests.set(push_repository.pending_manifests.all())
49+
50+
if copy_versions:
51+
for push_version in push_repository.versions.complete().order_by("number"):
52+
if push_version.number == 0 and not push_version.content.exists():
53+
continue
54+
with container_repository.new_version() as new_version:
55+
new_version.set_content(push_version.content.all())
56+
else:
57+
latest_version = push_repository.latest_version()
58+
if latest_version:
59+
with container_repository.new_version() as new_version:
60+
new_version.set_content(latest_version.content.all())
61+
62+
ContainerDistribution.objects.filter(repository=push_repository).update(
63+
repository=container_repository
64+
)
65+
66+
push_repository.delete()
67+
68+
CreatedResource(content_object=container_repository).save()
69+
70+
return ContainerRepositorySerializer(
71+
instance=container_repository, context={"request": None}
72+
).data

pulp_container/app/viewsets.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1135,7 +1135,7 @@ class ContainerPushRepositoryViewSet(
11351135
],
11361136
},
11371137
{
1138-
"action": ["update", "partial_update", "set_label", "unset_label"],
1138+
"action": ["update", "partial_update", "set_label", "unset_label", "migrate"],
11391139
"principal": "authenticated",
11401140
"effect": "allow",
11411141
"condition_expression": [
@@ -1154,6 +1154,40 @@ class ContainerPushRepositoryViewSet(
11541154
}
11551155
LOCKED_ROLES = {}
11561156

1157+
@extend_schema(
1158+
description=(
1159+
"Trigger an asynchronous task to convert this push repository into a "
1160+
"container repository."
1161+
),
1162+
summary="Migrate push repository to container repository",
1163+
request=serializers.MigratePushRepositorySerializer,
1164+
responses={202: AsyncOperationResponseSerializer},
1165+
)
1166+
@action(detail=True, methods=["post"], serializer_class=serializers.MigratePushRepositorySerializer)
1167+
def migrate(self, request, pk):
1168+
"""
1169+
Create a task which converts a push repository into a container repository.
1170+
"""
1171+
repository = self.get_object()
1172+
1173+
serializer = serializers.MigratePushRepositorySerializer(
1174+
data=request.data, context={"request": request}
1175+
)
1176+
serializer.is_valid(raise_exception=True)
1177+
1178+
distributions = models.ContainerDistribution.objects.filter(repository=repository)
1179+
exclusive_resources = [repository, *distributions]
1180+
1181+
result = dispatch(
1182+
tasks.migrate_push_repository,
1183+
exclusive_resources=exclusive_resources,
1184+
kwargs={
1185+
"push_repository_pk": str(repository.pk),
1186+
"copy_versions": serializer.validated_data["copy_versions"],
1187+
},
1188+
)
1189+
return OperationPostponedResponse(result, request)
1190+
11571191
@extend_schema(
11581192
description=(
11591193
"Trigger an asynchronous task to remove a manifest and all its associated "
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Tests for migrating push repositories to container repositories."""
2+
3+
import uuid
4+
5+
from pulp_container.tests.functional.constants import REGISTRY_V2_REPO_PULP
6+
7+
8+
def test_migrate_push_repository(
9+
add_to_cleanup,
10+
container_push_repository_factory,
11+
registry_client,
12+
local_registry,
13+
container_bindings,
14+
full_path,
15+
monitor_task,
16+
):
17+
"""A push repository can be migrated to a container repository."""
18+
namespace_name = str(uuid.uuid4())
19+
repo_name = f"{namespace_name}/migrate"
20+
local_url = full_path(f"{repo_name}:1.0")
21+
22+
container_push_repository_factory(name=repo_name)
23+
image_path = f"{REGISTRY_V2_REPO_PULP}:manifest_a"
24+
registry_client.pull(image_path)
25+
local_registry.tag_and_push(image_path, local_url)
26+
27+
distribution = container_bindings.DistributionsContainerApi.list(name=repo_name).results[0]
28+
namespace = container_bindings.PulpContainerNamespacesApi.read(distribution.namespace)
29+
add_to_cleanup(container_bindings.PulpContainerNamespacesApi, namespace.pulp_href)
30+
31+
push_repository = container_bindings.RepositoriesContainerPushApi.list(name=repo_name).results[0]
32+
tags_before = container_bindings.ContentTagsApi.list(
33+
repository_version=push_repository.latest_version_href
34+
)
35+
assert tags_before.count == 1
36+
37+
migrate_response = container_bindings.RepositoriesContainerPushApi.migrate(
38+
push_repository.pulp_href, {"copy_versions": False}
39+
)
40+
task = monitor_task(migrate_response.task)
41+
container_repository = container_bindings.RepositoriesContainerApi.read(task.result["pulp_href"])
42+
assert container_repository.name == repo_name
43+
assert container_repository.pulp_href in task.created_resources
44+
assert container_repository.prn == task.result["prn"]
45+
tags_after = container_bindings.ContentTagsApi.list(
46+
repository_version=container_repository.latest_version_href
47+
)
48+
assert tags_after.count == 1
49+
assert tags_after.results[0].name == tags_before.results[0].name
50+
assert tags_after.results[0].prn == tags_before.results[0].prn
51+
52+
assert container_bindings.RepositoriesContainerPushApi.list(name=repo_name).count == 0
53+
54+
distribution = container_bindings.DistributionsContainerApi.list(name=repo_name).results[0]
55+
assert distribution.repository == container_repository.pulp_href

0 commit comments

Comments
 (0)