Skip to content

Commit 9df3158

Browse files
committed
Add ability to sync by digests
fixes: #1909 Assisted by: claude-sonnet-4.6 https://redhat.atlassian.net/browse/PULP-1740
1 parent 8479d52 commit 9df3158

7 files changed

Lines changed: 309 additions & 59 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Renamed `include_tags` and `exclude_tags` to `includes` and `excludes` on the remote.

CHANGES/1909.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for syncing manifests by digest through new `includes` field on the remote.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import django.contrib.postgres.fields
2+
from django.db import migrations, models
3+
4+
5+
def migrate_to_includes_excludes(apps, schema_editor):
6+
"""Copy include_tags -> includes, exclude_tags -> excludes."""
7+
ContainerRemote = apps.get_model("container", "ContainerRemote")
8+
remotes = []
9+
for remote in ContainerRemote.objects.only("include_tags", "exclude_tags").iterator():
10+
remote.includes = remote.include_tags or None
11+
remote.excludes = remote.exclude_tags or None
12+
remotes.append(remote)
13+
if len(remotes) > 1000:
14+
ContainerRemote.objects.bulk_update(remotes, fields=["includes", "excludes"])
15+
remotes = []
16+
if remotes:
17+
ContainerRemote.objects.bulk_update(remotes, fields=["includes", "excludes"])
18+
19+
def down_migrate_to_include_exclude_tags(apps, schema_editor):
20+
"""Copy includes + excludes -> include_tags + exclude_tags."""
21+
ContainerRemote = apps.get_model("container", "ContainerRemote")
22+
remotes = []
23+
for remote in ContainerRemote.objects.only("includes", "excludes").iterator():
24+
remote.include_tags = remote.includes or None
25+
remote.exclude_tags = remote.excludes or None
26+
remotes.append(remote)
27+
if len(remotes) > 1000:
28+
ContainerRemote.objects.bulk_update(remotes, fields=["include_tags", "exclude_tags"])
29+
remotes = []
30+
if remotes:
31+
ContainerRemote.objects.bulk_update(remotes, fields=["include_tags", "exclude_tags"])
32+
33+
34+
class Migration(migrations.Migration):
35+
36+
dependencies = [
37+
("container", "0047_containernamespace_pulp_labels"),
38+
]
39+
40+
operations = [
41+
migrations.AddField(
42+
model_name="containerremote",
43+
name="includes",
44+
field=django.contrib.postgres.fields.ArrayField(
45+
base_field=models.TextField(null=True),
46+
null=True,
47+
size=None,
48+
),
49+
),
50+
migrations.AddField(
51+
model_name="containerremote",
52+
name="excludes",
53+
field=django.contrib.postgres.fields.ArrayField(
54+
base_field=models.TextField(null=True),
55+
null=True,
56+
size=None,
57+
),
58+
),
59+
# 2. Copy existing data.
60+
migrations.RunPython(
61+
migrate_to_includes_excludes,
62+
down_migrate_to_include_exclude_tags,
63+
),
64+
]

pulp_container/app/models.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,17 +483,22 @@ class ContainerRemote(Remote, AutoAddObjPermsMixin):
483483
upstream_name (models.TextField): The name of the image at the remote.
484484
include_foreign_layers (models.BooleanField): Foreign layers in the remote
485485
are included. They are not included by default.
486-
include_tags (fields.ArrayField): List of tags to include during sync.
487-
exclude_tags (fields.ArrayField): List of tags to exclude during sync.
486+
includes (fields.ArrayField): List of tags (with optional wildcards) and/or
487+
digests (sha256:<hex>) to sync.
488+
excludes (fields.ArrayField): List of tag patterns to exclude from sync.
488489
sigstore (models.TextField): The URL to a sigstore where signatures of container images
489490
should be synced from.
490491
"""
491492

492493
upstream_name = models.TextField(db_index=True)
493494
include_foreign_layers = models.BooleanField(default=False)
495+
includes = fields.ArrayField(models.TextField(null=True), null=True)
496+
excludes = fields.ArrayField(models.TextField(null=True), null=True)
497+
sigstore = models.TextField(null=True)
498+
499+
# Deprecated: kept for ZDT upgrades
494500
include_tags = fields.ArrayField(models.TextField(null=True), null=True)
495501
exclude_tags = fields.ArrayField(models.TextField(null=True), null=True)
496-
sigstore = models.TextField(null=True)
497502

498503
TYPE = "container"
499504

pulp_container/app/serializers.py

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -296,27 +296,58 @@ class ContainerRemoteSerializer(RemoteSerializer):
296296
upstream_name = serializers.CharField(
297297
required=True, allow_blank=False, help_text=_("Name of the upstream repository")
298298
)
299+
includes = serializers.ListField(
300+
child=serializers.CharField(max_length=255),
301+
allow_null=True,
302+
required=False,
303+
help_text=_(
304+
"A list of tags (wildcards *, ? are recognized) and/or digests "
305+
"(format: 'sha256:<hex>') to include during sync. "
306+
"'includes' is evaluated before 'excludes'."
307+
),
308+
)
309+
excludes = serializers.ListField(
310+
child=serializers.CharField(max_length=255),
311+
allow_null=True,
312+
required=False,
313+
help_text=_(
314+
"A list of tag patterns to exclude during sync. "
315+
"Wildcards *, ? are recognized. "
316+
"'excludes' is evaluated after 'includes'."
317+
),
318+
)
319+
320+
# ---------------------------------------------------------------------------
321+
# Deprecated write-only fields kept for backwards compatibility.
322+
# They are merged into `includes` / `excludes` during validation.
323+
# ---------------------------------------------------------------------------
299324
include_tags = serializers.ListField(
300325
child=serializers.CharField(max_length=255),
301326
allow_null=True,
302327
required=False,
303-
help_text=_("""
304-
A list of tags to include during sync.
305-
Wildcards *, ? are recognized.
306-
'include_tags' is evaluated before 'exclude_tags'.
307-
"""),
328+
write_only=True,
329+
help_text=_("Deprecated. Use 'includes' instead."),
308330
)
309331
exclude_tags = serializers.ListField(
310332
child=serializers.CharField(max_length=255),
311333
allow_null=True,
312334
required=False,
313-
help_text=_("""
314-
A list of tags to exclude during sync.
315-
Wildcards *, ? are recognized.
316-
'exclude_tags' is evaluated after 'include_tags'.
317-
"""),
335+
write_only=True,
336+
help_text=_("Deprecated. Use 'excludes' instead."),
318337
)
319338

339+
def validate(self, data):
340+
include_tags = data.pop("include_tags", None)
341+
exclude_tags = data.pop("exclude_tags", None)
342+
343+
if include_tags:
344+
data["includes"] = list(data.get("includes") or []) + list(include_tags)
345+
346+
if exclude_tags:
347+
data["excludes"] = list(data.get("excludes") or []) + list(exclude_tags)
348+
349+
return data
350+
320351
policy = serializers.ChoiceField(
321352
help_text="""
322353
immediate - All manifests and blobs are downloaded and saved during a sync.
@@ -337,6 +368,8 @@ class ContainerRemoteSerializer(RemoteSerializer):
337368
class Meta:
338369
fields = RemoteSerializer.Meta.fields + (
339370
"upstream_name",
371+
"includes",
372+
"excludes",
340373
"include_tags",
341374
"exclude_tags",
342375
"sigstore",

0 commit comments

Comments
 (0)