Skip to content

Commit f6418ab

Browse files
Moustafa-Moustafaggainey
authored andcommitted
Add retain_checkpoints field to Repository
Add a retain_checkpoints field to the Repository model that limits the number of checkpoint publications per repository. When set, older checkpoint publications beyond the limit have their checkpoint flag cleared automatically. Cleanup is triggered both when retain_checkpoints is updated on an existing repository and when a new checkpoint publication is created. closes #7428 Assisted-by: GitHub Copilot
1 parent abf7a6c commit f6418ab

8 files changed

Lines changed: 146 additions & 12 deletions

File tree

CHANGES/7428.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added ``retain_checkpoints`` field to Repository to automatically clear the checkpoint flag on older publications exceeding the limit.

docs/user/guides/checkpoint.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ http https://pulp.example/pulp/content/checkpoint/myfile
7373
</html>
7474
```
7575

76+
## Limiting Checkpoints
77+
78+
By default, all checkpoint publications are retained indefinitely. You can limit the number of
79+
checkpoints per repository by setting `retain_checkpoints` on the repository. When set, only the
80+
most recent N checkpoint publications are kept. Older checkpoint publications have their checkpoint
81+
flag cleared but are not deleted and the underlying publications and repository versions remain intact.
82+
83+
```bash
84+
pulp file repository update --name myrepo --retain-checkpoints 10
85+
```
86+
87+
Setting this to `null` (the default) disables checkpoint retention and keeps all checkpoints.
88+
89+
Note that clearing the checkpoint flag on a publication also removes its repository version from
90+
checkpoint protection, making it eligible for cleanup by `retain_repo_versions` if configured.
91+
7692
### Accessing a Specific Checkpoint
7793
To access a specific checkpoint, suffix the checkpoint distribution's path with a timestamp in the format
7894
`yyyyMMddTHHmmssZ` (e.g. 20250130T205339Z), If a checkpoint was created at this time, it will be
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.12 on 2026-03-09 22:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("core", "0145_domainize_import_export"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="repository",
15+
name="retain_checkpoints",
16+
field=models.PositiveIntegerField(default=None, null=True),
17+
),
18+
]

pulpcore/app/models/publication.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,11 @@ def __exit__(self, exc_type, exc_val, exc_tb):
233233
self.delete()
234234
raise
235235

236+
# Unmark old checkpoints if retention is configured
237+
if self.checkpoint:
238+
repository = self.repository_version.repository
239+
repository.cleanup_old_checkpoints()
240+
236241
# invalidate cache
237242
if settings.CACHE_ENABLED:
238243
base_paths = Distribution.objects.filter(

pulpcore/app/models/repository.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class Repository(MasterModel):
5252
next_version (models.PositiveIntegerField): A record of the next version number to be
5353
created.
5454
retain_repo_versions (models.PositiveIntegerField): Number of repo versions to keep
55+
retain_checkpoints (models.PositiveIntegerField): Number of checkpoint publications to
56+
keep as checkpoints.
5557
user_hidden (models.BooleanField): Whether to expose this repo to users via the API
5658
5759
Relations:
@@ -71,6 +73,7 @@ class Repository(MasterModel):
7173
description = models.TextField(null=True)
7274
next_version = models.PositiveIntegerField(default=0)
7375
retain_repo_versions = models.PositiveIntegerField(default=None, null=True)
76+
retain_checkpoints = models.PositiveIntegerField(default=None, null=True)
7477
user_hidden = models.BooleanField(default=False)
7578
content = models.ManyToManyField(
7679
"Content", through="RepositoryContent", related_name="repositories"
@@ -419,6 +422,33 @@ def cleanup_old_versions(self):
419422
)
420423
version.delete()
421424

425+
@hook(AFTER_UPDATE, when="retain_checkpoints", has_changed=True)
426+
def _cleanup_old_checkpoints_hook(self):
427+
transaction.on_commit(self.cleanup_old_checkpoints)
428+
429+
def cleanup_old_checkpoints(self):
430+
"""Unmark old checkpoint publications based on retain_checkpoints."""
431+
from .publication import Publication
432+
433+
if self.retain_checkpoints:
434+
checkpoint_pubs = Publication.objects.filter(
435+
repository_version__repository=self,
436+
checkpoint=True,
437+
complete=True,
438+
).order_by("-pulp_created")
439+
expired_pks = list(
440+
checkpoint_pubs[self.retain_checkpoints :].values_list("pk", flat=True)
441+
)
442+
if expired_pks:
443+
Publication.objects.filter(pk__in=expired_pks).update(checkpoint=False)
444+
_logger.info(
445+
"Cleared checkpoint flag on %d publication(s) for repository %s "
446+
"due to checkpoint retention limit of %d.",
447+
len(expired_pks),
448+
self.name,
449+
self.retain_checkpoints,
450+
)
451+
422452
def delete(self, **kwargs):
423453
"""
424454
Delete the repository.

pulpcore/app/serializers/repository.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ class RepositorySerializer(ModelSerializer):
4444
required=False,
4545
min_value=1,
4646
)
47+
retain_checkpoints = serializers.IntegerField(
48+
help_text=_(
49+
"Retain X checkpoint publications for the repository. "
50+
"Default is null which retains all checkpoints."
51+
),
52+
allow_null=True,
53+
required=False,
54+
min_value=1,
55+
)
4756
remote = DetailRelatedField(
4857
help_text=_("An optional remote to use by default when syncing."),
4958
view_name_pattern=r"remotes(-.*/.*)-detail",
@@ -69,6 +78,7 @@ class Meta:
6978
"name",
7079
"description",
7180
"retain_repo_versions",
81+
"retain_checkpoints",
7282
"remote",
7383
)
7484

pulpcore/app/viewsets/repository.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Meta:
9797
fields = {
9898
"name": NAME_FILTER_OPTIONS,
9999
"retain_repo_versions": NULLABLE_NUMERIC_FILTER_OPTIONS,
100+
"retain_checkpoints": NULLABLE_NUMERIC_FILTER_OPTIONS,
100101
}
101102

102103

pulpcore/tests/functional/api/using_plugin/test_checkpoint.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,18 @@ def _content_factory(name):
2424

2525

2626
@pytest.fixture(scope="class")
27-
def setup(
28-
file_repository_factory,
29-
file_distribution_factory,
30-
content_factory,
31-
file_bindings,
32-
monitor_task,
33-
):
34-
def create_publication(repo, checkpoint):
35-
nonlocal content_counter
27+
def create_publication(content_factory, file_bindings, monitor_task):
28+
counter = [0]
29+
30+
def _create_publication(repo, checkpoint):
31+
content_href = content_factory(f"{counter[0]}")
32+
counter[0] += 1
33+
3634
monitor_task(
3735
file_bindings.RepositoriesFileApi.modify(
38-
repo.pulp_href, {"add_content_units": [content_factory(f"{content_counter}")]}
36+
repo.pulp_href, {"add_content_units": [content_href]}
3937
).task
4038
)
41-
content_counter += 1
4239

4340
response = monitor_task(
4441
file_bindings.PublicationsFileApi.create(
@@ -47,11 +44,19 @@ def create_publication(repo, checkpoint):
4744
)
4845
return file_bindings.PublicationsFileApi.read(response.created_resources[0])
4946

47+
return _create_publication
48+
49+
50+
@pytest.fixture(scope="class")
51+
def setup(
52+
file_repository_factory,
53+
file_distribution_factory,
54+
create_publication,
55+
):
5056
repo = file_repository_factory()
5157
distribution = file_distribution_factory(repository=repo.pulp_href, checkpoint=True)
5258

5359
pubs = []
54-
content_counter = 0
5560
pubs.append(create_publication(repo, False))
5661
sleep(1)
5762
pubs.append(create_publication(repo, True))
@@ -227,3 +232,51 @@ def test_checkpoint_publication_with_repository_version_fails(
227232
file_bindings.PublicationsFileApi,
228233
{"repository_version": repo.latest_version_href, "checkpoint": True},
229234
)
235+
236+
237+
@pytest.mark.parallel
238+
def test_checkpoint_retention(
239+
file_bindings,
240+
file_repository_factory,
241+
file_distribution_factory,
242+
create_publication,
243+
monitor_task,
244+
):
245+
"""Test retain_checkpoints for repositories.
246+
247+
When retain_checkpoints is set, only the N most recent checkpoint publications should
248+
retain their checkpoint=True flag. Older ones get their checkpoint flag cleared.
249+
"""
250+
repo = file_repository_factory()
251+
file_distribution_factory(repository=repo.pulp_href, checkpoint=True)
252+
253+
# Create 4 checkpoint publications
254+
checkpoint_pubs = []
255+
for _ in range(4):
256+
checkpoint_pubs.append(create_publication(repo, True))
257+
258+
# Verify all 4 publications are checkpoints
259+
for pub in checkpoint_pubs:
260+
assert file_bindings.PublicationsFileApi.read(pub.pulp_href).checkpoint is True
261+
262+
# Set retain_checkpoints=2 — should clear checkpoint flag on the 2 oldest
263+
task = file_bindings.RepositoriesFileApi.partial_update(
264+
repo.pulp_href, {"retain_checkpoints": 2}
265+
).task
266+
monitor_task(task)
267+
268+
# Verify the 2 oldest had their flag cleared
269+
for pub in checkpoint_pubs[:2]:
270+
assert file_bindings.PublicationsFileApi.read(pub.pulp_href).checkpoint is False
271+
272+
# Verify the 2 most recent still have checkpoint=True
273+
for pub in checkpoint_pubs[2:]:
274+
assert file_bindings.PublicationsFileApi.read(pub.pulp_href).checkpoint is True
275+
276+
# Create another checkpoint — should trigger steady-state cleanup
277+
new_pub = create_publication(repo, True)
278+
279+
# checkpoint_pubs[2] should now be cleared too
280+
assert file_bindings.PublicationsFileApi.read(checkpoint_pubs[2].pulp_href).checkpoint is False
281+
assert file_bindings.PublicationsFileApi.read(checkpoint_pubs[3].pulp_href).checkpoint is True
282+
assert file_bindings.PublicationsFileApi.read(new_pub.pulp_href).checkpoint is True

0 commit comments

Comments
 (0)