Skip to content

Commit 510c96a

Browse files
committed
Merge branch 'main' into add-pulp-exceptions
2 parents 005810f + 0873f64 commit 510c96a

17 files changed

Lines changed: 697 additions & 35 deletions

File tree

.github/workflows/scripts/release.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ towncrier build --yes --version "${NEW_VERSION}"
2424
bump-my-version bump release --commit --message "Release {new_version}" --tag --tag-name "{new_version}" --tag-message "Release {new_version}" --allow-dirty
2525
bump-my-version bump patch --commit
2626

27-
git push origin "${BRANCH}" "${NEW_VERSION}"
27+
# Git push is not atomic by default!
28+
git push --atomic origin "${BRANCH}" "${NEW_VERSION}"

.github/workflows/update_ci.yml

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,37 @@ jobs:
165165
env:
166166
GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}"
167167
continue-on-error: true
168+
- uses: "actions/checkout@v6"
169+
with:
170+
fetch-depth: 0
171+
path: "pulp_python"
172+
ref: "3.19"
173+
174+
- name: "Run update"
175+
working-directory: "pulp_python"
176+
run: |
177+
../plugin_template/scripts/update_ci.sh --release
178+
179+
- name: "Create Pull Request for CI files"
180+
uses: "peter-evans/create-pull-request@v8"
181+
id: "create_pr_3_19"
182+
with:
183+
token: "${{ secrets.RELEASE_TOKEN }}"
184+
path: "pulp_python"
185+
committer: "pulpbot <pulp-infra@redhat.com>"
186+
author: "pulpbot <pulp-infra@redhat.com>"
187+
title: "Update CI files for branch 3.19"
188+
branch: "update-ci/3.19"
189+
base: "3.19"
190+
delete-branch: true
191+
- name: "Mark PR automerge"
192+
working-directory: "pulp_python"
193+
run: |
194+
gh pr merge --rebase --auto "${{ steps.create_pr_3_19.outputs.pull-request-number }}"
195+
if: "steps.create_pr_3_19.outputs.pull-request-number"
196+
env:
197+
GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}"
198+
continue-on-error: true
168199
- uses: "actions/checkout@v6"
169200
with:
170201
fetch-depth: 0
@@ -200,7 +231,7 @@ jobs:
200231
with:
201232
fetch-depth: 0
202233
path: "pulp_python"
203-
ref: "3.28"
234+
ref: "3.29"
204235

205236
- name: "Run update"
206237
working-directory: "pulp_python"
@@ -209,21 +240,21 @@ jobs:
209240
210241
- name: "Create Pull Request for CI files"
211242
uses: "peter-evans/create-pull-request@v8"
212-
id: "create_pr_3_28"
243+
id: "create_pr_3_29"
213244
with:
214245
token: "${{ secrets.RELEASE_TOKEN }}"
215246
path: "pulp_python"
216247
committer: "pulpbot <pulp-infra@redhat.com>"
217248
author: "pulpbot <pulp-infra@redhat.com>"
218-
title: "Update CI files for branch 3.28"
219-
branch: "update-ci/3.28"
220-
base: "3.28"
249+
title: "Update CI files for branch 3.29"
250+
branch: "update-ci/3.29"
251+
base: "3.29"
221252
delete-branch: true
222253
- name: "Mark PR automerge"
223254
working-directory: "pulp_python"
224255
run: |
225-
gh pr merge --rebase --auto "${{ steps.create_pr_3_28.outputs.pull-request-number }}"
226-
if: "steps.create_pr_3_28.outputs.pull-request-number"
256+
gh pr merge --rebase --auto "${{ steps.create_pr_3_29.outputs.pull-request-number }}"
257+
if: "steps.create_pr_3_29.outputs.pull-request-number"
227258
env:
228259
GH_TOKEN: "${{ secrets.RELEASE_TOKEN }}"
229260
continue-on-error: true

CHANGES.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,30 @@
88

99
[//]: # (towncrier release notes start)
1010

11+
## 3.29.0 (2026-04-17) {: #3.29.0 }
12+
13+
#### Features {: #3.29.0-feature }
14+
15+
- Added repository-specific package blocklist.
16+
[#1166](https://github.com/pulp/pulp_python/issues/1166)
17+
18+
#### Bugfixes {: #3.29.0-bugfix }
19+
20+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
21+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
22+
- Support "atomic" replications in pulpcore 3.107
23+
24+
---
25+
26+
## 3.28.2 (2026-04-14) {: #3.28.2 }
27+
28+
#### Bugfixes {: #3.28.2-bugfix }
29+
30+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
31+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
32+
33+
---
34+
1135
## 3.28.1 (2026-04-01) {: #3.28.1 }
1236

1337
#### Bugfixes {: #3.28.1-bugfix }
@@ -33,6 +57,15 @@
3357

3458
---
3559

60+
## 3.27.2 (2026-04-14) {: #3.27.2 }
61+
62+
#### Bugfixes {: #3.27.2-bugfix }
63+
64+
- Fixed "Worker has gone missing" errors during repair_metadata on large repositories (1000+ packages) by reducing peak memory consumption.
65+
[#1188](https://github.com/pulp/pulp_python/issues/1188)
66+
67+
---
68+
3669
## 3.27.1 (2026-04-01) {: #3.27.1 }
3770

3871
#### Bugfixes {: #3.27.1-bugfix }
@@ -522,6 +555,12 @@ No significant changes.
522555

523556
---
524557

558+
## 3.11.8 (2026-04-21) {: #3.11.8 }
559+
560+
No significant changes.
561+
562+
---
563+
525564
## 3.11.7 (2025-11-18) {: #3.11.7 }
526565

527566
No significant changes.

CHANGES/+atomic-replication-support.bugfix

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/user/guides/_SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
* [Host Python Content](host.md)
55
* [Vulnerability Report](vulnerability_report.md)
66
* [Attestation Hosting](attestation.md)
7+
* [Package Blocklist](blocklist.md)

docs/user/guides/blocklist.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Package Blocklist
2+
3+
A repository can have a blocklist that prevents specific packages from being added.
4+
Blocklist entries can match by package `name` (all versions), package `name` with an exact `version`, or exact `filename`.
5+
Exactly one of `name` or `filename` must be provided.
6+
7+
Each entry records the PRN of the user who created it in the `added_by` field.
8+
9+
## Setup
10+
11+
If you do not already have a repository, create one:
12+
13+
```bash
14+
pulp python repository create --name foo
15+
```
16+
17+
Set the API base URL and repository HREF for use in the subsequent commands:
18+
19+
```bash
20+
PULP_API="http://localhost:5001"
21+
REPO_HREF=$(pulp python repository show --name foo | jq -r ".pulp_href")
22+
```
23+
24+
## Add a blocklist entry
25+
26+
=== "By name (all versions)"
27+
28+
```bash
29+
# Block all versions of shelf-reader
30+
http POST "${PULP_API}${REPO_HREF}blocklist_entries/" name="shelf-reader"
31+
```
32+
33+
=== "By name and version"
34+
35+
```bash
36+
# Block only shelf-reader 0.1
37+
http POST "${PULP_API}${REPO_HREF}blocklist_entries/" name="shelf-reader" version="0.1"
38+
```
39+
40+
=== "By filename"
41+
42+
```bash
43+
# Block only shelf-reader-0.1.tar.gz
44+
http POST "${PULP_API}${REPO_HREF}blocklist_entries/" filename="shelf-reader-0.1.tar.gz"
45+
```
46+
47+
Set the UUID of a created entry for use in the subsequent commands:
48+
49+
```bash
50+
ENTRY_UUID=$(http GET "${PULP_API}${REPO_HREF}blocklist_entries/" | jq -r '.results[0].prn | split(":") | .[-1]')
51+
```
52+
53+
## List blocklist entries
54+
55+
List all entries for a repository:
56+
57+
```bash
58+
http GET "${PULP_API}${REPO_HREF}blocklist_entries/"
59+
```
60+
61+
Show a single entry:
62+
63+
```bash
64+
http GET "${PULP_API}${REPO_HREF}blocklist_entries/${ENTRY_UUID}/"
65+
```
66+
67+
## Remove a blocklist entry
68+
69+
```bash
70+
http DELETE "${PULP_API}${REPO_HREF}blocklist_entries/${ENTRY_UUID}/"
71+
```
72+
73+
Once an entry is removed, packages matching it can be added to the repository again.

pulp_python/app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
1010

1111
name = "pulp_python.app"
1212
label = "python"
13-
version = "3.29.0.dev"
13+
version = "3.30.0.dev"
1414
python_package_name = "pulp-python"
1515
domain_compatible = True
1616

pulp_python/app/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,19 @@ def __str__(self):
167167
return f"[{self.error_code}] " + _("Invalid attestations: {message}").format(
168168
message=self.message
169169
)
170+
171+
172+
class BlocklistedPackageError(PulpException):
173+
"""
174+
Raised when packages matching a blocklist entry are added to a repository.
175+
"""
176+
177+
error_code = "PYT0010"
178+
179+
def __init__(self, blocked):
180+
self.blocked = blocked
181+
182+
def __str__(self):
183+
return f"[{self.error_code}] " + _(
184+
"Blocklisted packages cannot be added to this repository: {blocked}"
185+
).format(blocked=", ".join(self.blocked))
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Generated by Django 5.2.10 on 2026-04-16 14:00
2+
3+
import django.db.models.deletion
4+
import django_lifecycle.mixins
5+
import pulpcore.app.models.base
6+
from django.db import migrations, models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
("python", "0021_pythonrepository_upload_duplicate_filenames"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="PythonBlocklistEntry",
18+
fields=[
19+
(
20+
"pulp_id",
21+
models.UUIDField(
22+
default=pulpcore.app.models.base.pulp_uuid,
23+
editable=False,
24+
primary_key=True,
25+
serialize=False,
26+
),
27+
),
28+
("pulp_created", models.DateTimeField(auto_now_add=True)),
29+
("pulp_last_updated", models.DateTimeField(auto_now=True, null=True)),
30+
("name", models.TextField(default=None, null=True)),
31+
("version", models.TextField(default=None, null=True)),
32+
("filename", models.TextField(default=None, null=True)),
33+
("added_by", models.TextField(default="")),
34+
(
35+
"repository",
36+
models.ForeignKey(
37+
on_delete=django.db.models.deletion.CASCADE,
38+
related_name="blocklist_entries",
39+
to="python.pythonrepository",
40+
),
41+
),
42+
],
43+
options={
44+
"default_related_name": "%(app_label)s_%(model_name)s",
45+
},
46+
bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model),
47+
),
48+
]

pulp_python/app/models.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
)
1414
from pulpcore.plugin.models import (
1515
AutoAddObjPermsMixin,
16+
BaseModel,
1617
Content,
1718
Publication,
1819
Distribution,
@@ -22,7 +23,7 @@
2223
from pulpcore.plugin.responses import ArtifactResponse
2324

2425
from pathlib import PurePath
25-
from .exceptions import PackageSubstitutionError
26+
from .exceptions import BlocklistedPackageError, PackageSubstitutionError
2627
from .provenance import Provenance
2728
from .utils import (
2829
artifact_to_python_content_data,
@@ -399,9 +400,12 @@ def finalize_new_version(self, new_version):
399400
400401
When allow_package_substitution is False, reject any new version that would implicitly
401402
replace existing content with different checksums (content substitution).
403+
404+
Also checks newly added content against the repository's blocklist entries.
402405
"""
403406
if not self.allow_package_substitution:
404407
self._check_for_package_substitution(new_version)
408+
self._check_blocklist(new_version)
405409
remove_duplicates(new_version)
406410
validate_repo_version(new_version)
407411

@@ -414,3 +418,60 @@ def _check_for_package_substitution(self, new_version):
414418
duplicates = collect_duplicates(qs, ("filename",))
415419
if duplicates:
416420
raise PackageSubstitutionError(duplicates)
421+
422+
def _check_blocklist(self, new_version):
423+
"""
424+
Check newly added content in a repository version against the blocklist.
425+
"""
426+
added_content = PythonPackageContent.objects.filter(
427+
pk__in=new_version.added().values_list("pk", flat=True)
428+
).only("filename", "name_normalized", "version")
429+
if added_content.exists():
430+
self.check_blocklist_for_packages(added_content)
431+
432+
def check_blocklist_for_packages(self, packages):
433+
"""
434+
Raise a ValidationError if any of the given packages match a blocklist entry.
435+
"""
436+
entries = PythonBlocklistEntry.objects.filter(repository=self)
437+
if not entries.exists():
438+
return
439+
440+
blocked = []
441+
for pkg in packages:
442+
for entry in entries:
443+
if entry.filename and entry.filename == pkg.filename:
444+
blocked.append(pkg.filename)
445+
break
446+
if entry.name == pkg.name_normalized:
447+
if not entry.version or entry.version == pkg.version:
448+
blocked.append(pkg.filename)
449+
break
450+
if blocked:
451+
raise BlocklistedPackageError(blocked)
452+
453+
454+
class PythonBlocklistEntry(BaseModel):
455+
"""
456+
An entry in a PythonRepository's package blocklist.
457+
458+
Blocklist entries prevent packages from being added to the repository.
459+
Entries can match by package `name` (all versions), package `name` + `version`,
460+
or exact `filename`. Exactly one of `name` or `filename` must be provided.
461+
"""
462+
463+
name = models.TextField(null=True, default=None)
464+
version = models.TextField(null=True, default=None)
465+
filename = models.TextField(null=True, default=None)
466+
added_by = models.TextField(default="")
467+
repository = models.ForeignKey(
468+
PythonRepository, on_delete=models.CASCADE, related_name="blocklist_entries"
469+
)
470+
471+
def __str__(self):
472+
if self.filename:
473+
return f"<{self._meta.object_name}: {self.filename}>"
474+
return f"<{self._meta.object_name}: {self.name} [{self.version or 'all'}]>"
475+
476+
class Meta:
477+
default_related_name = "%(app_label)s_%(model_name)s"

0 commit comments

Comments
 (0)